github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/workflow/fuzz-step/main.go (about) 1 // Copyright 2025 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "errors" 11 "flag" 12 "fmt" 13 "io" 14 "net/http" 15 "os" 16 "path/filepath" 17 "regexp" 18 "time" 19 20 "github.com/google/syzkaller/pkg/build" 21 "github.com/google/syzkaller/pkg/db" 22 "github.com/google/syzkaller/pkg/log" 23 "github.com/google/syzkaller/pkg/manager" 24 "github.com/google/syzkaller/pkg/mgrconfig" 25 "github.com/google/syzkaller/pkg/osutil" 26 "github.com/google/syzkaller/prog" 27 "github.com/google/syzkaller/syz-cluster/pkg/api" 28 "github.com/google/syzkaller/syz-cluster/pkg/app" 29 "github.com/google/syzkaller/syz-cluster/pkg/fuzzconfig" 30 "golang.org/x/sync/errgroup" 31 ) 32 33 var ( 34 flagConfig = flag.String("config", "", "path to the fuzz config") 35 flagSession = flag.String("session", "", "session ID") 36 flagBaseBuild = flag.String("base_build", "", "base build ID") 37 flagPatchedBuild = flag.String("patched_build", "", "patched build ID") 38 flagTime = flag.String("time", "1h", "how long to fuzz") 39 flagWorkdir = flag.String("workdir", "/workdir", "base workdir path") 40 ) 41 42 func main() { 43 flag.Parse() 44 if *flagConfig == "" || *flagSession == "" || *flagTime == "" { 45 app.Fatalf("--config, --session and --time must be set") 46 } 47 client := app.DefaultClient() 48 d, err := time.ParseDuration(*flagTime) 49 if err != nil { 50 app.Fatalf("invalid --time: %v", err) 51 } 52 if !prog.GitRevisionKnown() { 53 log.Fatalf("the binary is built without the git revision information") 54 } 55 56 config := readFuzzConfig() 57 ctx := context.Background() 58 if err := reportStatus(ctx, config, client, api.TestRunning, nil); err != nil { 59 app.Fatalf("failed to report the test: %v", err) 60 } 61 62 artifactsDir := filepath.Join(*flagWorkdir, "artifacts") 63 osutil.MkdirAll(artifactsDir) 64 store := &manager.DiffFuzzerStore{BasePath: artifactsDir} 65 66 // We want to only cancel the run() operation in order to be able to also report 67 // the final test result back. 68 runCtx, cancel := context.WithTimeout(context.Background(), d) 69 defer cancel() 70 err = run(runCtx, config, client, d, store) 71 status := api.TestPassed // TODO: what about TestFailed? 72 if errors.Is(err, errSkipFuzzing) { 73 status = api.TestSkipped 74 } else if err != nil && !errors.Is(err, context.DeadlineExceeded) { 75 app.Errorf("the step failed: %v", err) 76 status = api.TestError 77 } 78 log.Logf(0, "fuzzing is finished") 79 logFinalState(store) 80 if err := reportStatus(ctx, config, client, status, store); err != nil { 81 app.Fatalf("failed to update the test: %v", err) 82 } 83 } 84 85 func readFuzzConfig() *api.FuzzConfig { 86 raw, err := os.ReadFile(*flagConfig) 87 if err != nil { 88 app.Fatalf("failed to read config: %v", err) 89 return nil 90 } 91 var req api.FuzzConfig 92 err = json.Unmarshal(raw, &req) 93 if err != nil { 94 app.Fatalf("failed to unmarshal request: %v, %s", err, raw) 95 return nil 96 } 97 return &req 98 } 99 100 func logFinalState(store *manager.DiffFuzzerStore) { 101 log.Logf(0, "status at the end:\n%s", store.PlainTextDump()) 102 103 // There can be findings that we did not report only because we failed 104 // to come up with a reproducer. 105 // Let's log such cases so that it's easier to find and manually review them. 106 const countCutOff = 10 107 for _, bug := range store.List() { 108 if bug.Base.Crashes == 0 && bug.Patched.Crashes >= countCutOff { 109 log.Logf(0, "possibly patched-only: %s", bug.Title) 110 } 111 } 112 } 113 114 var errSkipFuzzing = errors.New("skip") 115 116 func run(baseCtx context.Context, config *api.FuzzConfig, client *api.Client, 117 timeout time.Duration, store *manager.DiffFuzzerStore) error { 118 series, err := client.GetSessionSeries(baseCtx, *flagSession) 119 if err != nil { 120 return fmt.Errorf("failed to query the series info: %w", err) 121 } 122 123 // Until there's a way to pass the log.Logger object and capture all, 124 // use the global log collection. 125 const MB = 1000000 126 log.EnableLogCaching(100000, 10*MB) 127 128 base, patched, err := generateConfigs(config) 129 if err != nil { 130 return fmt.Errorf("failed to load configs: %w", err) 131 } 132 133 baseSymbols, patchedSymbols, err := readSymbolHashes() 134 if err != nil { 135 app.Errorf("failed to read symbol hashes: %v", err) 136 } 137 138 if shouldSkipFuzzing(baseSymbols, patchedSymbols) { 139 return errSkipFuzzing 140 } 141 manager.PatchFocusAreas(patched, series.PatchBodies(), baseSymbols.Text, patchedSymbols.Text) 142 143 if len(config.CorpusURLs) > 0 { 144 err := prepareCorpus(baseCtx, patched.Workdir, config.CorpusURLs, patched.Target) 145 if err != nil { 146 app.Errorf("failed to download the corpus: %v", err) 147 } 148 } 149 150 eg, ctx := errgroup.WithContext(baseCtx) 151 bugs := make(chan *manager.UniqueBug) 152 baseCrashes := make(chan string, 16) 153 eg.Go(func() error { 154 defer log.Logf(0, "bug reporting terminated") 155 for { 156 select { 157 case title := <-baseCrashes: 158 err := client.UploadBaseFinding(ctx, &api.BaseFindingInfo{ 159 BuildID: *flagBaseBuild, 160 Title: title, 161 }) 162 if err != nil { 163 app.Errorf("failed to report a base kernel crash %q: %v", title, err) 164 } 165 case bug := <-bugs: 166 err := reportFinding(ctx, config, client, bug) 167 if err != nil { 168 app.Errorf("failed to report a finding %q: %v", bug.Report.Title, err) 169 } 170 case <-ctx.Done(): 171 return nil 172 } 173 } 174 }) 175 eg.Go(func() error { 176 defer log.Logf(0, "diff fuzzing terminated") 177 return manager.RunDiffFuzzer(ctx, base, patched, manager.DiffFuzzerConfig{ 178 Debug: false, 179 PatchedOnly: bugs, 180 BaseCrashes: baseCrashes, 181 Store: store, 182 MaxTriageTime: timeout / 2, 183 FuzzToReachPatched: fuzzToReachPatched(config), 184 IgnoreCrash: func(ctx context.Context, title string) (bool, error) { 185 if !titleMatchesFilter(config, title) { 186 log.Logf(1, "crash %q doesn't match the filter", title) 187 return true, nil 188 } 189 ret, err := client.BaseFindingStatus(ctx, &api.BaseFindingInfo{ 190 BuildID: *flagBaseBuild, 191 Title: title, 192 }) 193 if err != nil { 194 return false, err 195 } 196 if ret.Observed { 197 log.Logf(1, "crash %q is already known", title) 198 } 199 return ret.Observed, nil 200 }, 201 }) 202 }) 203 const ( 204 updatePeriod = 5 * time.Minute 205 artifactUploadPeriod = 30 * time.Minute 206 ) 207 lastArtifactUpdate := time.Now() 208 eg.Go(func() error { 209 defer log.Logf(0, "status reporting terminated") 210 for { 211 select { 212 case <-ctx.Done(): 213 return nil 214 case <-time.After(updatePeriod): 215 } 216 var useStore *manager.DiffFuzzerStore 217 if time.Since(lastArtifactUpdate) > artifactUploadPeriod { 218 lastArtifactUpdate = time.Now() 219 useStore = store 220 } 221 err := reportStatus(ctx, config, client, api.TestRunning, useStore) 222 if err != nil { 223 app.Errorf("failed to update status: %v", err) 224 } 225 } 226 }) 227 err = eg.Wait() 228 if errors.Is(err, manager.ErrPatchedAreaNotReached) { 229 // We did not reach the modified parts of the kernel, but that's fine. 230 return nil 231 } 232 return err 233 } 234 235 func prepareCorpus(ctx context.Context, workdir string, urls []string, target *prog.Target) error { 236 corpusFile := filepath.Join(workdir, "corpus.db") 237 var otherFiles []string 238 for i, url := range urls { 239 log.Logf(0, "downloading corpus #%d: %q", i+1, url) 240 downloadTo := corpusFile 241 if i > 0 { 242 downloadTo = fmt.Sprintf("%s.%d", corpusFile, i) 243 otherFiles = append(otherFiles, downloadTo) 244 } 245 out, err := os.Create(corpusFile) 246 if err != nil { 247 return err 248 } 249 defer out.Close() 250 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 251 if err != nil { 252 return err 253 } 254 resp, err := (&http.Client{}).Do(req) 255 if err != nil { 256 return err 257 } 258 defer resp.Body.Close() 259 if resp.StatusCode != http.StatusOK { 260 return fmt.Errorf("status is not 200: %s", resp.Status) 261 } 262 _, err = io.Copy(out, resp.Body) 263 if err != nil { 264 return err 265 } 266 } 267 if len(otherFiles) > 0 { 268 log.Logf(0, "merging corpuses") 269 skipped, err := db.Merge(corpusFile, otherFiles, target) 270 if err != nil { 271 return err 272 } else if len(skipped) > 0 { 273 log.Logf(0, "skipped %d entries", len(skipped)) 274 } 275 } 276 return nil 277 } 278 279 func generateConfigs(config *api.FuzzConfig) (*mgrconfig.Config, *mgrconfig.Config, error) { 280 base, err := fuzzconfig.GenerateBase(config) 281 if err != nil { 282 return nil, nil, fmt.Errorf("failed to prepare base config: %w", err) 283 } 284 patched, err := fuzzconfig.GeneratePatched(config) 285 if err != nil { 286 return nil, nil, fmt.Errorf("failed to prepare patched config: %w", err) 287 } 288 base.Workdir = filepath.Join(*flagWorkdir, "base") 289 osutil.MkdirAll(base.Workdir) 290 patched.Workdir = filepath.Join(*flagWorkdir, "patched") 291 osutil.MkdirAll(patched.Workdir) 292 err = mgrconfig.Complete(base) 293 if err != nil { 294 return nil, nil, fmt.Errorf("failed to complete the base config: %w", err) 295 } 296 err = mgrconfig.Complete(patched) 297 if err != nil { 298 return nil, nil, fmt.Errorf("failed to complete the patched config: %w", err) 299 } 300 return base, patched, nil 301 } 302 303 func reportStatus(ctx context.Context, config *api.FuzzConfig, client *api.Client, 304 status string, store *manager.DiffFuzzerStore) error { 305 testName := getTestName(config) 306 testResult := &api.TestResult{ 307 SessionID: *flagSession, 308 TestName: testName, 309 BaseBuildID: *flagBaseBuild, 310 PatchedBuildID: *flagPatchedBuild, 311 Result: status, 312 Log: []byte(log.CachedLogOutput()), 313 } 314 err := client.UploadTestResult(ctx, testResult) 315 if err != nil { 316 return fmt.Errorf("failed to upload the status: %w", err) 317 } 318 if store == nil { 319 return nil 320 } 321 tarGzReader, err := compressArtifacts(store.BasePath) 322 if errors.Is(err, errWriteOverLimit) { 323 app.Errorf("the artifacts archive is too big to upload") 324 } else if err != nil { 325 return fmt.Errorf("failed to compress the artifacts dir: %w", err) 326 } else { 327 err = client.UploadTestArtifacts(ctx, *flagSession, testName, tarGzReader) 328 if err != nil { 329 return fmt.Errorf("failed to upload the status: %w", err) 330 } 331 } 332 return nil 333 } 334 335 func reportFinding(ctx context.Context, config *api.FuzzConfig, client *api.Client, bug *manager.UniqueBug) error { 336 finding := &api.NewFinding{ 337 SessionID: *flagSession, 338 TestName: getTestName(config), 339 Title: bug.Report.Title, 340 Report: bug.Report.Report, 341 Log: bug.Report.Output, 342 } 343 if repro := bug.Repro; repro != nil { 344 if repro.Prog != nil { 345 finding.SyzRepro = repro.Prog.Serialize() 346 finding.SyzReproOpts = repro.Opts.Serialize() 347 } 348 if repro.CRepro { 349 var err error 350 finding.CRepro, err = repro.CProgram() 351 if err != nil { 352 app.Errorf("failed to generate C program: %v", err) 353 } 354 } 355 } 356 return client.UploadFinding(ctx, finding) 357 } 358 359 func getTestName(config *api.FuzzConfig) string { 360 return fmt.Sprintf("[%s] Fuzzing", config.Track) 361 } 362 363 var ignoreLinuxVariables = map[string]bool{ 364 "raw_data": true, // from arch/x86/entry/vdso/vdso-image 365 // Build versions / timestamps. 366 "linux_banner": true, 367 "vermagic": true, 368 "init_uts_ns": true, 369 } 370 371 func shouldSkipFuzzing(base, patched build.SectionHashes) bool { 372 if len(base.Text) == 0 || len(patched.Text) == 0 { 373 // Likely, something went wrong during the kernel build step. 374 log.Logf(0, "skipped the binary equality check because some of them have 0 symbols") 375 return false 376 } 377 same := len(base.Text) == len(patched.Text) && len(base.Data) == len(patched.Data) 378 // For .text, demand all symbols to be equal. 379 for name, hash := range base.Text { 380 if patched.Text[name] != hash { 381 same = false 382 break 383 } 384 } 385 // For data sections ignore some of them. 386 for name, hash := range base.Data { 387 if !ignoreLinuxVariables[name] && patched.Data[name] != hash { 388 log.Logf(1, "symbol %q has different values in base vs patch", name) 389 same = false 390 break 391 } 392 } 393 if same { 394 log.Logf(0, "binaries are the same, no sense to do fuzzing") 395 return true 396 } 397 log.Logf(0, "binaries are different, continuing fuzzing") 398 return false 399 } 400 401 func titleMatchesFilter(config *api.FuzzConfig, title string) bool { 402 matched, err := regexp.MatchString(config.BugTitleRe, title) 403 if err != nil { 404 app.Fatalf("invalid BugTitleRe regexp: %v", err) 405 } 406 return matched 407 } 408 409 func readSymbolHashes() (base, patched build.SectionHashes, err error) { 410 // These are saved by the build step. 411 base, err = readSectionHashes("/base/symbol_hashes.json") 412 if err != nil { 413 return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read base hashes: %w", err) 414 } 415 patched, err = readSectionHashes("/patched/symbol_hashes.json") 416 if err != nil { 417 return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read patched hashes: %w", err) 418 } 419 log.Logf(0, "extracted %d text symbol hashes for base and %d for patched", len(base.Text), len(patched.Text)) 420 return 421 } 422 423 func readSectionHashes(file string) (build.SectionHashes, error) { 424 f, err := os.Open(file) 425 if err != nil { 426 return build.SectionHashes{}, err 427 } 428 defer f.Close() 429 430 var data build.SectionHashes 431 err = json.NewDecoder(f).Decode(&data) 432 if err != nil { 433 return build.SectionHashes{}, err 434 } 435 return data, nil 436 } 437 438 func fuzzToReachPatched(config *api.FuzzConfig) time.Duration { 439 if config.SkipCoverCheck { 440 return 0 441 } 442 // Allow up to 30 minutes after the corpus triage to reach the patched code. 443 return time.Minute * 30 444 } 445 446 func compressArtifacts(dir string) (io.Reader, error) { 447 var buf bytes.Buffer 448 lw := &LimitedWriter{ 449 writer: &buf, 450 // Don't create an archive larger than 64MB. 451 limit: 64 * 1000 * 1000, 452 } 453 err := osutil.TarGzDirectory(dir, lw) 454 if err != nil { 455 return nil, err 456 } 457 return &buf, nil 458 } 459 460 type LimitedWriter struct { 461 written int 462 limit int 463 writer io.Writer 464 } 465 466 var errWriteOverLimit = errors.New("the writer exceeded the limit") 467 468 func (lw *LimitedWriter) Write(p []byte) (n int, err error) { 469 if len(p)+lw.written > lw.limit { 470 return 0, errWriteOverLimit 471 } 472 n, err = lw.writer.Write(p) 473 lw.written += n 474 return 475 }