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