code-intelligence.com/cifuzz@v0.40.0/internal/build/bazel/build.go (about) 1 package bazel 2 3 import ( 4 "fmt" 5 "io" 6 "io/fs" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "github.com/pkg/errors" 14 15 "code-intelligence.com/cifuzz/internal/build" 16 "code-intelligence.com/cifuzz/internal/cmdutils" 17 "code-intelligence.com/cifuzz/pkg/dependencies" 18 "code-intelligence.com/cifuzz/pkg/log" 19 "code-intelligence.com/cifuzz/pkg/runfiles" 20 "code-intelligence.com/cifuzz/util/archiveutil" 21 "code-intelligence.com/cifuzz/util/envutil" 22 "code-intelligence.com/cifuzz/util/fileutil" 23 ) 24 25 type BuilderOptions struct { 26 ProjectDir string 27 Args []string 28 NumJobs uint 29 Stdout io.Writer 30 Stderr io.Writer 31 TempDir string 32 Verbose bool 33 } 34 35 func (opts *BuilderOptions) Validate() error { 36 // Check that the project dir is set 37 if opts.ProjectDir == "" { 38 return errors.New("ProjectDir is not set") 39 } 40 41 // Check that the project dir exists and can be accessed 42 _, err := os.Stat(opts.ProjectDir) 43 if err != nil { 44 return errors.WithStack(err) 45 } 46 47 // Check that the TempDir is set. This is not set by the user, so 48 // we panic if it's not set 49 if opts.TempDir == "" { 50 panic("TempDir is not set") 51 } 52 53 return nil 54 } 55 56 type Builder struct { 57 *BuilderOptions 58 } 59 60 func NewBuilder(opts *BuilderOptions) (*Builder, error) { 61 err := opts.Validate() 62 if err != nil { 63 return nil, err 64 } 65 66 err = checkCIFuzzBazelRepoCommit() 67 if err != nil { 68 return nil, err 69 } 70 71 err = checkRulesFuzzingVersion() 72 if err != nil { 73 return nil, err 74 } 75 76 b := &Builder{BuilderOptions: opts} 77 return b, nil 78 } 79 80 // BuildForRun builds the specified fuzz tests with bazel. It expects 81 // labels of targets of the cc_fuzz_test rule provided by rules_fuzzing: 82 // https://github.com/bazelbuild/rules_fuzzing/blob/master/docs/cc-fuzzing-rules.md#cc_fuzz_test 83 // 84 // TODO: Unfortunately, the cc_fuzz_test rule currently doesn't 85 // support combining sanitizers, so we can't build with both ASan 86 // and UBSan. Therefore, we only build with ASan and plan to upstream 87 // support for combining sanitizers. 88 func (b *Builder) BuildForRun(fuzzTests []string) ([]*build.Result, error) { 89 var err error 90 91 var binLabels []string 92 for i := range fuzzTests { 93 // The cc_fuzz_test rule defines multiple bazel targets: If the 94 // name is "foo", it defines the targets "foo", "foo_bin", and 95 // others. We need to run the "foo_bin" target but want to 96 // allow users to specify either "foo" or "foo_bin", so we check 97 // if the fuzz test name appended with "_bin" is a valid target 98 // and use that in that case 99 cmd := exec.Command("bazel", "query", fuzzTests[i]+"_bin") 100 err := cmd.Run() 101 if err == nil { 102 binLabels = append(binLabels, fuzzTests[i]+"_bin") 103 } else { 104 fuzzTests[i] = strings.TrimSuffix(fuzzTests[i], "_bin") 105 binLabels = append(binLabels, fuzzTests[i]+"_bin") 106 } 107 } 108 109 // The BuildDir field of the build results is expected to be a 110 // parent directory of all the artifacts, so that a single minijail 111 // binding allows access to all artifacts in the sandbox. 112 // When building via bazel, the "output_base" directory contains 113 // all artifacts, so we use that as the BuildDir. 114 cmd := exec.Command("bazel", "info", "output_base") 115 out, err := cmd.Output() 116 if err != nil { 117 return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd) 118 } 119 buildDir := strings.TrimSpace(string(out)) 120 fuzzScript := filepath.Join(b.TempDir, "fuzz.sh") 121 122 // To avoid part of the loading and/or analysis phase to rerun, we 123 // use the same flags for all bazel commands (except for those which 124 // are not supported by all bazel commands we use). 125 buildEnv, err := build.CommonBuildEnv() 126 if err != nil { 127 return nil, err 128 } 129 commonFlags := []string{ 130 "--repo_env=CC=" + envutil.Getenv(buildEnv, "CC"), 131 "--repo_env=CXX=" + envutil.Getenv(buildEnv, "CXX"), 132 // Don't use the LLVM from Xcode 133 "--repo_env=BAZEL_USE_CPP_ONLY_TOOLCHAIN=1", 134 } 135 if b.NumJobs != 0 { 136 commonFlags = append(commonFlags, "--jobs", fmt.Sprint(b.NumJobs)) 137 } 138 139 // Flags which should only be used for bazel run because they are 140 // not supported by the other bazel commands we use 141 runFlags := []string{ 142 // Build with debug symbols 143 "--copt", "-g", 144 // Tell bazel to do an optimized build, which includes debug 145 // symbols (in contrast to the default "fastbuild" compilation 146 // mode which strips debug symbols). 147 "--compilation_mode=opt", 148 // Do optimizations which don't harm debugging 149 "--copt", "-Og", 150 // Enable asserts (disabled by --compilation_mode=opt). 151 "--copt", "-UNDEBUG", 152 // Disable source fortification, which is currently not supported 153 // in combination with ASan, see https://github.com/google/sanitizers/issues/247 154 "--copt", "-U_FORTIFY_SOURCE", 155 // Build with libFuzzer 156 "--@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer", 157 "--@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer", 158 // Build with ASan instrumentation 159 "--@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan", 160 // Build with UBSan instrumentation 161 "--@rules_fuzzing//fuzzing:cc_engine_sanitizer=ubsan", 162 // Link in our additional libFuzzer logic that dumps inputs for non-fatal crashes. 163 "--@cifuzz//:__internal_has_libfuzzer", 164 "--verbose_failures", 165 "--script_path=" + fuzzScript, 166 } 167 168 if os.Getenv("BAZEL_SUBCOMMANDS") != "" { 169 runFlags = append(runFlags, "--subcommands") 170 } 171 172 args := []string{"run"} 173 args = append(args, commonFlags...) 174 args = append(args, runFlags...) 175 args = append(args, b.Args...) 176 args = append(args, binLabels...) 177 178 cmd = exec.Command("bazel", args...) 179 cmd.Stdout = b.Stdout 180 cmd.Stderr = b.Stderr 181 if err != nil { 182 return nil, err 183 } 184 log.Debugf("Command: %s", cmd.String()) 185 err = cmd.Run() 186 if err != nil { 187 return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd) 188 } 189 190 // Assemble the build results 191 var results []*build.Result 192 193 for _, fuzzTest := range fuzzTests { 194 // Turn the fuzz test label into a valid path 195 path, err := PathFromLabel(fuzzTest, commonFlags) 196 if err != nil { 197 return nil, err 198 } 199 seedCorpus := filepath.Join(b.ProjectDir, path+"_inputs") 200 generatedCorpusBasename := "." + filepath.Base(path) + "_cifuzz_corpus" 201 generatedCorpus := filepath.Join(b.ProjectDir, filepath.Dir(path), generatedCorpusBasename) 202 203 result := &build.Result{ 204 Name: path, 205 Executable: fuzzScript, 206 GeneratedCorpus: generatedCorpus, 207 SeedCorpus: seedCorpus, 208 BuildDir: buildDir, 209 Sanitizers: []string{"address"}, 210 } 211 results = append(results, result) 212 } 213 214 return results, nil 215 } 216 217 func (b *Builder) BuildForBundle(sanitizers []string, fuzzTests []string) ([]*build.Result, error) { 218 var err error 219 220 env, err := build.CommonBuildEnv() 221 if err != nil { 222 return nil, err 223 } 224 225 env, err = b.setLibFuzzerEnv(env) 226 if err != nil { 227 return nil, err 228 } 229 230 // To avoid part of the loading and/or analysis phase to rerun, we 231 // use the same flags for all bazel commands (except for those which 232 // are not supported by all bazel commands we use). 233 commonFlags := []string{ 234 "--repo_env=CC=" + envutil.Getenv(env, "CC"), 235 "--repo_env=CXX=" + envutil.Getenv(env, "CXX"), 236 "--repo_env=FUZZING_CFLAGS=" + envutil.Getenv(env, "FUZZING_CFLAGS"), 237 "--repo_env=FUZZING_CXXFLAGS=" + envutil.Getenv(env, "FUZZING_CXXFLAGS"), 238 "--repo_env=LIB_FUZZING_ENGINE=" + envutil.Getenv(env, "LIB_FUZZING_ENGINE"), 239 // Don't use the LLVM from Xcode 240 "--repo_env=BAZEL_USE_CPP_ONLY_TOOLCHAIN=1", 241 // rules_fuzzing only links in the UBSan C++ runtime when the 242 // sanitizer is set to "undefined" 243 "--repo_env=SANITIZER=undefined", 244 } 245 if b.NumJobs != 0 { 246 commonFlags = append(commonFlags, "--jobs", fmt.Sprint(b.NumJobs)) 247 } 248 249 // Flags which should only be used for bazel build 250 buildAndCQueryFlags := []string{ 251 // Tell bazel to do an optimized build, which includes debug 252 // symbols (in contrast to the default "fastbuild" compilation 253 // mode which strips debug symbols). 254 "--compilation_mode=opt", 255 "--@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing_oss_fuzz//:oss_fuzz_engine", 256 "--verbose_failures", 257 } 258 259 if os.Getenv("BAZEL_SUBCOMMANDS") != "" { 260 buildAndCQueryFlags = append(buildAndCQueryFlags, "--subcommands") 261 } 262 263 // Add sanitizer-specific flags 264 if len(sanitizers) == 1 && sanitizers[0] == "coverage" { 265 llvmCov, err := runfiles.Finder.LLVMCovPath() 266 if err != nil { 267 return nil, err 268 } 269 llvmProfData, err := runfiles.Finder.LLVMProfDataPath() 270 if err != nil { 271 return nil, err 272 } 273 commonFlags = append(commonFlags, 274 "--repo_env=BAZEL_USE_LLVM_NATIVE_COVERAGE=1", 275 "--repo_env=GCOV="+llvmProfData, 276 "--repo_env=BAZEL_LLVM_COV="+llvmCov, 277 ) 278 buildAndCQueryFlags = append(buildAndCQueryFlags, 279 "--instrument_test_targets", 280 "--experimental_use_llvm_covmap", 281 "--experimental_generate_llvm_lcov", 282 "--collect_code_coverage", 283 ) 284 } else { 285 buildAndCQueryFlags = append(buildAndCQueryFlags, 286 "--@rules_fuzzing//fuzzing:cc_engine_instrumentation=oss-fuzz") 287 for _, sanitizer := range sanitizers { 288 switch sanitizer { 289 case "address", "undefined": 290 // ASan and UBSan are already enabled above by the call 291 // to b.setLibFuzzerEnv, which sets the respective flags 292 // via the FUZZING_CFLAGS environment variable. These 293 // variables are then picked up by the OSS-Fuzz engine 294 // instrumentation. 295 default: 296 panic(fmt.Sprintf("Invalid sanitizer: %q", sanitizer)) 297 } 298 } 299 } 300 301 args := []string{"build"} 302 args = append(args, commonFlags...) 303 args = append(args, b.Args...) 304 args = append(args, buildAndCQueryFlags...) 305 306 // We have to build the "*_oss_fuzz" target defined by the 307 // cc_fuzz_test rule 308 var labels []string 309 for _, fuzzTestLabel := range fuzzTests { 310 labels = append(labels, fuzzTestLabel+"_oss_fuzz") 311 } 312 args = append(args, labels...) 313 314 cmd := exec.Command("bazel", args...) 315 cmd.Stdout = b.Stdout 316 cmd.Stderr = b.Stderr 317 log.Debugf("Command: %s", cmd.String()) 318 err = cmd.Run() 319 if err != nil { 320 return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd) 321 } 322 323 // Assemble the build results 324 var results []*build.Result 325 326 for _, fuzzTest := range fuzzTests { 327 // Get the path to the archive created by the build 328 args := []string{"cquery", "--output=starlark", "--starlark:expr=target.files.to_list()[0].path"} 329 args = append(args, commonFlags...) 330 args = append(args, buildAndCQueryFlags...) 331 args = append(args, fuzzTest+"_oss_fuzz") 332 cmd = exec.Command("bazel", args...) 333 out, err := cmd.Output() 334 if err != nil { 335 return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd) 336 } 337 ossFuzzArchive := strings.TrimSpace(string(out)) 338 339 // Extract the archive 340 extractedDir, err := os.MkdirTemp(b.TempDir, "extracted-") 341 if err != nil { 342 return nil, errors.WithStack(err) 343 } 344 err = archiveutil.UntarFile(ossFuzzArchive, extractedDir) 345 if err != nil { 346 return nil, err 347 } 348 349 path, err := PathFromLabel(fuzzTest, commonFlags) 350 if err != nil { 351 return nil, err 352 } 353 executable := filepath.Join(extractedDir, filepath.Base(path)) 354 355 // Extract the seed corpus 356 ossFuzzSeedCorpus := executable + "_seed_corpus.zip" 357 extractedCorpus := executable + "_seed_corpus" 358 err = archiveutil.Unzip(ossFuzzSeedCorpus, extractedCorpus) 359 if err != nil { 360 return nil, err 361 } 362 363 // Find the runtime dependencies. The bundler will include them 364 // in the bundle because below we set the BuildDir field of the 365 // build.Result to extractedCorpus, which contains all the 366 // runtime dependencies, causing the bundler to treat them all 367 // as created by the build and therefore including them in the 368 // bundle. 369 var runtimeDeps []string 370 runfilesDir := executable + ".runfiles" 371 exists, err := fileutil.Exists(runfilesDir) 372 if err != nil { 373 return nil, err 374 } 375 if exists { 376 err = filepath.WalkDir(runfilesDir, func(path string, d fs.DirEntry, err error) error { 377 if err != nil { 378 return errors.WithStack(err) 379 } 380 if d.IsDir() { 381 return nil 382 } 383 runtimeDeps = append(runtimeDeps, path) 384 return nil 385 }) 386 if err != nil { 387 return nil, err 388 } 389 } 390 391 result := &build.Result{ 392 Name: path, 393 Executable: executable, 394 SeedCorpus: extractedCorpus, 395 BuildDir: extractedDir, 396 // Bazel builds files with PWD=/proc/self/cwd 397 ProjectDir: "/proc/self/cwd", 398 Sanitizers: sanitizers, 399 RuntimeDeps: runtimeDeps, 400 } 401 results = append(results, result) 402 } 403 404 return results, nil 405 } 406 407 func (b *Builder) setLibFuzzerEnv(env []string) ([]string, error) { 408 var err error 409 410 // Set FUZZING_CFLAGS and FUZZING_CXXFLAGS. 411 cflags := build.LibFuzzerCFlags() 412 env, err = envutil.Setenv(env, "FUZZING_CFLAGS", strings.Join(cflags, " ")) 413 if err != nil { 414 return nil, err 415 } 416 env, err = envutil.Setenv(env, "FUZZING_CXXFLAGS", strings.Join(cflags, " ")) 417 if err != nil { 418 return nil, err 419 } 420 421 // Set LIB_FUZZING_ENGINE, which is added as a linkopt to the fuzz 422 // test itself. 423 env, err = envutil.Setenv(env, "LIB_FUZZING_ENGINE", "-fsanitize=fuzzer") 424 if err != nil { 425 return nil, err 426 } 427 428 return env, nil 429 } 430 431 // PathFromLabel turns a bazel label into a valid path, which can for 432 // example be used to create the fuzz test's corpus directory. 433 // Flags which should be passed to the `bazel query` command can be 434 // passed via the flags argument (to avoid bazel discarding the analysis 435 // cache). 436 func PathFromLabel(label string, flags []string) (string, error) { 437 // Get a canonical form of label via `bazel query` 438 args := append([]string{"query"}, flags...) 439 args = append(args, label) 440 cmd := exec.Command("bazel", args...) 441 log.Debugf("Command: %s", cmd.String()) 442 out, err := cmd.Output() 443 if err != nil { 444 return "", cmdutils.WrapExecError(errors.WithStack(err), cmd) 445 } 446 canonicalLabel := strings.TrimSpace(string(out)) 447 448 // Transform the label into a valid path below the directory which 449 // contains the BUILD file, which: 450 // * Doesn't contain any leading '//' 451 // * Has any ':' and '/' replaced with the path separator (':' is 452 // not allowed in filenames on Windows) 453 res := strings.TrimPrefix(canonicalLabel, "//") 454 res = strings.ReplaceAll(res, ":", "/") 455 res = strings.ReplaceAll(res, "/", string(filepath.Separator)) 456 457 return res, nil 458 } 459 460 // Parses formatted bazel query --output=build output such as: 461 // 462 // git_repository( 463 // name = "cifuzz", 464 // remote = "https://github.com/CodeIntelligenceTesting/cifuzz-bazel", 465 // commit = "ccb0bb7f27864626f668cca6d6e87776e6f87bd", 466 // ) 467 // 468 // For backwards compatibility, this regex also matches a branch that 469 // was used in cifuzz v0.9.0 and earlier. The branch will never be equal 470 // to a commit hash. 471 var cifuzzCommitRegex = regexp.MustCompile(`(?m)^\s*(?:commit|branch)\s*=\s*"([^"]*)"`) 472 473 var rulesFuzzingSHA256Regex = regexp.MustCompile(`(?m)^\s*sha256\s*=\s*"([^"]*)"`) 474 475 func checkCIFuzzBazelRepoCommit() error { 476 cmd := exec.Command("bazel", "query", "--output=build", "//external:cifuzz") 477 out, err := cmd.Output() 478 if err != nil { 479 // If the reason for the error is that the cifuzz repository is 480 // missing, produce a more helpful error message. 481 if strings.Contains(err.Error(), "target 'cifuzz' not declared in package") { 482 return cmdutils.WrapExecError(errors.Errorf(`The "cifuzz" repository is not defined in the WORKSPACE file, 483 run 'cifuzz init' to see setup instructions.`), cmd) 484 } 485 return cmdutils.WrapExecError(errors.WithStack(err), cmd) 486 } 487 matches := cifuzzCommitRegex.FindSubmatch(out) 488 if matches == nil { 489 return cmdutils.WrapExecError(errors.Errorf( 490 `Failed to parse the definition of the "cifuzz" repository in the WORKSPACE file, 491 run 'cifuzz init' to see setup instructions. 492 bazel query output: 493 %s`, string(out)), cmd) 494 } 495 496 cifuzzRepoCommit := string(matches[1]) 497 if cifuzzRepoCommit != dependencies.CIFuzzBazelCommit { 498 return cmdutils.WrapExecError(errors.Errorf( 499 `Please update the commit specified for the "cifuzz" repository in the WORKSPACE file. 500 Required: %[1]s 501 Current : %[2]s`, 502 fmt.Sprintf(`commit = %q`, dependencies.CIFuzzBazelCommit), 503 strings.TrimSpace(string(matches[0]))), cmd) 504 } 505 506 return nil 507 } 508 509 func checkRulesFuzzingVersion() error { 510 cmd := exec.Command("bazel", "query", "--output=build", "//external:rules_fuzzing") 511 out, err := cmd.Output() 512 if err != nil { 513 // If the reason for the error is that the cifuzz repository is 514 // missing, produce a more helpful error message. 515 if strings.Contains(err.Error(), "target 'cifuzz' not declared in package") { 516 return cmdutils.WrapExecError(errors.Errorf( 517 `The "rules_fuzzing" repository is not defined in the WORKSPACE file, 518 run 'cifuzz init' to see setup instructions.`), 519 cmd) 520 } 521 return cmdutils.WrapExecError(errors.WithStack(err), cmd) 522 } 523 524 matches := rulesFuzzingSHA256Regex.FindSubmatch(out) 525 if len(matches) == 0 || string(matches[1]) != dependencies.RulesFuzzingSHA256 { 526 return cmdutils.WrapExecError(errors.Errorf( 527 `Please update the http_archive rule of the "rules_fuzzing" repository in the WORKSPACE file to: 528 529 %s 530 531 `, dependencies.RulesFuzzingHTTPArchiveRule), cmd) 532 } 533 534 return nil 535 }