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

     1  package cmake
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/sha256"
     7  	"encoding/base32"
     8  	"encoding/binary"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"strings"
    17  
    18  	"github.com/pkg/errors"
    19  
    20  	"code-intelligence.com/cifuzz/internal/build"
    21  	"code-intelligence.com/cifuzz/internal/cmdutils"
    22  	"code-intelligence.com/cifuzz/internal/ldd"
    23  	"code-intelligence.com/cifuzz/pkg/log"
    24  	"code-intelligence.com/cifuzz/util/fileutil"
    25  	"code-intelligence.com/cifuzz/util/sliceutil"
    26  )
    27  
    28  // The CMake configuration (also called "build type") to use for fuzzing runs.
    29  // See enable_fuzz_testing in tools/cmake/modules/cifuzz-functions.cmake for the rationale for using this
    30  // build type.
    31  const cmakeBuildConfiguration = "RelWithDebInfo"
    32  
    33  // System library dependencies, which should not be considered as runtime dependencies
    34  var wellKnownSystemLibraries = map[string][]*regexp.Regexp{
    35  	"windows": {
    36  		regexp.MustCompile("^api-ms"),
    37  		regexp.MustCompile("^ext-ms"),
    38  	},
    39  }
    40  
    41  type ParallelOptions struct {
    42  	Enabled bool
    43  	NumJobs uint
    44  }
    45  
    46  type BuilderOptions struct {
    47  	ProjectDir string
    48  	Args       []string
    49  	Sanitizers []string
    50  	Parallel   ParallelOptions
    51  	Stdout     io.Writer
    52  	Stderr     io.Writer
    53  	BuildOnly  bool
    54  
    55  	FindRuntimeDeps bool
    56  }
    57  
    58  func (opts *BuilderOptions) Validate() error {
    59  	// Check that the project dir is set
    60  	if opts.ProjectDir == "" {
    61  		return errors.New("ProjectDir is not set")
    62  	}
    63  	// Check that the project dir exists and can be accessed
    64  	_, err := os.Stat(opts.ProjectDir)
    65  	if err != nil {
    66  		return errors.WithStack(err)
    67  	}
    68  	return nil
    69  }
    70  
    71  type Builder struct {
    72  	*BuilderOptions
    73  	env []string
    74  }
    75  
    76  func NewBuilder(opts *BuilderOptions) (*Builder, error) {
    77  	err := opts.Validate()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	b := &Builder{BuilderOptions: opts}
    83  
    84  	// Ensure that the build directory exists.
    85  	buildDir, err := b.BuildDir()
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	err = os.MkdirAll(buildDir, 0755)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	b.env, err = build.CommonBuildEnv()
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return b, nil
   100  }
   101  
   102  func (b *Builder) Opts() *BuilderOptions {
   103  	return b.BuilderOptions
   104  }
   105  
   106  func (b *Builder) BuildDir() (string, error) {
   107  	// Note: Invoking CMake on the same build directory with different cache
   108  	// variables is a no-op. For this reason, we have to encode all choices made
   109  	// for the cache variables below in the path to the build directory.
   110  	// Currently, this includes the fuzzing engine, the choice of sanitizers
   111  	// and optional user arguments
   112  	sanitizersSegment := strings.Join(b.Sanitizers, "+")
   113  	if sanitizersSegment == "" {
   114  		sanitizersSegment = "none"
   115  	}
   116  
   117  	buildDir := sanitizersSegment
   118  
   119  	if len(b.Args) > 0 {
   120  		// Add the hash of all user arguments to the build dir name in order to
   121  		// create different build directories for different combinations of arguments
   122  		hash := sha256.New()
   123  		for _, arg := range b.Args {
   124  			// Prepend the length of each argument in order to differentiate
   125  			// between arguments like {"foo", "bar"} and {"foobar"}
   126  			err := binary.Write(hash, binary.BigEndian, uint32(len(arg)))
   127  			if err != nil {
   128  				return "", errors.WithStack(err)
   129  			}
   130  			err = binary.Write(hash, binary.BigEndian, []byte(arg))
   131  			if err != nil {
   132  				return "", errors.WithStack(err)
   133  			}
   134  		}
   135  		// Use only the first 8 characters in order to prevent errors on
   136  		// Windows, which cannot handle long file paths.
   137  		hashString := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:8]
   138  		buildDir = fmt.Sprintf("%s-%s", sanitizersSegment, hashString)
   139  	}
   140  
   141  	buildDir = filepath.Join(b.ProjectDir, ".cifuzz-build", "libfuzzer", buildDir)
   142  
   143  	return buildDir, nil
   144  }
   145  
   146  // Configure calls cmake to "Generate a project buildsystem" (that's the
   147  // phrasing used by the CMake man page).
   148  // Note: This is usually a no-op after the directory has been created once,
   149  // even if cache variables change. However, if a previous invocation of this
   150  // command failed during CMake generation and the command is run again, the
   151  // build step would only result in a very unhelpful error message about
   152  // missing Makefiles. By reinvoking CMake's configuration explicitly here,
   153  // we either get a helpful error message or the build step will succeed if
   154  // the user fixed the issue in the meantime.
   155  func (b *Builder) Configure() error {
   156  	buildDir, err := b.BuildDir()
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	cacheArgs := []string{
   162  		"-DCIFUZZ_ENGINE=libfuzzer",
   163  		"-DCIFUZZ_SANITIZERS=" + strings.Join(b.Sanitizers, ";"),
   164  		"-DCIFUZZ_TESTING:BOOL=ON",
   165  	}
   166  	if runtime.GOOS != "windows" {
   167  		// CMAKE_BUILD_TYPE is ignored when building with MSBuild.
   168  		// The config only has to be specified in the build step with
   169  		// --config cmakeBuildConfiguration.
   170  		cacheArgs = append(cacheArgs, "-DCMAKE_BUILD_TYPE="+cmakeBuildConfiguration)
   171  		// Use relative paths in RPATH/RUNPATH so that binaries from the
   172  		// build directory can find their shared libraries even when
   173  		// packaged into an artifact.
   174  		// On Windows, where there is no RPATH, there are two ways the user or
   175  		// we can handle this:
   176  		// 1. Use the TARGET_RUNTIME_DLLS generator expression introduced in
   177  		//    CMake 3.21 to copy all DLLs into the directory of the executable
   178  		//    in a post-build action.
   179  		// 2. Add all library directories to PATH.
   180  		cacheArgs = append(cacheArgs, "-DCMAKE_BUILD_RPATH_USE_ORIGIN:BOOL=ON")
   181  	} else {
   182  		// "-T ClangCL" is needed in order to use clang-cl instead of MSVC
   183  		cacheArgs = append(cacheArgs, "-T ClangCL")
   184  	}
   185  
   186  	args := cacheArgs
   187  	args = append(args, b.Args...)
   188  	args = append(args, b.ProjectDir)
   189  
   190  	cmd := exec.Command("cmake", args...)
   191  	cmd.Stdout = b.Stdout
   192  	cmd.Stderr = b.Stderr
   193  	cmd.Env = b.env
   194  	cmd.Dir = buildDir
   195  	log.Debugf("Working directory: %s", cmd.Dir)
   196  	log.Debugf("Command: %s", cmd.String())
   197  	err = cmd.Run()
   198  	if err != nil {
   199  		return cmdutils.WrapExecError(errors.WithStack(err), cmd)
   200  	}
   201  	return nil
   202  }
   203  
   204  // Build builds the specified fuzz tests with CMake. The fuzz tests must
   205  // not contain duplicates.
   206  func (b *Builder) Build(fuzzTests []string) ([]*build.Result, error) {
   207  	buildDir, err := b.BuildDir()
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	flags := append([]string{
   213  		"--build", buildDir,
   214  		"--config", cmakeBuildConfiguration,
   215  		"--target"}, fuzzTests...)
   216  
   217  	if b.Parallel.Enabled {
   218  		flags = append(flags, "--parallel")
   219  		if b.Parallel.NumJobs != 0 {
   220  			flags = append(flags, fmt.Sprint(b.Parallel.NumJobs))
   221  		}
   222  	}
   223  
   224  	cmd := exec.Command("cmake", flags...)
   225  	cmd.Stdout = b.Stdout
   226  	cmd.Stderr = b.Stderr
   227  	cmd.Env = b.env
   228  	log.Debugf("Command: %s", cmd.String())
   229  	err = cmd.Run()
   230  	if err != nil {
   231  		return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd)
   232  	}
   233  
   234  	if b.BuildOnly {
   235  		return nil, nil
   236  	}
   237  
   238  	var results []*build.Result
   239  	for _, fuzzTest := range fuzzTests {
   240  		executable, err := b.findFuzzTestExecutable(fuzzTest)
   241  		if err != nil {
   242  			return nil, err
   243  		}
   244  		seedCorpus, err := b.findFuzzTestSeedCorpus(fuzzTest)
   245  		if err != nil {
   246  			return nil, err
   247  		}
   248  
   249  		var runtimeDeps []string
   250  		if b.FindRuntimeDeps {
   251  			// TODO if we have another solution for windows/darwin we should remove
   252  			// the getRuntimeDeps and the related code in cifuzz-functions.cmake
   253  			if runtime.GOOS == "linux" {
   254  				runtimeDeps, err = ldd.NonSystemSharedLibraries(executable)
   255  			} else {
   256  				runtimeDeps, err = b.getRuntimeDeps(fuzzTest)
   257  			}
   258  			if err != nil {
   259  				return nil, err
   260  			}
   261  		}
   262  
   263  		generatedCorpus := filepath.Join(b.ProjectDir, ".cifuzz-corpus", fuzzTest)
   264  		result := &build.Result{
   265  			Name:            fuzzTest,
   266  			Executable:      executable,
   267  			GeneratedCorpus: generatedCorpus,
   268  			SeedCorpus:      seedCorpus,
   269  			BuildDir:        buildDir,
   270  			ProjectDir:      b.ProjectDir,
   271  			Sanitizers:      b.Sanitizers,
   272  			RuntimeDeps:     runtimeDeps,
   273  		}
   274  		results = append(results, result)
   275  	}
   276  
   277  	return results, nil
   278  }
   279  
   280  // findFuzzTestExecutable uses the info files emitted by the CMake integration
   281  // in the configure step to look up the canonical path of a fuzz test's
   282  // executable.
   283  func (b *Builder) findFuzzTestExecutable(fuzzTest string) (string, error) {
   284  	return b.readInfoFileAsPath(fuzzTest, "executable")
   285  }
   286  
   287  // findFuzzTestSeedCorpus uses the info files emitted by the CMake integration
   288  // in the configure step to look up the canonical path of a fuzz test's
   289  // seed corpus directory.
   290  func (b *Builder) findFuzzTestSeedCorpus(fuzzTest string) (string, error) {
   291  	return b.readInfoFileAsPath(fuzzTest, "seed_corpus")
   292  }
   293  
   294  // ListFuzzTests lists all fuzz tests defined in the CMake project after
   295  // Configure has been run.
   296  func (b *Builder) ListFuzzTests() ([]string, error) {
   297  	fuzzTestsDir, err := b.fuzzTestsInfoDir()
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	fuzzTestEntries, err := os.ReadDir(fuzzTestsDir)
   302  	if err != nil {
   303  		return nil, errors.WithStack(err)
   304  	}
   305  
   306  	var fuzzTests []string
   307  	for _, entry := range fuzzTestEntries {
   308  		fuzzTests = append(fuzzTests, entry.Name())
   309  	}
   310  	fuzzTests = sliceutil.RemoveDuplicates(fuzzTests)
   311  	return fuzzTests, nil
   312  }
   313  
   314  // getRuntimeDeps returns the canonical paths of all (transitive) runtime
   315  // dependencies of the given fuzz test. It prints a warning if any dependency
   316  // couldn't be resolved or resolves to more than one file.
   317  func (b *Builder) getRuntimeDeps(fuzzTest string) ([]string, error) {
   318  	buildDir, err := b.BuildDir()
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	cmd := exec.Command(
   324  		"cmake",
   325  		"--install",
   326  		buildDir,
   327  		"--config", cmakeBuildConfiguration,
   328  		"--component", "cifuzz_internal_deps_"+fuzzTest,
   329  	)
   330  	log.Debugf("Command: %s", cmd.String())
   331  	stdout, err := cmd.Output()
   332  	if err != nil {
   333  		return nil, cmdutils.WrapExecError(errors.WithStack(err), cmd)
   334  	}
   335  
   336  	var resolvedDeps []string
   337  	var unresolvedDeps []string
   338  	var conflictingDeps []string
   339  	scanner := bufio.NewScanner(bytes.NewReader(stdout))
   340  	for scanner.Scan() {
   341  		line := scanner.Text()
   342  		// Typical lines in the output of the install command look like this:
   343  		//
   344  		// <arbitrary CMake output>
   345  		// -- CIFUZZ RESOLVED /usr/lib/system.so
   346  		// -- CIFUZZ RESOLVED /home/user/git/project/build/lib/bar.so
   347  		// -- CIFUZZ UNRESOLVED not_found.so
   348  
   349  		// Skip over CMake output.
   350  		if !strings.HasPrefix(line, "-- CIFUZZ ") {
   351  			continue
   352  		}
   353  		statusAndDep := strings.TrimPrefix(line, "-- CIFUZZ ")
   354  		endOfStatus := strings.Index(statusAndDep, " ")
   355  		if endOfStatus == -1 {
   356  			return nil, errors.Errorf("invalid runtime dep line: %s", line)
   357  		}
   358  		status := statusAndDep[:endOfStatus]
   359  		dep := statusAndDep[endOfStatus+1:]
   360  
   361  		// Filter well known system libraries
   362  		if isSystemLibrary(dep) {
   363  			continue
   364  		}
   365  
   366  		switch status {
   367  		case "UNRESOLVED":
   368  			unresolvedDeps = append(unresolvedDeps, dep)
   369  		case "CONFLICTING":
   370  			conflictingDeps = append(conflictingDeps, dep)
   371  		case "RESOLVED":
   372  			resolvedDeps = append(resolvedDeps, dep)
   373  		default:
   374  			return nil, errors.Errorf("invalid status '%s' in runtime dep line: %s", status, line)
   375  		}
   376  	}
   377  
   378  	if len(unresolvedDeps) > 0 || len(conflictingDeps) > 0 {
   379  		var warning strings.Builder
   380  		if len(unresolvedDeps) > 0 {
   381  			warning.WriteString(
   382  				fmt.Sprintf("The following shared library dependencies of %s could not be resolved:\n", fuzzTest))
   383  			for _, unresolvedDep := range unresolvedDeps {
   384  				warning.WriteString(fmt.Sprintf("  %s\n", unresolvedDep))
   385  			}
   386  		}
   387  		if len(conflictingDeps) > 0 {
   388  			warning.WriteString(
   389  				fmt.Sprintf("The following shared library dependencies of %s could not be resolved unambiguously:\n", fuzzTest))
   390  			for _, conflictingDep := range conflictingDeps {
   391  				warning.WriteString(fmt.Sprintf("  %s\n", conflictingDep))
   392  			}
   393  		}
   394  		warning.WriteString("The archive may be incomplete.\n")
   395  		log.Warn(warning.String())
   396  	}
   397  
   398  	return resolvedDeps, nil
   399  }
   400  
   401  // readInfoFileAsPath returns the contents of the CMake-generated info file of type kind for the given fuzz test,
   402  // interpreted as a path. All symlinks are followed.
   403  func (b *Builder) readInfoFileAsPath(fuzzTest string, kind string) (string, error) {
   404  
   405  	fuzzTestsInfoDir, err := b.fuzzTestsInfoDir()
   406  	if err != nil {
   407  		return "", err
   408  	}
   409  	infoFile := filepath.Join(fuzzTestsInfoDir, fuzzTest, kind)
   410  	content, err := os.ReadFile(infoFile)
   411  	if err != nil {
   412  		return "", errors.WithStack(err)
   413  	}
   414  	return string(content), nil
   415  }
   416  
   417  func (b *Builder) fuzzTestsInfoDir() (string, error) {
   418  	buildDir, err := b.BuildDir()
   419  	if err != nil {
   420  		return "", err
   421  	}
   422  	// The path to the info file for single-configuration CMake generators (e.g. Makefiles).
   423  	fuzzTestsDir := filepath.Join(buildDir, ".cifuzz", "fuzz_tests")
   424  	log.Debugf("Searching for test info file in %s", fuzzTestsDir)
   425  	if fileutil.IsDir(fuzzTestsDir) {
   426  		return fuzzTestsDir, nil
   427  	}
   428  	// The path to the info file for multi-configuration CMake generators (e.g. MSBuild).
   429  	fuzzTestsDir = filepath.Join(buildDir, cmakeBuildConfiguration, ".cifuzz", "fuzz_tests")
   430  	log.Debugf("Searching for test info file in %s", fuzzTestsDir)
   431  	if fileutil.IsDir(fuzzTestsDir) {
   432  		return fuzzTestsDir, nil
   433  	}
   434  	log.Warn("Did not find test info file")
   435  	return "", errors.WithStack(os.ErrNotExist)
   436  }
   437  
   438  func isSystemLibrary(dep string) bool {
   439  	for _, wellKnownSystemLibrary := range wellKnownSystemLibraries[runtime.GOOS] {
   440  		if wellKnownSystemLibrary.MatchString(dep) {
   441  			return true
   442  		}
   443  	}
   444  
   445  	return false
   446  }