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 }