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 }