github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-ci/syz-ci.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 // syz-ci is a continuous fuzzing system for syzkaller. 5 // It runs several syz-manager's, polls and rebuilds images for managers 6 // and polls and rebuilds syzkaller binaries. 7 // For usage instructions see: docs/ci.md. 8 package main 9 10 // Implementation details: 11 // 12 // 2 main components: 13 // - SyzUpdater: handles syzkaller updates 14 // - Manager: handles kernel build and syz-manager process (one per manager) 15 // Both operate in a similar way and keep 2 builds: 16 // - latest: latest known good build (i.e. we tested it) 17 // preserved across restarts/reboots, i.e. we can start fuzzing even when 18 // current syzkaller/kernel git head is broken, or git is down, or anything else 19 // - current: currently used build (a copy of one of the latest builds) 20 // Other important points: 21 // - syz-ci is always built on the same revision as the rest of syzkaller binaries, 22 // this allows us to handle e.g. changes in manager config format. 23 // - consequently, syzkaller binaries are never updated on-the-fly, 24 // instead we re-exec and then update 25 // - we understand when the latest build is fresh even after reboot, 26 // i.e. we store enough information to identify it (git hash, compiler identity, etc), 27 // so we don't rebuild unnecessary (kernel builds take time) 28 // - we generally avoid crashing the process and handle all errors gracefully 29 // (this is a continuous system), except for some severe/user errors during start 30 // (e.g. bad config file, or can't create necessary dirs) 31 // 32 // Directory/file structure: 33 // syz-ci : current executable 34 // syzkaller/ 35 // latest/ : latest good syzkaller build 36 // current/ : syzkaller build currently in use 37 // managers/ 38 // manager1/ : one dir per manager 39 // kernel/ : kernel checkout 40 // workdir/ : manager workdir (never deleted) 41 // latest/ : latest good kernel image build 42 // current/ : kernel image currently in use 43 // jobs/ 44 // linux/ : one dir per target OS 45 // kernel/ : kernel checkout 46 // image/ : currently used image 47 // workdir/ : some temp files 48 // 49 // Current executable, syzkaller and kernel builds are marked with tag files. 50 // Tag files uniquely identify the build (git hash, compiler identity, kernel config, etc). 51 // For tag files both contents and modification time are important, 52 // modification time allows us to understand if we need to rebuild after a restart. 53 54 import ( 55 "context" 56 "encoding/json" 57 "errors" 58 "flag" 59 "fmt" 60 "net" 61 "net/http" 62 _ "net/http/pprof" 63 "os" 64 "path/filepath" 65 "strings" 66 "sync" 67 "time" 68 69 "github.com/google/syzkaller/dashboard/dashapi" 70 "github.com/google/syzkaller/pkg/asset" 71 "github.com/google/syzkaller/pkg/config" 72 "github.com/google/syzkaller/pkg/log" 73 "github.com/google/syzkaller/pkg/mgrconfig" 74 "github.com/google/syzkaller/pkg/osutil" 75 "github.com/google/syzkaller/pkg/updater" 76 "github.com/google/syzkaller/pkg/vcs" 77 ) 78 79 var ( 80 flagConfig = flag.String("config", "", "config file") 81 flagAutoUpdate = flag.Bool("autoupdate", true, "auto-update the binary (for testing)") 82 flagManagers = flag.Bool("managers", true, "start managers (for testing)") 83 flagDebug = flag.Bool("debug", false, "debug mode (for testing)") 84 // nolint: lll 85 flagExitOnUpgrade = flag.Bool("exit-on-upgrade", false, "exit after a syz-ci upgrade is applied; otherwise syz-ci restarts") 86 ) 87 88 type Config struct { 89 Name string `json:"name"` 90 HTTP string `json:"http"` 91 // If manager http address is not specified, give it an address starting from this port. Optional. 92 // This is also used to auto-assign ports for test instances. 93 ManagerPort int `json:"manager_port_start"` 94 // If manager rpc address is not specified, give it addresses starting from this port. By default 30000. 95 // This is also used to auto-assign ports for test instances. 96 RPCPort int `json:"rpc_port_start"` 97 DashboardAddr string `json:"dashboard_addr"` // Optional. 98 DashboardClient string `json:"dashboard_client"` // Optional. 99 DashboardKey string `json:"dashboard_key"` // Optional. 100 HubAddr string `json:"hub_addr"` // Optional. 101 HubKey string `json:"hub_key"` // Optional. 102 Goroot string `json:"goroot"` // Go 1.8+ toolchain dir. 103 SyzkallerRepo string `json:"syzkaller_repo"` 104 SyzkallerBranch string `json:"syzkaller_branch"` // Defaults to "master". 105 // Dir with additional syscall descriptions. 106 // - *.txt and *.const files are copied to syzkaller/sys/linux/ 107 // - *.test files are copied to syzkaller/sys/linux/test/ 108 // - *.h files are copied to syzkaller/executor/ 109 SyzkallerDescriptions string `json:"syzkaller_descriptions"` 110 // Path to upload coverage reports from managers (optional). 111 // Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://). 112 CoverUploadPath string `json:"cover_upload_path"` 113 // Path to upload managers syz programs and their coverage in jsonl (optional). 114 CoverProgramsPath string `json:"cover_programs_path"` 115 // Path to upload json coverage reports from managers (optional). 116 CoverPipelinePath string `json:"cover_pipeline_path"` 117 // Path to upload corpus.db from managers (optional). 118 // Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://). 119 CorpusUploadPath string `json:"corpus_upload_path"` 120 // Make files uploaded via CoverUploadPath, CorpusUploadPath and CoverProgramsPath public. 121 PublishGCS bool `json:"publish_gcs"` 122 // Path to upload bench data from instances (optional). 123 // Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://). 124 BenchUploadPath string `json:"bench_upload_path"` 125 // BinDir must point to a dir that contains compilers required to build 126 // older versions of the kernel. For linux, it needs to include several 127 // compiler versions. 128 BisectBinDir string `json:"bisect_bin_dir"` 129 // Keys of BisectIgnore are full commit hashes that should never be reported 130 // in bisection results. 131 // Values of the map are ignored and can e.g. serve as comments. 132 BisectIgnore map[string]string `json:"bisect_ignore"` 133 // Extra commits to cherry-pick to older kernel revisions. 134 // The list is concatenated with the similar parameter from ManagerConfig. 135 BisectBackports []vcs.BackportCommit `json:"bisect_backports"` 136 Ccache string `json:"ccache"` 137 // BuildCPUs defines the maximum number of parallel kernel build threads. 138 BuildCPUs int `json:"build_cpus"` 139 Managers []*ManagerConfig `json:"managers"` 140 // Poll period for jobs in seconds (optional, defaults to 10 seconds) 141 JobPollPeriod int `json:"job_poll_period"` 142 // Set up a second (parallel) job processor to speed up processing. 143 // For now, this second job processor only handles patch testing requests. 144 ParallelJobs bool `json:"parallel_jobs"` 145 // Poll period for commits in seconds (optional, defaults to 3600 seconds) 146 CommitPollPeriod int `json:"commit_poll_period"` 147 // Asset Storage config. 148 AssetStorage *asset.Config `json:"asset_storage"` 149 // Per-vm type JSON diffs that will be applied to every instace of the 150 // corresponding VM type. 151 PatchVMConfigs map[string]json.RawMessage `json:"patch_vm_configs"` 152 // Some commits don't live long. 153 // Push all commits used in kernel builds to this git repo URL. 154 // The archive is later used by coverage merger. 155 GitArchive string `json:"git_archive"` 156 } 157 158 type ManagerConfig struct { 159 // If Name is specified, syz-manager name is set to Config.Name-ManagerConfig.Name. 160 // This is old naming scheme, it does not allow to move managers between ci instances. 161 // For new naming scheme set ManagerConfig.ManagerConfig.Name instead and leave this field empty. 162 // This allows to move managers as their name does not depend on cfg.Name. 163 // Generally, if you have: 164 // { 165 // "name": "ci", 166 // "managers": [ 167 // { 168 // "name": "foo", 169 // ... 170 // } 171 // ] 172 // } 173 // you want to change it to: 174 // { 175 // "name": "ci", 176 // "managers": [ 177 // { 178 // ... 179 // "manager_config": { 180 // "name": "ci-foo" 181 // } 182 // } 183 // ] 184 // } 185 // and rename managers/foo to managers/ci-foo. Then this instance can be moved 186 // to another ci along with managers/ci-foo dir. 187 Name string `json:"name"` 188 Disabled string `json:"disabled"` // If not empty, don't build/start this manager. 189 DashboardClient string `json:"dashboard_client"` // Optional. 190 DashboardKey string `json:"dashboard_key"` // Optional. 191 Repo string `json:"repo"` 192 // Short name of the repo (e.g. "linux-next"), used only for reporting. 193 RepoAlias string `json:"repo_alias"` 194 Branch string `json:"branch"` // Defaults to "master". 195 // Currently either 'gcc' or 'clang'. Note that pkg/bisect requires 196 // explicit plumbing for every os/compiler combination. 197 CompilerType string `json:"compiler_type"` // Defaults to "gcc" 198 Compiler string `json:"compiler"` 199 Make string `json:"make"` 200 Linker string `json:"linker"` 201 Ccache string `json:"ccache"` 202 Userspace string `json:"userspace"` 203 KernelConfig string `json:"kernel_config"` 204 // KernelSrcSuffix adds a suffix to the kernel_src manager config. This is needed for cases where 205 // the kernel source root as reported in the coverage UI is a subdirectory of the VCS root. 206 KernelSrcSuffix string `json:"kernel_src_suffix"` 207 // Build-type-specific parameters. 208 // Parameters for concrete types are in Config type in pkg/build/TYPE.go, e.g. pkg/build/android.go. 209 Build json.RawMessage `json:"build"` 210 // Baseline config for bisection, see pkg/bisect.KernelConfig.BaselineConfig. 211 // If not specified, syz-ci generates a `-base.config` path counterpart for `kernel_config` and, 212 // if it exists, uses it as default. 213 KernelBaselineConfig string `json:"kernel_baseline_config"` 214 // File with kernel cmdline values (optional). 215 KernelCmdline string `json:"kernel_cmdline"` 216 // File with sysctl values (e.g. output of sysctl -a, optional). 217 KernelSysctl string `json:"kernel_sysctl"` 218 Jobs ManagerJobs `json:"jobs"` 219 // Extra commits to cherry pick to older kernel revisions. 220 BisectBackports []vcs.BackportCommit `json:"bisect_backports"` 221 // Base syz-manager config for the instance. 222 ManagerConfig json.RawMessage `json:"manager_config"` 223 // By default we want to archive git commits. 224 // This opt-out is needed for *BSD systems. 225 DisableGitArchive bool `json:"disable_git_archive"` 226 // If the kernel's commit is older than MaxKernelLagDays days, 227 // fuzzing won't be started on this instance. 228 // By default it's 30 days. 229 MaxKernelLagDays int `json:"max_kernel_lag_days"` 230 managercfg *mgrconfig.Config 231 232 // Auto-assigned ports used by test instances. 233 testRPCPort int 234 } 235 236 type ManagerJobs struct { 237 TestPatches bool `json:"test_patches"` // enable patch testing jobs 238 PollCommits bool `json:"poll_commits"` // poll info about fix commits 239 BisectCause bool `json:"bisect_cause"` // do cause bisection 240 BisectFix bool `json:"bisect_fix"` // do fix bisection 241 } 242 243 func (m *ManagerJobs) AnyEnabled() bool { 244 return m.TestPatches || m.PollCommits || m.BisectCause || m.BisectFix 245 } 246 247 func (m *ManagerJobs) Filter(filter *ManagerJobs) *ManagerJobs { 248 return &ManagerJobs{ 249 TestPatches: m.TestPatches && filter.TestPatches, 250 PollCommits: m.PollCommits && filter.PollCommits, 251 BisectCause: m.BisectCause && filter.BisectCause, 252 BisectFix: m.BisectFix && filter.BisectFix, 253 } 254 } 255 256 func main() { 257 flag.Parse() 258 log.EnableLogCaching(1000, 1<<20) 259 cfg, err := loadConfig(*flagConfig) 260 if err != nil { 261 log.Fatalf("failed to load config: %v", err) 262 } 263 log.SetName(cfg.Name) 264 265 shutdownPending := make(chan struct{}) 266 osutil.HandleInterrupts(shutdownPending) 267 268 serveHTTP(cfg) 269 270 if cfg.Goroot != "" { 271 os.Setenv("GOROOT", cfg.Goroot) 272 os.Setenv("PATH", filepath.Join(cfg.Goroot, "bin")+ 273 string(filepath.ListSeparator)+os.Getenv("PATH")) 274 } 275 276 updatePending := make(chan struct{}) 277 updateTargets := make(map[updater.Target]bool) 278 for _, mgr := range cfg.Managers { 279 updateTargets[updater.Target{ 280 OS: mgr.managercfg.TargetOS, 281 VMArch: mgr.managercfg.TargetVMArch, 282 Arch: mgr.managercfg.TargetArch, 283 }] = true 284 } 285 updater, err := updater.New(&updater.Config{ 286 ExitOnUpdate: *flagExitOnUpgrade, 287 BuildSem: buildSem, 288 ReportBuildError: func(commit *vcs.Commit, compilerID string, buildErr error) { 289 uploadSyzkallerBuildError(cfg, commit, compilerID, buildErr) 290 }, 291 SyzkallerRepo: cfg.SyzkallerRepo, 292 SyzkallerBranch: cfg.SyzkallerBranch, 293 SyzkallerDescriptions: cfg.SyzkallerDescriptions, 294 Targets: updateTargets, 295 }) 296 if err != nil { 297 log.Fatal(err) 298 } 299 updater.UpdateOnStart(*flagAutoUpdate, updatePending, shutdownPending) 300 301 ctx, stop := context.WithCancel(context.Background()) 302 var managers []*Manager 303 for _, mgrcfg := range cfg.Managers { 304 mgr, err := createManager(cfg, mgrcfg, *flagDebug) 305 if err != nil { 306 log.Errorf("failed to create manager %v: %v", mgrcfg.Name, err) 307 continue 308 } 309 managers = append(managers, mgr) 310 } 311 if len(managers) == 0 { 312 log.Fatalf("failed to create all managers") 313 } 314 var wg sync.WaitGroup 315 if *flagManagers { 316 for _, mgr := range managers { 317 wg.Add(1) 318 go func() { 319 defer wg.Done() 320 mgr.loop(ctx) 321 }() 322 } 323 } 324 325 ctxJobs, stopJobs := context.WithCancel(ctx) 326 wgJobs := sync.WaitGroup{} 327 if cfg.DashboardAddr != "" { 328 jm, err := newJobManager(cfg, managers, shutdownPending) 329 if err != nil { 330 log.Fatalf("failed to create dashapi connection %v", err) 331 } 332 jm.startLoop(ctxJobs, &wgJobs) 333 } 334 335 // For testing. Racy. Use with care. 336 http.HandleFunc("/upload_cover", func(w http.ResponseWriter, r *http.Request) { 337 for _, mgr := range managers { 338 if err := mgr.uploadCoverReport(ctx); err != nil { 339 fmt.Fprintf(w, "failed for %v: %v <br>\n", mgr.name, err) 340 return 341 } 342 fmt.Fprintf(w, "upload cover for %v <br>\n", mgr.name) 343 } 344 }) 345 346 wg.Add(1) 347 go deprecateAssets(ctx, cfg, &wg) 348 349 select { 350 case <-shutdownPending: 351 case <-updatePending: 352 } 353 stopJobs() 354 wgJobs.Wait() 355 stop() 356 wg.Wait() 357 358 select { 359 case <-shutdownPending: 360 default: 361 updater.UpdateAndRestart() 362 } 363 } 364 365 func deprecateAssets(ctx context.Context, cfg *Config, wg *sync.WaitGroup) { 366 defer wg.Done() 367 if cfg.DashboardAddr == "" || cfg.AssetStorage.IsEmpty() || 368 !cfg.AssetStorage.DoDeprecation { 369 return 370 } 371 dash, err := dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey) 372 if err != nil { 373 log.Fatalf("failed to create dashapi during asset deprecation: %v", err) 374 return 375 } 376 storage, err := asset.StorageFromConfig(cfg.AssetStorage, dash) 377 if err != nil { 378 log.Errorf("failed to create asset storage during asset deprecation: %v", err) 379 return 380 } 381 loop: 382 for { 383 const sleepDuration = 6 * time.Hour 384 select { 385 case <-ctx.Done(): 386 break loop 387 case <-time.After(sleepDuration): 388 } 389 log.Logf(1, "start asset deprecation") 390 stats, err := storage.DeprecateAssets() 391 if err != nil { 392 log.Errorf("asset deprecation failed: %v", err) 393 } 394 log.Logf(0, "asset deprecation: needed=%d, existing=%d, deleted=%d", 395 stats.Needed, stats.Existing, stats.Deleted) 396 } 397 } 398 399 func serveHTTP(cfg *Config) { 400 ln, err := net.Listen("tcp4", cfg.HTTP) 401 if err != nil { 402 log.Fatalf("failed to listen on %v: %v", cfg.HTTP, err) 403 } 404 log.Logf(0, "serving http on http://%v", ln.Addr()) 405 go func() { 406 err := http.Serve(ln, nil) 407 log.Fatalf("failed to serve http: %v", err) 408 }() 409 } 410 411 func uploadSyzkallerBuildError(cfg *Config, commit *vcs.Commit, compilerID string, buildErr error) { 412 var output []byte 413 var verbose *osutil.VerboseError 414 title := buildErr.Error() 415 if errors.As(buildErr, &verbose) { 416 output = verbose.Output 417 } 418 title = "syzkaller: " + title 419 for _, mgrcfg := range cfg.Managers { 420 if cfg.DashboardAddr == "" || mgrcfg.DashboardClient == "" { 421 log.Logf(0, "not uploading build error for %v: no dashboard", mgrcfg.Name) 422 continue 423 } 424 dash, err := dashapi.New(mgrcfg.DashboardClient, cfg.DashboardAddr, mgrcfg.DashboardKey) 425 if err != nil { 426 log.Logf(0, "failed to report build error for %v: %v", mgrcfg.Name, err) 427 return 428 } 429 managercfg := mgrcfg.managercfg 430 req := &dashapi.BuildErrorReq{ 431 Build: dashapi.Build{ 432 Manager: managercfg.Name, 433 ID: commit.Hash, 434 OS: managercfg.TargetOS, 435 Arch: managercfg.TargetArch, 436 VMArch: managercfg.TargetVMArch, 437 SyzkallerCommit: commit.Hash, 438 SyzkallerCommitDate: commit.CommitDate, 439 CompilerID: compilerID, 440 KernelRepo: cfg.SyzkallerRepo, 441 KernelBranch: cfg.SyzkallerBranch, 442 }, 443 Crash: dashapi.Crash{ 444 Title: title, 445 Log: output, 446 }, 447 } 448 if err := dash.ReportBuildError(req); err != nil { 449 // TODO: log ReportBuildError error to dashboard. 450 log.Logf(0, "failed to report build error for %v: %v", mgrcfg.Name, err) 451 } 452 } 453 } 454 455 func loadConfig(filename string) (*Config, error) { 456 cfg := &Config{ 457 SyzkallerRepo: "https://github.com/google/syzkaller.git", 458 SyzkallerBranch: "master", 459 ManagerPort: 10000, 460 RPCPort: 30000, 461 Goroot: os.Getenv("GOROOT"), 462 JobPollPeriod: 10, 463 CommitPollPeriod: 3600, 464 } 465 if err := config.LoadFile(filename, cfg); err != nil { 466 return nil, err 467 } 468 if cfg.Name == "" { 469 return nil, fmt.Errorf("param 'name' is empty") 470 } 471 if cfg.HTTP == "" { 472 return nil, fmt.Errorf("param 'http' is empty") 473 } 474 cfg.Goroot = osutil.Abs(cfg.Goroot) 475 cfg.SyzkallerDescriptions = osutil.Abs(cfg.SyzkallerDescriptions) 476 cfg.BisectBinDir = osutil.Abs(cfg.BisectBinDir) 477 cfg.Ccache = osutil.Abs(cfg.Ccache) 478 var managers []*ManagerConfig 479 for _, mgr := range cfg.Managers { 480 if mgr.Disabled == "" { 481 managers = append(managers, mgr) 482 } 483 if err := loadManagerConfig(cfg, mgr); err != nil { 484 return nil, err 485 } 486 } 487 cfg.Managers = managers 488 if len(cfg.Managers) == 0 { 489 return nil, fmt.Errorf("no managers specified") 490 } 491 if cfg.AssetStorage != nil { 492 if err := cfg.AssetStorage.Validate(); err != nil { 493 return nil, fmt.Errorf("asset storage config error: %w", err) 494 } 495 } 496 return cfg, nil 497 } 498 499 func loadManagerConfig(cfg *Config, mgr *ManagerConfig) error { 500 managercfg, err := mgrconfig.LoadPartialData(mgr.ManagerConfig) 501 if err != nil { 502 return fmt.Errorf("manager config: %w", err) 503 } 504 if managercfg.Name != "" && mgr.Name != "" { 505 return fmt.Errorf("both managercfg.Name=%q and mgr.Name=%q are specified", managercfg.Name, mgr.Name) 506 } 507 if managercfg.Name == "" && mgr.Name == "" { 508 return fmt.Errorf("no managercfg.Name nor mgr.Name are specified") 509 } 510 if managercfg.Name != "" { 511 mgr.Name = managercfg.Name 512 } else { 513 managercfg.Name = cfg.Name + "-" + mgr.Name 514 } 515 if mgr.CompilerType == "" { 516 mgr.CompilerType = "gcc" 517 } 518 if mgr.Branch == "" { 519 mgr.Branch = "master" 520 } 521 mgr.managercfg = managercfg 522 managercfg.Syzkaller = filepath.FromSlash("syzkaller/current") 523 if managercfg.HTTP == "" { 524 managercfg.HTTP = fmt.Sprintf(":%v", cfg.ManagerPort) 525 cfg.ManagerPort++ 526 } 527 if managercfg.RPC == ":0" { 528 managercfg.RPC = fmt.Sprintf(":%v", cfg.RPCPort) 529 cfg.RPCPort++ 530 } 531 mgr.testRPCPort = cfg.RPCPort 532 cfg.RPCPort++ 533 // Note: we don't change Compiler/Ccache because it may be just "gcc" referring 534 // to the system binary, or pkg/build/netbsd.go uses "g++" and "clang++" as special marks. 535 mgr.Userspace = osutil.Abs(mgr.Userspace) 536 mgr.KernelConfig = osutil.Abs(mgr.KernelConfig) 537 mgr.KernelBaselineConfig = osutil.Abs(mgr.KernelBaselineConfig) 538 mgr.KernelCmdline = osutil.Abs(mgr.KernelCmdline) 539 mgr.KernelSysctl = osutil.Abs(mgr.KernelSysctl) 540 if mgr.KernelConfig != "" && mgr.KernelBaselineConfig == "" { 541 mgr.KernelBaselineConfig = inferBaselineConfig(mgr.KernelConfig) 542 } 543 if mgr.MaxKernelLagDays == 0 { 544 mgr.MaxKernelLagDays = 30 545 } 546 if err := mgr.validate(cfg); err != nil { 547 return err 548 } 549 550 if cfg.PatchVMConfigs[managercfg.Type] != nil { 551 managercfg.VM, err = config.MergeJSONs(managercfg.VM, cfg.PatchVMConfigs[managercfg.Type]) 552 if err != nil { 553 return fmt.Errorf("failed to patch manager %v's VM: %w", mgr.Name, err) 554 } 555 } 556 return nil 557 } 558 559 func inferBaselineConfig(kernelConfig string) string { 560 suffixPos := strings.LastIndex(kernelConfig, ".config") 561 if suffixPos < 0 { 562 return "" 563 } 564 candidate := kernelConfig[:suffixPos] + "-base.config" 565 if !osutil.IsExist(candidate) { 566 return "" 567 } 568 return candidate 569 }