code-intelligence.com/cifuzz@v0.40.0/internal/build/other/other.go (about)

     1  package other
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  	"golang.org/x/exp/slices"
    14  
    15  	"code-intelligence.com/cifuzz/internal/build"
    16  	"code-intelligence.com/cifuzz/internal/cmdutils"
    17  	"code-intelligence.com/cifuzz/internal/ldd"
    18  	"code-intelligence.com/cifuzz/pkg/dependencies"
    19  	"code-intelligence.com/cifuzz/pkg/log"
    20  	"code-intelligence.com/cifuzz/pkg/runfiles"
    21  	"code-intelligence.com/cifuzz/util/envutil"
    22  	"code-intelligence.com/cifuzz/util/fileutil"
    23  )
    24  
    25  // Warning: Changing these will lead to a breaking change!
    26  const (
    27  	// EnvBuildStep states for what a fuzz test is build.
    28  	// e.g. "coverage", "fuzzing"
    29  	EnvBuildStep string = "CIFUZZ_BUILD_STEP"
    30  
    31  	// EnvBuildLocation states the path of the executable.
    32  	// Default is identical with FUZZ_TEST.
    33  	EnvBuildLocation string = "CIFUZZ_BUILD_LOCATION"
    34  
    35  	// EnvCommand holds the name of the command cifuzz was called with.
    36  	// e.g. "run", "bundle", "remote-run"
    37  	EnvCommand string = "CIFUZZ_COMMAND"
    38  
    39  	// EnvFuzzTest holds the name of the fuzz test.
    40  	EnvFuzzTest string = "FUZZ_TEST"
    41  
    42  	// EnvFuzzTestCFlags hold the CFLAGS used for building the fuzz test.
    43  	EnvFuzzTestCFlags string = "FUZZ_TEST_CFLAGS"
    44  
    45  	// EnvFuzzTestCXXFlags hold the CXXFLAGS used for building the fuzz test.
    46  	EnvFuzzTestCXXFlags string = "FUZZ_TEST_CXXFLAGS"
    47  
    48  	// EnvFuzzTestLDFlags hold the LDFLAGS used for building the fuzz test.
    49  	EnvFuzzTestLDFlags string = "FUZZ_TEST_LDFLAGS"
    50  )
    51  
    52  type BuilderOptions struct {
    53  	ProjectDir   string
    54  	BuildCommand string
    55  	CleanCommand string
    56  	Sanitizers   []string
    57  
    58  	RunfilesFinder runfiles.RunfilesFinder
    59  	Stdout         io.Writer
    60  	Stderr         io.Writer
    61  }
    62  
    63  func (opts *BuilderOptions) Validate() error {
    64  	// Check that the project dir is set
    65  	if opts.ProjectDir == "" {
    66  		return errors.New("ProjectDir is not set")
    67  	}
    68  	// Check that the project dir exists and can be accessed
    69  	_, err := os.Stat(opts.ProjectDir)
    70  	if err != nil {
    71  		return errors.WithStack(err)
    72  	}
    73  
    74  	if opts.RunfilesFinder == nil {
    75  		opts.RunfilesFinder = runfiles.Finder
    76  	}
    77  
    78  	return nil
    79  }
    80  
    81  type Builder struct {
    82  	*BuilderOptions
    83  	env      []string
    84  	buildDir string
    85  }
    86  
    87  func NewBuilder(opts *BuilderOptions) (*Builder, error) {
    88  	err := opts.Validate()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	b := &Builder{BuilderOptions: opts}
    94  
    95  	// Create a temporary build directory
    96  	b.buildDir, err = os.MkdirTemp("", "cifuzz-build-")
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	b.env, err = build.CommonBuildEnv()
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Set CFLAGS, CXXFLAGS, LDFLAGS, and FUZZ_TEST_LDFLAGS which must
   107  	// be passed to the build commands by the build system.
   108  	if len(opts.Sanitizers) == 1 && opts.Sanitizers[0] == "coverage" {
   109  		err = b.setCoverageEnv()
   110  	} else {
   111  		for _, sanitizer := range opts.Sanitizers {
   112  			if sanitizer != "address" && sanitizer != "undefined" {
   113  				panic(fmt.Sprintf("Invalid sanitizer: %q", sanitizer))
   114  			}
   115  		}
   116  		err = b.setLibFuzzerEnv()
   117  	}
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	return b, nil
   123  }
   124  
   125  // Build builds the specified fuzz test via the user-specified build command
   126  func (b *Builder) Build(fuzzTest string) (*build.Result, error) {
   127  	var err error
   128  
   129  	if !slices.Equal(b.Sanitizers, []string{"coverage"}) {
   130  		// We compile the dumper without any user-provided flags. This
   131  		// should be safe as it does not use any stdlib functions.
   132  		dumperSource, err := b.RunfilesFinder.DumperSourcePath()
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		clang, err := b.RunfilesFinder.ClangPath()
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  		// Compile with -fPIC just in case the fuzz test is a PIE.
   141  		cmd := exec.Command(clang, "-fPIC", "-c", dumperSource, "-o", filepath.Join(b.buildDir, "dumper.o"))
   142  		cmd.Stdout = b.Stdout
   143  		cmd.Stderr = b.Stderr
   144  		log.Debugf("Command: %s", cmd.String())
   145  		err = cmd.Run()
   146  		if err != nil {
   147  			return nil, errors.WithStack(err)
   148  		}
   149  	}
   150  
   151  	err = b.setBuildCommandEnv(fuzzTest)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	// Run the build command
   157  	cmd := exec.Command("/bin/sh", "-c", b.BuildCommand)
   158  	cmd.Stdout = b.Stdout
   159  	cmd.Stderr = b.Stderr
   160  	cmd.Env = b.env
   161  	log.Debugf("Command: %s", cmd.String())
   162  	err = cmd.Run()
   163  	if err != nil {
   164  		return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd)
   165  	}
   166  
   167  	executable, err := b.findFuzzTestExecutable(fuzzTest)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	if executable == "" {
   173  		return nil, cmdutils.WrapExecError(errors.Errorf("Could not find executable for fuzz test %q", fuzzTest), cmd)
   174  	}
   175  
   176  	// For the build system type "other", we expect the default seed corpus next
   177  	// to the fuzzer executable.
   178  	seedCorpus := executable + "_inputs"
   179  	runtimeDeps, err := ldd.NonSystemSharedLibraries(executable)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	wd, err := os.Getwd()
   185  	if err != nil {
   186  		return nil, errors.WithStack(err)
   187  	}
   188  
   189  	generatedCorpus := filepath.Join(b.ProjectDir, ".cifuzz-corpus", fuzzTest)
   190  	return &build.Result{
   191  		Name:            fuzzTest,
   192  		Executable:      executable,
   193  		GeneratedCorpus: generatedCorpus,
   194  		SeedCorpus:      seedCorpus,
   195  		BuildDir:        wd,
   196  		ProjectDir:      b.ProjectDir,
   197  		Sanitizers:      b.Sanitizers,
   198  		RuntimeDeps:     runtimeDeps,
   199  	}, nil
   200  }
   201  
   202  // Clean cleans the project's build artifacts user-specified build command.
   203  func (b *Builder) Clean() error {
   204  	if b.CleanCommand == "" {
   205  		return nil
   206  	}
   207  
   208  	err := b.setCleanCommandEnv()
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	// Run the clean command
   214  	cmd := exec.Command("/bin/sh", "-c", b.CleanCommand)
   215  	cmd.Stdout = b.Stdout
   216  	cmd.Stderr = b.Stderr
   217  	cmd.Env = b.env
   218  	log.Debugf("Command: %s", cmd.String())
   219  	if err := cmd.Run(); err != nil {
   220  		return cmdutils.WrapExecError(errors.WithStack(err), cmd)
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func (b *Builder) setBuildCommandEnv(fuzzTest string) error {
   227  	var err error
   228  
   229  	b.env, err = envutil.Setenv(b.env, EnvCommand, cmdutils.CurrentInvocation.Command)
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	b.env, err = envutil.Setenv(b.env, EnvFuzzTest, fuzzTest)
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	b.env, err = envutil.Setenv(b.env, EnvBuildLocation, fuzzTest)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func (b *Builder) setCleanCommandEnv() error {
   248  	var err error
   249  
   250  	b.env, err = envutil.Setenv(b.env, EnvCommand, cmdutils.CurrentInvocation.Command)
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	return nil
   256  }
   257  
   258  func (b *Builder) setLibFuzzerEnv() error {
   259  	var err error
   260  
   261  	b.env, err = envutil.Setenv(b.env, EnvBuildStep, "fuzzing")
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	// Set CFLAGS and CXXFLAGS
   267  	cflags := build.LibFuzzerCFlags()
   268  	b.env, err = envutil.Setenv(b.env, "CFLAGS", strings.Join(cflags, " "))
   269  	if err != nil {
   270  		return err
   271  	}
   272  	b.env, err = envutil.Setenv(b.env, "CXXFLAGS", strings.Join(cflags, " "))
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	ldflags := []string{
   278  		// ----- Flags used to build with ASan -----
   279  		// Link ASan and UBSan runtime
   280  		"-fsanitize=address,undefined",
   281  	}
   282  	b.env, err = envutil.Setenv(b.env, "LDFLAGS", strings.Join(ldflags, " "))
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	// Users should pass the environment variable FUZZ_TEST_CFLAGS or
   288  	// FUZZ_TEST_CXXFLAGS to the compiler command building the fuzz test.
   289  	cifuzzIncludePath, err := b.RunfilesFinder.CIFuzzIncludePath()
   290  	if err != nil {
   291  		return err
   292  	}
   293  	fuzzTestCFlags := []string{fmt.Sprintf("-I%s", cifuzzIncludePath)}
   294  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestCFlags, strings.Join(fuzzTestCFlags, " "))
   295  	if err != nil {
   296  		return err
   297  	}
   298  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestCXXFlags, strings.Join(fuzzTestCFlags, " "))
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	// Users should pass the environment variable FUZZ_TEST_LDFLAGS to
   304  	// the linker command building the fuzz test. For libfuzzer, we set
   305  	// it to "-fsanitize=fuzzer" to build a libfuzzer binary.
   306  	// We also link in an additional object to ensure that non-fatal
   307  	// sanitizer findings still have an input attached.
   308  	// See src/dumper.c for details.
   309  	var fuzzTestLdflags []string
   310  	if runtime.GOOS != "darwin" {
   311  		fuzzTestLdflags = append(fuzzTestLdflags, "-Wl,--wrap=__sanitizer_set_death_callback")
   312  	}
   313  	fuzzTestLdflags = append(fuzzTestLdflags, "-fsanitize=fuzzer", filepath.Join(b.buildDir, "dumper.o"))
   314  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestLDFlags, strings.Join(fuzzTestLdflags, " "))
   315  	if err != nil {
   316  		return err
   317  	}
   318  
   319  	return nil
   320  }
   321  
   322  func (b *Builder) setCoverageEnv() error {
   323  	var err error
   324  
   325  	b.env, err = envutil.Setenv(b.env, EnvBuildStep, "coverage")
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	// Set CFLAGS and CXXFLAGS. Note that these flags must not contain
   331  	// spaces, because the environment variables are space separated.
   332  	//
   333  	// Note: Keep in sync with share/cmake/cifuzz-functions.cmake
   334  	clangVersion, err := dependencies.Version(dependencies.Clang, b.ProjectDir)
   335  	if err != nil {
   336  		log.Warnf("Failed to determine version of clang: %v", err)
   337  	}
   338  	cflags := build.CoverageCFlags(clangVersion)
   339  
   340  	b.env, err = envutil.Setenv(b.env, "CFLAGS", strings.Join(cflags, " "))
   341  	if err != nil {
   342  		return err
   343  	}
   344  	b.env, err = envutil.Setenv(b.env, "CXXFLAGS", strings.Join(cflags, " "))
   345  	if err != nil {
   346  		return err
   347  	}
   348  
   349  	ldflags := []string{
   350  		// ----- Flags used to link in coverage runtime -----
   351  		"-fprofile-instr-generate",
   352  	}
   353  	b.env, err = envutil.Setenv(b.env, "LDFLAGS", strings.Join(ldflags, " "))
   354  	if err != nil {
   355  		return err
   356  	}
   357  
   358  	// Users should pass the environment variable FUZZ_TEST_CFLAGS or
   359  	// FUZZ_TEST_CXXFLAGS to the compiler command building the fuzz test.
   360  	cifuzzIncludePath, err := b.RunfilesFinder.CIFuzzIncludePath()
   361  	if err != nil {
   362  		return err
   363  	}
   364  	fuzzTestCFlags := []string{fmt.Sprintf("-I%s", cifuzzIncludePath)}
   365  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestCFlags, strings.Join(fuzzTestCFlags, " "))
   366  	if err != nil {
   367  		return err
   368  	}
   369  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestCXXFlags, strings.Join(fuzzTestCFlags, " "))
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	// Users should pass the environment variable FUZZ_TEST_LDFLAGS to
   375  	// the linker command building the fuzz test. We use it to link in libFuzzer
   376  	// in coverage builds to use its crash-resistant merge feature.
   377  	b.env, err = envutil.Setenv(b.env, EnvFuzzTestLDFlags, "-fsanitize=fuzzer")
   378  	if err != nil {
   379  		return err
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  func (b *Builder) findFuzzTestExecutable(fuzzTest string) (string, error) {
   386  	if exists, _ := fileutil.Exists(fuzzTest); exists {
   387  		absPath, err := filepath.Abs(fuzzTest)
   388  		if err != nil {
   389  			return "", errors.WithStack(err)
   390  		}
   391  		return absPath, nil
   392  	}
   393  
   394  	var executable string
   395  	err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
   396  		if err != nil {
   397  			return errors.WithStack(err)
   398  		}
   399  		if info.IsDir() {
   400  			return nil
   401  		}
   402  		if runtime.GOOS == "windows" {
   403  			if info.Name() == fuzzTest+".exe" {
   404  				executable = path
   405  			}
   406  		} else {
   407  			// As a heuristic, verify that the executable candidate has some
   408  			// executable bit set - it may not be sufficient to actually execute
   409  			// it as the current user.
   410  			if info.Name() == fuzzTest && (info.Mode()&0111 != 0) {
   411  				executable = path
   412  			}
   413  		}
   414  		return nil
   415  	})
   416  	if err != nil {
   417  		return "", err
   418  	}
   419  	// No executable was found, we handle this error in the caller
   420  	if executable == "" {
   421  		return "", nil
   422  	}
   423  	absPath, err := filepath.Abs(executable)
   424  	if err != nil {
   425  		return "", errors.WithStack(err)
   426  	}
   427  	return absPath, nil
   428  }