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 }