blob: e9e12e0e64019c4e908043cdd91d915792427648 [file] [log] [blame]
// 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
// 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"
"net"
"net/http"
_ "net/http/pprof"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/asset"
"github.com/google/syzkaller/pkg/config"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/vcs"
)
var (
flagConfig = flag.String("config", "", "config file")
flagAutoUpdate = flag.Bool("autoupdate", true, "auto-update the binary (for testing)")
flagManagers = flag.Bool("managers", true, "start managers (for testing)")
flagDebug = flag.Bool("debug", false, "debug mode (for testing)")
// nolint: lll
flagExitOnUpgrade = flag.Bool("exit-on-upgrade", false, "exit after a syz-ci upgrade is applied; otherwise syz-ci restarts")
)
type Config struct {
Name string `json:"name"`
HTTP string `json:"http"`
// If manager http address is not specified, give it an address starting from this port. Optional.
ManagerPort int `json:"manager_port_start"`
// If manager rpc address is not specified, give it addresses starting from this port. By default 30000.
RPCPort int `json:"rpc_port_start"`
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"` // Defaults to "master".
// Dir with additional syscall descriptions.
// - *.txt and *.const files are copied to syzkaller/sys/linux/
// - *.test files are copied to syzkaller/sys/linux/test/
// - *.h files are copied to syzkaller/executor/
SyzkallerDescriptions string `json:"syzkaller_descriptions"`
// Path to upload coverage reports from managers (optional).
// Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://).
CoverUploadPath string `json:"cover_upload_path"`
// Path to upload json coverage reports from managers (optional).
CoverPipelinePath string `json:"cover_pipeline_path"`
// Path to upload corpus.db from managers (optional).
// Supported protocols: GCS (gs://) and HTTP PUT (http:// or https://).
CorpusUploadPath string `json:"corpus_upload_path"`
// Make files uploaded via CoverUploadPath and CorpusUploadPath public.
PublishGCS bool `json:"publish_gcs"`
// BinDir must point to a dir that contains compilers required to build
// older versions of the kernel. For linux, it needs to include several
// compiler versions.
BisectBinDir string `json:"bisect_bin_dir"`
// Keys of BisectIgnore are full commit hashes that should never be reported
// in bisection results.
// Values of the map are ignored and can e.g. serve as comments.
BisectIgnore map[string]string `json:"bisect_ignore"`
// Extra commits to cherry-pick to older kernel revisions.
// The list is concatenated with the similar parameter from ManagerConfig.
BisectBackports []vcs.BackportCommit `json:"bisect_backports"`
Ccache string `json:"ccache"`
Managers []*ManagerConfig `json:"managers"`
// Poll period for jobs in seconds (optional, defaults to 10 seconds)
JobPollPeriod int `json:"job_poll_period"`
// Set up a second (parallel) job processor to speed up processing.
// For now, this second job processor only handles patch testing requests.
ParallelJobs bool `json:"parallel_jobs"`
// Poll period for commits in seconds (optional, defaults to 3600 seconds)
CommitPollPeriod int `json:"commit_poll_period"`
// Asset Storage config.
AssetStorage *asset.Config `json:"asset_storage"`
// Per-vm type JSON diffs that will be applied to every instace of the
// corresponding VM type.
PatchVMConfigs map[string]json.RawMessage `json:"patch_vm_configs"`
}
type ManagerConfig struct {
// If Name is specified, syz-manager name is set to Config.Name-ManagerConfig.Name.
// This is old naming scheme, it does not allow to move managers between ci instances.
// For new naming scheme set ManagerConfig.ManagerConfig.Name instead and leave this field empty.
// This allows to move managers as their name does not depend on cfg.Name.
// Generally, if you have:
// {
// "name": "ci",
// "managers": [
// {
// "name": "foo",
// ...
// }
// ]
// }
// you want to change it to:
// {
// "name": "ci",
// "managers": [
// {
// ...
// "manager_config": {
// "name": "ci-foo"
// }
// }
// ]
// }
// and rename managers/foo to managers/ci-foo. Then this instance can be moved
// to another ci along with managers/ci-foo dir.
Name string `json:"name"`
Disabled string `json:"disabled"` // If not empty, don't build/start this manager.
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"` // Defaults to "master".
// Currently either 'gcc' or 'clang'. Note that pkg/bisect requires
// explicit plumbing for every os/compiler combination.
CompilerType string `json:"compiler_type"` // Defaults to "gcc"
Compiler string `json:"compiler"`
Linker string `json:"linker"`
Ccache string `json:"ccache"`
Userspace string `json:"userspace"`
KernelConfig string `json:"kernel_config"`
// KernelSrcSuffix adds a suffix to the kernel_src manager config. This is needed for cases where
// the kernel source root as reported in the coverage UI is a subdirectory of the VCS root.
KernelSrcSuffix string `json:"kernel_src_suffix"`
// Build-type-specific parameters.
// Parameters for concrete types are in Config type in pkg/build/TYPE.go, e.g. pkg/build/android.go.
Build json.RawMessage `json:"build"`
// Baseline config for bisection, see pkg/bisect.KernelConfig.BaselineConfig.
// If not specified, syz-ci generates a `-base.config` path counterpart for `kernel_config` and,
// if it exists, uses it as default.
KernelBaselineConfig string `json:"kernel_baseline_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"`
Jobs ManagerJobs `json:"jobs"`
// Extra commits to cherry pick to older kernel revisions.
BisectBackports []vcs.BackportCommit `json:"bisect_backports"`
// Base syz-manager config for the instance.
ManagerConfig json.RawMessage `json:"manager_config"`
// If the kernel's commit is older than MaxKernelLagDays days,
// fuzzing won't be started on this instance.
// By default it's 30 days.
MaxKernelLagDays int `json:"max_kernel_lag_days"`
managercfg *mgrconfig.Config
}
type ManagerJobs struct {
TestPatches bool `json:"test_patches"` // enable patch testing jobs
PollCommits bool `json:"poll_commits"` // poll info about fix commits
BisectCause bool `json:"bisect_cause"` // do cause bisection
BisectFix bool `json:"bisect_fix"` // do fix bisection
}
func (m *ManagerJobs) AnyEnabled() bool {
return m.TestPatches || m.PollCommits || m.BisectCause || m.BisectFix
}
func (m *ManagerJobs) Filter(filter *ManagerJobs) *ManagerJobs {
return &ManagerJobs{
TestPatches: m.TestPatches && filter.TestPatches,
PollCommits: m.PollCommits && filter.PollCommits,
BisectCause: m.BisectCause && filter.BisectCause,
BisectFix: m.BisectFix && filter.BisectFix,
}
}
func main() {
flag.Parse()
log.EnableLogCaching(1000, 1<<20)
cfg, err := loadConfig(*flagConfig)
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
log.SetName(cfg.Name)
shutdownPending := make(chan struct{})
osutil.HandleInterrupts(shutdownPending)
serveHTTP(cfg)
os.Unsetenv("GOPATH")
if cfg.Goroot != "" {
os.Setenv("GOROOT", cfg.Goroot)
os.Setenv("PATH", filepath.Join(cfg.Goroot, "bin")+
string(filepath.ListSeparator)+os.Getenv("PATH"))
}
updatePending := make(chan struct{})
updater := NewSyzUpdater(cfg)
updater.UpdateOnStart(*flagAutoUpdate, shutdownPending)
if *flagAutoUpdate {
go func() {
updater.WaitForUpdate()
close(updatePending)
}()
}
stop := make(chan struct{})
var managers []*Manager
for _, mgrcfg := range cfg.Managers {
mgr, err := createManager(cfg, mgrcfg, stop, *flagDebug)
if err != nil {
log.Errorf("failed to create manager %v: %v", mgrcfg.Name, err)
continue
}
managers = append(managers, mgr)
}
if len(managers) == 0 {
log.Fatalf("failed to create all managers")
}
var wg sync.WaitGroup
if *flagManagers {
for _, mgr := range managers {
mgr := mgr
wg.Add(1)
go func() {
defer wg.Done()
mgr.loop()
}()
}
}
jp, err := newJobManager(cfg, managers, shutdownPending)
if err != nil {
log.Fatalf("failed to create dashapi connection %v", err)
}
stopJobs := jp.startLoop(&wg)
// For testing. Racy. Use with care.
http.HandleFunc("/upload_cover", func(w http.ResponseWriter, r *http.Request) {
for _, mgr := range managers {
if err := mgr.uploadCoverReport(); err != nil {
w.Write([]byte(fmt.Sprintf("failed for %v: %v <br>\n", mgr.name, err)))
return
}
w.Write([]byte(fmt.Sprintf("upload cover for %v <br>\n", mgr.name)))
}
})
wg.Add(1)
go deprecateAssets(cfg, stop, &wg)
select {
case <-shutdownPending:
case <-updatePending:
}
stopJobs() // Gracefully wait for the running jobs to finish.
close(stop)
wg.Wait()
select {
case <-shutdownPending:
default:
updater.UpdateAndRestart()
}
}
func deprecateAssets(cfg *Config, stop chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
if cfg.DashboardAddr == "" || cfg.AssetStorage.IsEmpty() ||
!cfg.AssetStorage.DoDeprecation {
return
}
dash, err := dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey)
if err != nil {
log.Fatalf("failed to create dashapi during asset deprecation: %v", err)
return
}
storage, err := asset.StorageFromConfig(cfg.AssetStorage, dash)
if err != nil {
log.Errorf("failed to create asset storage during asset deprecation: %v", err)
return
}
loop:
for {
const sleepDuration = 6 * time.Hour
select {
case <-stop:
break loop
case <-time.After(sleepDuration):
}
log.Logf(0, "deprecating assets")
err := storage.DeprecateAssets()
if err != nil {
log.Errorf("asset deprecation failed: %v", err)
}
}
}
func serveHTTP(cfg *Config) {
ln, err := net.Listen("tcp4", cfg.HTTP)
if err != nil {
log.Fatalf("failed to listen on %v: %v", cfg.HTTP, err)
}
log.Logf(0, "serving http on http://%v", ln.Addr())
go func() {
err := http.Serve(ln, nil)
log.Fatalf("failed to serve http: %v", err)
}()
}
func loadConfig(filename string) (*Config, error) {
cfg := &Config{
SyzkallerRepo: "https://github.com/google/syzkaller.git",
SyzkallerBranch: "master",
ManagerPort: 10000,
RPCPort: 30000,
Goroot: os.Getenv("GOROOT"),
JobPollPeriod: 10,
CommitPollPeriod: 3600,
}
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")
}
cfg.Goroot = osutil.Abs(cfg.Goroot)
cfg.SyzkallerDescriptions = osutil.Abs(cfg.SyzkallerDescriptions)
cfg.BisectBinDir = osutil.Abs(cfg.BisectBinDir)
cfg.Ccache = osutil.Abs(cfg.Ccache)
var managers []*ManagerConfig
for _, mgr := range cfg.Managers {
if mgr.Disabled == "" {
managers = append(managers, mgr)
}
if err := loadManagerConfig(cfg, mgr); err != nil {
return nil, err
}
}
cfg.Managers = managers
if len(cfg.Managers) == 0 {
return nil, fmt.Errorf("no managers specified")
}
if cfg.AssetStorage != nil {
if err := cfg.AssetStorage.Validate(); err != nil {
return nil, fmt.Errorf("asset storage config error: %w", err)
}
}
return cfg, nil
}
func loadManagerConfig(cfg *Config, mgr *ManagerConfig) error {
managercfg, err := mgrconfig.LoadPartialData(mgr.ManagerConfig)
if err != nil {
return fmt.Errorf("manager config: %w", err)
}
if managercfg.Name != "" && mgr.Name != "" {
return fmt.Errorf("both managercfg.Name=%q and mgr.Name=%q are specified", managercfg.Name, mgr.Name)
}
if managercfg.Name == "" && mgr.Name == "" {
return fmt.Errorf("no managercfg.Name nor mgr.Name are specified")
}
if managercfg.Name != "" {
mgr.Name = managercfg.Name
} else {
managercfg.Name = cfg.Name + "-" + mgr.Name
}
if mgr.CompilerType == "" {
mgr.CompilerType = "gcc"
}
if mgr.Branch == "" {
mgr.Branch = "master"
}
mgr.managercfg = managercfg
managercfg.Syzkaller = filepath.FromSlash("syzkaller/current")
if managercfg.HTTP == "" {
managercfg.HTTP = fmt.Sprintf(":%v", cfg.ManagerPort)
cfg.ManagerPort++
}
if managercfg.RPC == ":0" {
managercfg.RPC = fmt.Sprintf(":%v", cfg.RPCPort)
cfg.RPCPort++
}
// Note: we don't change Compiler/Ccache because it may be just "gcc" referring
// to the system binary, or pkg/build/netbsd.go uses "g++" and "clang++" as special marks.
mgr.Userspace = osutil.Abs(mgr.Userspace)
mgr.KernelConfig = osutil.Abs(mgr.KernelConfig)
mgr.KernelBaselineConfig = osutil.Abs(mgr.KernelBaselineConfig)
mgr.KernelCmdline = osutil.Abs(mgr.KernelCmdline)
mgr.KernelSysctl = osutil.Abs(mgr.KernelSysctl)
if mgr.KernelConfig != "" && mgr.KernelBaselineConfig == "" {
mgr.KernelBaselineConfig = inferBaselineConfig(mgr.KernelConfig)
}
if mgr.MaxKernelLagDays == 0 {
mgr.MaxKernelLagDays = 30
}
if err := mgr.validate(cfg); err != nil {
return err
}
if cfg.PatchVMConfigs[managercfg.Type] != nil {
managercfg.VM, err = config.MergeJSONs(managercfg.VM, cfg.PatchVMConfigs[managercfg.Type])
if err != nil {
return fmt.Errorf("failed to patch manager %v's VM: %w", mgr.Name, err)
}
}
return nil
}
func inferBaselineConfig(kernelConfig string) string {
suffixPos := strings.LastIndex(kernelConfig, ".config")
if suffixPos < 0 {
return ""
}
candidate := kernelConfig[:suffixPos] + "-base.config"
if !osutil.IsExist(candidate) {
return ""
}
return candidate
}