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  }