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