| // Copyright 2017 syzkaller project authors. All rights reserved. |
| // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. |
| |
| // syz-ci is a continuous fuzzing system for syzkaller. |
| // It runs several syz-manager's, polls and rebuilds images for managers |
| // and polls and rebuilds syzkaller binaries. |
| // For usage instructions see: docs/ci.md |
| package main |
| |
| // Implementation details: |
| // |
| // 2 main components: |
| // - SyzUpdater: handles syzkaller updates |
| // - Manager: handles kernel build and syz-manager process (one per manager) |
| // Both operate in a similar way and keep 2 builds: |
| // - latest: latest known good build (i.e. we tested it) |
| // preserved across restarts/reboots, i.e. we can start fuzzing even when |
| // current syzkaller/kernel git head is broken, or git is down, or anything else |
| // - current: currently used build (a copy of one of the latest builds) |
| // Other important points: |
| // - syz-ci is always built on the same revision as the rest of syzkaller binaries, |
| // this allows us to handle e.g. changes in manager config format. |
| // - consequently, syzkaller binaries are never updated on-the-fly, |
| // instead we re-exec and then update |
| // - we understand when the latest build is fresh even after reboot, |
| // i.e. we store enough information to identify it (git hash, compiler identity, etc), |
| // so we don't rebuild unnecessary (kernel builds take time) |
| // - we generally avoid crashing the process and handle all errors gracefully |
| // (this is a continuous system), except for some severe/user errors during start |
| // (e.g. bad config file, or can't create necessary dirs) |
| // |
| // Directory/file structure: |
| // syz-ci : current executable |
| // syz-ci.tag : tag of the current executable (syzkaller git hash) |
| // syzkaller/ |
| // latest/ : latest good syzkaller build |
| // current/ : syzkaller build currently in use |
| // managers/ |
| // manager1/ : one dir per manager |
| // kernel/ : kernel checkout |
| // workdir/ : manager workdir (never deleted) |
| // latest/ : latest good kernel image build |
| // current/ : kernel image currently in use |
| // jobs/ |
| // linux/ : one dir per target OS |
| // kernel/ : kernel checkout |
| // image/ : currently used image |
| // workdir/ : some temp files |
| // |
| // Current executable, syzkaller and kernel builds are marked with tag files. |
| // Tag files uniquely identify the build (git hash, compiler identity, kernel config, etc). |
| // For tag files both contents and modification time are important, |
| // modification time allows us to understand if we need to rebuild after a restart. |
| |
| import ( |
| "encoding/json" |
| "flag" |
| "fmt" |
| "os" |
| "sync" |
| |
| "github.com/google/syzkaller/pkg/config" |
| "github.com/google/syzkaller/pkg/log" |
| "github.com/google/syzkaller/pkg/mgrconfig" |
| "github.com/google/syzkaller/pkg/osutil" |
| ) |
| |
| var flagConfig = flag.String("config", "", "config file") |
| |
| type Config struct { |
| Name string `json:"name"` |
| HTTP string `json:"http"` |
| DashboardAddr string `json:"dashboard_addr"` // Optional. |
| DashboardClient string `json:"dashboard_client"` // Optional. |
| DashboardKey string `json:"dashboard_key"` // Optional. |
| HubAddr string `json:"hub_addr"` // Optional. |
| HubKey string `json:"hub_key"` // Optional. |
| Goroot string `json:"goroot"` // Go 1.8+ toolchain dir. |
| SyzkallerRepo string `json:"syzkaller_repo"` |
| SyzkallerBranch string `json:"syzkaller_branch"` |
| // Dir with additional syscall descriptions (.txt and .const files). |
| SyzkallerDescriptions string `json:"syzkaller_descriptions"` |
| // Enable patch testing jobs. |
| EnableJobs bool `json:"enable_jobs"` |
| Managers []*ManagerConfig `json:"managers"` |
| } |
| |
| type ManagerConfig struct { |
| Name string `json:"name"` |
| DashboardClient string `json:"dashboard_client"` |
| DashboardKey string `json:"dashboard_key"` |
| Repo string `json:"repo"` |
| // Short name of the repo (e.g. "linux-next"), used only for reporting. |
| RepoAlias string `json:"repo_alias"` |
| Branch string `json:"branch"` |
| Compiler string `json:"compiler"` |
| Userspace string `json:"userspace"` |
| KernelConfig string `json:"kernel_config"` |
| // File with kernel cmdline values (optional). |
| KernelCmdline string `json:"kernel_cmdline"` |
| // File with sysctl values (e.g. output of sysctl -a, optional). |
| KernelSysctl string `json:"kernel_sysctl"` |
| ManagerConfig json.RawMessage `json:"manager_config"` |
| } |
| |
| func main() { |
| flag.Parse() |
| log.EnableLogCaching(1000, 1<<20) |
| cfg, err := loadConfig(*flagConfig) |
| if err != nil { |
| log.Fatalf("failed to load config: %v", err) |
| } |
| |
| shutdownPending := make(chan struct{}) |
| osutil.HandleInterrupts(shutdownPending) |
| |
| updater := NewSyzUpdater(cfg) |
| updater.UpdateOnStart(shutdownPending) |
| updatePending := make(chan struct{}) |
| go func() { |
| updater.WaitForUpdate() |
| close(updatePending) |
| }() |
| |
| var wg sync.WaitGroup |
| wg.Add(1) |
| stop := make(chan struct{}) |
| go func() { |
| select { |
| case <-shutdownPending: |
| case <-updatePending: |
| } |
| close(stop) |
| wg.Done() |
| }() |
| |
| managers := make([]*Manager, len(cfg.Managers)) |
| for i, mgrcfg := range cfg.Managers { |
| managers[i] = createManager(cfg, mgrcfg, stop) |
| } |
| for _, mgr := range managers { |
| mgr := mgr |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| mgr.loop() |
| }() |
| } |
| if cfg.EnableJobs { |
| jp := newJobProcessor(cfg, managers) |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| jp.loop(stop) |
| }() |
| } |
| |
| wg.Wait() |
| |
| select { |
| case <-shutdownPending: |
| case <-updatePending: |
| updater.UpdateAndRestart() |
| } |
| } |
| |
| func loadConfig(filename string) (*Config, error) { |
| cfg := &Config{ |
| SyzkallerRepo: "https://github.com/google/syzkaller.git", |
| SyzkallerBranch: "master", |
| Goroot: os.Getenv("GOROOT"), |
| } |
| if err := config.LoadFile(filename, cfg); err != nil { |
| return nil, err |
| } |
| if cfg.Name == "" { |
| return nil, fmt.Errorf("param 'name' is empty") |
| } |
| if cfg.HTTP == "" { |
| return nil, fmt.Errorf("param 'http' is empty") |
| } |
| if len(cfg.Managers) == 0 { |
| return nil, fmt.Errorf("no managers specified") |
| } |
| for i, mgr := range cfg.Managers { |
| if mgr.Name == "" { |
| return nil, fmt.Errorf("param 'managers[%v].name' is empty", i) |
| } |
| mgrcfg := new(mgrconfig.Config) |
| if err := config.LoadData(mgr.ManagerConfig, mgrcfg); err != nil { |
| return nil, fmt.Errorf("manager %v: %v", mgr.Name, err) |
| } |
| } |
| return cfg, nil |
| } |