code-intelligence.com/cifuzz@v0.40.0/internal/bundler/bundler.go (about) 1 package bundler 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/pkg/errors" 11 12 "code-intelligence.com/cifuzz/internal/bundler/archive" 13 "code-intelligence.com/cifuzz/internal/cmdutils" 14 "code-intelligence.com/cifuzz/internal/config" 15 "code-intelligence.com/cifuzz/pkg/log" 16 "code-intelligence.com/cifuzz/pkg/vcs" 17 "code-intelligence.com/cifuzz/util/fileutil" 18 "code-intelligence.com/cifuzz/util/sliceutil" 19 ) 20 21 // The (possibly empty) directory inside the fuzzing artifact archive that will 22 // be the fuzzers working directory. 23 const archiveWorkDirPath = "work_dir" 24 25 type Bundler struct { 26 opts *Opts 27 } 28 29 func New(opts *Opts) *Bundler { 30 return &Bundler{opts: opts} 31 } 32 33 func (b *Bundler) Bundle() (string, error) { 34 var err error 35 36 // Create temp dir 37 b.opts.tempDir, err = os.MkdirTemp("", "cifuzz-bundle-") 38 if err != nil { 39 return "", errors.WithStack(err) 40 } 41 defer fileutil.Cleanup(b.opts.tempDir) 42 43 var bundle *os.File 44 bundle, err = b.createEmptyBundle() 45 if err != nil { 46 return "", err 47 } 48 // if an error occurs during bundling we should make sure that 49 // the bundle gets removed 50 defer func() { 51 bundle.Close() 52 if err != nil { 53 os.Remove(bundle.Name()) 54 } 55 }() 56 57 // Create archive writer 58 bufWriter := bufio.NewWriter(bundle) 59 archiveWriter := archive.NewTarArchiveWriter(bufWriter, true) 60 61 var fuzzers []*archive.Fuzzer 62 switch b.opts.BuildSystem { 63 case config.BuildSystemCMake, config.BuildSystemBazel, config.BuildSystemOther: 64 fuzzers, err = newLibfuzzerBundler(b.opts, archiveWriter).bundle() 65 case config.BuildSystemMaven, config.BuildSystemGradle: 66 fuzzers, err = newJazzerBundler(b.opts, archiveWriter).bundle() 67 default: 68 err = errors.Errorf("Unknown build system for bundler: %s", b.opts.BuildSystem) 69 } 70 if err != nil { 71 return "", err 72 } 73 74 dockerImageUsedInBundle := b.determineDockerImageForBundle() 75 err = b.createMetadataFileInArchive(fuzzers, archiveWriter, dockerImageUsedInBundle) 76 if err != nil { 77 return "", err 78 } 79 80 err = b.createWorkDirInArchive(archiveWriter) 81 if err != nil { 82 return "", err 83 } 84 85 err = b.copyAdditionalFilesToArchive(archiveWriter) 86 if err != nil { 87 return "", err 88 } 89 90 // Container bundle does not define build.log? 91 if b.opts.BundleBuildLogFile != "" { 92 err = archiveWriter.WriteFile("build.log", b.opts.BundleBuildLogFile) 93 if err != nil { 94 return "", errors.WithStack(err) 95 } 96 } 97 98 err = archiveWriter.Close() 99 if err != nil { 100 return "", errors.WithStack(err) 101 } 102 err = bufWriter.Flush() 103 if err != nil { 104 return "", errors.WithStack(err) 105 } 106 err = bundle.Close() 107 if err != nil { 108 return "", errors.WithStack(err) 109 } 110 111 return bundle.Name(), nil 112 } 113 114 func (b *Bundler) createEmptyBundle() (*os.File, error) { 115 archiveExt := ".tar.gz" 116 117 if b.opts.OutputPath != "" { 118 // do nothing 119 } else if len(b.opts.FuzzTests) == 1 { 120 fuzzTestName := strings.ReplaceAll(b.opts.FuzzTests[0], "::", "_") 121 b.opts.OutputPath = filepath.Base(fuzzTestName) + archiveExt 122 } else { 123 b.opts.OutputPath = "fuzz_tests" + archiveExt 124 } 125 126 bundle, err := os.Create(b.opts.OutputPath) 127 if err != nil { 128 return nil, errors.Wrap(errors.WithStack(err), "failed to create fuzzing artifact archive") 129 } 130 131 return bundle, nil 132 } 133 134 func (b *Bundler) determineDockerImageForBundle() string { 135 dockerImageUsedInBundle := b.opts.DockerImage 136 if dockerImageUsedInBundle == "" { 137 switch b.opts.BuildSystem { 138 case config.BuildSystemCMake, config.BuildSystemBazel, config.BuildSystemOther: 139 // Use default Ubuntu Docker image for CMake, Bazel, and other build systems 140 dockerImageUsedInBundle = "ubuntu:rolling" 141 case config.BuildSystemMaven, config.BuildSystemGradle: 142 // Maven and Gradle should use a Docker image with Java 143 dockerImageUsedInBundle = "eclipse-temurin:20" 144 } 145 } 146 147 return dockerImageUsedInBundle 148 } 149 150 func (b *Bundler) createMetadataFileInArchive(fuzzers []*archive.Fuzzer, archiveWriter archive.ArchiveWriter, dockerImageUsedInBundle string) error { 151 // Create and add the top-level metadata file. 152 metadata := &archive.Metadata{ 153 Fuzzers: fuzzers, 154 RunEnvironment: &archive.RunEnvironment{ 155 Docker: dockerImageUsedInBundle, 156 }, 157 CodeRevision: b.getCodeRevision(), 158 } 159 160 metadataYamlContent, err := metadata.ToYaml() 161 if err != nil { 162 return err 163 } 164 metadataYamlPath := filepath.Join(b.opts.tempDir, archive.MetadataFileName) 165 err = os.WriteFile(metadataYamlPath, metadataYamlContent, 0o644) 166 if err != nil { 167 return errors.Wrapf(errors.WithStack(err), "failed to write %s", archive.MetadataFileName) 168 } 169 err = archiveWriter.WriteFile(archive.MetadataFileName, metadataYamlPath) 170 if err != nil { 171 return err 172 } 173 174 return nil 175 } 176 177 func (b *Bundler) createWorkDirInArchive(archiveWriter archive.ArchiveWriter) error { 178 // The fuzzing artifact archive spec requires this directory even if it is empty. 179 tempWorkDirPath := filepath.Join(b.opts.tempDir, archiveWorkDirPath) 180 err := os.Mkdir(tempWorkDirPath, 0o755) 181 if err != nil { 182 return errors.WithStack(err) 183 } 184 err = archiveWriter.WriteDir(archiveWorkDirPath, tempWorkDirPath) 185 if err != nil { 186 return err 187 } 188 189 return nil 190 } 191 192 func (b *Bundler) copyAdditionalFilesToArchive(archiveWriter archive.ArchiveWriter) error { 193 for _, arg := range b.opts.AdditionalFiles { 194 source, target, err := parseAdditionalFilesArgument(arg) 195 if err != nil { 196 return err 197 } 198 199 if !filepath.IsAbs(source) { 200 source = filepath.Join(b.opts.ProjectDir, source) 201 } 202 203 if fileutil.IsDir(source) { 204 err = archiveWriter.WriteDir(target, source) 205 if err != nil { 206 return err 207 } 208 } else { 209 err = archiveWriter.WriteFile(target, source) 210 if err != nil { 211 return err 212 } 213 } 214 } 215 216 return nil 217 } 218 219 // getCodeRevision returns the code revision of the project, if it can be 220 // determined. If it cannot be determined, nil is returned. 221 func (b *Bundler) getCodeRevision() *archive.CodeRevision { 222 var err error 223 var gitCommit string 224 var gitBranch string 225 226 if b.opts.Commit == "" { 227 gitCommit, err = vcs.GitCommit() 228 if err != nil { 229 // if this returns an error (e.g. if users don't have git installed), we 230 // don't want to fail the bundle creation, so we just log that we 231 // couldn't get the git commit and branch and continue without it. 232 log.Debugf("failed to get Git commit. continuing without Git commit and branch. error: %+v", 233 cmdutils.WrapSilentError(err)) 234 return nil 235 } 236 } else { 237 gitCommit = b.opts.Commit 238 } 239 240 if b.opts.Branch == "" { 241 gitBranch, err = vcs.GitBranch() 242 if err != nil { 243 log.Debugf("failed to get Git branch. continuing without Git commit and branch. error: %+v", 244 cmdutils.WrapSilentError(err)) 245 return nil 246 } 247 } else { 248 gitBranch = b.opts.Branch 249 } 250 251 if vcs.GitIsDirty() { 252 log.Warnf("The Git repository has uncommitted changes. Archive metadata may be inaccurate.") 253 } 254 255 return &archive.CodeRevision{ 256 Git: &archive.GitRevision{ 257 Commit: gitCommit, 258 Branch: gitBranch, 259 }, 260 } 261 } 262 263 func prepareSeeds(seedCorpusDirs []string, archiveSeedsDir string, archiveWriter archive.ArchiveWriter) error { 264 var targetDirs []string 265 for _, sourceDir := range seedCorpusDirs { 266 // Put the seeds into subdirectories of the "seeds" directory 267 // to avoid seeds with the same name to override each other. 268 269 // Choose a name for the target directory which wasn't used 270 // before 271 basename := filepath.Join(archiveSeedsDir, filepath.Base(sourceDir)) 272 targetDir := basename 273 i := 1 274 for sliceutil.Contains(targetDirs, targetDir) { 275 targetDir = fmt.Sprintf("%s-%d", basename, i) 276 i++ 277 } 278 targetDirs = append(targetDirs, targetDir) 279 280 // Add the seeds of the seed corpus directory to the target directory 281 err := archiveWriter.WriteDir(targetDir, sourceDir) 282 if err != nil { 283 return err 284 } 285 } 286 return nil 287 } 288 289 func parseAdditionalFilesArgument(arg string) (string, string, error) { 290 var source, target string 291 parts := strings.Split(arg, ";") 292 293 if len(parts) == 1 { 294 // if there is no ; separator just use the work_dir 295 // handles "test.txt" 296 source = parts[0] 297 target = filepath.Join(archiveWorkDirPath, filepath.Base(arg)) 298 } else { 299 // handles test.txt;test2.txt 300 source = parts[0] 301 target = parts[1] 302 } 303 304 if len(parts) > 2 || source == "" || target == "" { 305 return "", "", errors.New("could not parse '--add' argument") 306 } 307 308 if filepath.IsAbs(target) { 309 return "", "", errors.New("when using '--add source;target', target has to be a relative path") 310 } 311 312 return source, target, nil 313 }