github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/syz-ci/updater.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  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"syscall"
    13  	"time"
    14  
    15  	"github.com/google/syzkaller/dashboard/dashapi"
    16  	"github.com/google/syzkaller/pkg/instance"
    17  	"github.com/google/syzkaller/pkg/log"
    18  	"github.com/google/syzkaller/pkg/osutil"
    19  	"github.com/google/syzkaller/pkg/vcs"
    20  	"github.com/google/syzkaller/prog"
    21  	"github.com/google/syzkaller/sys/targets"
    22  )
    23  
    24  const (
    25  	syzkallerRebuildPeriod = 12 * time.Hour
    26  	buildRetryPeriod       = 10 * time.Minute // used for both syzkaller and kernel
    27  )
    28  
    29  // SyzUpdater handles everything related to syzkaller updates.
    30  // As kernel builder, it maintains 2 builds:
    31  //   - latest: latest known good syzkaller build
    32  //   - current: currently used syzkaller build
    33  //
    34  // Additionally it updates and restarts the current executable as necessary.
    35  // Current executable is always built on the same revision as the rest of syzkaller binaries.
    36  type SyzUpdater struct {
    37  	repo          vcs.Repo
    38  	exe           string
    39  	repoAddress   string
    40  	branch        string
    41  	descriptions  string
    42  	gopathDir     string
    43  	syzkallerDir  string
    44  	latestDir     string
    45  	currentDir    string
    46  	syzFiles      map[string]bool
    47  	targets       map[string]bool
    48  	dashboardAddr string
    49  	compilerID    string
    50  	cfg           *Config
    51  }
    52  
    53  func NewSyzUpdater(cfg *Config) *SyzUpdater {
    54  	wd, err := os.Getwd()
    55  	if err != nil {
    56  		log.Fatalf("failed to get wd: %v", err)
    57  	}
    58  	bin := os.Args[0]
    59  	if !filepath.IsAbs(bin) {
    60  		bin = filepath.Join(wd, bin)
    61  	}
    62  	bin = filepath.Clean(bin)
    63  	exe := filepath.Base(bin)
    64  	if wd != filepath.Dir(bin) {
    65  		log.Fatalf("%v executable must be in cwd (it will be overwritten on update)", exe)
    66  	}
    67  
    68  	gopath := filepath.Join(wd, "gopath")
    69  	syzkallerDir := filepath.Join(gopath, "src", "github.com", "google", "syzkaller")
    70  	osutil.MkdirAll(syzkallerDir)
    71  
    72  	// List of required files in syzkaller build (contents of latest/current dirs).
    73  	syzFiles := map[string]bool{
    74  		"tag":             true, // contains syzkaller repo git hash
    75  		"bin/syz-ci":      true, // these are just copied from syzkaller dir
    76  		"bin/syz-manager": true,
    77  		"sys/*/test/*":    true,
    78  	}
    79  	targets := make(map[string]bool)
    80  	for _, mgr := range cfg.Managers {
    81  		mgrcfg := mgr.managercfg
    82  		os, vmarch, arch := mgrcfg.TargetOS, mgrcfg.TargetVMArch, mgrcfg.TargetArch
    83  		targets[os+"/"+vmarch+"/"+arch] = true
    84  		syzFiles[fmt.Sprintf("bin/%v_%v/syz-fuzzer", os, vmarch)] = true
    85  		syzFiles[fmt.Sprintf("bin/%v_%v/syz-execprog", os, vmarch)] = true
    86  		if mgrcfg.SysTarget.ExecutorBin == "" {
    87  			syzFiles[fmt.Sprintf("bin/%v_%v/syz-executor", os, arch)] = true
    88  		}
    89  	}
    90  	compilerID, err := osutil.RunCmd(time.Minute, "", "go", "version")
    91  	if err != nil {
    92  		log.Fatalf("%v", err)
    93  	}
    94  	return &SyzUpdater{
    95  		repo:          vcs.NewSyzkallerRepo(syzkallerDir),
    96  		exe:           exe,
    97  		repoAddress:   cfg.SyzkallerRepo,
    98  		branch:        cfg.SyzkallerBranch,
    99  		descriptions:  cfg.SyzkallerDescriptions,
   100  		gopathDir:     gopath,
   101  		syzkallerDir:  syzkallerDir,
   102  		latestDir:     filepath.Join("syzkaller", "latest"),
   103  		currentDir:    filepath.Join("syzkaller", "current"),
   104  		syzFiles:      syzFiles,
   105  		targets:       targets,
   106  		dashboardAddr: cfg.DashboardAddr,
   107  		compilerID:    strings.TrimSpace(string(compilerID)),
   108  		cfg:           cfg,
   109  	}
   110  }
   111  
   112  // UpdateOnStart does 3 things:
   113  //   - ensures that the current executable is fresh
   114  //   - ensures that we have a working syzkaller build in current
   115  func (upd *SyzUpdater) UpdateOnStart(autoupdate bool, shutdown chan struct{}) {
   116  	os.RemoveAll(upd.currentDir)
   117  	latestTag := upd.checkLatest()
   118  	if latestTag != "" {
   119  		var exeMod time.Time
   120  		if st, err := os.Stat(upd.exe); err == nil {
   121  			exeMod = st.ModTime()
   122  		}
   123  		uptodate := prog.GitRevisionBase == latestTag && time.Since(exeMod) < time.Minute
   124  		if uptodate || !autoupdate {
   125  			if uptodate {
   126  				// Have a fresh up-to-date build, probably just restarted.
   127  				log.Logf(0, "current executable is up-to-date (%v)", latestTag)
   128  			} else {
   129  				log.Logf(0, "autoupdate is turned off, using latest build %v", latestTag)
   130  			}
   131  			if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
   132  				log.Fatal(err)
   133  			}
   134  			return
   135  		}
   136  	}
   137  	log.Logf(0, "current executable is on %v", prog.GitRevision)
   138  	log.Logf(0, "latest syzkaller build is on %v", latestTag)
   139  
   140  	// No syzkaller build or executable is stale.
   141  	lastCommit := prog.GitRevisionBase
   142  	if lastCommit != latestTag {
   143  		// Latest build and syz-ci are inconsistent. Rebuild everything.
   144  		lastCommit = ""
   145  		latestTag = ""
   146  	}
   147  	for {
   148  		lastCommit = upd.pollAndBuild(lastCommit)
   149  		latestTag := upd.checkLatest()
   150  		if latestTag != "" {
   151  			// The build was successful or we had the latest build from previous runs.
   152  			// Either way, use the latest build.
   153  			log.Logf(0, "using syzkaller built on %v", latestTag)
   154  			if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
   155  				log.Fatal(err)
   156  			}
   157  			if autoupdate && prog.GitRevisionBase != latestTag {
   158  				upd.UpdateAndRestart()
   159  			}
   160  			return
   161  		}
   162  
   163  		// No good build at all, try again later.
   164  		log.Logf(0, "retrying in %v", buildRetryPeriod)
   165  		select {
   166  		case <-time.After(buildRetryPeriod):
   167  		case <-shutdown:
   168  			os.Exit(0)
   169  		}
   170  	}
   171  }
   172  
   173  // WaitForUpdate polls and rebuilds syzkaller.
   174  // Returns when we have a new good build in latest.
   175  func (upd *SyzUpdater) WaitForUpdate() {
   176  	time.Sleep(syzkallerRebuildPeriod)
   177  	latestTag := upd.checkLatest()
   178  	lastCommit := latestTag
   179  	for {
   180  		lastCommit = upd.pollAndBuild(lastCommit)
   181  		if latestTag != upd.checkLatest() {
   182  			break
   183  		}
   184  		time.Sleep(buildRetryPeriod)
   185  	}
   186  	log.Logf(0, "syzkaller: update available, restarting")
   187  }
   188  
   189  // UpdateAndRestart updates and restarts the current executable.
   190  // Does not return.
   191  func (upd *SyzUpdater) UpdateAndRestart() {
   192  	log.Logf(0, "restarting executable for update")
   193  	latestBin := filepath.Join(upd.latestDir, "bin", upd.exe)
   194  	if err := osutil.CopyFile(latestBin, upd.exe); err != nil {
   195  		log.Fatal(err)
   196  	}
   197  	if *flagExitOnUpgrade {
   198  		log.Logf(0, "exiting, please restart syz-ci to run the new version")
   199  		os.Exit(0)
   200  	}
   201  	if err := syscall.Exec(upd.exe, os.Args, os.Environ()); err != nil {
   202  		log.Fatal(err)
   203  	}
   204  	log.Fatalf("not reachable")
   205  }
   206  
   207  func (upd *SyzUpdater) pollAndBuild(lastCommit string) string {
   208  	commit, err := upd.repo.Poll(upd.repoAddress, upd.branch)
   209  	if err != nil {
   210  		log.Logf(0, "syzkaller: failed to poll: %v", err)
   211  		return lastCommit
   212  	}
   213  	log.Logf(0, "syzkaller: poll: %v (%v)", commit.Hash, commit.Title)
   214  	if lastCommit == commit.Hash {
   215  		return lastCommit
   216  	}
   217  	log.Logf(0, "syzkaller: building ...")
   218  	if err := upd.build(commit); err != nil {
   219  		log.Logf(0, "syzkaller: %v", err)
   220  		upd.uploadBuildError(commit, err)
   221  	}
   222  	return commit.Hash
   223  }
   224  
   225  // nolint: goconst // "GOPATH=" looks good here, ignore
   226  func (upd *SyzUpdater) build(commit *vcs.Commit) error {
   227  	// syzkaller testing may be slowed down by concurrent kernel builds too much
   228  	// and cause timeout failures, so we serialize it with other builds:
   229  	// https://groups.google.com/forum/#!msg/syzkaller-openbsd-bugs/o-G3vEsyQp4/f_nFpoNKBQAJ
   230  	buildSem.Wait()
   231  	defer buildSem.Signal()
   232  
   233  	if upd.descriptions != "" {
   234  		files, err := os.ReadDir(upd.descriptions)
   235  		if err != nil {
   236  			return fmt.Errorf("failed to read descriptions dir: %w", err)
   237  		}
   238  		for _, f := range files {
   239  			src := filepath.Join(upd.descriptions, f.Name())
   240  			dst := ""
   241  			switch filepath.Ext(src) {
   242  			case ".txt", ".const":
   243  				dst = filepath.Join(upd.syzkallerDir, "sys", targets.Linux, f.Name())
   244  			case ".test":
   245  				dst = filepath.Join(upd.syzkallerDir, "sys", targets.Linux, "test", f.Name())
   246  			case ".h":
   247  				dst = filepath.Join(upd.syzkallerDir, "executor", f.Name())
   248  			default:
   249  				continue
   250  			}
   251  			if err := osutil.CopyFile(src, dst); err != nil {
   252  				return err
   253  			}
   254  		}
   255  	}
   256  	// This will also generate descriptions and should go before the 'go test' below.
   257  	cmd := osutil.Command(instance.MakeBin, "host", "ci")
   258  	cmd.Dir = upd.syzkallerDir
   259  	cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
   260  	if _, err := osutil.Run(time.Hour, cmd); err != nil {
   261  		return osutil.PrependContext("make host failed", err)
   262  	}
   263  	for target := range upd.targets {
   264  		parts := strings.Split(target, "/")
   265  		cmd = osutil.Command(instance.MakeBin, "target")
   266  		cmd.Dir = upd.syzkallerDir
   267  		cmd.Env = append([]string{}, os.Environ()...)
   268  		cmd.Env = append(cmd.Env,
   269  			"GOPATH="+upd.gopathDir,
   270  			"TARGETOS="+parts[0],
   271  			"TARGETVMARCH="+parts[1],
   272  			"TARGETARCH="+parts[2],
   273  		)
   274  		if _, err := osutil.Run(time.Hour, cmd); err != nil {
   275  			return osutil.PrependContext("make target failed", err)
   276  		}
   277  	}
   278  	cmd = osutil.Command("go", "test", "-short", "./...")
   279  	cmd.Dir = upd.syzkallerDir
   280  	cmd.Env = append([]string{
   281  		"GOPATH=" + upd.gopathDir,
   282  		"SYZ_DISABLE_SANDBOXING=yes",
   283  	}, os.Environ()...)
   284  	if _, err := osutil.Run(time.Hour, cmd); err != nil {
   285  		return osutil.PrependContext("testing failed", err)
   286  	}
   287  	tagFile := filepath.Join(upd.syzkallerDir, "tag")
   288  	if err := osutil.WriteFile(tagFile, []byte(commit.Hash)); err != nil {
   289  		return fmt.Errorf("failed to write tag file: %w", err)
   290  	}
   291  	if err := osutil.CopyFiles(upd.syzkallerDir, upd.latestDir, upd.syzFiles); err != nil {
   292  		return fmt.Errorf("failed to copy syzkaller: %w", err)
   293  	}
   294  	return nil
   295  }
   296  
   297  func (upd *SyzUpdater) uploadBuildError(commit *vcs.Commit, buildErr error) {
   298  	var title string
   299  	var output []byte
   300  	var verbose *osutil.VerboseError
   301  	if errors.As(buildErr, &verbose) {
   302  		title = verbose.Title
   303  		output = verbose.Output
   304  	} else {
   305  		title = buildErr.Error()
   306  	}
   307  	title = "syzkaller: " + title
   308  	for _, mgrcfg := range upd.cfg.Managers {
   309  		if upd.dashboardAddr == "" || mgrcfg.DashboardClient == "" {
   310  			log.Logf(0, "not uploading build error for %v: no dashboard", mgrcfg.Name)
   311  			continue
   312  		}
   313  		dash, err := dashapi.New(mgrcfg.DashboardClient, upd.dashboardAddr, mgrcfg.DashboardKey)
   314  		if err != nil {
   315  			log.Logf(0, "failed to report build error for %v: %v", mgrcfg.Name, err)
   316  			return
   317  		}
   318  		managercfg := mgrcfg.managercfg
   319  		req := &dashapi.BuildErrorReq{
   320  			Build: dashapi.Build{
   321  				Manager:             managercfg.Name,
   322  				ID:                  commit.Hash,
   323  				OS:                  managercfg.TargetOS,
   324  				Arch:                managercfg.TargetArch,
   325  				VMArch:              managercfg.TargetVMArch,
   326  				SyzkallerCommit:     commit.Hash,
   327  				SyzkallerCommitDate: commit.CommitDate,
   328  				CompilerID:          upd.compilerID,
   329  				KernelRepo:          upd.repoAddress,
   330  				KernelBranch:        upd.branch,
   331  			},
   332  			Crash: dashapi.Crash{
   333  				Title: title,
   334  				Log:   output,
   335  			},
   336  		}
   337  		if err := dash.ReportBuildError(req); err != nil {
   338  			// TODO: log ReportBuildError error to dashboard.
   339  			log.Logf(0, "failed to report build error for %v: %v", mgrcfg.Name, err)
   340  		}
   341  	}
   342  }
   343  
   344  // checkLatest returns tag of the latest build,
   345  // or an empty string if latest build is missing/broken.
   346  func (upd *SyzUpdater) checkLatest() string {
   347  	if !osutil.FilesExist(upd.latestDir, upd.syzFiles) {
   348  		return ""
   349  	}
   350  	tag, _ := os.ReadFile(filepath.Join(upd.latestDir, "tag"))
   351  	return string(tag)
   352  }