code-intelligence.com/cifuzz@v0.40.0/internal/bundler/libfuzzer_bundler.go (about) 1 package bundler 2 3 import ( 4 "crypto/sha256" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "regexp" 10 "runtime" 11 "sort" 12 "strings" 13 14 "github.com/otiai10/copy" 15 "github.com/pkg/errors" 16 "github.com/spf13/viper" 17 "golang.org/x/exp/maps" 18 19 "code-intelligence.com/cifuzz/internal/build" 20 "code-intelligence.com/cifuzz/internal/build/bazel" 21 "code-intelligence.com/cifuzz/internal/build/cmake" 22 "code-intelligence.com/cifuzz/internal/build/other" 23 "code-intelligence.com/cifuzz/internal/bundler/archive" 24 "code-intelligence.com/cifuzz/internal/cmdutils" 25 "code-intelligence.com/cifuzz/internal/cmdutils/logging" 26 "code-intelligence.com/cifuzz/internal/config" 27 "code-intelligence.com/cifuzz/pkg/dependencies" 28 "code-intelligence.com/cifuzz/pkg/log" 29 "code-intelligence.com/cifuzz/util/envutil" 30 "code-intelligence.com/cifuzz/util/fileutil" 31 "code-intelligence.com/cifuzz/util/sliceutil" 32 ) 33 34 type configureVariant struct { 35 Sanitizers []string 36 } 37 38 // System library dependencies that are so common that we shouldn't emit a warning for them - they will be contained in 39 // any reasonable Docker image. 40 var wellKnownSystemLibraries = map[string][]*regexp.Regexp{ 41 "linux": { 42 versionedLibraryRegexp("ld-linux-x86-64.so"), 43 versionedLibraryRegexp("libc.so"), 44 versionedLibraryRegexp("libgcc_s.so"), 45 versionedLibraryRegexp("libm.so"), 46 versionedLibraryRegexp("libstdc++.so"), 47 }, 48 } 49 50 func versionedLibraryRegexp(unversionedBasename string) *regexp.Regexp { 51 return regexp.MustCompile(".*/" + regexp.QuoteMeta(unversionedBasename) + "[.0-9]*") 52 } 53 54 type libfuzzerBundler struct { 55 opts *Opts 56 archiveWriter archive.ArchiveWriter 57 } 58 59 func newLibfuzzerBundler(opts *Opts, archiveWriter archive.ArchiveWriter) *libfuzzerBundler { 60 return &libfuzzerBundler{opts, archiveWriter} 61 } 62 63 func (b *libfuzzerBundler) bundle() ([]*archive.Fuzzer, error) { 64 err := b.checkDependencies() 65 if err != nil { 66 return nil, err 67 } 68 69 buildResults, err := b.buildAllVariants() 70 if err != nil { 71 return nil, err 72 } 73 74 // Add all fuzz test artifacts to the archive. There will be one "Fuzzer" metadata object for each pair of fuzz test 75 // and Builder instance. 76 var fuzzers []*archive.Fuzzer 77 deduplicatedSystemDeps := make(map[string]struct{}) 78 for _, buildResult := range buildResults { 79 fuzzTestFuzzers, systemDeps, err := b.assembleArtifacts(buildResult) 80 if err != nil { 81 return nil, err 82 } 83 fuzzers = append(fuzzers, fuzzTestFuzzers...) 84 for _, systemDep := range systemDeps { 85 deduplicatedSystemDeps[systemDep] = struct{}{} 86 } 87 } 88 89 systemDeps := maps.Keys(deduplicatedSystemDeps) 90 sort.Strings(systemDeps) 91 if len(systemDeps) != 0 { 92 log.Warnf(`The following system libraries are not part of the artifact and have to be provided by the Docker image %q: 93 %s`, b.opts.DockerImage, strings.Join(systemDeps, "\n ")) 94 } 95 96 return fuzzers, nil 97 } 98 99 func (b *libfuzzerBundler) buildAllVariants() ([]*build.Result, error) { 100 fuzzingVariant := configureVariant{ 101 // TODO: Do not hardcode these values. 102 Sanitizers: []string{"address"}, 103 } 104 // UBSan is not supported by MSVb. 105 // TODO: Not needed anymore when sanitizers are configurable, 106 // then we do want to fail if the user explicitly asked for 107 // UBSan. 108 if runtime.GOOS != "windows" { 109 fuzzingVariant.Sanitizers = append(fuzzingVariant.Sanitizers, "undefined") 110 } 111 configureVariants := []configureVariant{fuzzingVariant} 112 113 // Coverage builds are not supported by MSVb. 114 if runtime.GOOS != "windows" { 115 coverageVariant := configureVariant{ 116 Sanitizers: []string{"coverage"}, 117 } 118 configureVariants = append(configureVariants, coverageVariant) 119 } 120 121 switch b.opts.BuildSystem { 122 case config.BuildSystemBazel: 123 return b.buildAllVariantsBazel(configureVariants) 124 case config.BuildSystemCMake: 125 return b.buildAllVariantsCMake(configureVariants) 126 case config.BuildSystemOther: 127 return b.buildAllVariantsOther(configureVariants) 128 default: 129 // We panic here instead of returning an error because it's a 130 // programming error if this function was called with an 131 // unsupported build system, that case should have been handled 132 // in the Opts.Validate function. 133 panic(fmt.Sprintf("Unsupported build system: %v", b.opts.BuildSystem)) 134 } 135 } 136 137 func (b *libfuzzerBundler) buildAllVariantsBazel(configureVariants []configureVariant) ([]*build.Result, error) { 138 var allResults []*build.Result 139 for i, variant := range configureVariants { 140 builder, err := bazel.NewBuilder(&bazel.BuilderOptions{ 141 ProjectDir: b.opts.ProjectDir, 142 Args: b.opts.BuildSystemArgs, 143 NumJobs: b.opts.NumBuildJobs, 144 Stdout: b.opts.BuildStdout, 145 Stderr: b.opts.BuildStderr, 146 TempDir: b.opts.tempDir, 147 Verbose: viper.GetBool("verbose"), 148 }) 149 if err != nil { 150 return nil, err 151 } 152 153 b.printBuildingMsg(variant, i) 154 155 if len(b.opts.FuzzTests) == 0 { 156 // We panic here instead of returning an error because it's a 157 // programming error if this function was called without any 158 // fuzz tests, that case should have been handled in the 159 // Opts.Validate function. 160 panic("No fuzz tests specified") 161 } 162 163 results, err := builder.BuildForBundle(variant.Sanitizers, b.opts.FuzzTests) 164 if err != nil { 165 return nil, err 166 } 167 allResults = append(allResults, results...) 168 } 169 170 return allResults, nil 171 } 172 173 func (b *libfuzzerBundler) buildAllVariantsCMake(configureVariants []configureVariant) ([]*build.Result, error) { 174 var allResults []*build.Result 175 for i, variant := range configureVariants { 176 builder, err := cmake.NewBuilder(&cmake.BuilderOptions{ 177 ProjectDir: b.opts.ProjectDir, 178 Args: b.opts.BuildSystemArgs, 179 Sanitizers: variant.Sanitizers, 180 Parallel: cmake.ParallelOptions{ 181 Enabled: viper.IsSet("build-jobs"), 182 NumJobs: b.opts.NumBuildJobs, 183 }, 184 Stdout: b.opts.BuildStdout, 185 Stderr: b.opts.BuildStderr, 186 FindRuntimeDeps: true, 187 }) 188 if err != nil { 189 return nil, err 190 } 191 192 b.printBuildingMsg(variant, i) 193 194 err = builder.Configure() 195 if err != nil { 196 return nil, err 197 } 198 199 var fuzzTests []string 200 if len(b.opts.FuzzTests) == 0 { 201 fuzzTests, err = builder.ListFuzzTests() 202 if err != nil { 203 return nil, err 204 } 205 } else { 206 fuzzTests = b.opts.FuzzTests 207 } 208 209 // The fuzz tests passed to builder.Build must not contain 210 // duplicates, which is ensured by builder.ListFuzzTests() 211 // and the Opts.Validate() function. 212 results, err := builder.Build(fuzzTests) 213 if err != nil { 214 return nil, err 215 } 216 allResults = append(allResults, results...) 217 } 218 219 return allResults, nil 220 } 221 222 func (b *libfuzzerBundler) printBuildingMsg(variant configureVariant, i int) { 223 var typeDisplayString string 224 if isCoverageBuild(variant.Sanitizers) { 225 typeDisplayString = "coverage" 226 } else { 227 typeDisplayString = "fuzzing" 228 } 229 230 // Print a newline to separate the build logs unless this is the 231 232 // first variant build 233 if i > 0 && !logging.ShouldLogBuildToFile() { 234 log.Print() 235 } 236 237 log.Infof("Building for %s...", typeDisplayString) 238 } 239 240 func (b *libfuzzerBundler) buildAllVariantsOther(configureVariants []configureVariant) ([]*build.Result, error) { 241 if len(b.opts.BuildSystemArgs) > 0 { 242 log.Warnf("Passing additional arguments is not supported for build system type \"other\".\n"+ 243 "These arguments are ignored: %s", strings.Join(b.opts.BuildSystemArgs, " ")) 244 } 245 246 var results []*build.Result 247 for i, variant := range configureVariants { 248 builder, err := other.NewBuilder(&other.BuilderOptions{ 249 ProjectDir: b.opts.ProjectDir, 250 BuildCommand: b.opts.BuildCommand, 251 CleanCommand: b.opts.CleanCommand, 252 Sanitizers: variant.Sanitizers, 253 Stdout: b.opts.BuildStdout, 254 Stderr: b.opts.BuildStderr, 255 }) 256 if err != nil { 257 return nil, err 258 } 259 260 b.printBuildingMsg(variant, i) 261 262 if len(b.opts.FuzzTests) == 0 { 263 // We panic here instead of returning an error because it's a 264 // programming error if this function was called without any 265 // fuzz tests, that case should have been handled in the 266 // Opts.Validate function. 267 panic("No fuzz tests specified") 268 } 269 270 if err := builder.Clean(); err != nil { 271 return nil, err 272 } 273 274 for _, fuzzTest := range b.opts.FuzzTests { 275 result, err := builder.Build(fuzzTest) 276 if err != nil { 277 return nil, err 278 } 279 280 // To avoid that subsequent builds overwrite the artifacts 281 // from this build, we copy them to a temporary directory 282 // and adjust the paths in the build.Result struct 283 tempDir := filepath.Join(b.opts.tempDir, fuzzTestPrefix(result)) 284 err = b.copyArtifactsToTempdir(result, tempDir) 285 if err != nil { 286 return nil, err 287 } 288 289 results = append(results, result) 290 } 291 } 292 293 return results, nil 294 } 295 296 func (b *libfuzzerBundler) copyArtifactsToTempdir(buildResult *build.Result, tempDir string) error { 297 fuzzTestExecutableAbsPath := buildResult.Executable 298 isBelow, err := fileutil.IsBelow(fuzzTestExecutableAbsPath, buildResult.BuildDir) 299 if err != nil { 300 return err 301 } 302 if isBelow { 303 relPath, err := filepath.Rel(buildResult.BuildDir, fuzzTestExecutableAbsPath) 304 if err != nil { 305 return errors.WithStack(err) 306 } 307 newExecutablePath := filepath.Join(tempDir, relPath) 308 err = copy.Copy(buildResult.Executable, newExecutablePath) 309 if err != nil { 310 return errors.WithStack(err) 311 } 312 buildResult.Executable = newExecutablePath 313 } 314 315 // Try to copy the regular files first before copying the corresponding symlinks. 316 // Failing to do so results in errors that target of the symlink does not exist 317 // in the temp directory. 318 sort.Slice(buildResult.RuntimeDeps, func(i, j int) bool { 319 return !fileutil.IsSymlink(buildResult.RuntimeDeps[i]) 320 }) 321 322 for i, dep := range buildResult.RuntimeDeps { 323 isBelow, err = fileutil.IsBelow(dep, buildResult.BuildDir) 324 if err != nil { 325 return err 326 } 327 var topDir string 328 if isBelow { 329 topDir = buildResult.BuildDir 330 } else { 331 topDir = "/" 332 } 333 334 relPath, err := filepath.Rel(topDir, dep) 335 if err != nil { 336 return errors.WithStack(err) 337 } 338 newDepPath := filepath.Join(tempDir, relPath) 339 340 // When dealing with symlinks, resolve the path so that we copy the actual file 341 // to the temporary directory. This ensures that the dynamic dependencies resolved 342 // by ldd and added into the bundle are valid files. 343 resolvedPath, err := filepath.EvalSymlinks(dep) 344 if err != nil { 345 return errors.WithStack(err) 346 } 347 err = copy.Copy(resolvedPath, newDepPath) 348 if err != nil { 349 return errors.WithStack(err) 350 } 351 352 buildResult.RuntimeDeps[i] = newDepPath 353 } 354 buildResult.BuildDir = tempDir 355 356 return nil 357 } 358 359 func (b *libfuzzerBundler) checkDependencies() error { 360 var deps []dependencies.Key 361 switch b.opts.BuildSystem { 362 case config.BuildSystemCMake: 363 deps = []dependencies.Key{dependencies.Clang, dependencies.CMake} 364 case config.BuildSystemOther: 365 deps = []dependencies.Key{dependencies.Clang} 366 } 367 err := dependencies.Check(deps, b.opts.ProjectDir) 368 if err != nil { 369 log.Error(err) 370 return cmdutils.WrapSilentError(err) 371 } 372 return nil 373 } 374 375 //nolint:nonamedreturns 376 func (b *libfuzzerBundler) assembleArtifacts(buildResult *build.Result) ( 377 fuzzers []*archive.Fuzzer, 378 systemDeps []string, 379 err error, 380 ) { 381 fuzzTestExecutableAbsPath := buildResult.Executable 382 383 // Add all build artifacts under a subdirectory of the fuzz test base path so that these files don't clash with 384 // seeds and dictionaries. 385 buildArtifactsPrefix := filepath.Join(fuzzTestPrefix(buildResult), "bin") 386 387 // Add the fuzz test executable. 388 ok, err := fileutil.IsBelow(fuzzTestExecutableAbsPath, buildResult.BuildDir) 389 if err != nil { 390 return 391 } 392 if !ok { 393 err = errors.Errorf("fuzz test executable (%s) is not below build directory (%s)", fuzzTestExecutableAbsPath, buildResult.BuildDir) 394 return 395 } 396 fuzzTestExecutableRelPath, err := filepath.Rel(buildResult.BuildDir, fuzzTestExecutableAbsPath) 397 if err != nil { 398 err = errors.WithStack(err) 399 return 400 } 401 fuzzTestArchivePath := filepath.Join(buildArtifactsPrefix, fuzzTestExecutableRelPath) 402 err = b.archiveWriter.WriteFile(fuzzTestArchivePath, fuzzTestExecutableAbsPath) 403 if err != nil { 404 return 405 } 406 407 // On macOS, debug information is collected in a separate .dSYM file. We bundle it in to get source locations 408 // resolved in stack traces. 409 fuzzTestDsymAbsPath := fuzzTestExecutableAbsPath + ".dSYM" 410 dsymExists, err := fileutil.Exists(fuzzTestDsymAbsPath) 411 if err != nil { 412 err = errors.WithStack(err) 413 return 414 } 415 if dsymExists { 416 fuzzTestDsymArchivePath := fuzzTestArchivePath + ".dSYM" 417 err = b.archiveWriter.WriteDir(fuzzTestDsymArchivePath, fuzzTestDsymAbsPath) 418 if err != nil { 419 return 420 } 421 } 422 423 var libraryPaths []string 424 // Add the runtime dependencies of the fuzz test executable. 425 externalLibrariesPrefix := "" 426 depsLoop: 427 for _, dep := range buildResult.RuntimeDeps { 428 var isBelowBuildDir bool 429 isBelowBuildDir, err = fileutil.IsBelow(dep, buildResult.BuildDir) 430 if err != nil { 431 return 432 } 433 if isBelowBuildDir { 434 var buildDirRelPath string 435 buildDirRelPath, err = filepath.Rel(buildResult.BuildDir, dep) 436 if err != nil { 437 err = errors.WithStack(err) 438 return 439 } 440 441 if b.opts.BuildSystem == config.BuildSystemOther { 442 libraryPath := filepath.Join(buildArtifactsPrefix, filepath.Dir(buildDirRelPath)) 443 if !sliceutil.Contains(libraryPaths, libraryPath) { 444 libraryPaths = append(libraryPaths, libraryPath) 445 } 446 } 447 448 var hash string 449 hash, err = sha256sum(dep) 450 if err != nil { 451 return 452 } 453 casPath := filepath.Join("cas", hash[:2], hash[2:], filepath.Base(dep)) 454 if !b.archiveWriter.HasFileEntry(casPath) { 455 err = b.archiveWriter.WriteFile(casPath, dep) 456 if err != nil { 457 return 458 } 459 } 460 err = b.archiveWriter.WriteHardLink(casPath, filepath.Join(buildArtifactsPrefix, buildDirRelPath)) 461 if err != nil { 462 return 463 } 464 continue 465 } 466 467 // The runtime dependency is not built as part of the current project. It will be of one of the following types: 468 // 1. A standard system library that is available in all reasonable Docker images. 469 // 2. A more uncommon system library that may require additional packages to be installed (e.g. X11), but still 470 // lives in a standard system library directory (e.g. /usr/lib). Such dependencies are expected to be 471 // provided by the Docker image used as the run environment. 472 // 3. Any other external dependency (e.g. a CMake target imported from another CMake project with a separate 473 // build directory). These are not expected to be part of the Docker image and thus added to the archive 474 // in a special directory that is added to the library search path at runtime. 475 476 // 1. is handled by ignoring these runtime dependencies. 477 for _, wellKnownSystemLibrary := range wellKnownSystemLibraries[runtime.GOOS] { 478 if wellKnownSystemLibrary.MatchString(dep) { 479 continue depsLoop 480 } 481 } 482 483 // 2. is handled by returning a list of these libraries that is shown to the user as a warning about the 484 // required contents of the Docker image specified as the run environment. 485 if fileutil.IsSystemLibrary(dep) { 486 systemDeps = append(systemDeps, dep) 487 continue depsLoop 488 } 489 490 // 3. is handled by staging the dependency in a special external library directory in the archive that is added 491 // to the library search path in the run environment. 492 // Note: Since all libraries are placed in a single directory, we have to ensure that basenames of external 493 // libraries are unique. If they aren't, we report a conflict. 494 externalLibrariesPrefix = filepath.Join(fuzzTestPrefix(buildResult), "external_libs") 495 archivePath := filepath.Join(externalLibrariesPrefix, filepath.Base(dep)) 496 if b.archiveWriter.HasFileEntry(archivePath) { 497 err = errors.Errorf( 498 "fuzz test %q has conflicting runtime dependencies: %s and %s", 499 buildResult.Name, 500 dep, 501 b.archiveWriter.GetSourcePath(archivePath), 502 ) 503 return 504 } 505 err = b.archiveWriter.WriteFile(archivePath, dep) 506 if err != nil { 507 return 508 } 509 } 510 511 // Add dictionary to archive 512 var archiveDict string 513 if b.opts.Dictionary != "" { 514 archiveDict = filepath.Join(fuzzTestPrefix(buildResult), "dict") 515 err = b.archiveWriter.WriteFile(archiveDict, b.opts.Dictionary) 516 if err != nil { 517 return 518 } 519 } 520 521 // Add seeds from user-specified seed corpus dirs (if any) and the 522 // default seed corpus (if it exists) to the seeds directory in the 523 // archive 524 seedCorpusDirs := b.opts.SeedCorpusDirs 525 exists, err := fileutil.Exists(buildResult.SeedCorpus) 526 if err != nil { 527 return 528 } 529 if exists { 530 seedCorpusDirs = append([]string{buildResult.SeedCorpus}, seedCorpusDirs...) 531 } 532 var archiveSeedsDir string 533 if len(seedCorpusDirs) > 0 { 534 archiveSeedsDir = filepath.Join(fuzzTestPrefix(buildResult), "seeds") 535 536 err = prepareSeeds(seedCorpusDirs, archiveSeedsDir, b.archiveWriter) 537 if err != nil { 538 return 539 } 540 } 541 542 // Set NO_CIFUZZ=1 to avoid that remotely executed fuzz tests try 543 // to start cifuzz 544 env, err := envutil.Setenv(b.opts.Env, "NO_CIFUZZ", "1") 545 if err != nil { 546 return 547 } 548 549 baseFuzzerInfo := archive.Fuzzer{ 550 Target: buildResult.Name, 551 Path: fuzzTestArchivePath, 552 ProjectDir: buildResult.ProjectDir, 553 Dictionary: archiveDict, 554 Seeds: archiveSeedsDir, 555 EngineOptions: archive.EngineOptions{ 556 Env: env, 557 Flags: b.opts.EngineArgs, 558 }, 559 MaxRunTime: uint(b.opts.Timeout.Seconds()), 560 } 561 562 if externalLibrariesPrefix != "" { 563 libraryPaths = append(libraryPaths, externalLibrariesPrefix) 564 } 565 baseFuzzerInfo.LibraryPaths = libraryPaths 566 567 if isCoverageBuild(buildResult.Sanitizers) { 568 fuzzer := baseFuzzerInfo 569 fuzzer.Engine = "LLVM_COV" 570 // We use libFuzzer's crash-resistant merge mode. The first positional argument has to be an empty directory, 571 // for which we use the working directory (empty at the beginning of a job as we include an empty work_dir in 572 // the bundle). The second positional argument is the corpus directory passed in by the backend. 573 // Since most libFuzzer options are not useful or potentially disruptive for coverage runs, we do not include 574 // flags passed in via `--engine_args`. 575 fuzzer.EngineOptions.Flags = []string{"-merge=1", "."} 576 fuzzers = []*archive.Fuzzer{&fuzzer} 577 // Coverage builds are separate from sanitizer builds, so we don't have any other fuzzers to add. 578 return 579 } 580 581 for _, sanitizer := range buildResult.Sanitizers { 582 if sanitizer == "undefined" { 583 // The artifact archive spec does not support UBSan as a standalone sanitizer. 584 continue 585 } 586 fuzzer := baseFuzzerInfo 587 fuzzer.Engine = "LIBFUZZER" 588 fuzzer.Sanitizer = strings.ToUpper(sanitizer) 589 fuzzers = append(fuzzers, &fuzzer) 590 } 591 592 return 593 } 594 595 // fuzzTestPrefix returns the path in the resulting artifact archive under which fuzz test specific files should be 596 // added. 597 func fuzzTestPrefix(buildResult *build.Result) string { 598 sanitizerSegment := strings.Join(buildResult.Sanitizers, "+") 599 if sanitizerSegment == "" { 600 sanitizerSegment = "none" 601 } 602 engine := "libfuzzer" 603 if isCoverageBuild(buildResult.Sanitizers) { 604 // The backend currently only passes the corpus directory (rather than the files contained in it) as 605 // an argument to the coverage binary if it finds the substring "replayer/coverage" in the fuzz test archive 606 // path. 607 // FIXME: Remove this workaround as soon as the artifact spec provides a way to specify compatibility with 608 // directory arguments. 609 engine = "replayer" 610 } 611 return filepath.Join(engine, sanitizerSegment, buildResult.Name) 612 } 613 614 func isCoverageBuild(sanitizers []string) bool { 615 return len(sanitizers) == 1 && sanitizers[0] == "coverage" 616 } 617 618 func sha256sum(filename string) (string, error) { 619 f, err := os.Open(filename) 620 if err != nil { 621 return "", errors.WithStack(err) 622 } 623 defer f.Close() 624 625 h := sha256.New() 626 _, err = io.Copy(h, f) 627 if err != nil { 628 return "", errors.WithStack(err) 629 } 630 631 return fmt.Sprintf("%x", h.Sum(nil)), nil 632 }