github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/syz-ci/manager.go (about) 1 // Copyright 2017 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 "crypto/sha256" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "math/rand" 13 "net/http" 14 "net/url" 15 "os" 16 "path" 17 "path/filepath" 18 "regexp" 19 "strings" 20 "time" 21 22 "github.com/google/syzkaller/dashboard/dashapi" 23 "github.com/google/syzkaller/pkg/asset" 24 "github.com/google/syzkaller/pkg/build" 25 "github.com/google/syzkaller/pkg/config" 26 "github.com/google/syzkaller/pkg/cover" 27 "github.com/google/syzkaller/pkg/gcs" 28 "github.com/google/syzkaller/pkg/hash" 29 "github.com/google/syzkaller/pkg/instance" 30 "github.com/google/syzkaller/pkg/log" 31 "github.com/google/syzkaller/pkg/mgrconfig" 32 "github.com/google/syzkaller/pkg/osutil" 33 "github.com/google/syzkaller/pkg/report" 34 "github.com/google/syzkaller/pkg/vcs" 35 "github.com/google/syzkaller/prog" 36 _ "github.com/google/syzkaller/sys" 37 "github.com/google/syzkaller/sys/targets" 38 "github.com/google/syzkaller/vm" 39 ) 40 41 // This is especially slightly longer than syzkaller rebuild period. 42 // If we set kernelRebuildPeriod = syzkallerRebuildPeriod and both are changed 43 // during that period (or around that period), we can rebuild kernel, restart 44 // manager and then instantly shutdown everything for syzkaller update. 45 // Instead we rebuild syzkaller, restart and then rebuild kernel. 46 const kernelRebuildPeriod = syzkallerRebuildPeriod + time.Hour 47 48 // List of required files in kernel build (contents of latest/current dirs). 49 var imageFiles = map[string]bool{ 50 "tag": true, // serialized BuildInfo 51 "kernel.config": false, // kernel config used for build 52 "image": true, // kernel image 53 "kernel": false, 54 "initrd": false, 55 "key": false, // root ssh key for the image 56 } 57 58 func init() { 59 for _, arches := range targets.List { 60 for _, arch := range arches { 61 if arch.KernelObject != "" { 62 imageFiles["obj/"+arch.KernelObject] = false 63 } 64 } 65 } 66 } 67 68 // Manager represents a single syz-manager instance. 69 // Handles kernel polling, image rebuild and manager process management. 70 // As syzkaller builder, it maintains 2 builds: 71 // - latest: latest known good kernel build 72 // - current: currently used kernel build 73 type Manager struct { 74 name string 75 workDir string 76 kernelBuildDir string 77 kernelSrcDir string 78 currentDir string 79 latestDir string 80 configTag string 81 configData []byte 82 cfg *Config 83 repo vcs.Repo 84 mgrcfg *ManagerConfig 85 managercfg *mgrconfig.Config 86 cmd *ManagerCmd 87 dash ManagerDashapi 88 debugStorage bool 89 storage *asset.Storage 90 stop chan struct{} 91 debug bool 92 lastBuild *dashapi.Build 93 buildFailed bool 94 } 95 96 type ManagerDashapi interface { 97 ReportBuildError(req *dashapi.BuildErrorReq) error 98 UploadBuild(build *dashapi.Build) error 99 BuilderPoll(manager string) (*dashapi.BuilderPollResp, error) 100 LogError(name, msg string, args ...interface{}) 101 CommitPoll() (*dashapi.CommitPollResp, error) 102 UploadCommits(commits []dashapi.Commit) error 103 } 104 105 func createManager(cfg *Config, mgrcfg *ManagerConfig, stop chan struct{}, 106 debug bool) (*Manager, error) { 107 dir := osutil.Abs(filepath.Join("managers", mgrcfg.Name)) 108 err := osutil.MkdirAll(dir) 109 if err != nil { 110 log.Fatal(err) 111 } 112 if mgrcfg.RepoAlias == "" { 113 mgrcfg.RepoAlias = mgrcfg.Repo 114 } 115 116 var dash *dashapi.Dashboard 117 if cfg.DashboardAddr != "" && mgrcfg.DashboardClient != "" { 118 dash, err = dashapi.New(mgrcfg.DashboardClient, cfg.DashboardAddr, mgrcfg.DashboardKey) 119 if err != nil { 120 return nil, err 121 } 122 } 123 var assetStorage *asset.Storage 124 if !cfg.AssetStorage.IsEmpty() { 125 assetStorage, err = asset.StorageFromConfig(cfg.AssetStorage, dash) 126 if err != nil { 127 log.Fatalf("failed to create asset storage: %v", err) 128 } 129 } 130 var configData []byte 131 if mgrcfg.KernelConfig != "" { 132 if configData, err = os.ReadFile(mgrcfg.KernelConfig); err != nil { 133 return nil, err 134 } 135 } 136 kernelDir := filepath.Join(dir, "kernel") 137 repo, err := vcs.NewRepo(mgrcfg.managercfg.TargetOS, mgrcfg.managercfg.Type, kernelDir) 138 if err != nil { 139 log.Fatalf("failed to create repo for %v: %v", mgrcfg.Name, err) 140 } 141 142 mgr := &Manager{ 143 name: mgrcfg.managercfg.Name, 144 workDir: filepath.Join(dir, "workdir"), 145 kernelSrcDir: path.Join(kernelDir, mgrcfg.KernelSrcSuffix), 146 kernelBuildDir: kernelDir, 147 currentDir: filepath.Join(dir, "current"), 148 latestDir: filepath.Join(dir, "latest"), 149 configTag: hash.String(configData), 150 configData: configData, 151 cfg: cfg, 152 repo: repo, 153 mgrcfg: mgrcfg, 154 managercfg: mgrcfg.managercfg, 155 dash: dash, 156 storage: assetStorage, 157 debugStorage: !cfg.AssetStorage.IsEmpty() && cfg.AssetStorage.Debug, 158 stop: stop, 159 debug: debug, 160 } 161 162 os.RemoveAll(mgr.currentDir) 163 return mgr, nil 164 } 165 166 // Gates kernel builds, syzkaller builds and coverage report generation. 167 // Kernel builds take whole machine, so we don't run more than one at a time. 168 // Also current image build script uses some global resources (/dev/nbd0) and can't run in parallel. 169 var buildSem = instance.NewSemaphore(1) 170 171 // Gates tests that require extra VMs. 172 // Currently we overcommit instances in such cases, so we'd like to minimize the number of 173 // simultaneous env.Test calls. 174 var testSem = instance.NewSemaphore(1) 175 176 const fuzzingMinutesBeforeCover = 360 177 178 func (mgr *Manager) loop() { 179 lastCommit := "" 180 nextBuildTime := time.Now() 181 var managerRestartTime, artifactUploadTime time.Time 182 latestInfo := mgr.checkLatest() 183 if latestInfo != nil && time.Since(latestInfo.Time) < kernelRebuildPeriod/2 && 184 mgr.managercfg.TargetOS != targets.Fuchsia { 185 // If we have a reasonably fresh build, 186 // start manager straight away and don't rebuild kernel for a while. 187 // Fuchsia is a special case: it builds with syz-executor, so if we just updated syzkaller, we need 188 // to rebuild fuchsia as well. 189 log.Logf(0, "%v: using latest image built on %v", mgr.name, latestInfo.KernelCommit) 190 managerRestartTime = latestInfo.Time 191 nextBuildTime = time.Now().Add(kernelRebuildPeriod) 192 mgr.restartManager() 193 } else if latestInfo != nil { 194 log.Logf(0, "%v: latest image is on %v", mgr.name, latestInfo.KernelCommit) 195 } 196 197 ticker := time.NewTicker(buildRetryPeriod) 198 defer ticker.Stop() 199 200 loop: 201 for { 202 if time.Since(nextBuildTime) >= 0 { 203 var rebuildAfter time.Duration 204 lastCommit, latestInfo, rebuildAfter = mgr.pollAndBuild(lastCommit, latestInfo) 205 nextBuildTime = time.Now().Add(rebuildAfter) 206 } 207 if !artifactUploadTime.IsZero() && time.Now().After(artifactUploadTime) { 208 artifactUploadTime = time.Time{} 209 if err := mgr.uploadCoverReport(); err != nil { 210 mgr.Errorf("failed to upload cover report: %v", err) 211 } 212 if err := mgr.uploadCoverStat(fuzzingMinutesBeforeCover); err != nil { 213 mgr.Errorf("failed to upload coverage stat: %v", err) 214 } 215 if err := mgr.uploadCorpus(); err != nil { 216 mgr.Errorf("failed to upload corpus: %v", err) 217 } 218 } 219 220 select { 221 case <-mgr.stop: 222 break loop 223 default: 224 } 225 226 if latestInfo != nil && (latestInfo.Time != managerRestartTime || mgr.cmd == nil) { 227 managerRestartTime = latestInfo.Time 228 mgr.restartManager() 229 if mgr.cmd != nil { 230 artifactUploadTime = time.Now().Add(fuzzingMinutesBeforeCover * time.Minute) 231 } 232 } 233 234 select { 235 case <-ticker.C: 236 case <-mgr.stop: 237 break loop 238 } 239 } 240 241 if mgr.cmd != nil { 242 mgr.cmd.Close() 243 mgr.cmd = nil 244 } 245 log.Logf(0, "%v: stopped", mgr.name) 246 } 247 248 func (mgr *Manager) pollAndBuild(lastCommit string, latestInfo *BuildInfo) ( 249 string, *BuildInfo, time.Duration) { 250 rebuildAfter := buildRetryPeriod 251 commit, err := mgr.repo.Poll(mgr.mgrcfg.Repo, mgr.mgrcfg.Branch) 252 if err != nil { 253 mgr.buildFailed = true 254 mgr.Errorf("failed to poll: %v", err) 255 } else { 256 log.Logf(0, "%v: poll: %v", mgr.name, commit.Hash) 257 needsUpdate := (latestInfo == nil || 258 commit.Hash != latestInfo.KernelCommit || 259 mgr.configTag != latestInfo.KernelConfigTag) 260 mgr.buildFailed = needsUpdate 261 if commit.Hash != lastCommit && needsUpdate { 262 lastCommit = commit.Hash 263 select { 264 case <-buildSem.WaitC(): 265 log.Logf(0, "%v: building kernel...", mgr.name) 266 if err := mgr.build(commit); err != nil { 267 log.Logf(0, "%v: %v", mgr.name, err) 268 } else { 269 log.Logf(0, "%v: build successful, [re]starting manager", mgr.name) 270 mgr.buildFailed = false 271 rebuildAfter = kernelRebuildPeriod 272 latestInfo = mgr.checkLatest() 273 if latestInfo == nil { 274 mgr.Errorf("failed to read build info after build") 275 } 276 } 277 buildSem.Signal() 278 case <-mgr.stop: 279 } 280 } 281 } 282 return lastCommit, latestInfo, rebuildAfter 283 } 284 285 // BuildInfo characterizes a kernel build. 286 type BuildInfo struct { 287 Time time.Time // when the build was done 288 Tag string // unique tag combined from compiler id, kernel commit and config tag 289 CompilerID string // compiler identity string (e.g. "gcc 7.1.1") 290 KernelRepo string 291 KernelBranch string 292 KernelCommit string // git hash of kernel checkout 293 KernelCommitTitle string 294 KernelCommitDate time.Time 295 KernelConfigTag string // SHA1 hash of .config contents 296 } 297 298 func loadBuildInfo(dir string) (*BuildInfo, error) { 299 info := new(BuildInfo) 300 if err := config.LoadFile(filepath.Join(dir, "tag"), info); err != nil { 301 return nil, err 302 } 303 return info, nil 304 } 305 306 // checkLatest checks if we have a good working latest build and returns its build info. 307 // If the build is missing/broken, nil is returned. 308 func (mgr *Manager) checkLatest() *BuildInfo { 309 if !osutil.FilesExist(mgr.latestDir, imageFiles) { 310 return nil 311 } 312 info, _ := loadBuildInfo(mgr.latestDir) 313 return info 314 } 315 316 func (mgr *Manager) createBuildInfo(kernelCommit *vcs.Commit, compilerID string) *BuildInfo { 317 var tagData []byte 318 tagData = append(tagData, mgr.name...) 319 tagData = append(tagData, kernelCommit.Hash...) 320 tagData = append(tagData, compilerID...) 321 tagData = append(tagData, mgr.configTag...) 322 return &BuildInfo{ 323 Time: time.Now(), 324 Tag: hash.String(tagData), 325 CompilerID: compilerID, 326 KernelRepo: mgr.mgrcfg.Repo, 327 KernelBranch: mgr.mgrcfg.Branch, 328 KernelCommit: kernelCommit.Hash, 329 KernelCommitTitle: kernelCommit.Title, 330 KernelCommitDate: kernelCommit.CommitDate, 331 KernelConfigTag: mgr.configTag, 332 } 333 } 334 335 func (mgr *Manager) build(kernelCommit *vcs.Commit) error { 336 // We first form the whole image in tmp dir and then rename it to latest. 337 tmpDir := mgr.latestDir + ".tmp" 338 if err := os.RemoveAll(tmpDir); err != nil { 339 return fmt.Errorf("failed to remove tmp dir: %w", err) 340 } 341 if err := osutil.MkdirAll(tmpDir); err != nil { 342 return fmt.Errorf("failed to create tmp dir: %w", err) 343 } 344 params := build.Params{ 345 TargetOS: mgr.managercfg.TargetOS, 346 TargetArch: mgr.managercfg.TargetVMArch, 347 VMType: mgr.managercfg.Type, 348 KernelDir: mgr.kernelBuildDir, 349 OutputDir: tmpDir, 350 Compiler: mgr.mgrcfg.Compiler, 351 Linker: mgr.mgrcfg.Linker, 352 Ccache: mgr.mgrcfg.Ccache, 353 UserspaceDir: mgr.mgrcfg.Userspace, 354 CmdlineFile: mgr.mgrcfg.KernelCmdline, 355 SysctlFile: mgr.mgrcfg.KernelSysctl, 356 Config: mgr.configData, 357 Build: mgr.mgrcfg.Build, 358 } 359 details, err := build.Image(params) 360 info := mgr.createBuildInfo(kernelCommit, details.CompilerID) 361 if err != nil { 362 rep := &report.Report{ 363 Title: fmt.Sprintf("%v build error", mgr.mgrcfg.RepoAlias), 364 } 365 var kernelError *build.KernelError 366 var verboseError *osutil.VerboseError 367 switch { 368 case errors.As(err, &kernelError): 369 rep.Report = kernelError.Report 370 rep.Output = kernelError.Output 371 rep.Recipients = kernelError.Recipients 372 case errors.As(err, &verboseError): 373 rep.Report = []byte(verboseError.Title) 374 rep.Output = verboseError.Output 375 default: 376 rep.Report = []byte(err.Error()) 377 } 378 if err := mgr.reportBuildError(rep, info, tmpDir); err != nil { 379 mgr.Errorf("failed to report image error: %v", err) 380 } 381 return fmt.Errorf("kernel build failed: %w", err) 382 } 383 384 if err := config.SaveFile(filepath.Join(tmpDir, "tag"), info); err != nil { 385 return fmt.Errorf("failed to write tag file: %w", err) 386 } 387 388 if err := mgr.testImage(tmpDir, info); err != nil { 389 return err 390 } 391 392 // Now try to replace latest with our tmp dir as atomically as we can get on Linux. 393 if err := os.RemoveAll(mgr.latestDir); err != nil { 394 return fmt.Errorf("failed to remove latest dir: %w", err) 395 } 396 return osutil.Rename(tmpDir, mgr.latestDir) 397 } 398 399 func (mgr *Manager) restartManager() { 400 if !osutil.FilesExist(mgr.latestDir, imageFiles) { 401 mgr.Errorf("can't start manager, image files missing") 402 return 403 } 404 if mgr.cmd != nil { 405 mgr.cmd.Close() 406 mgr.cmd = nil 407 } 408 if err := osutil.LinkFiles(mgr.latestDir, mgr.currentDir, imageFiles); err != nil { 409 mgr.Errorf("failed to create current image dir: %v", err) 410 return 411 } 412 info, err := loadBuildInfo(mgr.currentDir) 413 if err != nil { 414 mgr.Errorf("failed to load build info: %v", err) 415 return 416 } 417 buildTag, err := mgr.uploadBuild(info, mgr.currentDir) 418 if err != nil { 419 mgr.Errorf("failed to upload build: %v", err) 420 return 421 } 422 daysSinceCommit := time.Since(info.KernelCommitDate).Hours() / 24 423 if mgr.buildFailed && daysSinceCommit > float64(mgr.mgrcfg.MaxKernelLagDays) { 424 log.Logf(0, "%s: the kernel is now too old (%.1f days since last commit), fuzzing is stopped", 425 mgr.name, daysSinceCommit) 426 return 427 } 428 cfgFile, err := mgr.writeConfig(buildTag) 429 if err != nil { 430 mgr.Errorf("failed to create manager config: %v", err) 431 return 432 } 433 bin := filepath.FromSlash("syzkaller/current/bin/syz-manager") 434 logFile := filepath.Join(mgr.currentDir, "manager.log") 435 args := []string{"-config", cfgFile, "-vv", "1"} 436 if mgr.debug { 437 args = append(args, "-debug") 438 } 439 mgr.cmd = NewManagerCmd(mgr.name, logFile, mgr.Errorf, bin, args...) 440 } 441 442 func (mgr *Manager) testImage(imageDir string, info *BuildInfo) error { 443 log.Logf(0, "%v: testing image...", mgr.name) 444 mgrcfg, err := mgr.createTestConfig(imageDir, info) 445 if err != nil { 446 return fmt.Errorf("failed to create manager config: %w", err) 447 } 448 defer os.RemoveAll(mgrcfg.Workdir) 449 if !vm.AllowsOvercommit(mgrcfg.Type) { 450 return nil // No support for creating machines out of thin air. 451 } 452 env, err := instance.NewEnv(mgrcfg, buildSem, testSem) 453 if err != nil { 454 return err 455 } 456 const ( 457 testVMs = 3 458 maxFailures = 1 459 ) 460 results, err := env.Test(testVMs, nil, nil, nil) 461 if err != nil { 462 return err 463 } 464 failures := 0 465 var failureErr error 466 for _, res := range results { 467 if res.Error == nil { 468 continue 469 } 470 failures++ 471 var err *instance.TestError 472 switch { 473 case errors.As(res.Error, &err): 474 if rep := err.Report; rep != nil { 475 what := "test" 476 if err.Boot { 477 what = "boot" 478 } 479 rep.Title = fmt.Sprintf("%v %v error: %v", 480 mgr.mgrcfg.RepoAlias, what, rep.Title) 481 // There are usually no duplicates for boot errors, so we reset AltTitles. 482 // But if we pass them, we would need to add the same prefix as for Title 483 // in order to avoid duping boot bugs with non-boot bugs. 484 rep.AltTitles = nil 485 if err := mgr.reportBuildError(rep, info, imageDir); err != nil { 486 mgr.Errorf("failed to report image error: %v", err) 487 } 488 } 489 if err.Boot { 490 failureErr = fmt.Errorf("VM boot failed with: %w", err) 491 } else { 492 failureErr = fmt.Errorf("VM testing failed with: %w", err) 493 } 494 default: 495 failureErr = res.Error 496 } 497 } 498 if failures > maxFailures { 499 return failureErr 500 } 501 return nil 502 } 503 504 func (mgr *Manager) reportBuildError(rep *report.Report, info *BuildInfo, imageDir string) error { 505 if mgr.dash == nil { 506 log.Logf(0, "%v: image testing failed: %v\n\n%s\n\n%s", 507 mgr.name, rep.Title, rep.Report, rep.Output) 508 return nil 509 } 510 build, err := mgr.createDashboardBuild(info, imageDir, "error") 511 if err != nil { 512 return err 513 } 514 if mgr.storage != nil { 515 // We have to send assets together with the other info because the report 516 // might be generated immediately. 517 uploadedAssets, err := mgr.uploadBuildAssets(build, imageDir) 518 if err == nil { 519 build.Assets = uploadedAssets 520 } else { 521 log.Logf(0, "%v: failed to upload build assets: %s", mgr.name, err) 522 } 523 } 524 req := &dashapi.BuildErrorReq{ 525 Build: *build, 526 Crash: dashapi.Crash{ 527 Title: rep.Title, 528 AltTitles: rep.AltTitles, 529 Corrupted: false, // Otherwise they get merged with other corrupted reports. 530 Recipients: rep.Recipients.ToDash(), 531 Log: rep.Output, 532 Report: rep.Report, 533 }, 534 } 535 if rep.GuiltyFile != "" { 536 req.Crash.GuiltyFiles = []string{rep.GuiltyFile} 537 } 538 if err := mgr.dash.ReportBuildError(req); err != nil { 539 return err 540 } 541 return nil 542 } 543 544 func (mgr *Manager) createTestConfig(imageDir string, info *BuildInfo) (*mgrconfig.Config, error) { 545 mgrcfg := new(mgrconfig.Config) 546 *mgrcfg = *mgr.managercfg 547 mgrcfg.Name += "-test" 548 mgrcfg.Tag = info.KernelCommit 549 mgrcfg.Workdir = filepath.Join(imageDir, "workdir") 550 if err := instance.SetConfigImage(mgrcfg, imageDir, true); err != nil { 551 return nil, err 552 } 553 mgrcfg.KernelSrc = mgr.kernelSrcDir 554 if err := mgrconfig.Complete(mgrcfg); err != nil { 555 return nil, fmt.Errorf("bad manager config: %w", err) 556 } 557 return mgrcfg, nil 558 } 559 560 func (mgr *Manager) writeConfig(buildTag string) (string, error) { 561 mgrcfg := new(mgrconfig.Config) 562 *mgrcfg = *mgr.managercfg 563 564 if mgr.dash != nil { 565 mgrcfg.DashboardClient = mgr.mgrcfg.DashboardClient 566 mgrcfg.DashboardAddr = mgr.cfg.DashboardAddr 567 mgrcfg.DashboardKey = mgr.mgrcfg.DashboardKey 568 mgrcfg.AssetStorage = mgr.cfg.AssetStorage 569 } 570 if mgr.cfg.HubAddr != "" { 571 mgrcfg.HubClient = mgr.cfg.Name 572 mgrcfg.HubAddr = mgr.cfg.HubAddr 573 mgrcfg.HubKey = mgr.cfg.HubKey 574 } 575 mgrcfg.Tag = buildTag 576 mgrcfg.Workdir = mgr.workDir 577 // There's not much point in keeping disabled progs in the syz-ci corpuses. 578 // If the syscalls on some instance are enabled again, syz-hub will provide 579 // it with the missing progs over time. 580 // And, on the other hand, PreserveCorpus=false lets us disable syscalls in 581 // the least destructive way for the rest of the corpus - calls will be cut 582 // out the of programs and the leftovers will be retriaged. 583 mgrcfg.PreserveCorpus = false 584 if err := instance.SetConfigImage(mgrcfg, mgr.currentDir, false); err != nil { 585 return "", err 586 } 587 // Strictly saying this is somewhat racy as builder can concurrently 588 // update the source, or even delete and re-clone. If this causes 589 // problems, we need to make a copy of sources after build. 590 mgrcfg.KernelSrc = mgr.kernelSrcDir 591 if err := mgrconfig.Complete(mgrcfg); err != nil { 592 return "", fmt.Errorf("bad manager config: %w", err) 593 } 594 configFile := filepath.Join(mgr.currentDir, "manager.cfg") 595 if err := config.SaveFile(configFile, mgrcfg); err != nil { 596 return "", err 597 } 598 return configFile, nil 599 } 600 601 func (mgr *Manager) uploadBuild(info *BuildInfo, imageDir string) (string, error) { 602 if mgr.dash == nil { 603 // Dashboard identifies builds by unique tags that are combined 604 // from kernel tag, compiler tag and config tag. 605 // This combined tag is meaningless without dashboard, 606 // so we use kenrel tag (commit tag) because it communicates 607 // at least some useful information. 608 return info.KernelCommit, nil 609 } 610 611 build, err := mgr.createDashboardBuild(info, imageDir, "normal") 612 if err != nil { 613 return "", err 614 } 615 mgr.lastBuild = build 616 commitTitles, fixCommits, err := mgr.pollCommits(info.KernelCommit) 617 if err != nil { 618 // This is not critical for operation. 619 mgr.Errorf("failed to poll commits: %v", err) 620 } 621 build.Commits = commitTitles 622 build.FixCommits = fixCommits 623 if mgr.storage != nil { 624 // We always upload build assets -- we create a separate Build object not just for 625 // different kernel commits, but also for different syzkaller commits, configs, etc. 626 // Since we deduplicate assets by hashing, this should not be a problem -- no assets 627 // will be actually duplicated, only the records in the DB. 628 assets, err := mgr.uploadBuildAssets(build, imageDir) 629 if err != nil { 630 mgr.Errorf("failed to upload build assets: %v", err) 631 return "", err 632 } 633 build.Assets = assets 634 } 635 if err := mgr.dash.UploadBuild(build); err != nil { 636 return "", err 637 } 638 return build.ID, nil 639 } 640 641 func (mgr *Manager) createDashboardBuild(info *BuildInfo, imageDir, typ string) (*dashapi.Build, error) { 642 var kernelConfig []byte 643 if kernelConfigFile := filepath.Join(imageDir, "kernel.config"); osutil.IsExist(kernelConfigFile) { 644 var err error 645 if kernelConfig, err = os.ReadFile(kernelConfigFile); err != nil { 646 return nil, fmt.Errorf("failed to read kernel.config: %w", err) 647 } 648 } 649 // Resulting build depends on both kernel build tag and syzkaller commmit. 650 // Also mix in build type, so that image error builds are not merged into normal builds. 651 var tagData []byte 652 tagData = append(tagData, info.Tag...) 653 tagData = append(tagData, prog.GitRevisionBase...) 654 tagData = append(tagData, typ...) 655 build := &dashapi.Build{ 656 Manager: mgr.name, 657 ID: hash.String(tagData), 658 OS: mgr.managercfg.TargetOS, 659 Arch: mgr.managercfg.TargetArch, 660 VMArch: mgr.managercfg.TargetVMArch, 661 SyzkallerCommit: prog.GitRevisionBase, 662 SyzkallerCommitDate: prog.GitRevisionDate, 663 CompilerID: info.CompilerID, 664 KernelRepo: info.KernelRepo, 665 KernelBranch: info.KernelBranch, 666 KernelCommit: info.KernelCommit, 667 KernelCommitTitle: info.KernelCommitTitle, 668 KernelCommitDate: info.KernelCommitDate, 669 KernelConfig: kernelConfig, 670 } 671 return build, nil 672 } 673 674 // pollCommits asks dashboard what commits it is interested in (i.e. fixes for 675 // open bugs) and returns subset of these commits that are present in a build 676 // on commit buildCommit. 677 func (mgr *Manager) pollCommits(buildCommit string) ([]string, []dashapi.Commit, error) { 678 resp, err := mgr.dash.BuilderPoll(mgr.name) 679 if err != nil || len(resp.PendingCommits) == 0 && resp.ReportEmail == "" { 680 return nil, nil, err 681 } 682 683 // We don't want to spend too much time querying commits from the history, 684 // so let's pick a random subset of them each time. 685 const sampleCommits = 25 686 687 pendingCommits := resp.PendingCommits 688 if len(pendingCommits) > sampleCommits { 689 rand.New(rand.NewSource(time.Now().UnixNano())).Shuffle( 690 len(pendingCommits), func(i, j int) { 691 pendingCommits[i], pendingCommits[j] = 692 pendingCommits[j], pendingCommits[i] 693 }) 694 pendingCommits = pendingCommits[:sampleCommits] 695 } 696 697 var present []string 698 if len(pendingCommits) != 0 { 699 commits, _, err := mgr.repo.GetCommitsByTitles(pendingCommits) 700 if err != nil { 701 return nil, nil, err 702 } 703 m := make(map[string]bool, len(commits)) 704 for _, com := range commits { 705 m[vcs.CanonicalizeCommit(com.Title)] = true 706 } 707 for _, com := range pendingCommits { 708 if m[vcs.CanonicalizeCommit(com)] { 709 present = append(present, com) 710 } 711 } 712 } 713 var fixCommits []dashapi.Commit 714 if resp.ReportEmail != "" { 715 if !brokenRepo(mgr.mgrcfg.Repo) { 716 commits, err := mgr.repo.ExtractFixTagsFromCommits(buildCommit, resp.ReportEmail) 717 if err != nil { 718 return nil, nil, err 719 } 720 for _, com := range commits { 721 fixCommits = append(fixCommits, dashapi.Commit{ 722 Title: com.Title, 723 BugIDs: com.Tags, 724 Date: com.Date, 725 }) 726 } 727 } 728 } 729 return present, fixCommits, nil 730 } 731 732 func (mgr *Manager) backportCommits() []vcs.BackportCommit { 733 return append( 734 append([]vcs.BackportCommit{}, mgr.cfg.BisectBackports...), 735 mgr.mgrcfg.BisectBackports..., 736 ) 737 } 738 739 func (mgr *Manager) uploadBuildAssets(buildInfo *dashapi.Build, assetFolder string) ([]dashapi.NewAsset, error) { 740 if mgr.storage == nil { 741 // No reason to continue anyway. 742 return nil, fmt.Errorf("asset storage is not configured") 743 } 744 type pendingAsset struct { 745 path string 746 assetType dashapi.AssetType 747 name string 748 } 749 pending := []pendingAsset{} 750 kernelFile := filepath.Join(assetFolder, "kernel") 751 if osutil.IsExist(kernelFile) { 752 fileName := "kernel" 753 if buildInfo.OS == targets.Linux { 754 fileName = path.Base(build.LinuxKernelImage(buildInfo.Arch)) 755 } 756 pending = append(pending, pendingAsset{kernelFile, dashapi.KernelImage, fileName}) 757 } 758 imageFile := filepath.Join(assetFolder, "image") 759 if osutil.IsExist(imageFile) { 760 if mgr.managercfg.Type == "qemu" { 761 // For qemu we currently use non-bootable disk images. 762 pending = append(pending, pendingAsset{imageFile, dashapi.NonBootableDisk, 763 "non_bootable_disk.raw"}) 764 } else { 765 pending = append(pending, pendingAsset{imageFile, dashapi.BootableDisk, 766 "disk.raw"}) 767 } 768 } 769 target := mgr.managercfg.SysTarget 770 kernelObjFile := filepath.Join(assetFolder, "obj", target.KernelObject) 771 if osutil.IsExist(kernelObjFile) { 772 pending = append(pending, 773 pendingAsset{kernelObjFile, dashapi.KernelObject, target.KernelObject}) 774 } 775 // TODO: add initrd? 776 ret := []dashapi.NewAsset{} 777 for _, pendingAsset := range pending { 778 if !mgr.storage.AssetTypeEnabled(pendingAsset.assetType) { 779 continue 780 } 781 file, err := os.Open(pendingAsset.path) 782 if err != nil { 783 log.Logf(0, "failed to open an asset for uploading: %s, %s", 784 pendingAsset.path, err) 785 continue 786 } 787 if mgr.debugStorage { 788 log.Logf(0, "uploading an asset %s of type %s", 789 pendingAsset.path, pendingAsset.assetType) 790 } 791 extra := &asset.ExtraUploadArg{SkipIfExists: true} 792 hash := sha256.New() 793 if _, err := io.Copy(hash, file); err != nil { 794 log.Logf(0, "failed calculate hash for the asset %s: %s", pendingAsset.path, err) 795 continue 796 } 797 extra.UniqueTag = fmt.Sprintf("%x", hash.Sum(nil)) 798 // Now we need to go back to the beginning of the file again. 799 if _, err := file.Seek(0, io.SeekStart); err != nil { 800 log.Logf(0, "failed wind back the opened file for %s: %s", pendingAsset.path, err) 801 continue 802 } 803 info, err := mgr.storage.UploadBuildAsset(file, pendingAsset.name, 804 pendingAsset.assetType, buildInfo, extra) 805 if err != nil { 806 log.Logf(0, "failed to upload an asset: %s, %s", 807 pendingAsset.path, err) 808 continue 809 } else if mgr.debugStorage { 810 log.Logf(0, "uploaded an asset: %#v", info) 811 } 812 ret = append(ret, info) 813 } 814 return ret, nil 815 } 816 817 func (mgr *Manager) httpGET(path string) (resp *http.Response, err error) { 818 addr := mgr.managercfg.HTTP 819 if addr != "" && addr[0] == ':' { 820 addr = "127.0.0.1" + addr // in case addr is ":port" 821 } 822 client := &http.Client{ 823 Timeout: time.Hour, 824 } 825 return client.Get(fmt.Sprintf("http://%s%s", addr, path)) 826 } 827 828 func (mgr *Manager) uploadCoverReport() error { 829 directUpload := mgr.managercfg.Cover && mgr.cfg.CoverUploadPath != "" 830 if mgr.storage == nil && !directUpload { 831 // Cover report uploading is disabled. 832 return nil 833 } 834 if mgr.storage != nil && directUpload { 835 return fmt.Errorf("cover report must be either uploaded directly or via asset storage") 836 } 837 // Report generation can consume lots of memory. Generate one at a time. 838 select { 839 case <-buildSem.WaitC(): 840 case <-mgr.stop: 841 return nil 842 } 843 defer buildSem.Signal() 844 845 resp, err := mgr.httpGET("/cover") 846 if err != nil { 847 return fmt.Errorf("failed to get report: %w", err) 848 } 849 defer resp.Body.Close() 850 if directUpload { 851 return mgr.uploadFile(mgr.cfg.CoverUploadPath, mgr.name+".html", resp.Body, true) 852 } 853 // Upload via the asset storage. 854 newAsset, err := mgr.storage.UploadBuildAsset(resp.Body, mgr.name+".html", 855 dashapi.HTMLCoverageReport, mgr.lastBuild, nil) 856 if err != nil { 857 return fmt.Errorf("failed to upload html coverage report: %w", err) 858 } 859 err = mgr.storage.ReportBuildAssets(mgr.lastBuild, newAsset) 860 if err != nil { 861 return fmt.Errorf("failed to report the html coverage report asset: %w", err) 862 } 863 return nil 864 } 865 866 func (mgr *Manager) uploadCoverStat(fuzzingMinutes int) error { 867 if !mgr.managercfg.Cover || mgr.cfg.CoverPipelinePath == "" { 868 return nil 869 } 870 871 // Report generation consumes 40G RAM. Generate one at a time. 872 // TODO: remove it once #4585 (symbolization tuning) is closed 873 select { 874 case <-buildSem.WaitC(): 875 case <-mgr.stop: 876 return nil 877 } 878 defer buildSem.Signal() 879 880 // Coverage report generation consumes and caches lots of memory. 881 // In the syz-ci context report generation won't be used after this point, 882 // so tell manager to flush report generator. 883 resp, err := mgr.httpGET("/cover?jsonl=1&flush=1") 884 if err != nil { 885 return fmt.Errorf("failed to httpGet /cover?jsonl=1 report: %w", err) 886 } 887 defer resp.Body.Close() 888 if resp.StatusCode != http.StatusOK { 889 sb := new(strings.Builder) 890 io.Copy(sb, resp.Body) 891 return fmt.Errorf("failed to GET /cover?jsonl=1, httpStatus %d: %s", 892 resp.StatusCode, sb.String()) 893 } 894 895 curTime := time.Now() 896 pr, pw := io.Pipe() 897 defer pr.Close() 898 go func() { 899 decoder := json.NewDecoder(resp.Body) 900 for decoder.More() { 901 var covInfo cover.CoverageInfo 902 if err := decoder.Decode(&covInfo); err != nil { 903 pw.CloseWithError(fmt.Errorf("failed to decode CoverageInfo: %w", err)) 904 return 905 } 906 if err := cover.WriteCIJSONLine(pw, covInfo, cover.CIDetails{ 907 Version: 1, 908 Timestamp: curTime.Format(time.RFC3339Nano), 909 FuzzingMinutes: fuzzingMinutes, 910 Arch: mgr.lastBuild.Arch, 911 BuildID: mgr.lastBuild.ID, 912 Manager: mgr.name, 913 KernelRepo: mgr.lastBuild.KernelRepo, 914 KernelBranch: mgr.lastBuild.KernelBranch, 915 KernelCommit: mgr.lastBuild.KernelCommit, 916 }); err != nil { 917 pw.CloseWithError(fmt.Errorf("failed to write CIJSONLine: %w", err)) 918 return 919 } 920 } 921 pw.Close() 922 }() 923 fileName := fmt.Sprintf("%s/%s-%s-%d-%d.jsonl", 924 mgr.mgrcfg.DashboardClient, 925 mgr.name, curTime.Format(time.DateOnly), 926 curTime.Hour(), curTime.Minute()) 927 if err := mgr.uploadFile(mgr.cfg.CoverPipelinePath, fileName, pr, false); err != nil { 928 return fmt.Errorf("failed to uploadFileGCS(): %w", err) 929 } 930 return nil 931 } 932 933 func (mgr *Manager) uploadCorpus() error { 934 if mgr.cfg.CorpusUploadPath == "" { 935 return nil 936 } 937 f, err := os.Open(filepath.Join(mgr.workDir, "corpus.db")) 938 if err != nil { 939 return err 940 } 941 defer f.Close() 942 return mgr.uploadFile(mgr.cfg.CorpusUploadPath, mgr.name+"-corpus.db", f, true) 943 } 944 945 func (mgr *Manager) uploadFile(dstPath, name string, file io.Reader, allowPublishing bool) error { 946 URL, err := url.Parse(dstPath) 947 if err != nil { 948 return fmt.Errorf("failed to parse upload path: %w", err) 949 } 950 URL.Path = path.Join(URL.Path, name) 951 URLStr := URL.String() 952 log.Logf(0, "uploading %v to %v", name, URLStr) 953 if strings.HasPrefix(URLStr, "http://") || 954 strings.HasPrefix(URLStr, "https://") { 955 return uploadFileHTTPPut(URLStr, file) 956 } 957 return uploadFileGCS(URLStr, file, allowPublishing && mgr.cfg.PublishGCS) 958 } 959 960 func uploadFileGCS(URL string, file io.Reader, publish bool) error { 961 URL = strings.TrimPrefix(URL, "gs://") 962 GCS, err := gcs.NewClient() 963 if err != nil { 964 return fmt.Errorf("failed to create GCS client: %w", err) 965 } 966 defer GCS.Close() 967 gcsWriter, err := GCS.FileWriter(URL) 968 if err != nil { 969 return fmt.Errorf("failed to create GCS writer: %w", err) 970 } 971 if _, err := io.Copy(gcsWriter, file); err != nil { 972 gcsWriter.Close() 973 return fmt.Errorf("failed to copy report: %w", err) 974 } 975 if err := gcsWriter.Close(); err != nil { 976 return fmt.Errorf("failed to close gcs writer: %w", err) 977 } 978 if publish { 979 return GCS.Publish(URL) 980 } 981 return nil 982 } 983 984 func uploadFileHTTPPut(URL string, file io.Reader) error { 985 req, err := http.NewRequest(http.MethodPut, URL, file) 986 if err != nil { 987 return fmt.Errorf("failed to create HTTP PUT request: %w", err) 988 } 989 client := &http.Client{} 990 resp, err := client.Do(req) 991 if err != nil { 992 return fmt.Errorf("failed to perform HTTP PUT request: %w", err) 993 } 994 defer resp.Body.Close() 995 if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { 996 return fmt.Errorf("HTTP PUT failed with status code: %v", resp.StatusCode) 997 } 998 return nil 999 } 1000 1001 // Errorf logs non-fatal error and sends it to dashboard. 1002 func (mgr *Manager) Errorf(msg string, args ...interface{}) { 1003 log.Errorf(mgr.name+": "+msg, args...) 1004 if mgr.dash != nil { 1005 mgr.dash.LogError(mgr.name, msg, args...) 1006 } 1007 } 1008 1009 func (mgr *ManagerConfig) validate(cfg *Config) error { 1010 // Manager name must not contain dots because it is used as GCE image name prefix. 1011 managerNameRe := regexp.MustCompile("^[a-zA-Z0-9-_]{3,64}$") 1012 if !managerNameRe.MatchString(mgr.Name) { 1013 return fmt.Errorf("param 'managers.name' has bad value: %q", mgr.Name) 1014 } 1015 if mgr.Jobs.AnyEnabled() && (cfg.DashboardAddr == "" || cfg.DashboardClient == "") { 1016 return fmt.Errorf("manager %v: has jobs but no dashboard info", mgr.Name) 1017 } 1018 if mgr.Jobs.PollCommits && (cfg.DashboardAddr == "" || mgr.DashboardClient == "") { 1019 return fmt.Errorf("manager %v: commit_poll is set but no dashboard info", mgr.Name) 1020 } 1021 if (mgr.Jobs.BisectCause || mgr.Jobs.BisectFix) && cfg.BisectBinDir == "" { 1022 return fmt.Errorf("manager %v: enabled bisection but no bisect_bin_dir", mgr.Name) 1023 } 1024 return nil 1025 }