github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/syz-ci/manager.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  package main
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"math/rand"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"regexp"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/google/syzkaller/dashboard/dashapi"
    23  	"github.com/google/syzkaller/pkg/asset"
    24  	"github.com/google/syzkaller/pkg/build"
    25  	"github.com/google/syzkaller/pkg/config"
    26  	"github.com/google/syzkaller/pkg/cover"
    27  	"github.com/google/syzkaller/pkg/gcs"
    28  	"github.com/google/syzkaller/pkg/hash"
    29  	"github.com/google/syzkaller/pkg/instance"
    30  	"github.com/google/syzkaller/pkg/log"
    31  	"github.com/google/syzkaller/pkg/mgrconfig"
    32  	"github.com/google/syzkaller/pkg/osutil"
    33  	"github.com/google/syzkaller/pkg/report"
    34  	"github.com/google/syzkaller/pkg/vcs"
    35  	"github.com/google/syzkaller/prog"
    36  	_ "github.com/google/syzkaller/sys"
    37  	"github.com/google/syzkaller/sys/targets"
    38  	"github.com/google/syzkaller/vm"
    39  )
    40  
    41  // This is especially slightly longer than syzkaller rebuild period.
    42  // If we set kernelRebuildPeriod = syzkallerRebuildPeriod and both are changed
    43  // during that period (or around that period), we can rebuild kernel, restart
    44  // manager and then instantly shutdown everything for syzkaller update.
    45  // Instead we rebuild syzkaller, restart and then rebuild kernel.
    46  const kernelRebuildPeriod = syzkallerRebuildPeriod + time.Hour
    47  
    48  // List of required files in kernel build (contents of latest/current dirs).
    49  var imageFiles = map[string]bool{
    50  	"tag":           true,  // serialized BuildInfo
    51  	"kernel.config": false, // kernel config used for build
    52  	"image":         true,  // kernel image
    53  	"kernel":        false,
    54  	"initrd":        false,
    55  	"key":           false, // root ssh key for the image
    56  }
    57  
    58  func init() {
    59  	for _, arches := range targets.List {
    60  		for _, arch := range arches {
    61  			if arch.KernelObject != "" {
    62  				imageFiles["obj/"+arch.KernelObject] = false
    63  			}
    64  		}
    65  	}
    66  }
    67  
    68  // Manager represents a single syz-manager instance.
    69  // Handles kernel polling, image rebuild and manager process management.
    70  // As syzkaller builder, it maintains 2 builds:
    71  //   - latest: latest known good kernel build
    72  //   - current: currently used kernel build
    73  type Manager struct {
    74  	name           string
    75  	workDir        string
    76  	kernelBuildDir string
    77  	kernelSrcDir   string
    78  	currentDir     string
    79  	latestDir      string
    80  	configTag      string
    81  	configData     []byte
    82  	cfg            *Config
    83  	repo           vcs.Repo
    84  	mgrcfg         *ManagerConfig
    85  	managercfg     *mgrconfig.Config
    86  	cmd            *ManagerCmd
    87  	dash           ManagerDashapi
    88  	debugStorage   bool
    89  	storage        *asset.Storage
    90  	stop           chan struct{}
    91  	debug          bool
    92  	lastBuild      *dashapi.Build
    93  	buildFailed    bool
    94  }
    95  
    96  type ManagerDashapi interface {
    97  	ReportBuildError(req *dashapi.BuildErrorReq) error
    98  	UploadBuild(build *dashapi.Build) error
    99  	BuilderPoll(manager string) (*dashapi.BuilderPollResp, error)
   100  	LogError(name, msg string, args ...interface{})
   101  	CommitPoll() (*dashapi.CommitPollResp, error)
   102  	UploadCommits(commits []dashapi.Commit) error
   103  }
   104  
   105  func createManager(cfg *Config, mgrcfg *ManagerConfig, stop chan struct{},
   106  	debug bool) (*Manager, error) {
   107  	dir := osutil.Abs(filepath.Join("managers", mgrcfg.Name))
   108  	err := osutil.MkdirAll(dir)
   109  	if err != nil {
   110  		log.Fatal(err)
   111  	}
   112  	if mgrcfg.RepoAlias == "" {
   113  		mgrcfg.RepoAlias = mgrcfg.Repo
   114  	}
   115  
   116  	var dash *dashapi.Dashboard
   117  	if cfg.DashboardAddr != "" && mgrcfg.DashboardClient != "" {
   118  		dash, err = dashapi.New(mgrcfg.DashboardClient, cfg.DashboardAddr, mgrcfg.DashboardKey)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  	}
   123  	var assetStorage *asset.Storage
   124  	if !cfg.AssetStorage.IsEmpty() {
   125  		assetStorage, err = asset.StorageFromConfig(cfg.AssetStorage, dash)
   126  		if err != nil {
   127  			log.Fatalf("failed to create asset storage: %v", err)
   128  		}
   129  	}
   130  	var configData []byte
   131  	if mgrcfg.KernelConfig != "" {
   132  		if configData, err = os.ReadFile(mgrcfg.KernelConfig); err != nil {
   133  			return nil, err
   134  		}
   135  	}
   136  	kernelDir := filepath.Join(dir, "kernel")
   137  	repo, err := vcs.NewRepo(mgrcfg.managercfg.TargetOS, mgrcfg.managercfg.Type, kernelDir)
   138  	if err != nil {
   139  		log.Fatalf("failed to create repo for %v: %v", mgrcfg.Name, err)
   140  	}
   141  
   142  	mgr := &Manager{
   143  		name:           mgrcfg.managercfg.Name,
   144  		workDir:        filepath.Join(dir, "workdir"),
   145  		kernelSrcDir:   path.Join(kernelDir, mgrcfg.KernelSrcSuffix),
   146  		kernelBuildDir: kernelDir,
   147  		currentDir:     filepath.Join(dir, "current"),
   148  		latestDir:      filepath.Join(dir, "latest"),
   149  		configTag:      hash.String(configData),
   150  		configData:     configData,
   151  		cfg:            cfg,
   152  		repo:           repo,
   153  		mgrcfg:         mgrcfg,
   154  		managercfg:     mgrcfg.managercfg,
   155  		dash:           dash,
   156  		storage:        assetStorage,
   157  		debugStorage:   !cfg.AssetStorage.IsEmpty() && cfg.AssetStorage.Debug,
   158  		stop:           stop,
   159  		debug:          debug,
   160  	}
   161  
   162  	os.RemoveAll(mgr.currentDir)
   163  	return mgr, nil
   164  }
   165  
   166  // Gates kernel builds, syzkaller builds and coverage report generation.
   167  // Kernel builds take whole machine, so we don't run more than one at a time.
   168  // Also current image build script uses some global resources (/dev/nbd0) and can't run in parallel.
   169  var buildSem = instance.NewSemaphore(1)
   170  
   171  // Gates tests that require extra VMs.
   172  // Currently we overcommit instances in such cases, so we'd like to minimize the number of
   173  // simultaneous env.Test calls.
   174  var testSem = instance.NewSemaphore(1)
   175  
   176  const fuzzingMinutesBeforeCover = 360
   177  
   178  func (mgr *Manager) loop() {
   179  	lastCommit := ""
   180  	nextBuildTime := time.Now()
   181  	var managerRestartTime, artifactUploadTime time.Time
   182  	latestInfo := mgr.checkLatest()
   183  	if latestInfo != nil && time.Since(latestInfo.Time) < kernelRebuildPeriod/2 &&
   184  		mgr.managercfg.TargetOS != targets.Fuchsia {
   185  		// If we have a reasonably fresh build,
   186  		// start manager straight away and don't rebuild kernel for a while.
   187  		// Fuchsia is a special case: it builds with syz-executor, so if we just updated syzkaller, we need
   188  		// to rebuild fuchsia as well.
   189  		log.Logf(0, "%v: using latest image built on %v", mgr.name, latestInfo.KernelCommit)
   190  		managerRestartTime = latestInfo.Time
   191  		nextBuildTime = time.Now().Add(kernelRebuildPeriod)
   192  		mgr.restartManager()
   193  	} else if latestInfo != nil {
   194  		log.Logf(0, "%v: latest image is on %v", mgr.name, latestInfo.KernelCommit)
   195  	}
   196  
   197  	ticker := time.NewTicker(buildRetryPeriod)
   198  	defer ticker.Stop()
   199  
   200  loop:
   201  	for {
   202  		if time.Since(nextBuildTime) >= 0 {
   203  			var rebuildAfter time.Duration
   204  			lastCommit, latestInfo, rebuildAfter = mgr.pollAndBuild(lastCommit, latestInfo)
   205  			nextBuildTime = time.Now().Add(rebuildAfter)
   206  		}
   207  		if !artifactUploadTime.IsZero() && time.Now().After(artifactUploadTime) {
   208  			artifactUploadTime = time.Time{}
   209  			if err := mgr.uploadCoverReport(); err != nil {
   210  				mgr.Errorf("failed to upload cover report: %v", err)
   211  			}
   212  			if err := mgr.uploadCoverStat(fuzzingMinutesBeforeCover); err != nil {
   213  				mgr.Errorf("failed to upload coverage stat: %v", err)
   214  			}
   215  			if err := mgr.uploadCorpus(); err != nil {
   216  				mgr.Errorf("failed to upload corpus: %v", err)
   217  			}
   218  		}
   219  
   220  		select {
   221  		case <-mgr.stop:
   222  			break loop
   223  		default:
   224  		}
   225  
   226  		if latestInfo != nil && (latestInfo.Time != managerRestartTime || mgr.cmd == nil) {
   227  			managerRestartTime = latestInfo.Time
   228  			mgr.restartManager()
   229  			if mgr.cmd != nil {
   230  				artifactUploadTime = time.Now().Add(fuzzingMinutesBeforeCover * time.Minute)
   231  			}
   232  		}
   233  
   234  		select {
   235  		case <-ticker.C:
   236  		case <-mgr.stop:
   237  			break loop
   238  		}
   239  	}
   240  
   241  	if mgr.cmd != nil {
   242  		mgr.cmd.Close()
   243  		mgr.cmd = nil
   244  	}
   245  	log.Logf(0, "%v: stopped", mgr.name)
   246  }
   247  
   248  func (mgr *Manager) pollAndBuild(lastCommit string, latestInfo *BuildInfo) (
   249  	string, *BuildInfo, time.Duration) {
   250  	rebuildAfter := buildRetryPeriod
   251  	commit, err := mgr.repo.Poll(mgr.mgrcfg.Repo, mgr.mgrcfg.Branch)
   252  	if err != nil {
   253  		mgr.buildFailed = true
   254  		mgr.Errorf("failed to poll: %v", err)
   255  	} else {
   256  		log.Logf(0, "%v: poll: %v", mgr.name, commit.Hash)
   257  		needsUpdate := (latestInfo == nil ||
   258  			commit.Hash != latestInfo.KernelCommit ||
   259  			mgr.configTag != latestInfo.KernelConfigTag)
   260  		mgr.buildFailed = needsUpdate
   261  		if commit.Hash != lastCommit && needsUpdate {
   262  			lastCommit = commit.Hash
   263  			select {
   264  			case <-buildSem.WaitC():
   265  				log.Logf(0, "%v: building kernel...", mgr.name)
   266  				if err := mgr.build(commit); err != nil {
   267  					log.Logf(0, "%v: %v", mgr.name, err)
   268  				} else {
   269  					log.Logf(0, "%v: build successful, [re]starting manager", mgr.name)
   270  					mgr.buildFailed = false
   271  					rebuildAfter = kernelRebuildPeriod
   272  					latestInfo = mgr.checkLatest()
   273  					if latestInfo == nil {
   274  						mgr.Errorf("failed to read build info after build")
   275  					}
   276  				}
   277  				buildSem.Signal()
   278  			case <-mgr.stop:
   279  			}
   280  		}
   281  	}
   282  	return lastCommit, latestInfo, rebuildAfter
   283  }
   284  
   285  // BuildInfo characterizes a kernel build.
   286  type BuildInfo struct {
   287  	Time              time.Time // when the build was done
   288  	Tag               string    // unique tag combined from compiler id, kernel commit and config tag
   289  	CompilerID        string    // compiler identity string (e.g. "gcc 7.1.1")
   290  	KernelRepo        string
   291  	KernelBranch      string
   292  	KernelCommit      string // git hash of kernel checkout
   293  	KernelCommitTitle string
   294  	KernelCommitDate  time.Time
   295  	KernelConfigTag   string // SHA1 hash of .config contents
   296  }
   297  
   298  func loadBuildInfo(dir string) (*BuildInfo, error) {
   299  	info := new(BuildInfo)
   300  	if err := config.LoadFile(filepath.Join(dir, "tag"), info); err != nil {
   301  		return nil, err
   302  	}
   303  	return info, nil
   304  }
   305  
   306  // checkLatest checks if we have a good working latest build and returns its build info.
   307  // If the build is missing/broken, nil is returned.
   308  func (mgr *Manager) checkLatest() *BuildInfo {
   309  	if !osutil.FilesExist(mgr.latestDir, imageFiles) {
   310  		return nil
   311  	}
   312  	info, _ := loadBuildInfo(mgr.latestDir)
   313  	return info
   314  }
   315  
   316  func (mgr *Manager) createBuildInfo(kernelCommit *vcs.Commit, compilerID string) *BuildInfo {
   317  	var tagData []byte
   318  	tagData = append(tagData, mgr.name...)
   319  	tagData = append(tagData, kernelCommit.Hash...)
   320  	tagData = append(tagData, compilerID...)
   321  	tagData = append(tagData, mgr.configTag...)
   322  	return &BuildInfo{
   323  		Time:              time.Now(),
   324  		Tag:               hash.String(tagData),
   325  		CompilerID:        compilerID,
   326  		KernelRepo:        mgr.mgrcfg.Repo,
   327  		KernelBranch:      mgr.mgrcfg.Branch,
   328  		KernelCommit:      kernelCommit.Hash,
   329  		KernelCommitTitle: kernelCommit.Title,
   330  		KernelCommitDate:  kernelCommit.CommitDate,
   331  		KernelConfigTag:   mgr.configTag,
   332  	}
   333  }
   334  
   335  func (mgr *Manager) build(kernelCommit *vcs.Commit) error {
   336  	// We first form the whole image in tmp dir and then rename it to latest.
   337  	tmpDir := mgr.latestDir + ".tmp"
   338  	if err := os.RemoveAll(tmpDir); err != nil {
   339  		return fmt.Errorf("failed to remove tmp dir: %w", err)
   340  	}
   341  	if err := osutil.MkdirAll(tmpDir); err != nil {
   342  		return fmt.Errorf("failed to create tmp dir: %w", err)
   343  	}
   344  	params := build.Params{
   345  		TargetOS:     mgr.managercfg.TargetOS,
   346  		TargetArch:   mgr.managercfg.TargetVMArch,
   347  		VMType:       mgr.managercfg.Type,
   348  		KernelDir:    mgr.kernelBuildDir,
   349  		OutputDir:    tmpDir,
   350  		Compiler:     mgr.mgrcfg.Compiler,
   351  		Linker:       mgr.mgrcfg.Linker,
   352  		Ccache:       mgr.mgrcfg.Ccache,
   353  		UserspaceDir: mgr.mgrcfg.Userspace,
   354  		CmdlineFile:  mgr.mgrcfg.KernelCmdline,
   355  		SysctlFile:   mgr.mgrcfg.KernelSysctl,
   356  		Config:       mgr.configData,
   357  		Build:        mgr.mgrcfg.Build,
   358  	}
   359  	details, err := build.Image(params)
   360  	info := mgr.createBuildInfo(kernelCommit, details.CompilerID)
   361  	if err != nil {
   362  		rep := &report.Report{
   363  			Title: fmt.Sprintf("%v build error", mgr.mgrcfg.RepoAlias),
   364  		}
   365  		var kernelError *build.KernelError
   366  		var verboseError *osutil.VerboseError
   367  		switch {
   368  		case errors.As(err, &kernelError):
   369  			rep.Report = kernelError.Report
   370  			rep.Output = kernelError.Output
   371  			rep.Recipients = kernelError.Recipients
   372  		case errors.As(err, &verboseError):
   373  			rep.Report = []byte(verboseError.Title)
   374  			rep.Output = verboseError.Output
   375  		default:
   376  			rep.Report = []byte(err.Error())
   377  		}
   378  		if err := mgr.reportBuildError(rep, info, tmpDir); err != nil {
   379  			mgr.Errorf("failed to report image error: %v", err)
   380  		}
   381  		return fmt.Errorf("kernel build failed: %w", err)
   382  	}
   383  
   384  	if err := config.SaveFile(filepath.Join(tmpDir, "tag"), info); err != nil {
   385  		return fmt.Errorf("failed to write tag file: %w", err)
   386  	}
   387  
   388  	if err := mgr.testImage(tmpDir, info); err != nil {
   389  		return err
   390  	}
   391  
   392  	// Now try to replace latest with our tmp dir as atomically as we can get on Linux.
   393  	if err := os.RemoveAll(mgr.latestDir); err != nil {
   394  		return fmt.Errorf("failed to remove latest dir: %w", err)
   395  	}
   396  	return osutil.Rename(tmpDir, mgr.latestDir)
   397  }
   398  
   399  func (mgr *Manager) restartManager() {
   400  	if !osutil.FilesExist(mgr.latestDir, imageFiles) {
   401  		mgr.Errorf("can't start manager, image files missing")
   402  		return
   403  	}
   404  	if mgr.cmd != nil {
   405  		mgr.cmd.Close()
   406  		mgr.cmd = nil
   407  	}
   408  	if err := osutil.LinkFiles(mgr.latestDir, mgr.currentDir, imageFiles); err != nil {
   409  		mgr.Errorf("failed to create current image dir: %v", err)
   410  		return
   411  	}
   412  	info, err := loadBuildInfo(mgr.currentDir)
   413  	if err != nil {
   414  		mgr.Errorf("failed to load build info: %v", err)
   415  		return
   416  	}
   417  	buildTag, err := mgr.uploadBuild(info, mgr.currentDir)
   418  	if err != nil {
   419  		mgr.Errorf("failed to upload build: %v", err)
   420  		return
   421  	}
   422  	daysSinceCommit := time.Since(info.KernelCommitDate).Hours() / 24
   423  	if mgr.buildFailed && daysSinceCommit > float64(mgr.mgrcfg.MaxKernelLagDays) {
   424  		log.Logf(0, "%s: the kernel is now too old (%.1f days since last commit), fuzzing is stopped",
   425  			mgr.name, daysSinceCommit)
   426  		return
   427  	}
   428  	cfgFile, err := mgr.writeConfig(buildTag)
   429  	if err != nil {
   430  		mgr.Errorf("failed to create manager config: %v", err)
   431  		return
   432  	}
   433  	bin := filepath.FromSlash("syzkaller/current/bin/syz-manager")
   434  	logFile := filepath.Join(mgr.currentDir, "manager.log")
   435  	args := []string{"-config", cfgFile, "-vv", "1"}
   436  	if mgr.debug {
   437  		args = append(args, "-debug")
   438  	}
   439  	mgr.cmd = NewManagerCmd(mgr.name, logFile, mgr.Errorf, bin, args...)
   440  }
   441  
   442  func (mgr *Manager) testImage(imageDir string, info *BuildInfo) error {
   443  	log.Logf(0, "%v: testing image...", mgr.name)
   444  	mgrcfg, err := mgr.createTestConfig(imageDir, info)
   445  	if err != nil {
   446  		return fmt.Errorf("failed to create manager config: %w", err)
   447  	}
   448  	defer os.RemoveAll(mgrcfg.Workdir)
   449  	if !vm.AllowsOvercommit(mgrcfg.Type) {
   450  		return nil // No support for creating machines out of thin air.
   451  	}
   452  	env, err := instance.NewEnv(mgrcfg, buildSem, testSem)
   453  	if err != nil {
   454  		return err
   455  	}
   456  	const (
   457  		testVMs     = 3
   458  		maxFailures = 1
   459  	)
   460  	results, err := env.Test(testVMs, nil, nil, nil)
   461  	if err != nil {
   462  		return err
   463  	}
   464  	failures := 0
   465  	var failureErr error
   466  	for _, res := range results {
   467  		if res.Error == nil {
   468  			continue
   469  		}
   470  		failures++
   471  		var err *instance.TestError
   472  		switch {
   473  		case errors.As(res.Error, &err):
   474  			if rep := err.Report; rep != nil {
   475  				what := "test"
   476  				if err.Boot {
   477  					what = "boot"
   478  				}
   479  				rep.Title = fmt.Sprintf("%v %v error: %v",
   480  					mgr.mgrcfg.RepoAlias, what, rep.Title)
   481  				// There are usually no duplicates for boot errors, so we reset AltTitles.
   482  				// But if we pass them, we would need to add the same prefix as for Title
   483  				// in order to avoid duping boot bugs with non-boot bugs.
   484  				rep.AltTitles = nil
   485  				if err := mgr.reportBuildError(rep, info, imageDir); err != nil {
   486  					mgr.Errorf("failed to report image error: %v", err)
   487  				}
   488  			}
   489  			if err.Boot {
   490  				failureErr = fmt.Errorf("VM boot failed with: %w", err)
   491  			} else {
   492  				failureErr = fmt.Errorf("VM testing failed with: %w", err)
   493  			}
   494  		default:
   495  			failureErr = res.Error
   496  		}
   497  	}
   498  	if failures > maxFailures {
   499  		return failureErr
   500  	}
   501  	return nil
   502  }
   503  
   504  func (mgr *Manager) reportBuildError(rep *report.Report, info *BuildInfo, imageDir string) error {
   505  	if mgr.dash == nil {
   506  		log.Logf(0, "%v: image testing failed: %v\n\n%s\n\n%s",
   507  			mgr.name, rep.Title, rep.Report, rep.Output)
   508  		return nil
   509  	}
   510  	build, err := mgr.createDashboardBuild(info, imageDir, "error")
   511  	if err != nil {
   512  		return err
   513  	}
   514  	if mgr.storage != nil {
   515  		// We have to send assets together with the other info because the report
   516  		// might be generated immediately.
   517  		uploadedAssets, err := mgr.uploadBuildAssets(build, imageDir)
   518  		if err == nil {
   519  			build.Assets = uploadedAssets
   520  		} else {
   521  			log.Logf(0, "%v: failed to upload build assets: %s", mgr.name, err)
   522  		}
   523  	}
   524  	req := &dashapi.BuildErrorReq{
   525  		Build: *build,
   526  		Crash: dashapi.Crash{
   527  			Title:      rep.Title,
   528  			AltTitles:  rep.AltTitles,
   529  			Corrupted:  false, // Otherwise they get merged with other corrupted reports.
   530  			Recipients: rep.Recipients.ToDash(),
   531  			Log:        rep.Output,
   532  			Report:     rep.Report,
   533  		},
   534  	}
   535  	if rep.GuiltyFile != "" {
   536  		req.Crash.GuiltyFiles = []string{rep.GuiltyFile}
   537  	}
   538  	if err := mgr.dash.ReportBuildError(req); err != nil {
   539  		return err
   540  	}
   541  	return nil
   542  }
   543  
   544  func (mgr *Manager) createTestConfig(imageDir string, info *BuildInfo) (*mgrconfig.Config, error) {
   545  	mgrcfg := new(mgrconfig.Config)
   546  	*mgrcfg = *mgr.managercfg
   547  	mgrcfg.Name += "-test"
   548  	mgrcfg.Tag = info.KernelCommit
   549  	mgrcfg.Workdir = filepath.Join(imageDir, "workdir")
   550  	if err := instance.SetConfigImage(mgrcfg, imageDir, true); err != nil {
   551  		return nil, err
   552  	}
   553  	mgrcfg.KernelSrc = mgr.kernelSrcDir
   554  	if err := mgrconfig.Complete(mgrcfg); err != nil {
   555  		return nil, fmt.Errorf("bad manager config: %w", err)
   556  	}
   557  	return mgrcfg, nil
   558  }
   559  
   560  func (mgr *Manager) writeConfig(buildTag string) (string, error) {
   561  	mgrcfg := new(mgrconfig.Config)
   562  	*mgrcfg = *mgr.managercfg
   563  
   564  	if mgr.dash != nil {
   565  		mgrcfg.DashboardClient = mgr.mgrcfg.DashboardClient
   566  		mgrcfg.DashboardAddr = mgr.cfg.DashboardAddr
   567  		mgrcfg.DashboardKey = mgr.mgrcfg.DashboardKey
   568  		mgrcfg.AssetStorage = mgr.cfg.AssetStorage
   569  	}
   570  	if mgr.cfg.HubAddr != "" {
   571  		mgrcfg.HubClient = mgr.cfg.Name
   572  		mgrcfg.HubAddr = mgr.cfg.HubAddr
   573  		mgrcfg.HubKey = mgr.cfg.HubKey
   574  	}
   575  	mgrcfg.Tag = buildTag
   576  	mgrcfg.Workdir = mgr.workDir
   577  	// There's not much point in keeping disabled progs in the syz-ci corpuses.
   578  	// If the syscalls on some instance are enabled again, syz-hub will provide
   579  	// it with the missing progs over time.
   580  	// And, on the other hand, PreserveCorpus=false lets us disable syscalls in
   581  	// the least destructive way for the rest of the corpus - calls will be cut
   582  	// out the of programs and the leftovers will be retriaged.
   583  	mgrcfg.PreserveCorpus = false
   584  	if err := instance.SetConfigImage(mgrcfg, mgr.currentDir, false); err != nil {
   585  		return "", err
   586  	}
   587  	// Strictly saying this is somewhat racy as builder can concurrently
   588  	// update the source, or even delete and re-clone. If this causes
   589  	// problems, we need to make a copy of sources after build.
   590  	mgrcfg.KernelSrc = mgr.kernelSrcDir
   591  	if err := mgrconfig.Complete(mgrcfg); err != nil {
   592  		return "", fmt.Errorf("bad manager config: %w", err)
   593  	}
   594  	configFile := filepath.Join(mgr.currentDir, "manager.cfg")
   595  	if err := config.SaveFile(configFile, mgrcfg); err != nil {
   596  		return "", err
   597  	}
   598  	return configFile, nil
   599  }
   600  
   601  func (mgr *Manager) uploadBuild(info *BuildInfo, imageDir string) (string, error) {
   602  	if mgr.dash == nil {
   603  		// Dashboard identifies builds by unique tags that are combined
   604  		// from kernel tag, compiler tag and config tag.
   605  		// This combined tag is meaningless without dashboard,
   606  		// so we use kenrel tag (commit tag) because it communicates
   607  		// at least some useful information.
   608  		return info.KernelCommit, nil
   609  	}
   610  
   611  	build, err := mgr.createDashboardBuild(info, imageDir, "normal")
   612  	if err != nil {
   613  		return "", err
   614  	}
   615  	mgr.lastBuild = build
   616  	commitTitles, fixCommits, err := mgr.pollCommits(info.KernelCommit)
   617  	if err != nil {
   618  		// This is not critical for operation.
   619  		mgr.Errorf("failed to poll commits: %v", err)
   620  	}
   621  	build.Commits = commitTitles
   622  	build.FixCommits = fixCommits
   623  	if mgr.storage != nil {
   624  		// We always upload build assets -- we create a separate Build object not just for
   625  		// different kernel commits, but also for different syzkaller commits, configs, etc.
   626  		// Since we deduplicate assets by hashing, this should not be a problem -- no assets
   627  		// will be actually duplicated, only the records in the DB.
   628  		assets, err := mgr.uploadBuildAssets(build, imageDir)
   629  		if err != nil {
   630  			mgr.Errorf("failed to upload build assets: %v", err)
   631  			return "", err
   632  		}
   633  		build.Assets = assets
   634  	}
   635  	if err := mgr.dash.UploadBuild(build); err != nil {
   636  		return "", err
   637  	}
   638  	return build.ID, nil
   639  }
   640  
   641  func (mgr *Manager) createDashboardBuild(info *BuildInfo, imageDir, typ string) (*dashapi.Build, error) {
   642  	var kernelConfig []byte
   643  	if kernelConfigFile := filepath.Join(imageDir, "kernel.config"); osutil.IsExist(kernelConfigFile) {
   644  		var err error
   645  		if kernelConfig, err = os.ReadFile(kernelConfigFile); err != nil {
   646  			return nil, fmt.Errorf("failed to read kernel.config: %w", err)
   647  		}
   648  	}
   649  	// Resulting build depends on both kernel build tag and syzkaller commmit.
   650  	// Also mix in build type, so that image error builds are not merged into normal builds.
   651  	var tagData []byte
   652  	tagData = append(tagData, info.Tag...)
   653  	tagData = append(tagData, prog.GitRevisionBase...)
   654  	tagData = append(tagData, typ...)
   655  	build := &dashapi.Build{
   656  		Manager:             mgr.name,
   657  		ID:                  hash.String(tagData),
   658  		OS:                  mgr.managercfg.TargetOS,
   659  		Arch:                mgr.managercfg.TargetArch,
   660  		VMArch:              mgr.managercfg.TargetVMArch,
   661  		SyzkallerCommit:     prog.GitRevisionBase,
   662  		SyzkallerCommitDate: prog.GitRevisionDate,
   663  		CompilerID:          info.CompilerID,
   664  		KernelRepo:          info.KernelRepo,
   665  		KernelBranch:        info.KernelBranch,
   666  		KernelCommit:        info.KernelCommit,
   667  		KernelCommitTitle:   info.KernelCommitTitle,
   668  		KernelCommitDate:    info.KernelCommitDate,
   669  		KernelConfig:        kernelConfig,
   670  	}
   671  	return build, nil
   672  }
   673  
   674  // pollCommits asks dashboard what commits it is interested in (i.e. fixes for
   675  // open bugs) and returns subset of these commits that are present in a build
   676  // on commit buildCommit.
   677  func (mgr *Manager) pollCommits(buildCommit string) ([]string, []dashapi.Commit, error) {
   678  	resp, err := mgr.dash.BuilderPoll(mgr.name)
   679  	if err != nil || len(resp.PendingCommits) == 0 && resp.ReportEmail == "" {
   680  		return nil, nil, err
   681  	}
   682  
   683  	// We don't want to spend too much time querying commits from the history,
   684  	// so let's pick a random subset of them each time.
   685  	const sampleCommits = 25
   686  
   687  	pendingCommits := resp.PendingCommits
   688  	if len(pendingCommits) > sampleCommits {
   689  		rand.New(rand.NewSource(time.Now().UnixNano())).Shuffle(
   690  			len(pendingCommits), func(i, j int) {
   691  				pendingCommits[i], pendingCommits[j] =
   692  					pendingCommits[j], pendingCommits[i]
   693  			})
   694  		pendingCommits = pendingCommits[:sampleCommits]
   695  	}
   696  
   697  	var present []string
   698  	if len(pendingCommits) != 0 {
   699  		commits, _, err := mgr.repo.GetCommitsByTitles(pendingCommits)
   700  		if err != nil {
   701  			return nil, nil, err
   702  		}
   703  		m := make(map[string]bool, len(commits))
   704  		for _, com := range commits {
   705  			m[vcs.CanonicalizeCommit(com.Title)] = true
   706  		}
   707  		for _, com := range pendingCommits {
   708  			if m[vcs.CanonicalizeCommit(com)] {
   709  				present = append(present, com)
   710  			}
   711  		}
   712  	}
   713  	var fixCommits []dashapi.Commit
   714  	if resp.ReportEmail != "" {
   715  		if !brokenRepo(mgr.mgrcfg.Repo) {
   716  			commits, err := mgr.repo.ExtractFixTagsFromCommits(buildCommit, resp.ReportEmail)
   717  			if err != nil {
   718  				return nil, nil, err
   719  			}
   720  			for _, com := range commits {
   721  				fixCommits = append(fixCommits, dashapi.Commit{
   722  					Title:  com.Title,
   723  					BugIDs: com.Tags,
   724  					Date:   com.Date,
   725  				})
   726  			}
   727  		}
   728  	}
   729  	return present, fixCommits, nil
   730  }
   731  
   732  func (mgr *Manager) backportCommits() []vcs.BackportCommit {
   733  	return append(
   734  		append([]vcs.BackportCommit{}, mgr.cfg.BisectBackports...),
   735  		mgr.mgrcfg.BisectBackports...,
   736  	)
   737  }
   738  
   739  func (mgr *Manager) uploadBuildAssets(buildInfo *dashapi.Build, assetFolder string) ([]dashapi.NewAsset, error) {
   740  	if mgr.storage == nil {
   741  		// No reason to continue anyway.
   742  		return nil, fmt.Errorf("asset storage is not configured")
   743  	}
   744  	type pendingAsset struct {
   745  		path      string
   746  		assetType dashapi.AssetType
   747  		name      string
   748  	}
   749  	pending := []pendingAsset{}
   750  	kernelFile := filepath.Join(assetFolder, "kernel")
   751  	if osutil.IsExist(kernelFile) {
   752  		fileName := "kernel"
   753  		if buildInfo.OS == targets.Linux {
   754  			fileName = path.Base(build.LinuxKernelImage(buildInfo.Arch))
   755  		}
   756  		pending = append(pending, pendingAsset{kernelFile, dashapi.KernelImage, fileName})
   757  	}
   758  	imageFile := filepath.Join(assetFolder, "image")
   759  	if osutil.IsExist(imageFile) {
   760  		if mgr.managercfg.Type == "qemu" {
   761  			// For qemu we currently use non-bootable disk images.
   762  			pending = append(pending, pendingAsset{imageFile, dashapi.NonBootableDisk,
   763  				"non_bootable_disk.raw"})
   764  		} else {
   765  			pending = append(pending, pendingAsset{imageFile, dashapi.BootableDisk,
   766  				"disk.raw"})
   767  		}
   768  	}
   769  	target := mgr.managercfg.SysTarget
   770  	kernelObjFile := filepath.Join(assetFolder, "obj", target.KernelObject)
   771  	if osutil.IsExist(kernelObjFile) {
   772  		pending = append(pending,
   773  			pendingAsset{kernelObjFile, dashapi.KernelObject, target.KernelObject})
   774  	}
   775  	// TODO: add initrd?
   776  	ret := []dashapi.NewAsset{}
   777  	for _, pendingAsset := range pending {
   778  		if !mgr.storage.AssetTypeEnabled(pendingAsset.assetType) {
   779  			continue
   780  		}
   781  		file, err := os.Open(pendingAsset.path)
   782  		if err != nil {
   783  			log.Logf(0, "failed to open an asset for uploading: %s, %s",
   784  				pendingAsset.path, err)
   785  			continue
   786  		}
   787  		if mgr.debugStorage {
   788  			log.Logf(0, "uploading an asset %s of type %s",
   789  				pendingAsset.path, pendingAsset.assetType)
   790  		}
   791  		extra := &asset.ExtraUploadArg{SkipIfExists: true}
   792  		hash := sha256.New()
   793  		if _, err := io.Copy(hash, file); err != nil {
   794  			log.Logf(0, "failed calculate hash for the asset %s: %s", pendingAsset.path, err)
   795  			continue
   796  		}
   797  		extra.UniqueTag = fmt.Sprintf("%x", hash.Sum(nil))
   798  		// Now we need to go back to the beginning of the file again.
   799  		if _, err := file.Seek(0, io.SeekStart); err != nil {
   800  			log.Logf(0, "failed wind back the opened file for %s: %s", pendingAsset.path, err)
   801  			continue
   802  		}
   803  		info, err := mgr.storage.UploadBuildAsset(file, pendingAsset.name,
   804  			pendingAsset.assetType, buildInfo, extra)
   805  		if err != nil {
   806  			log.Logf(0, "failed to upload an asset: %s, %s",
   807  				pendingAsset.path, err)
   808  			continue
   809  		} else if mgr.debugStorage {
   810  			log.Logf(0, "uploaded an asset: %#v", info)
   811  		}
   812  		ret = append(ret, info)
   813  	}
   814  	return ret, nil
   815  }
   816  
   817  func (mgr *Manager) httpGET(path string) (resp *http.Response, err error) {
   818  	addr := mgr.managercfg.HTTP
   819  	if addr != "" && addr[0] == ':' {
   820  		addr = "127.0.0.1" + addr // in case addr is ":port"
   821  	}
   822  	client := &http.Client{
   823  		Timeout: time.Hour,
   824  	}
   825  	return client.Get(fmt.Sprintf("http://%s%s", addr, path))
   826  }
   827  
   828  func (mgr *Manager) uploadCoverReport() error {
   829  	directUpload := mgr.managercfg.Cover && mgr.cfg.CoverUploadPath != ""
   830  	if mgr.storage == nil && !directUpload {
   831  		// Cover report uploading is disabled.
   832  		return nil
   833  	}
   834  	if mgr.storage != nil && directUpload {
   835  		return fmt.Errorf("cover report must be either uploaded directly or via asset storage")
   836  	}
   837  	// Report generation can consume lots of memory. Generate one at a time.
   838  	select {
   839  	case <-buildSem.WaitC():
   840  	case <-mgr.stop:
   841  		return nil
   842  	}
   843  	defer buildSem.Signal()
   844  
   845  	resp, err := mgr.httpGET("/cover")
   846  	if err != nil {
   847  		return fmt.Errorf("failed to get report: %w", err)
   848  	}
   849  	defer resp.Body.Close()
   850  	if directUpload {
   851  		return mgr.uploadFile(mgr.cfg.CoverUploadPath, mgr.name+".html", resp.Body, true)
   852  	}
   853  	// Upload via the asset storage.
   854  	newAsset, err := mgr.storage.UploadBuildAsset(resp.Body, mgr.name+".html",
   855  		dashapi.HTMLCoverageReport, mgr.lastBuild, nil)
   856  	if err != nil {
   857  		return fmt.Errorf("failed to upload html coverage report: %w", err)
   858  	}
   859  	err = mgr.storage.ReportBuildAssets(mgr.lastBuild, newAsset)
   860  	if err != nil {
   861  		return fmt.Errorf("failed to report the html coverage report asset: %w", err)
   862  	}
   863  	return nil
   864  }
   865  
   866  func (mgr *Manager) uploadCoverStat(fuzzingMinutes int) error {
   867  	if !mgr.managercfg.Cover || mgr.cfg.CoverPipelinePath == "" {
   868  		return nil
   869  	}
   870  
   871  	// Report generation consumes 40G RAM. Generate one at a time.
   872  	// TODO: remove it once #4585 (symbolization tuning) is closed
   873  	select {
   874  	case <-buildSem.WaitC():
   875  	case <-mgr.stop:
   876  		return nil
   877  	}
   878  	defer buildSem.Signal()
   879  
   880  	// Coverage report generation consumes and caches lots of memory.
   881  	// In the syz-ci context report generation won't be used after this point,
   882  	// so tell manager to flush report generator.
   883  	resp, err := mgr.httpGET("/cover?jsonl=1&flush=1")
   884  	if err != nil {
   885  		return fmt.Errorf("failed to httpGet /cover?jsonl=1 report: %w", err)
   886  	}
   887  	defer resp.Body.Close()
   888  	if resp.StatusCode != http.StatusOK {
   889  		sb := new(strings.Builder)
   890  		io.Copy(sb, resp.Body)
   891  		return fmt.Errorf("failed to GET /cover?jsonl=1, httpStatus %d: %s",
   892  			resp.StatusCode, sb.String())
   893  	}
   894  
   895  	curTime := time.Now()
   896  	pr, pw := io.Pipe()
   897  	defer pr.Close()
   898  	go func() {
   899  		decoder := json.NewDecoder(resp.Body)
   900  		for decoder.More() {
   901  			var covInfo cover.CoverageInfo
   902  			if err := decoder.Decode(&covInfo); err != nil {
   903  				pw.CloseWithError(fmt.Errorf("failed to decode CoverageInfo: %w", err))
   904  				return
   905  			}
   906  			if err := cover.WriteCIJSONLine(pw, covInfo, cover.CIDetails{
   907  				Version:        1,
   908  				Timestamp:      curTime.Format(time.RFC3339Nano),
   909  				FuzzingMinutes: fuzzingMinutes,
   910  				Arch:           mgr.lastBuild.Arch,
   911  				BuildID:        mgr.lastBuild.ID,
   912  				Manager:        mgr.name,
   913  				KernelRepo:     mgr.lastBuild.KernelRepo,
   914  				KernelBranch:   mgr.lastBuild.KernelBranch,
   915  				KernelCommit:   mgr.lastBuild.KernelCommit,
   916  			}); err != nil {
   917  				pw.CloseWithError(fmt.Errorf("failed to write CIJSONLine: %w", err))
   918  				return
   919  			}
   920  		}
   921  		pw.Close()
   922  	}()
   923  	fileName := fmt.Sprintf("%s/%s-%s-%d-%d.jsonl",
   924  		mgr.mgrcfg.DashboardClient,
   925  		mgr.name, curTime.Format(time.DateOnly),
   926  		curTime.Hour(), curTime.Minute())
   927  	if err := mgr.uploadFile(mgr.cfg.CoverPipelinePath, fileName, pr, false); err != nil {
   928  		return fmt.Errorf("failed to uploadFileGCS(): %w", err)
   929  	}
   930  	return nil
   931  }
   932  
   933  func (mgr *Manager) uploadCorpus() error {
   934  	if mgr.cfg.CorpusUploadPath == "" {
   935  		return nil
   936  	}
   937  	f, err := os.Open(filepath.Join(mgr.workDir, "corpus.db"))
   938  	if err != nil {
   939  		return err
   940  	}
   941  	defer f.Close()
   942  	return mgr.uploadFile(mgr.cfg.CorpusUploadPath, mgr.name+"-corpus.db", f, true)
   943  }
   944  
   945  func (mgr *Manager) uploadFile(dstPath, name string, file io.Reader, allowPublishing bool) error {
   946  	URL, err := url.Parse(dstPath)
   947  	if err != nil {
   948  		return fmt.Errorf("failed to parse upload path: %w", err)
   949  	}
   950  	URL.Path = path.Join(URL.Path, name)
   951  	URLStr := URL.String()
   952  	log.Logf(0, "uploading %v to %v", name, URLStr)
   953  	if strings.HasPrefix(URLStr, "http://") ||
   954  		strings.HasPrefix(URLStr, "https://") {
   955  		return uploadFileHTTPPut(URLStr, file)
   956  	}
   957  	return uploadFileGCS(URLStr, file, allowPublishing && mgr.cfg.PublishGCS)
   958  }
   959  
   960  func uploadFileGCS(URL string, file io.Reader, publish bool) error {
   961  	URL = strings.TrimPrefix(URL, "gs://")
   962  	GCS, err := gcs.NewClient()
   963  	if err != nil {
   964  		return fmt.Errorf("failed to create GCS client: %w", err)
   965  	}
   966  	defer GCS.Close()
   967  	gcsWriter, err := GCS.FileWriter(URL)
   968  	if err != nil {
   969  		return fmt.Errorf("failed to create GCS writer: %w", err)
   970  	}
   971  	if _, err := io.Copy(gcsWriter, file); err != nil {
   972  		gcsWriter.Close()
   973  		return fmt.Errorf("failed to copy report: %w", err)
   974  	}
   975  	if err := gcsWriter.Close(); err != nil {
   976  		return fmt.Errorf("failed to close gcs writer: %w", err)
   977  	}
   978  	if publish {
   979  		return GCS.Publish(URL)
   980  	}
   981  	return nil
   982  }
   983  
   984  func uploadFileHTTPPut(URL string, file io.Reader) error {
   985  	req, err := http.NewRequest(http.MethodPut, URL, file)
   986  	if err != nil {
   987  		return fmt.Errorf("failed to create HTTP PUT request: %w", err)
   988  	}
   989  	client := &http.Client{}
   990  	resp, err := client.Do(req)
   991  	if err != nil {
   992  		return fmt.Errorf("failed to perform HTTP PUT request: %w", err)
   993  	}
   994  	defer resp.Body.Close()
   995  	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
   996  		return fmt.Errorf("HTTP PUT failed with status code: %v", resp.StatusCode)
   997  	}
   998  	return nil
   999  }
  1000  
  1001  // Errorf logs non-fatal error and sends it to dashboard.
  1002  func (mgr *Manager) Errorf(msg string, args ...interface{}) {
  1003  	log.Errorf(mgr.name+": "+msg, args...)
  1004  	if mgr.dash != nil {
  1005  		mgr.dash.LogError(mgr.name, msg, args...)
  1006  	}
  1007  }
  1008  
  1009  func (mgr *ManagerConfig) validate(cfg *Config) error {
  1010  	// Manager name must not contain dots because it is used as GCE image name prefix.
  1011  	managerNameRe := regexp.MustCompile("^[a-zA-Z0-9-_]{3,64}$")
  1012  	if !managerNameRe.MatchString(mgr.Name) {
  1013  		return fmt.Errorf("param 'managers.name' has bad value: %q", mgr.Name)
  1014  	}
  1015  	if mgr.Jobs.AnyEnabled() && (cfg.DashboardAddr == "" || cfg.DashboardClient == "") {
  1016  		return fmt.Errorf("manager %v: has jobs but no dashboard info", mgr.Name)
  1017  	}
  1018  	if mgr.Jobs.PollCommits && (cfg.DashboardAddr == "" || mgr.DashboardClient == "") {
  1019  		return fmt.Errorf("manager %v: commit_poll is set but no dashboard info", mgr.Name)
  1020  	}
  1021  	if (mgr.Jobs.BisectCause || mgr.Jobs.BisectFix) && cfg.BisectBinDir == "" {
  1022  		return fmt.Errorf("manager %v: enabled bisection but no bisect_bin_dir", mgr.Name)
  1023  	}
  1024  	return nil
  1025  }