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  }