diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2024-05-03 10:16:58 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2024-05-15 12:55:36 +0000 |
| commit | 0b3dad4606c0984ce2d81ba5dd698fa248ce91b8 (patch) | |
| tree | d732c2d7c4096a3a3223529088725c1adb54e3e0 /pkg/vminfo | |
| parent | 94b087b1f1dce14942bc35bb35a8f58e57b1fc63 (diff) | |
pkg/vminfo: move feature checking to host
Feature checking procedure is split into 2 phases:
1. syz-fuzzer invokes "syz-executor setup feature" for each feature one-by-one,
and checks if executor does not fail.
Executor can also return a special "this feature does not need custom setup",
this allows to not call setup of these features in each new VM.
2. pkg/vminfo runs a simple program with ipc.ExecOpts specific for a concrete feature,
e.g. for wifi injection it will try to run a program with wifi feature enabled,
if setup of the feature fails, executor should also exit with an error.
For coverage features we also additionally check that we actually got coverage.
Then pkg/vminfo combines results of these 2 checks into final result.
syz-execprog now also uses vminfo package and mimics the same checking procedure.
Update #1541
Diffstat (limited to 'pkg/vminfo')
| -rw-r--r-- | pkg/vminfo/features.go | 192 | ||||
| -rw-r--r-- | pkg/vminfo/linux_syscalls.go | 20 | ||||
| -rw-r--r-- | pkg/vminfo/linux_test.go | 12 | ||||
| -rw-r--r-- | pkg/vminfo/netbsd.go | 38 | ||||
| -rw-r--r-- | pkg/vminfo/openbsd.go | 36 | ||||
| -rw-r--r-- | pkg/vminfo/syscalls.go | 73 | ||||
| -rw-r--r-- | pkg/vminfo/vminfo.go | 14 | ||||
| -rw-r--r-- | pkg/vminfo/vminfo_test.go | 26 |
8 files changed, 366 insertions, 45 deletions
diff --git a/pkg/vminfo/features.go b/pkg/vminfo/features.go new file mode 100644 index 000000000..2ba76a255 --- /dev/null +++ b/pkg/vminfo/features.go @@ -0,0 +1,192 @@ +// Copyright 2024 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 vminfo + +import ( + "fmt" + "strings" + + "github.com/google/syzkaller/pkg/flatrpc" + "github.com/google/syzkaller/pkg/ipc" + "github.com/google/syzkaller/pkg/rpctype" + "github.com/google/syzkaller/prog" +) + +type Feature struct { + Enabled bool + NeedSetup bool + Reason string +} + +type Features map[flatrpc.Feature]Feature + +func (features Features) Enabled() flatrpc.Feature { + var mask flatrpc.Feature + for feat, info := range features { + if info.Enabled { + mask |= feat + } + } + return mask +} + +func (features Features) NeedSetup() flatrpc.Feature { + var mask flatrpc.Feature + for feat, info := range features { + if info.Enabled && info.NeedSetup { + mask |= feat + } + } + return mask +} + +type featureResult struct { + id flatrpc.Feature + reason string +} + +func (ctx *checkContext) checkFeatures() { + testProg := ctx.target.DataMmapProg() + for feat := range flatrpc.EnumNamesFeature { + feat := feat + ctx.pendingRequests++ + go func() { + envFlags, execFlags := ctx.featureToFlags(feat) + res := ctx.execProg(testProg, envFlags, execFlags) + reason := ctx.featureSucceeded(feat, testProg, res) + ctx.features <- featureResult{feat, reason} + }() + } +} + +func (ctx *checkContext) finishFeatures(featureInfos []flatrpc.FeatureInfo) (Features, error) { + // Feature checking consists of 2 parts: + // - we ask executor to try to setup each feature (results are returned in featureInfos) + // - we also try to run a simple program with feature-specific flags + // Here we combine both results. + features := make(Features) + for _, info := range featureInfos { + features[info.Id] = Feature{ + Reason: info.Reason, + NeedSetup: info.NeedSetup, + } + } + outputReplacer := strings.NewReplacer( + "SYZFAIL:", "", + "\n", ". ", + ) + for range flatrpc.EnumNamesFeature { + res := <-ctx.features + feat := features[res.id] + if feat.Reason == "" { + feat.Reason = res.reason + } + if feat.Reason == "" { + feat.Reason = "enabled" + feat.Enabled = true + } + if pos := strings.Index(feat.Reason, "loop exited with status"); pos != -1 { + feat.Reason = feat.Reason[:pos] + } + // If executor exited the output is prefixed with "executor 4: EOF". + const executorPrefix = ": EOF\n" + if pos := strings.Index(feat.Reason, executorPrefix); pos != -1 { + feat.Reason = feat.Reason[pos+len(executorPrefix):] + } + feat.Reason = strings.TrimSpace(outputReplacer.Replace(feat.Reason)) + features[res.id] = feat + } + if feat := features[flatrpc.FeatureSandboxSetuid]; !feat.Enabled { + return features, fmt.Errorf("execution of simple program fails: %v", feat.Reason) + } + if feat := features[flatrpc.FeatureCoverage]; ctx.cfg.Cover && !feat.Enabled { + return features, fmt.Errorf("coverage is not supported: %v", feat.Reason) + } + return features, nil +} + +// featureToFlags creates ipc flags required to test the feature on a simple program. +// For features that has setup procedure in the executor, we just execute with the default flags. +func (ctx *checkContext) featureToFlags(feat flatrpc.Feature) (ipc.EnvFlags, ipc.ExecFlags) { + envFlags := ctx.sandbox + // These don't have a corresponding feature and are always enabled. + envFlags |= ipc.FlagEnableCloseFds | ipc.FlagEnableCgroups | ipc.FlagEnableNetReset + execFlags := ipc.FlagThreaded + switch feat { + case flatrpc.FeatureCoverage: + envFlags |= ipc.FlagSignal + execFlags |= ipc.FlagCollectSignal | ipc.FlagCollectCover + case flatrpc.FeatureComparisons: + envFlags |= ipc.FlagSignal + execFlags |= ipc.FlagCollectComps + case flatrpc.FeatureExtraCoverage: + envFlags |= ipc.FlagSignal | ipc.FlagExtraCover + execFlags |= ipc.FlagCollectSignal | ipc.FlagCollectCover + case flatrpc.FeatureDelayKcovMmap: + envFlags |= ipc.FlagSignal | ipc.FlagDelayKcovMmap + execFlags |= ipc.FlagCollectSignal | ipc.FlagCollectCover + case flatrpc.FeatureSandboxSetuid: + // We use setuid sandbox feature to test that the simple program + // succeeds under the actual sandbox (not necessary setuid). + // We do this because we don't have a feature for sandbox 'none'. + case flatrpc.FeatureSandboxNamespace: + case flatrpc.FeatureSandboxAndroid: + case flatrpc.FeatureFault: + case flatrpc.FeatureLeak: + case flatrpc.FeatureNetInjection: + envFlags |= ipc.FlagEnableTun + case flatrpc.FeatureNetDevices: + envFlags |= ipc.FlagEnableNetDev + case flatrpc.FeatureKCSAN: + case flatrpc.FeatureDevlinkPCI: + envFlags |= ipc.FlagEnableDevlinkPCI + case flatrpc.FeatureNicVF: + envFlags |= ipc.FlagEnableNicVF + case flatrpc.FeatureUSBEmulation: + case flatrpc.FeatureVhciInjection: + envFlags |= ipc.FlagEnableVhciInjection + case flatrpc.FeatureWifiEmulation: + envFlags |= ipc.FlagEnableWifi + case flatrpc.FeatureLRWPANEmulation: + case flatrpc.FeatureBinFmtMisc: + case flatrpc.FeatureSwap: + default: + panic(fmt.Sprintf("unknown feature %v", flatrpc.EnumNamesFeature[feat])) + } + return envFlags, execFlags +} + +// featureSucceeded checks if execution of a simple program with feature-specific flags succeed. +// This generally checks that just all syscalls were executed and succeed, +// for coverage features we also check that we got actual coverage. +func (ctx *checkContext) featureSucceeded(feat flatrpc.Feature, testProg *prog.Prog, + res rpctype.ExecutionResult) string { + if res.Error != "" { + if len(res.Output) != 0 { + return string(res.Output) + } + return res.Error + } + if len(res.Info.Calls) != len(testProg.Calls) { + return fmt.Sprintf("only %v calls are executed out of %v", + len(res.Info.Calls), len(testProg.Calls)) + } + for i, call := range res.Info.Calls { + if call.Errno != 0 { + return fmt.Sprintf("call %v failed with errno %v", i, call.Errno) + } + } + call := res.Info.Calls[0] + switch feat { + case flatrpc.FeatureCoverage: + if len(call.Cover) == 0 || len(call.Signal) == 0 { + return "got no coverage" + } + case flatrpc.FeatureComparisons: + if len(call.Comps) == 0 { + return "got no coverage" + } + } + return "" +} diff --git a/pkg/vminfo/linux_syscalls.go b/pkg/vminfo/linux_syscalls.go index 529fed3bd..e96ce79c1 100644 --- a/pkg/vminfo/linux_syscalls.go +++ b/pkg/vminfo/linux_syscalls.go @@ -62,7 +62,7 @@ func linuxSupportedLSM(ctx *checkContext, call *prog.Syscall) string { } var linuxSyscallChecks = map[string]func(*checkContext, *prog.Syscall) string{ - "openat": linuxSupportedOpenat, + "openat": supportedOpenat, "mount": linuxSupportedMount, "socket": linuxSupportedSocket, "socketpair": linuxSupportedSocket, @@ -185,24 +185,6 @@ func linuxSyzKvmSetupCPUSupported(ctx *checkContext, call *prog.Syscall) string return "unsupported arch" } -func linuxSupportedOpenat(ctx *checkContext, call *prog.Syscall) string { - fname, ok := extractStringConst(call.Args[1].Type) - if !ok || fname[0] != '/' { - return "" - } - modes := ctx.allOpenModes() - // Attempt to extract flags from the syscall description. - if mode, ok := call.Args[2].Type.(*prog.ConstType); ok { - modes = []uint64{mode.Val} - } - var calls []string - for _, mode := range modes { - call := fmt.Sprintf("openat(0x%0x, &AUTO='%v', 0x%x, 0x0)", ctx.val("AT_FDCWD"), fname, mode) - calls = append(calls, call) - } - return ctx.anyCallSucceeds(calls, fmt.Sprintf("failed to open %v", fname)) -} - func linuxSupportedMount(ctx *checkContext, call *prog.Syscall) string { return linuxSupportedFilesystem(ctx, call, 2) } diff --git a/pkg/vminfo/linux_test.go b/pkg/vminfo/linux_test.go index 217740f6d..e9be133c1 100644 --- a/pkg/vminfo/linux_test.go +++ b/pkg/vminfo/linux_test.go @@ -45,8 +45,8 @@ func TestLinuxSyscalls(t *testing.T) { Data: []byte(strings.Join(filesystems, "\nnodev\t")), }, } - results := createSuccessfulResults(t, cfg.Target, checkProgs) - enabled, disabled, err := checker.FinishCheck(files, results) + results, featureInfos := createSuccessfulResults(t, cfg.Target, checkProgs) + enabled, disabled, features, err := checker.FinishCheck(files, results, featureInfos) if err != nil { t.Fatal(err) } @@ -74,6 +74,14 @@ func TestLinuxSyscalls(t *testing.T) { if len(enabled) != expectEnabled { t.Errorf("enabled only %v calls out of %v", len(enabled), expectEnabled) } + if len(features) != len(flatrpc.EnumNamesFeature) { + t.Errorf("enabled only %v features out of %v", len(features), len(flatrpc.EnumNamesFeature)) + } + for feat, info := range features { + if !info.Enabled { + t.Errorf("feature %v is not enabled: %v", flatrpc.EnumNamesFeature[feat], info.Reason) + } + } } func TestReadKVMInfo(t *testing.T) { diff --git a/pkg/vminfo/netbsd.go b/pkg/vminfo/netbsd.go new file mode 100644 index 000000000..cb6c1b33a --- /dev/null +++ b/pkg/vminfo/netbsd.go @@ -0,0 +1,38 @@ +// Copyright 2024 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 vminfo + +import ( + "github.com/google/syzkaller/pkg/cover" + "github.com/google/syzkaller/prog" +) + +type netbsd int + +func (netbsd) RequiredFiles() []string { + return nil +} + +func (netbsd) checkFiles() []string { + return nil +} + +func (netbsd) parseModules(files filesystem) ([]cover.KernelModule, error) { + return nil, nil +} + +func (netbsd) machineInfos() []machineInfoFunc { + return nil +} + +func (netbsd) syscallCheck(ctx *checkContext, call *prog.Syscall) string { + switch call.CallName { + case "openat": + return supportedOpenat(ctx, call) + case "syz_usb_connect", "syz_usb_disconnect": + return ctx.rootCanOpen("/dev/vhci0") + default: + return "" + } +} diff --git a/pkg/vminfo/openbsd.go b/pkg/vminfo/openbsd.go new file mode 100644 index 000000000..07ada0a98 --- /dev/null +++ b/pkg/vminfo/openbsd.go @@ -0,0 +1,36 @@ +// Copyright 2024 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 vminfo + +import ( + "github.com/google/syzkaller/pkg/cover" + "github.com/google/syzkaller/prog" +) + +type openbsd int + +func (openbsd) RequiredFiles() []string { + return nil +} + +func (openbsd) checkFiles() []string { + return nil +} + +func (openbsd) parseModules(files filesystem) ([]cover.KernelModule, error) { + return nil, nil +} + +func (openbsd) machineInfos() []machineInfoFunc { + return nil +} + +func (openbsd) syscallCheck(ctx *checkContext, call *prog.Syscall) string { + switch call.CallName { + case "openat": + return supportedOpenat(ctx, call) + default: + return "" + } +} diff --git a/pkg/vminfo/syscalls.go b/pkg/vminfo/syscalls.go index 60112ddac..78cc973c8 100644 --- a/pkg/vminfo/syscalls.go +++ b/pkg/vminfo/syscalls.go @@ -62,12 +62,13 @@ type checkContext struct { // Ready channel is closed after we've recevied results of execution of test // programs and file contents. After this results maps and fs are populated. ready chan bool - results map[int64]*ipc.ProgInfo + results map[int64]rpctype.ExecutionResult fs filesystem // Once checking of a syscall is finished, the result is sent to syscalls. // The main goroutine will wait for exactly pendingSyscalls messages. syscalls chan syscallResult pendingSyscalls int + features chan featureResult } type syscallResult struct { @@ -86,8 +87,9 @@ func newCheckContext(cfg *mgrconfig.Config, impl checker) *checkContext { target: cfg.Target, sandbox: sandbox, requests: make(chan []*rpctype.ExecutionRequest), - results: make(map[int64]*ipc.ProgInfo), + results: make(map[int64]rpctype.ExecutionResult), syscalls: make(chan syscallResult), + features: make(chan featureResult, 100), ready: make(chan bool), } } @@ -130,6 +132,7 @@ func (ctx *checkContext) startCheck() []rpctype.ExecutionRequest { ctx.syscalls <- syscallResult{call, reason} }() } + ctx.checkFeatures() var progs []rpctype.ExecutionRequest dedup := make(map[hash.Sig]int64) for i := 0; i < ctx.pendingRequests; i++ { @@ -148,12 +151,11 @@ func (ctx *checkContext) startCheck() []rpctype.ExecutionRequest { return progs } -func (ctx *checkContext) finishCheck(fileInfos []flatrpc.FileInfo, progs []rpctype.ExecutionResult) ( - map[*prog.Syscall]bool, map[*prog.Syscall]string, error) { +func (ctx *checkContext) finishCheck(fileInfos []flatrpc.FileInfo, progs []rpctype.ExecutionResult, + featureInfos []flatrpc.FeatureInfo) (map[*prog.Syscall]bool, map[*prog.Syscall]string, Features, error) { ctx.fs = createVirtualFilesystem(fileInfos) - for i := range progs { - res := &progs[i] - ctx.results[res.ID] = &res.Info + for _, res := range progs { + ctx.results[res.ID] = res } close(ctx.ready) enabled := make(map[*prog.Syscall]bool) @@ -166,7 +168,8 @@ func (ctx *checkContext) finishCheck(fileInfos []flatrpc.FileInfo, progs []rpcty disabled[res.call] = res.reason } } - return enabled, disabled, nil + features, err := ctx.finishFeatures(featureInfos) + return enabled, disabled, features, err } func (ctx *checkContext) rootCanOpen(file string) string { @@ -220,6 +223,24 @@ func (ctx *checkContext) supportedSyscalls(names []string) string { return "" } +func supportedOpenat(ctx *checkContext, call *prog.Syscall) string { + fname, ok := extractStringConst(call.Args[1].Type) + if !ok || fname[0] != '/' { + return "" + } + modes := ctx.allOpenModes() + // Attempt to extract flags from the syscall description. + if mode, ok := call.Args[2].Type.(*prog.ConstType); ok { + modes = []uint64{mode.Val} + } + var calls []string + for _, mode := range modes { + call := fmt.Sprintf("openat(0x%0x, &AUTO='%v', 0x%x, 0x0)", ctx.val("AT_FDCWD"), fname, mode) + calls = append(calls, call) + } + return ctx.anyCallSucceeds(calls, fmt.Sprintf("failed to open %v", fname)) +} + func (ctx *checkContext) allOpenModes() []uint64 { // Various open modes we need to try if we don't have a concrete mode. // Some files can be opened only for reading, some only for writing, @@ -307,14 +328,14 @@ func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root <-ctx.ready info := &ipc.ProgInfo{} for _, req := range requests { - res := ctx.results[req.ID] - if res == nil { + res, ok := ctx.results[req.ID] + if !ok { panic(fmt.Sprintf("no result for request %v", req.ID)) } - if len(res.Calls) == 0 { + if len(res.Info.Calls) == 0 { panic(fmt.Sprintf("result for request %v has no calls", req.ID)) } - info.Calls = append(info.Calls, res.Calls...) + info.Calls = append(info.Calls, res.Info.Calls...) } if len(info.Calls) != len(calls) { panic(fmt.Sprintf("got only %v results for program %v with %v calls:\n%s", @@ -323,6 +344,34 @@ func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root return info } +func (ctx *checkContext) execProg(p *prog.Prog, envFlags ipc.EnvFlags, + execFlags ipc.ExecFlags) rpctype.ExecutionResult { + if ctx.requests == nil { + panic("only one test execution per checker is supported") + } + data, err := p.SerializeForExec() + if err != nil { + panic(fmt.Sprintf("failed to serialize test program: %v\n%s", err, p.Serialize())) + } + req := &rpctype.ExecutionRequest{ + ProgData: data, + ReturnOutput: true, + ReturnError: true, + ExecOpts: ipc.ExecOpts{ + EnvFlags: envFlags, + ExecFlags: execFlags, + SandboxArg: ctx.cfg.SandboxArg, + }, + } + ctx.requests <- []*rpctype.ExecutionRequest{req} + <-ctx.ready + res, ok := ctx.results[req.ID] + if !ok { + panic(fmt.Sprintf("no result for request %v", req.ID)) + } + return res +} + func (ctx *checkContext) readFile(name string) ([]byte, error) { ctx.waitForResults() return ctx.fs.ReadFile(name) diff --git a/pkg/vminfo/vminfo.go b/pkg/vminfo/vminfo.go index efc471b7d..8c5af606e 100644 --- a/pkg/vminfo/vminfo.go +++ b/pkg/vminfo/vminfo.go @@ -36,9 +36,13 @@ type Checker struct { func New(cfg *mgrconfig.Config) *Checker { var impl checker - switch { - case cfg.TargetOS == targets.Linux: + switch cfg.TargetOS { + case targets.Linux: impl = new(linux) + case targets.NetBSD: + impl = new(netbsd) + case targets.OpenBSD: + impl = new(openbsd) default: impl = new(stub) } @@ -77,11 +81,11 @@ func (checker *Checker) StartCheck() ([]string, []rpctype.ExecutionRequest) { return checker.checkFiles(), checker.checkContext.startCheck() } -func (checker *Checker) FinishCheck(files []flatrpc.FileInfo, progs []rpctype.ExecutionResult) ( - map[*prog.Syscall]bool, map[*prog.Syscall]string, error) { +func (checker *Checker) FinishCheck(files []flatrpc.FileInfo, progs []rpctype.ExecutionResult, + featureInfos []flatrpc.FeatureInfo) (map[*prog.Syscall]bool, map[*prog.Syscall]string, Features, error) { ctx := checker.checkContext checker.checkContext = nil - return ctx.finishCheck(files, progs) + return ctx.finishCheck(files, progs, featureInfos) } type machineInfoFunc func(files filesystem, w io.Writer) (string, error) diff --git a/pkg/vminfo/vminfo_test.go b/pkg/vminfo/vminfo_test.go index de41b48fe..2f72131a5 100644 --- a/pkg/vminfo/vminfo_test.go +++ b/pkg/vminfo/vminfo_test.go @@ -56,8 +56,8 @@ func TestSyscalls(t *testing.T) { cfg := testConfig(t, target.OS, target.Arch) checker := New(cfg) _, checkProgs := checker.StartCheck() - results := createSuccessfulResults(t, cfg.Target, checkProgs) - enabled, disabled, err := checker.FinishCheck(nil, results) + results, featureInfos := createSuccessfulResults(t, cfg.Target, checkProgs) + enabled, disabled, _, err := checker.FinishCheck(nil, results, featureInfos) if err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestSyscalls(t *testing.T) { } func createSuccessfulResults(t *testing.T, target *prog.Target, - progs []rpctype.ExecutionRequest) []rpctype.ExecutionResult { + progs []rpctype.ExecutionRequest) ([]rpctype.ExecutionResult, []flatrpc.FeatureInfo) { var results []rpctype.ExecutionResult for _, req := range progs { p, err := target.DeserializeExec(req.ProgData, nil) @@ -82,13 +82,25 @@ func createSuccessfulResults(t *testing.T, target *prog.Target, } res := rpctype.ExecutionResult{ ID: req.ID, - Info: ipc.ProgInfo{ - Calls: make([]ipc.CallInfo, len(p.Calls)), - }, + } + for range p.Calls { + res.Info.Calls = append(res.Info.Calls, ipc.CallInfo{ + Cover: []uint32{1}, + Signal: []uint32{1}, + Comps: map[uint64]map[uint64]bool{ + 1: {2: true}, + }, + }) } results = append(results, res) } - return results + var features []flatrpc.FeatureInfo + for feat := range flatrpc.EnumNamesFeature { + features = append(features, flatrpc.FeatureInfo{ + Id: feat, + }) + } + return results, features } func hostChecker(t *testing.T) (*Checker, []flatrpc.FileInfo) { |
