github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/workflow/build-step/main.go (about) 1 // Copyright 2024 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 "bytes" 8 "context" 9 "encoding/json" 10 "errors" 11 "flag" 12 "fmt" 13 "log" 14 "os" 15 "path/filepath" 16 17 "github.com/google/syzkaller/pkg/build" 18 "github.com/google/syzkaller/pkg/debugtracer" 19 "github.com/google/syzkaller/pkg/osutil" 20 "github.com/google/syzkaller/pkg/vcs" 21 "github.com/google/syzkaller/sys/targets" 22 "github.com/google/syzkaller/syz-cluster/pkg/api" 23 "github.com/google/syzkaller/syz-cluster/pkg/app" 24 "github.com/google/syzkaller/syz-cluster/pkg/triage" 25 ) 26 27 var ( 28 flagRequest = flag.String("request", "", "path to a build request description") 29 flagRepository = flag.String("repository", "", "path to a kernel checkout") 30 flagOutput = flag.String("output", "", "path to save kernel build artifacts") 31 flagTestName = flag.String("test_name", "", "test name") 32 flagSession = flag.String("session", "", "session ID") 33 flagFindings = flag.Bool("findings", false, "report build failures as findings") 34 flagSmokeBuild = flag.Bool("smoke_build", false, "build only if new, don't report findings") 35 ) 36 37 func main() { 38 flag.Parse() 39 ensureFlags(*flagRequest, "--request", 40 *flagRepository, "--repository", 41 *flagOutput, "--output") 42 if !*flagSmokeBuild { 43 ensureFlags( 44 *flagTestName, "--test_name", 45 *flagSession, "--session", 46 ) 47 } 48 49 req := readRequest() 50 ctx := context.Background() 51 client := app.DefaultClient() 52 // TODO: (optimization) query whether the same BuildRequest has already been completed. 53 var series *api.Series 54 if req.SeriesID != "" { 55 var err error 56 series, err = client.GetSeries(ctx, req.SeriesID) 57 if err != nil { 58 app.Fatalf("failed to query the series info: %v", err) 59 } 60 } 61 uploadReq := &api.UploadBuildReq{ 62 Build: api.Build{ 63 Arch: req.Arch, 64 ConfigName: req.ConfigName, 65 TreeName: req.TreeName, 66 TreeURL: req.TreeURL, 67 SeriesID: req.SeriesID, 68 }, 69 } 70 output := new(bytes.Buffer) 71 tracer := &debugtracer.GenericTracer{ 72 WithTime: false, 73 TraceWriter: output, 74 OutDir: "", 75 } 76 commit, err := checkoutKernel(tracer, req, series) 77 if commit != nil { 78 uploadReq.CommitHash = commit.Hash 79 uploadReq.CommitDate = commit.CommitDate 80 } 81 ret := &BuildResult{} 82 if err != nil { 83 log.Printf("failed to checkout: %v", err) 84 reportResults(ctx, client, nil, nil, []byte(err.Error())) 85 return 86 } else { 87 if *flagSmokeBuild { 88 skip, err := alreadyBuilt(ctx, client, uploadReq) 89 if err != nil { 90 app.Fatalf("failed to query known builds: %v", err) 91 } else if skip { 92 log.Printf("%s already built, skipping", uploadReq.CommitHash) 93 return 94 } 95 } 96 ret, err = buildKernel(tracer, req) 97 if err != nil { 98 log.Printf("build process failed: %v", err) 99 reportResults(ctx, client, nil, nil, []byte(err.Error())) 100 return 101 } else { 102 uploadReq.Compiler = ret.Compiler 103 uploadReq.Config = ret.Config 104 if ret.Finding == nil { 105 uploadReq.BuildSuccess = true 106 } else { 107 log.Printf("%s", output.Bytes()) 108 log.Printf("failed: %s\n%s", ret.Finding.Title, ret.Finding.Report) 109 uploadReq.Log = ret.Finding.Log 110 } 111 } 112 } 113 reportResults(ctx, client, uploadReq, ret.Finding, output.Bytes()) 114 } 115 116 func reportResults(ctx context.Context, client *api.Client, 117 uploadReq *api.UploadBuildReq, finding *api.NewFinding, output []byte) { 118 var buildID string 119 status := api.TestPassed 120 if uploadReq != nil { 121 if !uploadReq.BuildSuccess { 122 status = api.TestFailed 123 } 124 buildInfo, err := client.UploadBuild(ctx, uploadReq) 125 if err != nil { 126 app.Fatalf("failed to upload build: %v", err) 127 } 128 log.Printf("uploaded build, reply: %q", buildInfo) 129 buildID = buildInfo.ID 130 } else { 131 status = api.TestError 132 } 133 osutil.WriteJSON(filepath.Join(*flagOutput, "result.json"), &api.BuildResult{ 134 BuildID: buildID, 135 Success: status == api.TestPassed, 136 }) 137 if *flagSmokeBuild { 138 return 139 } 140 testResult := &api.TestResult{ 141 SessionID: *flagSession, 142 TestName: *flagTestName, 143 Result: status, 144 Log: output, 145 } 146 if uploadReq != nil { 147 if uploadReq.SeriesID != "" { 148 testResult.PatchedBuildID = buildID 149 } else { 150 testResult.BaseBuildID = buildID 151 } 152 } 153 err := client.UploadTestResult(ctx, testResult) 154 if err != nil { 155 app.Fatalf("failed to report the test result: %v", err) 156 } 157 if *flagFindings && finding != nil { 158 err = client.UploadFinding(ctx, finding) 159 if err != nil { 160 app.Fatalf("failed to report the finding: %v", err) 161 } 162 } 163 } 164 165 func alreadyBuilt(ctx context.Context, client *api.Client, 166 req *api.UploadBuildReq) (bool, error) { 167 build, err := client.LastBuild(ctx, &api.LastBuildReq{ 168 Arch: req.Build.Arch, 169 ConfigName: req.Build.ConfigName, 170 TreeName: req.Build.TreeName, 171 Commit: req.CommitHash, 172 }) 173 if err != nil { 174 return false, err 175 } 176 return build != nil, nil 177 } 178 179 func readRequest() *api.BuildRequest { 180 raw, err := os.ReadFile(*flagRequest) 181 if err != nil { 182 app.Fatalf("failed to read request: %v", err) 183 return nil 184 } 185 var req api.BuildRequest 186 err = json.Unmarshal(raw, &req) 187 if err != nil { 188 app.Fatalf("failed to unmarshal request: %v, %s", err, raw) 189 return nil 190 } 191 return &req 192 } 193 194 func checkoutKernel(tracer debugtracer.DebugTracer, req *api.BuildRequest, series *api.Series) (*vcs.Commit, error) { 195 tracer.Log("checking out %q", req.CommitHash) 196 ops, err := triage.NewGitTreeOps(*flagRepository, true) 197 if err != nil { 198 return nil, err 199 } 200 commit, err := ops.Commit(req.TreeName, req.CommitHash) 201 if err != nil { 202 return nil, fmt.Errorf("failed to get commit info: %w", err) 203 } 204 var patches [][]byte 205 if series != nil { 206 patches = series.PatchBodies() 207 } 208 if len(patches) > 0 { 209 tracer.Log("applying %d patches", len(patches)) 210 } 211 err = ops.ApplySeries(commit.Hash, patches) 212 return commit, err 213 } 214 215 type BuildResult struct { 216 Config []byte 217 Compiler string 218 Finding *api.NewFinding 219 } 220 221 func buildKernel(tracer debugtracer.DebugTracer, req *api.BuildRequest) (*BuildResult, error) { 222 kernelConfig, err := os.ReadFile(filepath.Join("/kernel-configs", req.ConfigName)) 223 if err != nil { 224 return nil, fmt.Errorf("failed to read the kernel config: %w", err) 225 } 226 if req.Arch != "amd64" { 227 // TODO: lift this restriction. 228 return nil, fmt.Errorf("only amd64 builds are supported now") 229 } 230 params := build.Params{ 231 TargetOS: targets.Linux, 232 TargetArch: req.Arch, 233 VMType: "qemu", // TODO: support others. 234 KernelDir: *flagRepository, 235 OutputDir: *flagOutput, 236 Compiler: "clang", 237 Linker: "ld.lld", 238 UserspaceDir: "/disk-images/buildroot_amd64_2024.09", // See the Dockerfile. 239 Config: kernelConfig, 240 Tracer: tracer, 241 } 242 tracer.Log("started build: %q", req) 243 info, err := build.Image(params) 244 tracer.Log("compiler: %q", info.CompilerID) 245 tracer.Log("signature: %q", info.Signature) 246 // We can fill this regardless of whether it succeeded. 247 ret := &BuildResult{ 248 Compiler: info.CompilerID, 249 } 250 ret.Config, _ = os.ReadFile(filepath.Join(*flagOutput, "kernel.config")) 251 if err != nil { 252 ret.Finding = &api.NewFinding{ 253 SessionID: *flagSession, 254 TestName: *flagTestName, 255 Title: "kernel build error", 256 } 257 var kernelError *build.KernelError 258 var verboseError *osutil.VerboseError 259 switch { 260 case errors.As(err, &kernelError): 261 tracer.Log("kernel error: %q / %s", kernelError.Report, kernelError.Output) 262 ret.Finding.Report = kernelError.Report 263 ret.Finding.Log = kernelError.Output 264 return ret, nil 265 case errors.As(err, &verboseError): 266 tracer.Log("verbose error: %s / %s", verboseError, verboseError.Output) 267 ret.Finding.Report = []byte(verboseError.Error()) 268 ret.Finding.Log = verboseError.Output 269 return ret, nil 270 default: 271 tracer.Log("other error: %v", err) 272 } 273 return nil, err 274 } 275 tracer.Log("build finished successfully") 276 277 err = saveSymbolHashes(tracer) 278 if err != nil { 279 tracer.Log("failed to save symbol hashes: %s", err) 280 } 281 // Note: Output directory has the following structure: 282 // |-- image 283 // |-- symbol_hashes.json 284 // |-- kernel 285 // |-- kernel.config 286 // `-- obj 287 // `-- vmlinux 288 return ret, nil 289 } 290 291 func saveSymbolHashes(tracer debugtracer.DebugTracer) error { 292 hashes, err := build.ElfSymbolHashes(filepath.Join(*flagRepository, "vmlinux.o")) 293 if err != nil { 294 return fmt.Errorf("failed to query symbol hashes: %w", err) 295 } 296 tracer.Log("extracted hashes for %d text symbols and %d data symbols", 297 len(hashes.Text), len(hashes.Data)) 298 file, err := os.Create(filepath.Join(*flagOutput, "symbol_hashes.json")) 299 if err != nil { 300 return fmt.Errorf("failed to open symbol_hashes.json: %w", err) 301 } 302 defer file.Close() 303 err = json.NewEncoder(file).Encode(hashes) 304 if err != nil { 305 return fmt.Errorf("failed to serialize: %w", err) 306 } 307 return nil 308 } 309 310 func ensureFlags(args ...string) { 311 for i := 0; i+1 < len(args); i += 2 { 312 if args[i] == "" { 313 app.Fatalf("%s must be set", args[i+1]) 314 } 315 } 316 }