github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/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 "encoding/json" 56 "flag" 57 "fmt" 58 "net" 59 "net/http" 60 _ "net/http/pprof" 61 "os" 62 "path/filepath" 63 "strings" 64 "sync" 65 "time" 66 67 "github.com/google/syzkaller/dashboard/dashapi" 68 "github.com/google/syzkaller/pkg/asset" 69 "github.com/google/syzkaller/pkg/config" 70 "github.com/google/syzkaller/pkg/log" 71 "github.com/google/syzkaller/pkg/mgrconfig" 72 "github.com/google/syzkaller/pkg/osutil" 73 "github.com/google/syzkaller/pkg/vcs" 74 ) 75 76 var ( 77 flagConfig = flag.String("config", "", "config file") 78 flagAutoUpdate = flag.Bool("autoupdate", true, "auto-update the binary (for testing)") 79 flagManagers = flag.Bool("managers", true, "start managers (for testing)") 80 flagDebug = flag.Bool("debug", false, "debug mode (for testing)") 81 // nolint: lll 82 flagExitOnUpgrade = flag.Bool("exit-on-upgrade", false, "exit after a syz-ci upgrade is applied; otherwise syz-ci restarts") 83 ) 84 85 type Config struct { 86 Name string `json:"name"` 87 HTTP string `json:"http"` 88 // If manager http address is not specified, give it an address starting from this port. Optional. 89 ManagerPort int `json:"manager_port_start"` 90 // If manager rpc address is not specified, give it addresses starting from this port. By default 30000. 91 RPCPort int `json:"rpc_port_start"` 92 DashboardAddr string `json:"dashboard_addr"` // Optional. 93 DashboardClient string `json:"dashboard_client"` // Optional. 94 DashboardKey string `json:"dashboard_key"` // Optional. 95 HubAddr string `json:"hub_addr"` // Optional. 96 HubKey string `json:"hub_key"` // Optional. 97 Goroot string `json:"goroot"` // Go 1.8+ toolchain dir. 98 SyzkallerRepo string `json:"syzkaller_repo"` 99 SyzkallerBranch string `json:"syzkaller_branch"` // Defaults to "master". 100 // Dir with additional syscall descriptions. 101 // - *.txt and *.const files are copied to syzkaller/sys/linux/ 102 // - *.test files are copied to syzkaller/sys/linux/test/ 103 // - *.h files are copied to syzkaller/executor/ 104 SyzkallerDescriptions string `json:"syzkaller_descriptions"` 105 // Path to upload coverage reports from managers (optional). 106 // Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://). 107 CoverUploadPath string `json:"cover_upload_path"` 108 // Path to upload json coverage reports from managers (optional). 109 CoverPipelinePath string `json:"cover_pipeline_path"` 110 // Path to upload corpus.db from managers (optional). 111 // Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://). 112 CorpusUploadPath string `json:"corpus_upload_path"` 113 // Make files uploaded via CoverUploadPath and CorpusUploadPath public. 114 PublishGCS bool `json:"publish_gcs"` 115 // BinDir must point to a dir that contains compilers required to build 116 // older versions of the kernel. For linux, it needs to include several 117 // compiler versions. 118 BisectBinDir string `json:"bisect_bin_dir"` 119 // Keys of BisectIgnore are full commit hashes that should never be reported 120 // in bisection results. 121 // Values of the map are ignored and can e.g. serve as comments. 122 BisectIgnore map[string]string `json:"bisect_ignore"` 123 // Extra commits to cherry-pick to older kernel revisions. 124 // The list is concatenated with the similar parameter from ManagerConfig. 125 BisectBackports []vcs.BackportCommit `json:"bisect_backports"` 126 Ccache string `json:"ccache"` 127 Managers []*ManagerConfig `json:"managers"` 128 // Poll period for jobs in seconds (optional, defaults to 10 seconds) 129 JobPollPeriod int `json:"job_poll_period"` 130 // Set up a second (parallel) job processor to speed up processing. 131 // For now, this second job processor only handles patch testing requests. 132 ParallelJobs bool `json:"parallel_jobs"` 133 // Poll period for commits in seconds (optional, defaults to 3600 seconds) 134 CommitPollPeriod int `json:"commit_poll_period"` 135 // Asset Storage config. 136 AssetStorage *asset.Config `json:"asset_storage"` 137 // Per-vm type JSON diffs that will be applied to every instace of the 138 // corresponding VM type. 139 PatchVMConfigs map[string]json.RawMessage `json:"patch_vm_configs"` 140 } 141 142 type ManagerConfig struct { 143 // If Name is specified, syz-manager name is set to Config.Name-ManagerConfig.Name. 144 // This is old naming scheme, it does not allow to move managers between ci instances. 145 // For new naming scheme set ManagerConfig.ManagerConfig.Name instead and leave this field empty. 146 // This allows to move managers as their name does not depend on cfg.Name. 147 // Generally, if you have: 148 // { 149 // "name": "ci", 150 // "managers": [ 151 // { 152 // "name": "foo", 153 // ... 154 // } 155 // ] 156 // } 157 // you want to change it to: 158 // { 159 // "name": "ci", 160 // "managers": [ 161 // { 162 // ... 163 // "manager_config": { 164 // "name": "ci-foo" 165 // } 166 // } 167 // ] 168 // } 169 // and rename managers/foo to managers/ci-foo. Then this instance can be moved 170 // to another ci along with managers/ci-foo dir. 171 Name string `json:"name"` 172 Disabled string `json:"disabled"` // If not empty, don't build/start this manager. 173 DashboardClient string `json:"dashboard_client"` 174 DashboardKey string `json:"dashboard_key"` 175 Repo string `json:"repo"` 176 // Short name of the repo (e.g. "linux-next"), used only for reporting. 177 RepoAlias string `json:"repo_alias"` 178 Branch string `json:"branch"` // Defaults to "master". 179 // Currently either 'gcc' or 'clang'. Note that pkg/bisect requires 180 // explicit plumbing for every os/compiler combination. 181 CompilerType string `json:"compiler_type"` // Defaults to "gcc" 182 Compiler string `json:"compiler"` 183 Linker string `json:"linker"` 184 Ccache string `json:"ccache"` 185 Userspace string `json:"userspace"` 186 KernelConfig string `json:"kernel_config"` 187 // KernelSrcSuffix adds a suffix to the kernel_src manager config. This is needed for cases where 188 // the kernel source root as reported in the coverage UI is a subdirectory of the VCS root. 189 KernelSrcSuffix string `json:"kernel_src_suffix"` 190 // Build-type-specific parameters. 191 // Parameters for concrete types are in Config type in pkg/build/TYPE.go, e.g. pkg/build/android.go. 192 Build json.RawMessage `json:"build"` 193 // Baseline config for bisection, see pkg/bisect.KernelConfig.BaselineConfig. 194 // If not specified, syz-ci generates a `-base.config` path counterpart for `kernel_config` and, 195 // if it exists, uses it as default. 196 KernelBaselineConfig string `json:"kernel_baseline_config"` 197 // File with kernel cmdline values (optional). 198 KernelCmdline string `json:"kernel_cmdline"` 199 // File with sysctl values (e.g. output of sysctl -a, optional). 200 KernelSysctl string `json:"kernel_sysctl"` 201 Jobs ManagerJobs `json:"jobs"` 202 // Extra commits to cherry pick to older kernel revisions. 203 BisectBackports []vcs.BackportCommit `json:"bisect_backports"` 204 // Base syz-manager config for the instance. 205 ManagerConfig json.RawMessage `json:"manager_config"` 206 // If the kernel's commit is older than MaxKernelLagDays days, 207 // fuzzing won't be started on this instance. 208 // By default it's 30 days. 209 MaxKernelLagDays int `json:"max_kernel_lag_days"` 210 managercfg *mgrconfig.Config 211 } 212 213 type ManagerJobs struct { 214 TestPatches bool `json:"test_patches"` // enable patch testing jobs 215 PollCommits bool `json:"poll_commits"` // poll info about fix commits 216 BisectCause bool `json:"bisect_cause"` // do cause bisection 217 BisectFix bool `json:"bisect_fix"` // do fix bisection 218 } 219 220 func (m *ManagerJobs) AnyEnabled() bool { 221 return m.TestPatches || m.PollCommits || m.BisectCause || m.BisectFix 222 } 223 224 func (m *ManagerJobs) Filter(filter *ManagerJobs) *ManagerJobs { 225 return &ManagerJobs{ 226 TestPatches: m.TestPatches && filter.TestPatches, 227 PollCommits: m.PollCommits && filter.PollCommits, 228 BisectCause: m.BisectCause && filter.BisectCause, 229 BisectFix: m.BisectFix && filter.BisectFix, 230 } 231 } 232 233 func main() { 234 flag.Parse() 235 log.EnableLogCaching(1000, 1<<20) 236 cfg, err := loadConfig(*flagConfig) 237 if err != nil { 238 log.Fatalf("failed to load config: %v", err) 239 } 240 log.SetName(cfg.Name) 241 242 shutdownPending := make(chan struct{}) 243 osutil.HandleInterrupts(shutdownPending) 244 245 serveHTTP(cfg) 246 247 os.Unsetenv("GOPATH") 248 if cfg.Goroot != "" { 249 os.Setenv("GOROOT", cfg.Goroot) 250 os.Setenv("PATH", filepath.Join(cfg.Goroot, "bin")+ 251 string(filepath.ListSeparator)+os.Getenv("PATH")) 252 } 253 254 updatePending := make(chan struct{}) 255 updater := NewSyzUpdater(cfg) 256 updater.UpdateOnStart(*flagAutoUpdate, shutdownPending) 257 if *flagAutoUpdate { 258 go func() { 259 updater.WaitForUpdate() 260 close(updatePending) 261 }() 262 } 263 264 stop := make(chan struct{}) 265 var managers []*Manager 266 for _, mgrcfg := range cfg.Managers { 267 mgr, err := createManager(cfg, mgrcfg, stop, *flagDebug) 268 if err != nil { 269 log.Errorf("failed to create manager %v: %v", mgrcfg.Name, err) 270 continue 271 } 272 managers = append(managers, mgr) 273 } 274 if len(managers) == 0 { 275 log.Fatalf("failed to create all managers") 276 } 277 var wg sync.WaitGroup 278 if *flagManagers { 279 for _, mgr := range managers { 280 mgr := mgr 281 wg.Add(1) 282 go func() { 283 defer wg.Done() 284 mgr.loop() 285 }() 286 } 287 } 288 jp, err := newJobManager(cfg, managers, shutdownPending) 289 if err != nil { 290 log.Fatalf("failed to create dashapi connection %v", err) 291 } 292 stopJobs := jp.startLoop(&wg) 293 294 // For testing. Racy. Use with care. 295 http.HandleFunc("/upload_cover", func(w http.ResponseWriter, r *http.Request) { 296 for _, mgr := range managers { 297 if err := mgr.uploadCoverReport(); err != nil { 298 w.Write([]byte(fmt.Sprintf("failed for %v: %v <br>\n", mgr.name, err))) 299 return 300 } 301 w.Write([]byte(fmt.Sprintf("upload cover for %v <br>\n", mgr.name))) 302 } 303 }) 304 305 wg.Add(1) 306 go deprecateAssets(cfg, stop, &wg) 307 308 select { 309 case <-shutdownPending: 310 case <-updatePending: 311 } 312 stopJobs() // Gracefully wait for the running jobs to finish. 313 close(stop) 314 wg.Wait() 315 316 select { 317 case <-shutdownPending: 318 default: 319 updater.UpdateAndRestart() 320 } 321 } 322 323 func deprecateAssets(cfg *Config, stop chan struct{}, wg *sync.WaitGroup) { 324 defer wg.Done() 325 if cfg.DashboardAddr == "" || cfg.AssetStorage.IsEmpty() || 326 !cfg.AssetStorage.DoDeprecation { 327 return 328 } 329 dash, err := dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey) 330 if err != nil { 331 log.Fatalf("failed to create dashapi during asset deprecation: %v", err) 332 return 333 } 334 storage, err := asset.StorageFromConfig(cfg.AssetStorage, dash) 335 if err != nil { 336 log.Errorf("failed to create asset storage during asset deprecation: %v", err) 337 return 338 } 339 loop: 340 for { 341 const sleepDuration = 6 * time.Hour 342 select { 343 case <-stop: 344 break loop 345 case <-time.After(sleepDuration): 346 } 347 log.Logf(0, "deprecating assets") 348 err := storage.DeprecateAssets() 349 if err != nil { 350 log.Errorf("asset deprecation failed: %v", err) 351 } 352 } 353 } 354 355 func serveHTTP(cfg *Config) { 356 ln, err := net.Listen("tcp4", cfg.HTTP) 357 if err != nil { 358 log.Fatalf("failed to listen on %v: %v", cfg.HTTP, err) 359 } 360 log.Logf(0, "serving http on http://%v", ln.Addr()) 361 go func() { 362 err := http.Serve(ln, nil) 363 log.Fatalf("failed to serve http: %v", err) 364 }() 365 } 366 367 func loadConfig(filename string) (*Config, error) { 368 cfg := &Config{ 369 SyzkallerRepo: "https://github.com/google/syzkaller.git", 370 SyzkallerBranch: "master", 371 ManagerPort: 10000, 372 RPCPort: 30000, 373 Goroot: os.Getenv("GOROOT"), 374 JobPollPeriod: 10, 375 CommitPollPeriod: 3600, 376 } 377 if err := config.LoadFile(filename, cfg); err != nil { 378 return nil, err 379 } 380 if cfg.Name == "" { 381 return nil, fmt.Errorf("param 'name' is empty") 382 } 383 if cfg.HTTP == "" { 384 return nil, fmt.Errorf("param 'http' is empty") 385 } 386 cfg.Goroot = osutil.Abs(cfg.Goroot) 387 cfg.SyzkallerDescriptions = osutil.Abs(cfg.SyzkallerDescriptions) 388 cfg.BisectBinDir = osutil.Abs(cfg.BisectBinDir) 389 cfg.Ccache = osutil.Abs(cfg.Ccache) 390 var managers []*ManagerConfig 391 for _, mgr := range cfg.Managers { 392 if mgr.Disabled == "" { 393 managers = append(managers, mgr) 394 } 395 if err := loadManagerConfig(cfg, mgr); err != nil { 396 return nil, err 397 } 398 } 399 cfg.Managers = managers 400 if len(cfg.Managers) == 0 { 401 return nil, fmt.Errorf("no managers specified") 402 } 403 if cfg.AssetStorage != nil { 404 if err := cfg.AssetStorage.Validate(); err != nil { 405 return nil, fmt.Errorf("asset storage config error: %w", err) 406 } 407 } 408 return cfg, nil 409 } 410 411 func loadManagerConfig(cfg *Config, mgr *ManagerConfig) error { 412 managercfg, err := mgrconfig.LoadPartialData(mgr.ManagerConfig) 413 if err != nil { 414 return fmt.Errorf("manager config: %w", err) 415 } 416 if managercfg.Name != "" && mgr.Name != "" { 417 return fmt.Errorf("both managercfg.Name=%q and mgr.Name=%q are specified", managercfg.Name, mgr.Name) 418 } 419 if managercfg.Name == "" && mgr.Name == "" { 420 return fmt.Errorf("no managercfg.Name nor mgr.Name are specified") 421 } 422 if managercfg.Name != "" { 423 mgr.Name = managercfg.Name 424 } else { 425 managercfg.Name = cfg.Name + "-" + mgr.Name 426 } 427 if mgr.CompilerType == "" { 428 mgr.CompilerType = "gcc" 429 } 430 if mgr.Branch == "" { 431 mgr.Branch = "master" 432 } 433 mgr.managercfg = managercfg 434 managercfg.Syzkaller = filepath.FromSlash("syzkaller/current") 435 if managercfg.HTTP == "" { 436 managercfg.HTTP = fmt.Sprintf(":%v", cfg.ManagerPort) 437 cfg.ManagerPort++ 438 } 439 if managercfg.RPC == ":0" { 440 managercfg.RPC = fmt.Sprintf(":%v", cfg.RPCPort) 441 cfg.RPCPort++ 442 } 443 // Note: we don't change Compiler/Ccache because it may be just "gcc" referring 444 // to the system binary, or pkg/build/netbsd.go uses "g++" and "clang++" as special marks. 445 mgr.Userspace = osutil.Abs(mgr.Userspace) 446 mgr.KernelConfig = osutil.Abs(mgr.KernelConfig) 447 mgr.KernelBaselineConfig = osutil.Abs(mgr.KernelBaselineConfig) 448 mgr.KernelCmdline = osutil.Abs(mgr.KernelCmdline) 449 mgr.KernelSysctl = osutil.Abs(mgr.KernelSysctl) 450 if mgr.KernelConfig != "" && mgr.KernelBaselineConfig == "" { 451 mgr.KernelBaselineConfig = inferBaselineConfig(mgr.KernelConfig) 452 } 453 if mgr.MaxKernelLagDays == 0 { 454 mgr.MaxKernelLagDays = 30 455 } 456 if err := mgr.validate(cfg); err != nil { 457 return err 458 } 459 460 if cfg.PatchVMConfigs[managercfg.Type] != nil { 461 managercfg.VM, err = config.MergeJSONs(managercfg.VM, cfg.PatchVMConfigs[managercfg.Type]) 462 if err != nil { 463 return fmt.Errorf("failed to patch manager %v's VM: %w", mgr.Name, err) 464 } 465 } 466 return nil 467 } 468 469 func inferBaselineConfig(kernelConfig string) string { 470 suffixPos := strings.LastIndex(kernelConfig, ".config") 471 if suffixPos < 0 { 472 return "" 473 } 474 candidate := kernelConfig[:suffixPos] + "-base.config" 475 if !osutil.IsExist(candidate) { 476 return "" 477 } 478 return candidate 479 }