diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2018-06-12 14:05:02 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2018-06-12 14:53:22 +0200 |
| commit | 06ece2ca663d0565d9e4cd932c4c2d86767a5396 (patch) | |
| tree | 0ba1566273ece79a1570afc79a030cd78df8e3ef /pkg/host | |
| parent | 62d1af2467768d46623d446efaaf2f2cb6e8350e (diff) | |
pkg/host: rework host feature detection/setup
Currently host feature detection/setup code is spread
across platform-independent fuzzer code, pkg/host, pkg/ipc
and executor.
Move this all into pkg/host and show readable info
about features on manager start.
Fixes #46
Diffstat (limited to 'pkg/host')
| -rw-r--r-- | pkg/host/host.go | 79 | ||||
| -rw-r--r-- | pkg/host/host_akaros.go | 4 | ||||
| -rw-r--r-- | pkg/host/host_darwin.go | 12 | ||||
| -rw-r--r-- | pkg/host/host_freebsd.go | 4 | ||||
| -rw-r--r-- | pkg/host/host_fuchsia.go | 4 | ||||
| -rw-r--r-- | pkg/host/host_linux.go | 235 | ||||
| -rw-r--r-- | pkg/host/host_netbsd.go | 4 | ||||
| -rw-r--r-- | pkg/host/host_test.go | 13 | ||||
| -rw-r--r-- | pkg/host/host_windows.go | 4 |
9 files changed, 335 insertions, 24 deletions
diff --git a/pkg/host/host.go b/pkg/host/host.go index c8f786e5a..d4f4d71a8 100644 --- a/pkg/host/host.go +++ b/pkg/host/host.go @@ -26,3 +26,82 @@ func DetectSupportedSyscalls(target *prog.Target, sandbox string) ( } return supported, unsupported, nil } + +const ( + FeatureCoverage = iota + FeatureComparisons + FeatureSandboxSetuid + FeatureSandboxNamespace + FeatureFaultInjection + FeatureLeakChecking + FeatureNetworkInjection + numFeatures +) + +type Feature struct { + Name string + Enabled bool + Reason string +} + +type Features [numFeatures]Feature + +var checkFeature [numFeatures]func() string +var setupFeature [numFeatures]func() error +var callbFeature [numFeatures]func() + +func unconditionallyEnabled() string { return "" } + +// Check detects features supported on the host. +// Empty string for a feature means the feature is supported, +// otherwise the string contains the reason why the feature is not supported. +func Check() (*Features, error) { + const unsupported = "support is not implemented in syzkaller" + res := &Features{ + FeatureCoverage: {Name: "code coverage", Reason: unsupported}, + FeatureComparisons: {Name: "comparison tracing", Reason: unsupported}, + FeatureSandboxSetuid: {Name: "setuid sandbox", Reason: unsupported}, + FeatureSandboxNamespace: {Name: "namespace sandbox", Reason: unsupported}, + FeatureFaultInjection: {Name: "fault injection", Reason: unsupported}, + FeatureLeakChecking: {Name: "leak checking", Reason: unsupported}, + FeatureNetworkInjection: {Name: "net packed injection", Reason: unsupported}, + } + for n, check := range checkFeature { + if check == nil { + continue + } + if reason := check(); reason == "" { + res[n].Enabled = true + res[n].Reason = "enabled" + } else { + res[n].Reason = reason + } + } + return res, nil +} + +// Setup enables and does any one-time setup for the requested features on the host. +// Note: this can be called multiple times and must be idempotent. +func Setup(features *Features) (func(), error) { + var callback func() + for n, setup := range setupFeature { + if setup == nil || !features[n].Enabled { + continue + } + if err := setup(); err != nil { + return nil, err + } + cb := callbFeature[n] + if cb != nil { + prev := callback + callback = func() { + cb() + if prev != nil { + prev() + } + } + + } + } + return callback, nil +} diff --git a/pkg/host/host_akaros.go b/pkg/host/host_akaros.go index 9478c7cac..806bdb50f 100644 --- a/pkg/host/host_akaros.go +++ b/pkg/host/host_akaros.go @@ -12,7 +12,3 @@ import ( func isSupported(c *prog.Syscall, sandbox string) (bool, string) { return true, "" } - -func EnableFaultInjection() error { - return nil -} diff --git a/pkg/host/host_darwin.go b/pkg/host/host_darwin.go new file mode 100644 index 000000000..9fbe752bd --- /dev/null +++ b/pkg/host/host_darwin.go @@ -0,0 +1,12 @@ +// 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 host + +import ( + "github.com/google/syzkaller/prog" +) + +func isSupported(c *prog.Syscall, sandbox string) (bool, string) { + return false, "" +} diff --git a/pkg/host/host_freebsd.go b/pkg/host/host_freebsd.go index bada10ec7..238edd46d 100644 --- a/pkg/host/host_freebsd.go +++ b/pkg/host/host_freebsd.go @@ -11,6 +11,6 @@ func isSupported(c *prog.Syscall, sandbox string) (bool, string) { return true, "" } -func EnableFaultInjection() error { - return nil +func init() { + checkFeature[FeatureCoverage] = unconditionallyEnabled } diff --git a/pkg/host/host_fuchsia.go b/pkg/host/host_fuchsia.go index 87143d768..ea6a92700 100644 --- a/pkg/host/host_fuchsia.go +++ b/pkg/host/host_fuchsia.go @@ -12,7 +12,3 @@ import ( func isSupported(c *prog.Syscall, sandbox string) (bool, string) { return true, "" } - -func EnableFaultInjection() error { - return nil -} diff --git a/pkg/host/host_linux.go b/pkg/host/host_linux.go index 3cd096ba1..1e629b796 100644 --- a/pkg/host/host_linux.go +++ b/pkg/host/host_linux.go @@ -7,14 +7,18 @@ import ( "bytes" "fmt" "io/ioutil" + "os" "runtime" "strconv" "strings" "sync" "syscall" + "time" + "unsafe" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys/linux" ) func isSupported(c *prog.Syscall, sandbox string) (bool, string) { @@ -110,12 +114,8 @@ func isSupportedSyzkall(sandbox string, c *prog.Syscall) (bool, string) { } return onlySandboxNoneOrNamespace(sandbox) case "syz_emit_ethernet", "syz_extract_tcp_res": - fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR, 0) - if err != nil { - return false, fmt.Sprintf("open(/dev/net/tun) failed: %v", err) - } - syscall.Close(fd) - return true, "" + reason := checkNetworkInjection() + return reason == "", reason case "syz_kvm_setup_cpu": switch c.Name { case "syz_kvm_setup_cpu$x86": @@ -213,7 +213,87 @@ func extractStringConst(typ prog.Type) (string, bool) { return v, true } -func EnableFaultInjection() error { +func init() { + checkFeature[FeatureCoverage] = checkCoverage + checkFeature[FeatureComparisons] = checkComparisons + checkFeature[FeatureSandboxSetuid] = unconditionallyEnabled + checkFeature[FeatureSandboxNamespace] = checkSandboxNamespace + checkFeature[FeatureFaultInjection] = checkFaultInjection + setupFeature[FeatureFaultInjection] = setupFaultInjection + checkFeature[FeatureLeakChecking] = checkLeakChecking + setupFeature[FeatureLeakChecking] = setupLeakChecking + callbFeature[FeatureLeakChecking] = callbackLeakChecking + checkFeature[FeatureNetworkInjection] = checkNetworkInjection +} + +func checkCoverage() string { + if !osutil.IsExist("/sys/kernel/debug") { + return "debugfs is not enabled or not mounted" + } + if !osutil.IsExist("/sys/kernel/debug/kcov") { + return "CONFIG_KCOV is not enabled" + } + return "" +} + +func checkComparisons() string { + if !osutil.IsExist("/sys/kernel/debug") { + return "debugfs is not enabled or not mounted" + } + // TODO(dvyukov): this should run under target arch. + // E.g. KCOV ioctls were initially not supported on 386 (missing compat_ioctl), + // and a 386 executor won't be able to use them, but an amd64 fuzzer will be. + fd, err := syscall.Open("/sys/kernel/debug/kcov", syscall.O_RDWR, 0) + if err != nil { + return "CONFIG_KCOV is not enabled" + } + defer syscall.Close(fd) + // Trigger host target lazy initialization, it will fill linux.KCOV_INIT_TRACE. + // It's all wrong and needs to be refactored. + if _, err := prog.GetTarget(runtime.GOOS, runtime.GOARCH); err != nil { + return fmt.Sprintf("failed to get target: %v", err) + } + coverSize := uintptr(64 << 10) + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, uintptr(fd), linux.KCOV_INIT_TRACE, coverSize) + if errno != 0 { + return fmt.Sprintf("ioctl(KCOV_INIT_TRACE) failed: %v", errno) + } + mem, err := syscall.Mmap(fd, 0, int(coverSize*unsafe.Sizeof(uintptr(0))), + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return fmt.Sprintf("KCOV mmap failed: %v", err) + } + defer syscall.Munmap(mem) + _, _, errno = syscall.Syscall(syscall.SYS_IOCTL, + uintptr(fd), linux.KCOV_ENABLE, linux.KCOV_TRACE_CMP) + if errno != 0 { + if errno == syscall.ENOTTY { + return "kernel does not have comparison tracing support" + } + return fmt.Sprintf("ioctl(KCOV_TRACE_CMP) failed: %v", errno) + } + defer syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), linux.KCOV_DISABLE, 0) + return "" +} + +func checkFaultInjection() string { + if !osutil.IsExist("/proc/self/make-it-fail") { + return "CONFIG_FAULT_INJECTION is not enabled" + } + if !osutil.IsExist("/proc/thread-self/fail-nth") { + return "kernel does not have systematic fault injection support" + } + if !osutil.IsExist("/sys/kernel/debug") { + return "debugfs is not enabled or not mounted" + } + if !osutil.IsExist("/sys/kernel/debug/failslab/ignore-gfp-wait") { + return "CONFIG_FAULT_INJECTION_DEBUG_FS is not enabled" + } + return "" +} + +func setupFaultInjection() error { if err := osutil.WriteFile("/sys/kernel/debug/failslab/ignore-gfp-wait", []byte("N")); err != nil { return fmt.Errorf("failed to write /failslab/ignore-gfp-wait: %v", err) } @@ -231,3 +311,144 @@ func EnableFaultInjection() error { } return nil } + +func checkLeakChecking() string { + if !osutil.IsExist("/sys/kernel/debug") { + return "debugfs is not enabled or not mounted" + } + if !osutil.IsExist("/sys/kernel/debug/kmemleak") { + return "CONFIG_DEBUG_KMEMLEAK is not enabled" + } + return "" +} + +func setupLeakChecking() error { + fd, err := syscall.Open("/sys/kernel/debug/kmemleak", syscall.O_RDWR, 0) + if err != nil { + return fmt.Errorf("failed to open /sys/kernel/debug/kmemleak: %v", err) + } + defer syscall.Close(fd) + if _, err := syscall.Write(fd, []byte("scan=off")); err != nil { + // kmemleak returns EBUSY when kmemleak is already turned off. + if err != syscall.EBUSY { + return fmt.Errorf("write(kmemleak, scan=off) failed: %v", err) + } + } + // Flush boot leaks. + if _, err := syscall.Write(fd, []byte("scan")); err != nil { + return fmt.Errorf("write(kmemleak, scan) failed: %v", err) + } + time.Sleep(5 * time.Second) // account for MSECS_MIN_AGE + if _, err := syscall.Write(fd, []byte("scan")); err != nil { + return fmt.Errorf("write(kmemleak, scan) failed: %v", err) + } + if _, err := syscall.Write(fd, []byte("clear")); err != nil { + return fmt.Errorf("write(kmemleak, clear) failed: %v", err) + } + return nil +} + +func callbackLeakChecking() { + start := time.Now() + fd, err := syscall.Open("/sys/kernel/debug/kmemleak", syscall.O_RDWR, 0) + if err != nil { + panic(err) + } + defer syscall.Close(fd) + // KMEMLEAK has false positives. To mitigate most of them, it checksums + // potentially leaked objects, and reports them only on the next scan + // iff the checksum does not change. Because of that we do the following + // intricate dance: + // Scan, sleep, scan again. At this point we can get some leaks. + // If there are leaks, we sleep and scan again, this can remove + // false leaks. Then, read kmemleak again. If we get leaks now, then + // hopefully these are true positives during the previous testing cycle. + if _, err := syscall.Write(fd, []byte("scan")); err != nil { + panic(err) + } + time.Sleep(time.Second) + // Account for MSECS_MIN_AGE + // (1 second less because scanning will take at least a second). + for time.Since(start) < 4*time.Second { + time.Sleep(time.Second) + } + if _, err := syscall.Write(fd, []byte("scan")); err != nil { + panic(err) + } + buf := make([]byte, 128<<10) + n, err := syscall.Read(fd, buf) + if err != nil { + panic(err) + } + if n != 0 { + time.Sleep(time.Second) + if _, err := syscall.Write(fd, []byte("scan")); err != nil { + panic(err) + } + n, err := syscall.Read(fd, buf) + if err != nil { + panic(err) + } + nleaks := 0 + for buf = buf[:n]; len(buf) != 0; { + end := bytes.Index(buf[1:], []byte("unreferenced object")) + if end != -1 { + end++ + } else { + end = len(buf) + } + report := buf[:end] + buf = buf[end:] + if kmemleakIgnore(report) { + continue + } + // BUG in output should be recognized by manager. + fmt.Printf("BUG: memory leak\n%s\n", report) + nleaks++ + } + if nleaks != 0 { + os.Exit(1) + } + } + if _, err := syscall.Write(fd, []byte("clear")); err != nil { + panic(err) + } +} + +func kmemleakIgnore(report []byte) bool { + // kmemleak has a bunch of false positives (at least what looks like + // false positives at first glance). So we are conservative with what we report. + // First, we filter out any allocations that don't come from executor processes. + // Second, we ignore a bunch of functions entirely. + // Ideally, someone should debug/fix all these cases and remove ignores. + if !bytes.Contains(report, []byte(`comm "syz-executor`)) { + return true + } + for _, ignore := range []string{ + " copy_process", + " do_execveat_common", + " __ext4_", + " get_empty_filp", + " do_filp_open", + " new_inode", + } { + if bytes.Contains(report, []byte(ignore)) { + return true + } + } + return false +} + +func checkSandboxNamespace() string { + if !osutil.IsExist("/proc/self/ns/user") { + return "/proc/self/ns/user is not present" + } + return "" +} + +func checkNetworkInjection() string { + if !osutil.IsExist("/dev/net/tun") { + return "/dev/net/tun is not present" + } + return "" +} diff --git a/pkg/host/host_netbsd.go b/pkg/host/host_netbsd.go index bada10ec7..238edd46d 100644 --- a/pkg/host/host_netbsd.go +++ b/pkg/host/host_netbsd.go @@ -11,6 +11,6 @@ func isSupported(c *prog.Syscall, sandbox string) (bool, string) { return true, "" } -func EnableFaultInjection() error { - return nil +func init() { + checkFeature[FeatureCoverage] = unconditionallyEnabled } diff --git a/pkg/host/host_test.go b/pkg/host/host_test.go index aa7020942..8a16d5e5d 100644 --- a/pkg/host/host_test.go +++ b/pkg/host/host_test.go @@ -11,7 +11,7 @@ import ( _ "github.com/google/syzkaller/sys" ) -func TestLog(t *testing.T) { +func TestDetectSupportedSyscalls(t *testing.T) { t.Parallel() target, err := prog.GetTarget(runtime.GOOS, runtime.GOARCH) if err != nil { @@ -37,3 +37,14 @@ func TestLog(t *testing.T) { t.Logf("%v: %v", c.Name, reason) } } + +func TestCheck(t *testing.T) { + t.Parallel() + features, err := Check() + if err != nil { + t.Fatal(err) + } + for _, feat := range features { + t.Logf("%-24v: %v", feat.Name, feat.Reason) + } +} diff --git a/pkg/host/host_windows.go b/pkg/host/host_windows.go index bada10ec7..1d01cf743 100644 --- a/pkg/host/host_windows.go +++ b/pkg/host/host_windows.go @@ -10,7 +10,3 @@ import ( func isSupported(c *prog.Syscall, sandbox string) (bool, string) { return true, "" } - -func EnableFaultInjection() error { - return nil -} |
