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 }