github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/build/build.go (about) 1 // Copyright 2018 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 build contains helper functions for building kernels/images. 5 package build 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "os" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strings" 17 "time" 18 19 "github.com/google/syzkaller/pkg/debugtracer" 20 "github.com/google/syzkaller/pkg/osutil" 21 "github.com/google/syzkaller/pkg/report" 22 "github.com/google/syzkaller/pkg/vcs" 23 "github.com/google/syzkaller/sys/targets" 24 ) 25 26 // Params is input arguments for the Image and Clean functions. 27 type Params struct { 28 TargetOS string 29 TargetArch string 30 VMType string 31 KernelDir string 32 OutputDir string 33 Compiler string 34 Make string 35 Linker string 36 Ccache string 37 UserspaceDir string 38 CmdlineFile string 39 SysctlFile string 40 Config []byte 41 Tracer debugtracer.DebugTracer 42 BuildCPUs int // If 0, all CPUs will be used. 43 Build json.RawMessage 44 } 45 46 // Information that is returned from the Image function. 47 type ImageDetails struct { 48 Signature string 49 CompilerID string 50 } 51 52 func sanitize(params *Params) { 53 if params.Tracer == nil { 54 params.Tracer = &debugtracer.NullTracer{} 55 } 56 if params.BuildCPUs == 0 { 57 params.BuildCPUs = runtime.NumCPU() 58 } 59 } 60 61 // Image creates a disk image for the specified OS/ARCH/VM. 62 // Kernel is taken from KernelDir, userspace system is taken from UserspaceDir. 63 // If CmdlineFile is not empty, contents of the file are appended to the kernel command line. 64 // If SysctlFile is not empty, contents of the file are appended to the image /etc/sysctl.conf. 65 // Output is stored in OutputDir and includes (everything except for image is optional): 66 // - image: the image 67 // - key: ssh key for the image 68 // - kernel: kernel for injected boot 69 // - initrd: initrd for injected boot 70 // - kernel.config: actual kernel config used during build 71 // - obj/: directory with kernel object files (this should match KernelObject 72 // specified in sys/targets, e.g. vmlinux for linux) 73 // 74 // The returned structure contains a kernel ID that will be the same for kernels 75 // with the same runtime behavior, and different for kernels with different runtime 76 // behavior. Binary equal builds, or builds that differ only in e.g. debug info, 77 // have the same ID. The ID may be empty if OS implementation does not have 78 // a way to calculate such IDs. 79 // Also that structure provides a compiler ID field that contains the name and 80 // the version of the compiler/toolchain that was used to build the kernel. 81 // The CompilerID field is not guaranteed to be non-empty. 82 func Image(params Params) (details ImageDetails, err error) { 83 sanitize(¶ms) 84 var builder builder 85 builder, err = getBuilder(params.TargetOS, params.TargetArch, params.VMType) 86 if err != nil { 87 return 88 } 89 if err = osutil.MkdirAll(filepath.Join(params.OutputDir, "obj")); err != nil { 90 return 91 } 92 if len(params.Config) != 0 { 93 // Write kernel config early, so that it's captured on build failures. 94 if err = osutil.WriteFile(filepath.Join(params.OutputDir, "kernel.config"), params.Config); err != nil { 95 err = fmt.Errorf("failed to write config file: %w", err) 96 return 97 } 98 } 99 details, err = builder.build(params) 100 if details.CompilerID == "" { 101 // Fill in the compiler info even if the build failed. 102 var idErr error 103 details.CompilerID, idErr = compilerIdentity(params.Compiler) 104 if err == nil { 105 err = idErr 106 } // Try to preserve the build error otherwise. 107 } 108 if err != nil { 109 err = extractRootCause(err, params.TargetOS, params.KernelDir) 110 return 111 } 112 if key := filepath.Join(params.OutputDir, "key"); osutil.IsExist(key) { 113 if err := os.Chmod(key, 0600); err != nil { 114 return details, fmt.Errorf("failed to chmod 0600 %v: %w", key, err) 115 } 116 } 117 return 118 } 119 120 func Clean(params Params) error { 121 sanitize(¶ms) 122 builder, err := getBuilder(params.TargetOS, params.TargetArch, params.VMType) 123 if err != nil { 124 return err 125 } 126 return builder.clean(params) 127 } 128 129 type KernelError struct { 130 Report []byte 131 Output []byte 132 Recipients vcs.Recipients 133 guiltyFile string 134 } 135 136 func (err *KernelError) Error() string { 137 return string(err.Report) 138 } 139 140 type InfraError struct { 141 Title string 142 Output []byte 143 } 144 145 func (e InfraError) Error() string { 146 if len(e.Output) > 0 { 147 return fmt.Sprintf("%s: %s", e.Title, e.Output) 148 } 149 return e.Title 150 } 151 152 type builder interface { 153 build(params Params) (ImageDetails, error) 154 clean(params Params) error 155 } 156 157 func getBuilder(targetOS, targetArch, vmType string) (builder, error) { 158 if targetOS == targets.Linux { 159 switch vmType { 160 case targets.GVisor: 161 return gvisor{}, nil 162 case "cuttlefish": 163 return cuttlefish{}, nil 164 case "proxyapp:android": 165 return android{}, nil 166 case targets.Starnix: 167 return starnix{}, nil 168 } 169 } 170 builders := map[string]builder{ 171 targets.Linux: linux{}, 172 targets.Fuchsia: fuchsia{}, 173 targets.OpenBSD: openbsd{}, 174 targets.NetBSD: netbsd{}, 175 targets.FreeBSD: freebsd{}, 176 targets.Darwin: darwin{}, 177 targets.TestOS: test{}, 178 } 179 if builder, ok := builders[targetOS]; ok { 180 return builder, nil 181 } 182 return nil, fmt.Errorf("unsupported image type %v/%v/%v", targetOS, targetArch, vmType) 183 } 184 185 func compilerIdentity(compiler string) (string, error) { 186 if compiler == "" { 187 return "", nil 188 } 189 190 bazel := strings.HasSuffix(compiler, "bazel") 191 192 arg, timeout := "--version", time.Minute 193 if bazel { 194 // Bazel episodically fails with 1 min timeout. 195 timeout = 10 * time.Minute 196 } 197 output, err := osutil.RunCmd(timeout, "", compiler, arg) 198 if err != nil { 199 return "", err 200 } 201 for _, line := range strings.Split(string(output), "\n") { 202 if bazel { 203 // Strip extracting and log lines... 204 if strings.Contains(line, "Extracting Bazel") { 205 continue 206 } 207 if strings.HasPrefix(line, "INFO: ") { 208 continue 209 } 210 if strings.HasPrefix(line, "WARNING: ") { 211 continue 212 } 213 if strings.Contains(line, "Downloading https://releases.bazel") { 214 continue 215 } 216 } 217 218 return strings.TrimSpace(line), nil 219 } 220 return "", fmt.Errorf("no output from compiler --version") 221 } 222 223 func extractRootCause(err error, OS, kernelSrc string) error { 224 if err == nil { 225 return nil 226 } 227 var verr *osutil.VerboseError 228 if !errors.As(err, &verr) { 229 return err 230 } 231 reason, file := extractCauseInner(verr.Output, kernelSrc) 232 if len(reason) == 0 { 233 return err 234 } 235 // Don't report upon SIGKILL for Linux builds. 236 if OS == targets.Linux && string(reason) == "Killed" && verr.ExitCode == 137 { 237 return &InfraError{ 238 Title: string(reason), 239 Output: verr.Output, 240 } 241 } 242 kernelErr := &KernelError{ 243 Report: reason, 244 Output: verr.Output, 245 guiltyFile: file, 246 } 247 if file != "" && OS == targets.Linux { 248 maintainers, err := report.GetLinuxMaintainers(kernelSrc, file) 249 if err != nil { 250 kernelErr.Output = append(kernelErr.Output, err.Error()...) 251 } 252 kernelErr.Recipients = maintainers 253 } 254 return kernelErr 255 } 256 257 func extractCauseInner(s []byte, kernelSrc string) ([]byte, string) { 258 lines := extractCauseRaw(s) 259 const maxLines = 20 260 if len(lines) > maxLines { 261 lines = lines[:maxLines] 262 } 263 var stripPrefix []byte 264 if kernelSrc != "" { 265 stripPrefix = []byte(kernelSrc) 266 if stripPrefix[len(stripPrefix)-1] != filepath.Separator { 267 stripPrefix = append(stripPrefix, filepath.Separator) 268 } 269 } 270 file := "" 271 for i := range lines { 272 if stripPrefix != nil { 273 lines[i] = bytes.ReplaceAll(lines[i], stripPrefix, nil) 274 } 275 if file == "" { 276 for _, fileRe := range fileRes { 277 match := fileRe.FindSubmatch(lines[i]) 278 if match != nil { 279 file = string(match[1]) 280 if file[0] != '/' { 281 break 282 } 283 // We already removed kernel source prefix, 284 // if we still have an absolute path, it's probably pointing 285 // to compiler/system libraries (not going to work). 286 file = "" 287 } 288 } 289 } 290 } 291 file = strings.TrimPrefix(file, "./") 292 if strings.HasSuffix(file, ".o") { 293 // Linker may point to object files instead. 294 file = strings.TrimSuffix(file, ".o") + ".c" 295 } 296 res := bytes.Join(lines, []byte{'\n'}) 297 // gcc uses these weird quotes around identifiers, which may be 298 // mis-rendered by systems that don't understand utf-8. 299 res = bytes.ReplaceAll(res, []byte("‘"), []byte{'\''}) 300 res = bytes.ReplaceAll(res, []byte("’"), []byte{'\''}) 301 return res, file 302 } 303 304 func extractCauseRaw(s []byte) [][]byte { 305 weak := true 306 var cause [][]byte 307 dedup := make(map[string]bool) 308 for _, line := range bytes.Split(s, []byte{'\n'}) { 309 for _, pattern := range buildFailureCauses { 310 if !pattern.pattern.Match(line) { 311 continue 312 } 313 if weak && !pattern.weak { 314 cause = nil 315 dedup = make(map[string]bool) 316 } 317 if dedup[string(line)] { 318 continue 319 } 320 dedup[string(line)] = true 321 if cause == nil { 322 weak = pattern.weak 323 } 324 cause = append(cause, line) 325 break 326 } 327 } 328 return cause 329 } 330 331 type buildFailureCause struct { 332 pattern *regexp.Regexp 333 weak bool 334 } 335 336 var buildFailureCauses = [...]buildFailureCause{ 337 {pattern: regexp.MustCompile(`: error: `)}, 338 {pattern: regexp.MustCompile(`Error: `)}, 339 {pattern: regexp.MustCompile(`ERROR: `)}, 340 {pattern: regexp.MustCompile(`: fatal error: `)}, 341 {pattern: regexp.MustCompile(`: undefined reference to`)}, 342 {pattern: regexp.MustCompile(`: multiple definition of`)}, 343 {pattern: regexp.MustCompile(`: Permission denied`)}, 344 {pattern: regexp.MustCompile(`^([a-zA-Z0-9_\-/.]+):[0-9]+:([0-9]+:)?.*(error|invalid|fatal|wrong)`)}, 345 {pattern: regexp.MustCompile(`FAILED unresolved symbol`)}, 346 {pattern: regexp.MustCompile(`No rule to make target`)}, 347 {pattern: regexp.MustCompile(`^Killed$`)}, 348 {pattern: regexp.MustCompile(`error\[.*?\]: `)}, 349 {weak: true, pattern: regexp.MustCompile(`: not found`)}, 350 {weak: true, pattern: regexp.MustCompile(`: final link failed: `)}, 351 {weak: true, pattern: regexp.MustCompile(`collect2: error: `)}, 352 {weak: true, pattern: regexp.MustCompile(`(ERROR|FAILED): Build did NOT complete`)}, 353 } 354 355 var fileRes = []*regexp.Regexp{ 356 regexp.MustCompile(`^([a-zA-Z0-9_\-/.]+):[0-9]+:([0-9]+:)? `), 357 regexp.MustCompile(`^(?:ld: )?(([a-zA-Z0-9_\-/.]+?)\.o):`), 358 regexp.MustCompile(`; (([a-zA-Z0-9_\-/.]+?)\.o):`), 359 }