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