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