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(&params)
    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(&params)
   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  }