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  }