1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package main 6 7import ( 8 "context" 9 "flag" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "go.skia.org/infra/go/exec" 17 "go.skia.org/infra/go/git/git_common" 18 "go.skia.org/infra/go/skerr" 19 "go.skia.org/infra/go/sklog" 20 "go.skia.org/infra/task_driver/go/lib/os_steps" 21 "go.skia.org/infra/task_driver/go/td" 22) 23 24var sleepOnFail = flag.Bool("sleep_on_fail", false, "True if we should sleep for 30 minutes on failure instead of exiting (for inspection via SSH)") 25 26func main() { 27 var ( 28 // Required properties for this task. 29 fuzzDuration = flag.Duration("fuzz_duration", 600*time.Second, "The total time that the fuzzers run. Divided up between all fuzzers.") 30 gitExePath = flag.String("git_exe_path", "", "Path to a git exe. Used to checkout cifuzz repo.") 31 outPath = flag.String("out_path", "", "The directory to put any crashes/hangs/outputs found.") 32 projectID = flag.String("project_id", "", "ID of the Google Cloud project.") 33 skiaPath = flag.String("skia_path", "", "Path to skia repo root.") 34 taskID = flag.String("task_id", "", "task id this data was generated on") 35 taskName = flag.String("task_name", "", "Name of the task.") 36 workPath = flag.String("work_path", "", "The directory to use to store temporary files (e.g. fuzzers)") 37 38 // Debugging flags. 39 local = flag.Bool("local", false, "True if running locally (as opposed to on the bots)") 40 outputSteps = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.") 41 ) 42 43 // Setup. 44 ctx := td.StartRun(projectID, taskID, taskName, outputSteps, local) 45 defer td.EndRun(ctx) 46 47 // Absolute paths work more consistently than relative paths. 48 gitAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *gitExePath, "git_exe_path") 49 outAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *outPath, "out_path") 50 skiaAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *skiaPath, "skia_path") 51 workAbsPath := td.MustGetAbsolutePathOfFlag(ctx, *workPath, "work_path") 52 53 if !git_common.IsFromCIPD(gitAbsPath) { 54 fatalOrSleep(ctx, skerr.Fmt("Git %s must be from CIPD", gitAbsPath)) 55 } 56 57 workDir := filepath.Join(workAbsPath, "cifuzz") 58 if err := os_steps.MkdirAll(ctx, workDir); err != nil { 59 fatalOrSleep(ctx, skerr.Wrap(err)) 60 } 61 62 // Setup cifuzz repo and images 63 if err := setupCIFuzzRepoAndDocker(ctx, workDir, gitAbsPath); err != nil { 64 fatalOrSleep(ctx, skerr.Wrap(err)) 65 } 66 67 // Prepare the skia checkout to be built with fuzzers. 68 if err := prepareSkiaCheckout(ctx, skiaAbsPath, workDir, gitAbsPath); err != nil { 69 td.Fatal(ctx, skerr.Wrap(err)) 70 } 71 72 // build and run fuzzers. If it fails (errors), hold that until we cleanup and copy the output. 73 // That way, developers can have access to the crashes. 74 runErr := buildAndRunCIFuzz(ctx, workDir, skiaAbsPath, *fuzzDuration) 75 76 if err := extractOutput(ctx, workDir, outAbsPath); err != nil { 77 fatalOrSleep(ctx, skerr.Wrap(err)) 78 } 79 80 // Clean up compiled fuzzers, etc 81 if err := os_steps.RemoveAll(ctx, workDir); err != nil { 82 fatalOrSleep(ctx, skerr.Wrap(err)) 83 } 84 85 if runErr != nil { 86 fatalOrSleep(ctx, skerr.Wrap(runErr)) 87 } 88} 89 90func fatalOrSleep(ctx context.Context, err error) { 91 if *sleepOnFail { 92 sklog.Errorf("Sleeping after error: %s", err) 93 time.Sleep(30 * time.Minute) 94 } 95 td.Fatal(ctx, err) 96} 97 98const ( 99 ossFuzzRepo = "https://github.com/google/oss-fuzz.git" 100 swiftShaderRepo = "https://swiftshader.googlesource.com/SwiftShader" 101 dockerExe = "docker" 102 cifuzzDockerImage = "gcr.io/oss-fuzz-base/cifuzz-base:latest" 103 buildFuzzersDockerImage = "local_build_fuzzers" 104 runFuzzersDockerImage = "local_run_fuzzers" 105) 106 107func setupCIFuzzRepoAndDocker(ctx context.Context, workdir, gitAbsPath string) error { 108 ctx = td.StartStep(ctx, td.Props("setup cifuzz").Infra()) 109 defer td.EndStep(ctx) 110 111 // Make these directories for cifuzz exist so docker does not create it w/ root permissions. 112 if err := os_steps.MkdirAll(ctx, filepath.Join(workdir, "out")); err != nil { 113 return td.FailStep(ctx, skerr.Wrap(err)) 114 } 115 116 if _, err := exec.RunCwd(ctx, workdir, gitAbsPath, "clone", ossFuzzRepo, "--depth", "1"); err != nil { 117 return td.FailStep(ctx, skerr.Wrap(err)) 118 } 119 120 if _, err := exec.RunCwd(ctx, workdir, dockerExe, "pull", cifuzzDockerImage); err != nil { 121 return td.FailStep(ctx, skerr.Wrap(err)) 122 } 123 124 ossFuzzDir := filepath.Join(workdir, "oss-fuzz", "infra") 125 126 if _, err := exec.RunCwd(ctx, ossFuzzDir, dockerExe, "build", "--tag", buildFuzzersDockerImage, 127 "--file", "build_fuzzers.Dockerfile", "."); err != nil { 128 return td.FailStep(ctx, skerr.Wrap(err)) 129 } 130 131 if _, err := exec.RunCwd(ctx, ossFuzzDir, dockerExe, "build", "--tag", runFuzzersDockerImage, 132 "--file", "run_fuzzers.Dockerfile", "."); err != nil { 133 return td.FailStep(ctx, skerr.Wrap(err)) 134 } 135 136 return nil 137} 138 139func prepareSkiaCheckout(ctx context.Context, skiaAbsPath, workDir, gitAbsPath string) error { 140 ctx = td.StartStep(ctx, td.Props("prepare skia checkout for build").Infra()) 141 defer td.EndStep(ctx) 142 143 swiftshaderDir := filepath.Join(skiaAbsPath, "third_party", "externals", "swiftshader") 144 145 if _, err := exec.RunCwd(ctx, workDir, "rm", "-rf", swiftshaderDir); err != nil { 146 return td.FailStep(ctx, skerr.Wrap(err)) 147 } 148 149 // We have to clone swiftshader *and* its deps (which are not DEPS, but git submodules) in order 150 // to build it with fuzzers. 151 if _, err := exec.RunCwd(ctx, skiaAbsPath, gitAbsPath, "clone", "--recursive", swiftShaderRepo, swiftshaderDir); err != nil { 152 return td.FailStep(ctx, skerr.Wrap(err)) 153 } 154 155 return nil 156} 157 158func buildAndRunCIFuzz(ctx context.Context, workDir, skiaAbsPath string, duration time.Duration) error { 159 ctx = td.StartStep(ctx, td.Props("build skia fuzzers and run them")) 160 defer td.EndStep(ctx) 161 162 // See https://google.github.io/oss-fuzz/getting-started/continuous-integration/#optional-configuration 163 if _, err := exec.RunCwd(ctx, workDir, dockerExe, "run", 164 "--name", "build_fuzzers", "--rm", 165 "--env", "MANUAL_SRC_PATH="+skiaAbsPath, 166 "--env", "OSS_FUZZ_PROJECT_NAME=skia", 167 "--env", "GITHUB_WORKSPACE="+workDir, 168 "--env", "GITHUB_REPOSITORY=skia", // TODO(metzman) make this not required 169 "--env", "GITHUB_EVENT_NAME=push", // TODO(metzman) make this not required 170 "--env", "DRY_RUN=false", 171 "--env", "CI=true", 172 "--env", "CIFUZZ=true", 173 "--env", "SANITIZER=address", 174 "--env", "GITHUB_SHA=does_nothing", 175 "--volume", "/var/run/docker.sock:/var/run/docker.sock", 176 "--mount", fmt.Sprintf("type=bind,source=%s,destination=%s", skiaAbsPath, skiaAbsPath), 177 "--mount", fmt.Sprintf("type=bind,source=%s,destination=%s", workDir, workDir), 178 buildFuzzersDockerImage, 179 ); err != nil { 180 return td.FailStep(ctx, skerr.Wrap(err)) 181 } 182 183 args := []string{"run", 184 "--name", "run_fuzzers", "--rm", 185 "--env", "OSS_FUZZ_PROJECT_NAME=skia", 186 "--env", "GITHUB_WORKSPACE=" + workDir, 187 "--env", "GITHUB_REPOSITORY=skia", // TODO(metzman) make this not required 188 "--env", "GITHUB_EVENT_NAME=push", // TODO(metzman) make this not required 189 "--env", "DRY_RUN=false", 190 "--env", "CI=true", 191 "--env", "CIFUZZ=true", 192 "--env", "FUZZ_TIME=" + fmt.Sprintf("%d", duration/time.Second), // This is split up between all affected fuzzers. 193 "--env", "SANITIZER=address", 194 "--env", "GITHUB_SHA=does_nothing", 195 "--volume", "/var/run/docker.sock:/var/run/docker.sock", 196 "--mount", fmt.Sprintf("type=bind,source=%s,destination=%s", workDir, workDir), 197 runFuzzersDockerImage, 198 } 199 200 cmd := exec.Command{ 201 Name: dockerExe, 202 Args: args, 203 Dir: workDir, 204 Timeout: duration + 10*time.Minute, // Give a little padding in case fuzzing takes some extra time. 205 } 206 if _, err := exec.RunCommand(ctx, &cmd); err != nil { 207 if !exec.IsTimeout(err) { 208 return td.FailStep(ctx, skerr.Wrap(err)) 209 } else { 210 sklog.Warningf("Fuzzing timed out: %s", err) 211 } 212 } 213 return nil 214} 215 216func extractOutput(ctx context.Context, workDir, outAbsPath string) error { 217 ctx = td.StartStep(ctx, td.Props("copy output directory").Infra()) 218 defer td.EndStep(ctx) 219 220 cifuzzOutDir := filepath.Join(workDir, "out") 221 222 // Fix up permissions of output directory (we need to delete extra folders here so we can 223 // clean up after we copy out the crash/hang files). 224 if _, err := exec.RunCwd(ctx, workDir, dockerExe, "run", 225 "--mount", fmt.Sprintf("type=bind,source=%s,destination=/OUT", cifuzzOutDir), 226 cifuzzDockerImage, 227 "/bin/bash", "-c", `rm -rf /OUT/*/ && chmod 0666 /OUT/*`, 228 ); err != nil { 229 return td.FailStep(ctx, skerr.Wrap(err)) 230 } 231 232 // Make these directories for cifuzz exist so docker does not create it w/ root permissions. 233 if err := os_steps.MkdirAll(ctx, outAbsPath); err != nil { 234 return td.FailStep(ctx, skerr.Wrap(err)) 235 } 236 237 files, err := os_steps.ReadDir(ctx, cifuzzOutDir) 238 if err != nil { 239 return td.FailStep(ctx, skerr.Wrapf(err, "getting output from %s", cifuzzOutDir)) 240 } 241 242 for _, f := range files { 243 name := f.Name() 244 if strings.Contains(name, "crash-") || strings.Contains(name, "oom-") || strings.Contains(name, "timeout-") { 245 oldFile := filepath.Join(cifuzzOutDir, name) 246 newFile := filepath.Join(outAbsPath, name) 247 if err := os.Rename(oldFile, newFile); err != nil { 248 return td.FailStep(ctx, skerr.Wrapf(err, "copying %s to %s", oldFile, newFile)) 249 } 250 } 251 } 252 253 return nil 254} 255