aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2025-11-24 08:04:20 +0100
committerDmitry Vyukov <dvyukov@google.com>2025-11-24 08:55:49 +0000
commita6deb8053825b4c7024c2b04f5d4f5a12ace1272 (patch)
treed694d4ae16e078d185ded1caa5819a6380b3c932 /pkg
parent080caeb9e30c884ab78354eae80c53cc47d8d49b (diff)
pkg/updater: factor out of syz-ci
Factor syzkaller updating functionality out of syz-ci so that it can be reused in other binaries. No functional changes intended.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/updater/updater.go324
1 files changed, 324 insertions, 0 deletions
diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go
new file mode 100644
index 000000000..dd62a061a
--- /dev/null
+++ b/pkg/updater/updater.go
@@ -0,0 +1,324 @@
+// 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.
+
+package updater
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/google/syzkaller/pkg/instance"
+ "github.com/google/syzkaller/pkg/log"
+ "github.com/google/syzkaller/pkg/osutil"
+ "github.com/google/syzkaller/pkg/vcs"
+ "github.com/google/syzkaller/prog"
+ "github.com/google/syzkaller/sys/targets"
+)
+
+const (
+ RebuildPeriod = 12 * time.Hour
+ BuildRetryPeriod = 10 * time.Minute // used for both syzkaller and kernel
+)
+
+// Updater handles everything related to syzkaller updates.
+// As kernel builder, it maintains 2 builds:
+// - latest: latest known good syzkaller build
+// - current: currently used syzkaller build
+//
+// Additionally it updates and restarts the current executable as necessary.
+// Current executable is always built on the same revision as the rest of syzkaller binaries.
+type Updater struct {
+ repo vcs.Repo
+ exe string
+ repoAddress string
+ branch string
+ descriptions string
+ gopathDir string
+ syzkallerDir string
+ latestDir string
+ currentDir string
+ syzFiles map[string]bool
+ compilerID string
+ cfg *Config
+}
+
+type Config struct {
+ // If set, exit on updates instead of restarting the current binary.
+ ExitOnUpdate bool
+ BuildSem *instance.Semaphore
+ ReportBuildError func(commit *vcs.Commit, compilerID string, buildErr error)
+ SyzkallerRepo string
+ SyzkallerBranch string
+ SyzkallerDescriptions string
+ Targets map[Target]bool
+}
+
+type Target struct {
+ OS string
+ VMArch string
+ Arch string
+}
+
+func New(cfg *Config) (*Updater, error) {
+ os.Unsetenv("GOPATH")
+ wd, err := os.Getwd()
+ if err != nil {
+ return nil, fmt.Errorf("updater: failed to get wd: %w", err)
+ }
+ bin := os.Args[0]
+ if !filepath.IsAbs(bin) {
+ bin = filepath.Join(wd, bin)
+ }
+ bin = filepath.Clean(bin)
+ exe := filepath.Base(bin)
+ if wd != filepath.Dir(bin) {
+ return nil, fmt.Errorf("updater: %v executable must be in cwd (it will be overwritten on update)", exe)
+ }
+
+ gopath := filepath.Join(wd, "gopath")
+ syzkallerDir := filepath.Join(gopath, "src", "github.com", "google", "syzkaller")
+ osutil.MkdirAll(syzkallerDir)
+
+ // List of required files in syzkaller build (contents of latest/current dirs).
+ syzFiles := map[string]bool{
+ "tag": true, // contains syzkaller repo git hash
+ "bin/syz-ci": true, // these are just copied from syzkaller dir
+ "bin/syz-manager": true,
+ "sys/*/test/*": true,
+ }
+ for target := range cfg.Targets {
+ sysTarget := targets.Get(target.OS, target.VMArch)
+ if sysTarget == nil {
+ return nil, fmt.Errorf("unsupported OS/arch: %v/%v", target.OS, target.VMArch)
+ }
+ syzFiles[fmt.Sprintf("bin/%v_%v/syz-execprog", target.OS, target.VMArch)] = true
+ if sysTarget.ExecutorBin == "" {
+ syzFiles[fmt.Sprintf("bin/%v_%v/syz-executor", target.OS, target.Arch)] = true
+ }
+ }
+ compilerID, err := osutil.RunCmd(time.Minute, "", "go", "version")
+ if err != nil {
+ return nil, err
+ }
+ return &Updater{
+ repo: vcs.NewSyzkallerRepo(syzkallerDir),
+ exe: exe,
+ repoAddress: cfg.SyzkallerRepo,
+ branch: cfg.SyzkallerBranch,
+ descriptions: cfg.SyzkallerDescriptions,
+ gopathDir: gopath,
+ syzkallerDir: syzkallerDir,
+ latestDir: filepath.Join("syzkaller", "latest"),
+ currentDir: filepath.Join("syzkaller", "current"),
+ syzFiles: syzFiles,
+ compilerID: strings.TrimSpace(string(compilerID)),
+ cfg: cfg,
+ }, nil
+}
+
+// UpdateOnStart does 3 things:
+// - ensures that the current executable is fresh
+// - ensures that we have a working syzkaller build in current
+func (upd *Updater) UpdateOnStart(autoupdate bool, updatePending, shutdown chan struct{}) {
+ os.RemoveAll(upd.currentDir)
+ latestTag := upd.checkLatest()
+ if latestTag != "" {
+ var exeMod time.Time
+ if st, err := os.Stat(upd.exe); err == nil {
+ exeMod = st.ModTime()
+ }
+ uptodate := prog.GitRevisionBase == latestTag && time.Since(exeMod) < time.Minute
+ if uptodate || !autoupdate {
+ if uptodate {
+ // Have a fresh up-to-date build, probably just restarted.
+ log.Logf(0, "current executable is up-to-date (%v)", latestTag)
+ } else {
+ log.Logf(0, "autoupdate is turned off, using latest build %v", latestTag)
+ }
+ if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
+ log.Fatal(err)
+ }
+ return
+ }
+ }
+ log.Logf(0, "current executable is on %v", prog.GitRevision)
+ log.Logf(0, "latest syzkaller build is on %v", latestTag)
+
+ // No syzkaller build or executable is stale.
+ lastCommit := prog.GitRevisionBase
+ if lastCommit != latestTag {
+ // Latest build and syz-ci are inconsistent. Rebuild everything.
+ lastCommit = ""
+ latestTag = ""
+ }
+ for {
+ lastCommit = upd.pollAndBuild(lastCommit)
+ latestTag := upd.checkLatest()
+ if latestTag != "" {
+ // The build was successful or we had the latest build from previous runs.
+ // Either way, use the latest build.
+ log.Logf(0, "using syzkaller built on %v", latestTag)
+ if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
+ log.Fatal(err)
+ }
+ if autoupdate && prog.GitRevisionBase != latestTag {
+ upd.UpdateAndRestart()
+ }
+ break
+ }
+
+ // No good build at all, try again later.
+ log.Logf(0, "retrying in %v", BuildRetryPeriod)
+ select {
+ case <-time.After(BuildRetryPeriod):
+ case <-shutdown:
+ os.Exit(0)
+ }
+ }
+ if autoupdate {
+ go func() {
+ upd.waitForUpdate()
+ close(updatePending)
+ }()
+ }
+}
+
+// waitForUpdate polls and rebuilds syzkaller.
+// Returns when we have a new good build in latest.
+func (upd *Updater) waitForUpdate() {
+ time.Sleep(RebuildPeriod)
+ latestTag := upd.checkLatest()
+ lastCommit := latestTag
+ for {
+ lastCommit = upd.pollAndBuild(lastCommit)
+ if latestTag != upd.checkLatest() {
+ break
+ }
+ time.Sleep(BuildRetryPeriod)
+ }
+ log.Logf(0, "syzkaller: update available, restarting")
+}
+
+// UpdateAndRestart updates and restarts the current executable.
+// If ExitOnUpdate is set, exits without restarting instead.
+// Does not return.
+func (upd *Updater) UpdateAndRestart() {
+ log.Logf(0, "restarting executable for update")
+ latestBin := filepath.Join(upd.latestDir, "bin", upd.exe)
+ if err := osutil.CopyFile(latestBin, upd.exe); err != nil {
+ log.Fatal(err)
+ }
+ if upd.cfg.ExitOnUpdate {
+ log.Logf(0, "exiting, please restart syz-ci to run the new version")
+ os.Exit(0)
+ }
+ if err := syscall.Exec(upd.exe, os.Args, os.Environ()); err != nil {
+ log.Fatal(err)
+ }
+ log.Fatalf("not reachable")
+}
+
+func (upd *Updater) pollAndBuild(lastCommit string) string {
+ commit, err := upd.repo.Poll(upd.repoAddress, upd.branch)
+ if err != nil {
+ log.Logf(0, "syzkaller: failed to poll: %v", err)
+ return lastCommit
+ }
+ log.Logf(0, "syzkaller: poll: %v (%v)", commit.Hash, commit.Title)
+ if lastCommit == commit.Hash {
+ return lastCommit
+ }
+ log.Logf(0, "syzkaller: building ...")
+ if err := upd.build(commit); err != nil {
+ log.Errorf("syzkaller: failed to build: %v", err)
+ if upd.cfg.ReportBuildError != nil {
+ upd.cfg.ReportBuildError(commit, upd.compilerID, err)
+ }
+ }
+ return commit.Hash
+}
+
+// nolint: goconst // "GOPATH=" looks good here, ignore
+func (upd *Updater) build(commit *vcs.Commit) error {
+ // syzkaller testing may be slowed down by concurrent kernel builds too much
+ // and cause timeout failures, so we serialize it with other builds:
+ // https://groups.google.com/forum/#!msg/syzkaller-openbsd-bugs/o-G3vEsyQp4/f_nFpoNKBQAJ
+ upd.cfg.BuildSem.Wait()
+ defer upd.cfg.BuildSem.Signal()
+
+ if upd.descriptions != "" {
+ files, err := os.ReadDir(upd.descriptions)
+ if err != nil {
+ return fmt.Errorf("failed to read descriptions dir: %w", err)
+ }
+ for _, f := range files {
+ src := filepath.Join(upd.descriptions, f.Name())
+ dst := ""
+ switch filepath.Ext(src) {
+ case ".txt", ".const":
+ dst = filepath.Join(upd.syzkallerDir, "sys", targets.Linux, f.Name())
+ case ".test":
+ dst = filepath.Join(upd.syzkallerDir, "sys", targets.Linux, "test", f.Name())
+ case ".h":
+ dst = filepath.Join(upd.syzkallerDir, "executor", f.Name())
+ default:
+ continue
+ }
+ if err := osutil.CopyFile(src, dst); err != nil {
+ return err
+ }
+ }
+ }
+ // This will also generate descriptions and should go before the 'go test' below.
+ cmd := osutil.Command(instance.MakeBin, "host", "ci")
+ cmd.Dir = upd.syzkallerDir
+ cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
+ if _, err := osutil.Run(time.Hour, cmd); err != nil {
+ return fmt.Errorf("make host failed: %w", err)
+ }
+ for target := range upd.cfg.Targets {
+ cmd = osutil.Command(instance.MakeBin, "target")
+ cmd.Dir = upd.syzkallerDir
+ cmd.Env = append([]string{}, os.Environ()...)
+ cmd.Env = append(cmd.Env,
+ "GOPATH="+upd.gopathDir,
+ "TARGETOS="+target.OS,
+ "TARGETVMARCH="+target.VMArch,
+ "TARGETARCH="+target.Arch,
+ )
+ if _, err := osutil.Run(time.Hour, cmd); err != nil {
+ return fmt.Errorf("make target failed: %w", err)
+ }
+ }
+ cmd = osutil.Command("go", "test", "-short", "./...")
+ cmd.Dir = upd.syzkallerDir
+ cmd.Env = append([]string{
+ "GOPATH=" + upd.gopathDir,
+ "SYZ_DISABLE_SANDBOXING=yes",
+ }, os.Environ()...)
+ if _, err := osutil.Run(time.Hour, cmd); err != nil {
+ return fmt.Errorf("testing failed: %w", err)
+ }
+ tagFile := filepath.Join(upd.syzkallerDir, "tag")
+ if err := osutil.WriteFile(tagFile, []byte(commit.Hash)); err != nil {
+ return fmt.Errorf("failed to write tag file: %w", err)
+ }
+ if err := osutil.CopyFiles(upd.syzkallerDir, upd.latestDir, upd.syzFiles); err != nil {
+ return fmt.Errorf("failed to copy syzkaller: %w", err)
+ }
+ return nil
+}
+
+// checkLatest returns tag of the latest build,
+// or an empty string if latest build is missing/broken.
+func (upd *Updater) checkLatest() string {
+ if !osutil.FilesExist(upd.latestDir, upd.syzFiles) {
+ return ""
+ }
+ tag, _ := os.ReadFile(filepath.Join(upd.latestDir, "tag"))
+ return string(tag)
+}