aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2024-04-25 13:51:51 +0200
committerDmitry Vyukov <dvyukov@google.com>2024-05-02 16:24:59 +0000
commit22ee48a2879809608f79cc23c914859fa2335d59 (patch)
treeaa2b496eb81823999a8b48be011575ee7c650fff /pkg
parent3c7bb2247f61c5218d4cd58a558dc2496fba53a4 (diff)
pkg/vminfo: check enabled syscalls on the host
Move the syscall checking logic to the host. Diffing sets of disabled syscalls before/after this change in different configurations (none/setuid sandboxes, amd64/386 arches, large/small kernel configs) shows only some improvements/bug fixes. 1. socket$inet[6]_icmp are now enabled. Previously they were disabled due to net.ipv4.ping_group_range sysctl in the init namespace which prevented creation of ping sockets. In the new net namespace the sysctl gets default value which allows creation. 2. get_thread_area and set_thread_area are now disabled on amd64. They are available only in 32-bit mode, but they are present in /proc/kallsyms, so we enabled them always. 3. socket$bt_{bnep, cmtp, hidp, rfcomm} are now disabled. They cannot be created in non init net namespace. bt_sock_create() checks init_net and returns EAFNOSUPPORT immediately. This is a bug in descriptions we need to fix. Now we see it due to more precise checks. 4. fstat64/fstatat64/lstat64/stat64 are now enabled in 32-bit mode. They are not present in /proc/kallsyms as syscalls, so we have not enabled them. But they are available in 32-bit mode. 5. 78 openat variants + 10 socket variants + mount are now disabled with setuid sandbox. They are not permitted w/o root permissions, but we ignored that. This additionally leads to 700 transitively disabled syscalls. In all cases checking in the actual executor context/sandbox looks very positive, esp. for more restrictive sandboxes. Android sandbox should benefit as well. The additional benefit is full testability of the new code. The change includes only a basic test that covers all checks, and ensures the code does not crash/hang, all generated programs parse successfully, etc. But it's possible to unit-test every condition now. The new version also parallelizes checking across VMs, checking on a slow emulated qemu drops from 210 seconds to 140 seconds.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/rpctype/rpctype.go14
-rw-r--r--pkg/vminfo/linux.go9
-rw-r--r--pkg/vminfo/linux_syscalls.go345
-rw-r--r--pkg/vminfo/linux_test.go65
-rw-r--r--pkg/vminfo/syscalls.go370
-rw-r--r--pkg/vminfo/vminfo.go39
-rw-r--r--pkg/vminfo/vminfo_test.go36
7 files changed, 856 insertions, 22 deletions
diff --git a/pkg/rpctype/rpctype.go b/pkg/rpctype/rpctype.go
index d9119d24e..64caf6a04 100644
--- a/pkg/rpctype/rpctype.go
+++ b/pkg/rpctype/rpctype.go
@@ -76,10 +76,8 @@ type ConnectArgs struct {
}
type ConnectRes struct {
- EnabledCalls []int
MemoryLeakFrames []string
DataRaceFrames []string
- AllSandboxes bool
// This is forwarded from CheckArgs, if checking was already done.
Features *host.Features
// Fuzzer reads these files inside of the VM and returns contents in CheckArgs.Files.
@@ -88,13 +86,11 @@ type ConnectRes struct {
}
type CheckArgs struct {
- Name string
- Error string
- EnabledCalls map[string][]int
- DisabledCalls map[string][]SyscallReason
- Features *host.Features
- Globs map[string][]string
- Files []host.FileInfo
+ Name string
+ Error string
+ Features *host.Features
+ Globs map[string][]string
+ Files []host.FileInfo
}
type CheckRes struct {
diff --git a/pkg/vminfo/linux.go b/pkg/vminfo/linux.go
index c9cd1a3db..d5e84254f 100644
--- a/pkg/vminfo/linux.go
+++ b/pkg/vminfo/linux.go
@@ -27,6 +27,15 @@ func (linux) RequiredFiles() []string {
}
}
+func (linux) checkFiles() []string {
+ return []string{
+ "/proc/version",
+ "/proc/filesystems",
+ "/sys/kernel/security/lsm",
+ "/dev/raw-gadget",
+ }
+}
+
func (linux) machineInfos() []machineInfoFunc {
return []machineInfoFunc{
linuxReadCPUInfo,
diff --git a/pkg/vminfo/linux_syscalls.go b/pkg/vminfo/linux_syscalls.go
new file mode 100644
index 000000000..a11eb06b4
--- /dev/null
+++ b/pkg/vminfo/linux_syscalls.go
@@ -0,0 +1,345 @@
+// 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 (
+ "bytes"
+ "fmt"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "github.com/google/syzkaller/prog"
+ "github.com/google/syzkaller/sys/targets"
+)
+
+func (linux) syscallCheck(ctx *checkContext, call *prog.Syscall) string {
+ check := linuxSyscallChecks[call.CallName]
+ if check == nil {
+ check = func(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.supportedSyscalls([]string{call.Name})
+ }
+ }
+ if reason := check(ctx, call); reason != "" {
+ return reason
+ }
+ return linuxSupportedLSM(ctx, call)
+}
+
+func linuxSupportedLSM(ctx *checkContext, call *prog.Syscall) string {
+ for _, lsm := range []string{"selinux", "apparmor", "smack"} {
+ if !strings.Contains(strings.ToLower(call.Name), lsm) {
+ continue
+ }
+ data, err := ctx.readFile("/sys/kernel/security/lsm")
+ if err != nil {
+ // Securityfs may not be mounted, but it does not mean that no LSMs are enabled.
+ if os.IsNotExist(err) {
+ break
+ }
+ return err.Error()
+ }
+ if !bytes.Contains(data, []byte(lsm)) {
+ return fmt.Sprintf("%v is not enabled", lsm)
+ }
+ }
+ return ""
+}
+
+var linuxSyscallChecks = map[string]func(*checkContext, *prog.Syscall) string{
+ "openat": linuxSupportedOpenat,
+ "mount": linuxSupportedMount,
+ "socket": linuxSupportedSocket,
+ "socketpair": linuxSupportedSocket,
+ "pkey_alloc": linuxPkeysSupported,
+ "syz_open_dev": linuxSyzOpenDevSupported,
+ "syz_open_procfs": linuxSyzOpenProcfsSupported,
+ "syz_open_pts": alwaysSupported,
+ "syz_execute_func": alwaysSupported,
+ "syz_emit_ethernet": linuxNetInjectionSupported,
+ "syz_extract_tcp_res": linuxNetInjectionSupported,
+ "syz_usb_connect": linuxCheckUSBEmulation,
+ "syz_usb_connect_ath9k": linuxCheckUSBEmulation,
+ "syz_usb_disconnect": linuxCheckUSBEmulation,
+ "syz_usb_control_io": linuxCheckUSBEmulation,
+ "syz_usb_ep_write": linuxCheckUSBEmulation,
+ "syz_usb_ep_read": linuxCheckUSBEmulation,
+ "syz_kvm_setup_cpu": linuxSyzKvmSetupCPUSupported,
+ "syz_emit_vhci": linuxVhciInjectionSupported,
+ "syz_init_net_socket": linuxSyzInitNetSocketSupported,
+ "syz_genetlink_get_family_id": linuxSyzGenetlinkGetFamilyIDSupported,
+ "syz_mount_image": linuxSyzMountImageSupported,
+ "syz_read_part_table": linuxSyzReadPartTableSupported,
+ "syz_io_uring_setup": alwaysSupported,
+ "syz_io_uring_submit": alwaysSupported,
+ "syz_io_uring_complete": alwaysSupported,
+ "syz_memcpy_off": alwaysSupported,
+ "syz_btf_id_by_name": linuxBtfVmlinuxSupported,
+ "syz_fuse_handle_req": alwaysSupported,
+ "syz_80211_inject_frame": linuxWifiEmulationSupported,
+ "syz_80211_join_ibss": linuxWifiEmulationSupported,
+ "syz_usbip_server_init": linuxSyzUsbIPSupported,
+ "syz_clone": alwaysSupported,
+ "syz_clone3": alwaysSupported,
+ "syz_pkey_set": linuxPkeysSupported,
+ "syz_socket_connect_nvme_tcp": linuxSyzSocketConnectNvmeTCPSupported,
+ "syz_pidfd_open": alwaysSupported,
+}
+
+func linuxSyzOpenDevSupported(ctx *checkContext, call *prog.Syscall) string {
+ if _, ok := call.Args[0].Type.(*prog.ConstType); ok {
+ // This is for syz_open_dev$char/block.
+ return ""
+ }
+ fname, ok := extractStringConst(call.Args[0].Type)
+ if !ok {
+ panic("first open arg is not a pointer to string const")
+ }
+ hashCount := strings.Count(fname, "#")
+ if hashCount == 0 {
+ panic(fmt.Sprintf("%v does not contain # in the file name", call.Name))
+ }
+ if hashCount > 2 {
+ // If this fails, the logic below needs an adjustment.
+ panic(fmt.Sprintf("%v contains too many #", call.Name))
+ }
+ var ids []int
+ if _, ok := call.Args[1].Type.(*prog.ProcType); ok {
+ ids = []int{0}
+ } else {
+ for i := 0; i < 5; i++ {
+ for j := 0; j < 5; j++ {
+ if j == 0 || hashCount > 1 {
+ ids = append(ids, i+j*10)
+ }
+ }
+ }
+ }
+ modes := ctx.allOpenModes()
+ var calls []string
+ for _, id := range ids {
+ for _, mode := range modes {
+ call := fmt.Sprintf("%s(&AUTO='%v', 0x%x, 0x%x)", call.Name, fname, id, mode)
+ calls = append(calls, call)
+ }
+ }
+ reason := ctx.anyCallSucceeds(calls, fmt.Sprintf("failed to open %v", fname))
+ if reason != "" {
+ // These entries might not be available at boot time,
+ // but will be created by connected USB devices.
+ for _, prefix := range []string{"/dev/hidraw", "/dev/usb/hiddev", "/dev/input/"} {
+ if strings.HasPrefix(fname, prefix) {
+ // Note: ideally we use linuxSyzOpenDevSupported here,
+ // since we already issued test syscalls, we can't.
+ if _, err := ctx.readFile("/dev/raw-gadget"); !os.IsNotExist(err) {
+ reason = ""
+ }
+ }
+ }
+ }
+ return reason
+}
+
+func linuxNetInjectionSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.rootCanOpen("/dev/net/tun")
+}
+
+func linuxSyzOpenProcfsSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.canOpen("/proc/cmdline")
+}
+
+func linuxCheckUSBEmulation(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.rootCanOpen("/dev/raw-gadget")
+}
+
+func linuxSyzKvmSetupCPUSupported(ctx *checkContext, call *prog.Syscall) string {
+ switch call.Name {
+ case "syz_kvm_setup_cpu$x86":
+ if ctx.target.Arch == targets.AMD64 || ctx.target.Arch == targets.I386 {
+ return ""
+ }
+ case "syz_kvm_setup_cpu$arm64":
+ if ctx.target.Arch == targets.ARM64 {
+ return ""
+ }
+ case "syz_kvm_setup_cpu$ppc64":
+ if ctx.target.Arch == targets.PPC64LE {
+ return ""
+ }
+ }
+ 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)
+}
+
+func linuxSyzMountImageSupported(ctx *checkContext, call *prog.Syscall) string {
+ return linuxSupportedFilesystem(ctx, call, 0)
+}
+
+func linuxSupportedFilesystem(ctx *checkContext, call *prog.Syscall, fsarg int) string {
+ fstype, ok := extractStringConst(call.Args[fsarg].Type)
+ if !ok {
+ panic(fmt.Sprintf("%v: filesystem is not string const", call.Name))
+ }
+ switch fstype {
+ case "fuse", "fuseblk":
+ if reason := ctx.canOpen("/dev/fuse"); reason != "" {
+ return reason
+ }
+ if reason := ctx.onlySandboxNoneOrNamespace(); reason != "" {
+ return reason
+ }
+ default:
+ if reason := ctx.onlySandboxNone(); reason != "" {
+ return reason
+ }
+ }
+ filesystems, err := ctx.readFile("/proc/filesystems")
+ if err != nil {
+ return err.Error()
+ }
+ if !bytes.Contains(filesystems, []byte("\t"+fstype+"\n")) {
+ return fmt.Sprintf("/proc/filesystems does not contain %v", fstype)
+ }
+ return ""
+}
+
+func linuxSyzReadPartTableSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.onlySandboxNone()
+}
+
+func linuxSupportedSocket(ctx *checkContext, call *prog.Syscall) string {
+ if call.Name == "socket" || call.Name == "socketpair" {
+ return "" // generic versions are always supported
+ }
+ af := uint64(0)
+ if arg, ok := call.Args[0].Type.(*prog.ConstType); ok {
+ af = arg.Val
+ } else {
+ panic(fmt.Sprintf("socket family is not const in %v", call.Name))
+ }
+ typ, hasType := uint64(0), false
+ if arg, ok := call.Args[1].Type.(*prog.ConstType); ok {
+ typ, hasType = arg.Val, true
+ } else if arg, ok := call.Args[1].Type.(*prog.FlagsType); ok {
+ typ, hasType = arg.Vals[0], true
+ }
+ proto, hasProto := uint64(0), false
+ if arg, ok := call.Args[2].Type.(*prog.ConstType); ok {
+ proto, hasProto = arg.Val, true
+ }
+ syscallName := call.Name
+ if call.CallName == "socketpair" {
+ syscallName = "socket"
+ }
+ callStr := fmt.Sprintf("%s(0x%x, 0x%x, 0x%x)", syscallName, af, typ, proto)
+ errno := ctx.execCall(callStr)
+ if errno == syscall.ENOSYS || errno == syscall.EAFNOSUPPORT || hasProto && hasType && errno != 0 {
+ return fmt.Sprintf("%v failed: %v", callStr, errno)
+ }
+ return ""
+}
+
+func linuxSyzGenetlinkGetFamilyIDSupported(ctx *checkContext, call *prog.Syscall) string {
+ // TODO: try to obtain actual family ID here. It will disable whole sets of sendmsg syscalls.
+ return ctx.callSucceeds(fmt.Sprintf("socket(0x%x, 0x%x, 0x%x)",
+ ctx.val("AF_NETLINK"), ctx.val("SOCK_RAW"), ctx.val("NETLINK_GENERIC")))
+}
+
+func linuxPkeysSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.callSucceeds("pkey_alloc(0x0, 0x0)")
+}
+
+func linuxSyzSocketConnectNvmeTCPSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.onlySandboxNone()
+}
+
+func linuxVhciInjectionSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.rootCanOpen("/dev/vhci")
+}
+
+func linuxSyzInitNetSocketSupported(ctx *checkContext, call *prog.Syscall) string {
+ if reason := ctx.onlySandboxNone(); reason != "" {
+ return reason
+ }
+ return linuxSupportedSocket(ctx, call)
+}
+
+func linuxBtfVmlinuxSupported(ctx *checkContext, call *prog.Syscall) string {
+ if reason := ctx.onlySandboxNone(); reason != "" {
+ return reason
+ }
+ return ctx.canOpen("/sys/kernel/btf/vmlinux")
+}
+
+func linuxSyzUsbIPSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ctx.canWrite("/sys/devices/platform/vhci_hcd.0/attach")
+}
+
+func linuxWifiEmulationSupported(ctx *checkContext, call *prog.Syscall) string {
+ if reason := ctx.rootCanOpen("/sys/class/mac80211_hwsim/"); reason != "" {
+ return reason
+ }
+ // We use HWSIM_ATTR_PERM_ADDR which was added in 4.17.
+ return linuxRequireKernel(ctx, 4, 17)
+}
+
+func linuxRequireKernel(ctx *checkContext, major, minor int) string {
+ data, err := ctx.readFile("/proc/version")
+ if err != nil {
+ return err.Error()
+ }
+ if ok, bad := matchKernelVersion(string(data), major, minor); bad {
+ return fmt.Sprintf("failed to parse kernel version: %s", data)
+ } else if !ok {
+ return fmt.Sprintf("kernel %v.%v required, have %s", major, minor, data)
+ }
+ return ""
+}
+
+var kernelVersionRe = regexp.MustCompile(` ([0-9]+)\.([0-9]+)\.`)
+
+func matchKernelVersion(ver string, x, y int) (bool, bool) {
+ match := kernelVersionRe.FindStringSubmatch(ver)
+ if match == nil {
+ return false, true
+ }
+ major, err := strconv.Atoi(match[1])
+ if err != nil {
+ return false, true
+ }
+ if major <= 0 || major > 999 {
+ return false, true
+ }
+ minor, err := strconv.Atoi(match[2])
+ if err != nil {
+ return false, true
+ }
+ if minor <= 0 || minor > 999 {
+ return false, true
+ }
+ return major*1000+minor >= x*1000+y, false
+}
diff --git a/pkg/vminfo/linux_test.go b/pkg/vminfo/linux_test.go
index bdc018c11..56fea06fb 100644
--- a/pkg/vminfo/linux_test.go
+++ b/pkg/vminfo/linux_test.go
@@ -14,15 +14,78 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/syzkaller/pkg/host"
+ "github.com/google/syzkaller/pkg/ipc"
+ "github.com/google/syzkaller/pkg/rpctype"
"github.com/google/syzkaller/sys/targets"
"github.com/stretchr/testify/assert"
)
+func TestLinuxSyscalls(t *testing.T) {
+ cfg := testConfig(t, targets.Linux, targets.AMD64)
+ checker := New(cfg)
+ _, checkProgs := checker.StartCheck()
+ filesystems := []string{
+ "", "9p", "esdfs", "incremental-fs", "cgroup", "cgroup2",
+ "pvfs2", "nfs", "nfs4", "fuse", "fuseblk", "afs", "pipefs",
+ "sysfs", "tmpfs", "overlay", "bpf", "efs", "vfat", "xfs",
+ "qnx4", "hfs", "f2fs", "btrfs", "befs", "cramfs", "vxfs",
+ "ubifs", "jfs", "erofs", "udf", "squashfs", "romfs", "qnx6",
+ "ntfs", "ntfs3", "hfsplus", "bfs", "exfat", "affs", "jffs2",
+ "ext4", "gfs2", "msdos", "v7", "gfs2meta", "zonefs", "omfs",
+ "minix", "adfs", "ufs", "sysv", "reiserfs", "ocfs2", "nilfs2",
+ "iso9660", "hpfs", "binder", "",
+ }
+ files := []host.FileInfo{
+ {
+ Name: "/proc/version",
+ Exists: true,
+ Data: []byte("Linux version 6.8.0-dirty"),
+ },
+ {
+ Name: "/proc/filesystems",
+ Exists: true,
+ Data: []byte(strings.Join(filesystems, "\nnodev\t")),
+ },
+ }
+ var results []rpctype.ExecutionResult
+ for _, req := range checkProgs {
+ p, err := cfg.Target.DeserializeExec(req.ProgData, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res := rpctype.ExecutionResult{
+ ID: req.ID,
+ Info: ipc.ProgInfo{
+ Calls: make([]ipc.CallInfo, len(p.Calls)),
+ },
+ }
+ results = append(results, res)
+ }
+ enabled, disabled, err := checker.FinishCheck(files, results)
+ if err != nil {
+ t.Fatal(err)
+ }
+ expectDisabled := map[string]bool{
+ "syz_kvm_setup_cpu$arm64": true,
+ "syz_kvm_setup_cpu$ppc64": true,
+ }
+ for call, reason := range disabled {
+ if expectDisabled[call.Name] {
+ continue
+ }
+ t.Errorf("disabled call %v: %v", call.Name, reason)
+ }
+ expectEnabled := len(cfg.Syscalls) - len(expectDisabled)
+ if len(enabled) != expectEnabled {
+ t.Errorf("enabled only %v calls out of %v", len(enabled), expectEnabled)
+ }
+}
+
func TestReadKVMInfo(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("not linux")
}
- _, files := hostChecker()
+ _, files := hostChecker(t)
fs := createVirtualFilesystem(files)
buf := new(bytes.Buffer)
if _, err := linuxReadKVMInfo(fs, buf); err != nil {
diff --git a/pkg/vminfo/syscalls.go b/pkg/vminfo/syscalls.go
new file mode 100644
index 000000000..060215eef
--- /dev/null
+++ b/pkg/vminfo/syscalls.go
@@ -0,0 +1,370 @@
+// 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 (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "slices"
+ "strings"
+ "syscall"
+
+ "github.com/google/syzkaller/pkg/hash"
+ "github.com/google/syzkaller/pkg/host"
+ "github.com/google/syzkaller/pkg/ipc"
+ "github.com/google/syzkaller/pkg/mgrconfig"
+ "github.com/google/syzkaller/pkg/rpctype"
+ "github.com/google/syzkaller/prog"
+ "github.com/google/syzkaller/sys/targets"
+)
+
+// checkContext arranges checking of presence/support of all target syscalls.
+// The actual checking is done by OS-specific impl.syscallCheck,
+// while checkContext invokes that function for each syscall in a special manner
+// and provides primitives for reading target VM files, checking if a file can be opened,
+// executing test programs on the target VM, etc.
+//
+// To make use of this type simpler, we collect all test programs that need
+// to be executed on the target into a batch, send them to the target VM once,
+// then get results and finish the check. This means that impl.syscallCheck
+// cannot e.g. issue one test program, look at results, and then issue another one.
+// This is achieved by starting each impl.syscallCheck in a separate goroutine
+// and then waiting when it will call ctx.execRaw to submit syscalls that
+// need to be executed on the target. Once all impl.syscallCheck submit
+// their test syscalls, we know that we collected all of them.
+// impl.syscallCheck may also decide to read a file on the target VM instead
+// of executing a test program, this also counts as submitting an empty test program.
+// This means that impl.syscallCheck cannot execute a test program after reading a file,
+// but can do these things in opposite order (since all files are known ahead of time).
+// These rules are bit brittle, but all of the checkers are unit-tested
+// and misuse (trying to execute 2 programs, etc) will either crash or hang in tests.
+// Theoretically we can execute more than 1 program per checker, but it will
+// require some special arrangements, e.g. see handling of PseudoSyscallDeps.
+//
+// The external interface of this type contains only 2 methods:
+// startCheck - starts impl.syscallCheck goroutines and collects all test programs in progs,
+// finishCheck - accepts results of program execution, unblocks impl.syscallCheck goroutines,
+//
+// waits and returns results of checking.
+type checkContext struct {
+ impl checker
+ cfg *mgrconfig.Config
+ target *prog.Target
+ sandbox ipc.EnvFlags
+ // Checkers use requests channel to submit their test programs,
+ // main goroutine will wait for exactly pendingRequests message on this channel
+ // (similar to sync.WaitGroup, pendingRequests is incremented before starting
+ // a goroutine that will send on requests).
+ requests chan []*rpctype.ExecutionRequest
+ pendingRequests int
+ // 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
+ 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
+}
+
+type syscallResult struct {
+ call *prog.Syscall
+ reason string
+}
+
+func newCheckContext(cfg *mgrconfig.Config, impl checker) *checkContext {
+ sandbox, err := ipc.SandboxToFlags(cfg.Sandbox)
+ if err != nil {
+ panic(fmt.Sprintf("failed to parse sandbox: %v", err))
+ }
+ return &checkContext{
+ impl: impl,
+ cfg: cfg,
+ target: cfg.Target,
+ sandbox: sandbox,
+ requests: make(chan []*rpctype.ExecutionRequest),
+ results: make(map[int64]*ipc.ProgInfo),
+ syscalls: make(chan syscallResult),
+ ready: make(chan bool),
+ }
+}
+
+func (ctx *checkContext) startCheck() []rpctype.ExecutionRequest {
+ for _, id := range ctx.cfg.Syscalls {
+ call := ctx.target.Syscalls[id]
+ if call.Attrs.Disabled {
+ continue
+ }
+ ctx.pendingSyscalls++
+ syscallCheck := ctx.impl.syscallCheck
+ if strings.HasPrefix(call.CallName, "syz_ext_") {
+ // Non-mainline pseudo-syscalls in executor/common_ext.h can't have
+ // the checking function and are assumed to be unconditionally supported.
+ syscallCheck = alwaysSupported
+ }
+ // HostFuzzer targets can't run Go binaries on the targets,
+ // so we actually run on the host on another OS. The same for targets.TestOS OS.
+ if ctx.cfg.SysTarget.HostFuzzer || ctx.target.OS == targets.TestOS {
+ syscallCheck = alwaysSupported
+ }
+
+ var depsReason chan string
+ deps := ctx.cfg.SysTarget.PseudoSyscallDeps[call.CallName]
+ if len(deps) != 0 {
+ ctx.pendingRequests++
+ depsReason = make(chan string, 1)
+ go func() {
+ depsReason <- ctx.supportedSyscalls(deps)
+ }()
+ }
+ ctx.pendingRequests++
+ go func() {
+ reason := syscallCheck(ctx, call)
+ ctx.waitForResults()
+ if reason == "" && depsReason != nil {
+ reason = <-depsReason
+ }
+ ctx.syscalls <- syscallResult{call, reason}
+ }()
+ }
+ var progs []rpctype.ExecutionRequest
+ dedup := make(map[hash.Sig]int64)
+ for i := 0; i < ctx.pendingRequests; i++ {
+ for _, req := range <-ctx.requests {
+ sig := hashReq(req)
+ req.ID = dedup[sig]
+ if req.ID != 0 {
+ continue
+ }
+ req.ID = int64(len(dedup) + 1)
+ dedup[sig] = req.ID
+ progs = append(progs, *req)
+ }
+ }
+ ctx.requests = nil
+ return progs
+}
+
+func (ctx *checkContext) finishCheck(fileInfos []host.FileInfo, progs []rpctype.ExecutionResult) (
+ map[*prog.Syscall]bool, map[*prog.Syscall]string, error) {
+ ctx.fs = createVirtualFilesystem(fileInfos)
+ for i := range progs {
+ res := &progs[i]
+ ctx.results[res.ID] = &res.Info
+ }
+ close(ctx.ready)
+ enabled := make(map[*prog.Syscall]bool)
+ disabled := make(map[*prog.Syscall]string)
+ for i := 0; i < ctx.pendingSyscalls; i++ {
+ res := <-ctx.syscalls
+ if res.reason == "" {
+ enabled[res.call] = true
+ } else {
+ disabled[res.call] = res.reason
+ }
+ }
+ return enabled, disabled, nil
+}
+
+func (ctx *checkContext) rootCanOpen(file string) string {
+ return ctx.canOpenImpl(file, nil, true)
+}
+
+func (ctx *checkContext) canOpen(file string) string {
+ return ctx.canOpenImpl(file, nil, false)
+}
+
+func (ctx *checkContext) canWrite(file string) string {
+ return ctx.canOpenImpl(file, []uint64{ctx.val("O_WRONLY")}, false)
+}
+
+func (ctx *checkContext) canOpenImpl(file string, modes []uint64, root bool) string {
+ if len(modes) == 0 {
+ modes = ctx.allOpenModes()
+ }
+ var calls []string
+ for _, mode := range modes {
+ call := fmt.Sprintf("openat(0x%x, &AUTO='%s', 0x%x, 0x0)", ctx.val("AT_FDCWD"), file, mode)
+ calls = append(calls, call)
+ }
+ info := ctx.execRaw(calls, prog.StrictUnsafe, root)
+ for _, call := range info.Calls {
+ if call.Errno == 0 {
+ return ""
+ }
+ }
+ who := ""
+ if root {
+ who = "root "
+ }
+ return fmt.Sprintf("%vfailed to open %s: %v", who, file, syscall.Errno(info.Calls[0].Errno))
+}
+
+func (ctx *checkContext) supportedSyscalls(names []string) string {
+ var calls []string
+ for _, name := range names {
+ if strings.HasPrefix(name, "syz_") {
+ panic("generic syscall check used for pseudo-syscall: " + name)
+ }
+ calls = append(calls, name+"()")
+ }
+ info := ctx.execRaw(calls, prog.NonStrictUnsafe, false)
+ for i, res := range info.Calls {
+ if res.Errno == int(syscall.ENOSYS) {
+ return fmt.Sprintf("syscall %v is not present", names[i])
+ }
+ }
+ return ""
+}
+
+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,
+ // and some only in non-blocking mode.
+ // Note: some of these consts are different for different arches.
+ return []uint64{ctx.val("O_RDONLY"), ctx.val("O_WRONLY"), ctx.val("O_RDWR"),
+ ctx.val("O_RDONLY") | ctx.val("O_NONBLOCK")}
+}
+
+func (ctx *checkContext) callSucceeds(call string) string {
+ return ctx.anyCallSucceeds([]string{call}, call+" failed")
+}
+
+func (ctx *checkContext) execCall(call string) syscall.Errno {
+ info := ctx.execRaw([]string{call}, prog.StrictUnsafe, false)
+ return syscall.Errno(info.Calls[0].Errno)
+}
+
+func (ctx *checkContext) anyCallSucceeds(calls []string, msg string) string {
+ info := ctx.execRaw(calls, prog.StrictUnsafe, false)
+ for _, call := range info.Calls {
+ if call.Errno == 0 {
+ return ""
+ }
+ }
+ return fmt.Sprintf("%s: %v", msg, syscall.Errno(info.Calls[0].Errno))
+}
+
+func (ctx *checkContext) onlySandboxNone() string {
+ if ctx.sandbox != 0 {
+ return "only supported under root with sandbox=none"
+ }
+ return ""
+}
+
+func (ctx *checkContext) onlySandboxNoneOrNamespace() string {
+ if ctx.sandbox != 0 && ctx.sandbox != ipc.FlagSandboxNamespace {
+ return "only supported under root with sandbox=none/namespace"
+ }
+ return ""
+}
+
+func (ctx *checkContext) val(name string) uint64 {
+ val, ok := ctx.target.ConstMap[name]
+ if !ok {
+ panic(fmt.Sprintf("const %v is not present", name))
+ }
+ return val
+}
+
+func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root bool) *ipc.ProgInfo {
+ if ctx.requests == nil {
+ panic("only one test execution per checker is supported")
+ }
+ sandbox := ctx.sandbox
+ if root {
+ sandbox = 0
+ }
+ remain := calls
+ var requests []*rpctype.ExecutionRequest
+ for len(remain) != 0 {
+ // Don't put too many syscalls into a single program,
+ // it will have higher chances to time out.
+ ncalls := min(len(remain), prog.MaxCalls/2)
+ progStr := strings.Join(remain[:ncalls], "\n")
+ remain = remain[ncalls:]
+ p, err := ctx.target.Deserialize([]byte(progStr), mode)
+ if err != nil {
+ panic(fmt.Sprintf("failed to deserialize: %v\n%v", err, progStr))
+ }
+ data, err := p.SerializeForExec()
+ if err != nil {
+ panic(fmt.Sprintf("failed to serialize test program: %v\n%s", err, progStr))
+ }
+ requests = append(requests, &rpctype.ExecutionRequest{
+ ProgData: slices.Clone(data), // clone to reduce memory usage
+ ExecOpts: ipc.ExecOpts{
+ EnvFlags: sandbox,
+ ExecFlags: 0,
+ SandboxArg: ctx.cfg.SandboxArg,
+ },
+ })
+ }
+ ctx.requests <- requests
+ <-ctx.ready
+ info := &ipc.ProgInfo{}
+ for _, req := range requests {
+ res := ctx.results[req.ID]
+ if res == nil {
+ panic(fmt.Sprintf("no result for request %v", req.ID))
+ }
+ if len(res.Calls) == 0 {
+ panic(fmt.Sprintf("result for request %v has no calls", req.ID))
+ }
+ info.Calls = append(info.Calls, res.Calls...)
+ }
+ if len(info.Calls) != len(calls) {
+ panic(fmt.Sprintf("got only %v results for program %v with %v calls:\n%s",
+ len(info.Calls), requests[0].ID, len(calls), strings.Join(calls, "\n")))
+ }
+ return info
+}
+
+func (ctx *checkContext) readFile(name string) ([]byte, error) {
+ ctx.waitForResults()
+ return ctx.fs.ReadFile(name)
+}
+
+func (ctx *checkContext) waitForResults() {
+ // If syscallCheck has already executed a program, then it's also waited for ctx.ready.
+ // If it hasn't, then we need to unblock the loop in startCheck by sending a nil request.
+ if ctx.requests == nil {
+ return
+ }
+ ctx.requests <- nil
+ <-ctx.ready
+ if ctx.fs == nil {
+ panic("filesystem should be initialized by now")
+ }
+}
+
+func hashReq(req *rpctype.ExecutionRequest) hash.Sig {
+ buf := new(bytes.Buffer)
+ if err := gob.NewEncoder(buf).Encode(req.ExecOpts); err != nil {
+ panic(err)
+ }
+ return hash.Hash(req.ProgData, buf.Bytes())
+}
+
+func alwaysSupported(ctx *checkContext, call *prog.Syscall) string {
+ return ""
+}
+
+func extractStringConst(typ prog.Type) (string, bool) {
+ ptr, ok := typ.(*prog.PtrType)
+ if !ok {
+ panic("first open arg is not a pointer to string const")
+ }
+ str, ok := ptr.Elem.(*prog.BufferType)
+ if !ok || str.Kind != prog.BufferString || len(str.Values) == 0 {
+ return "", false
+ }
+ v := str.Values[0]
+ for v != "" && v[len(v)-1] == 0 {
+ v = v[:len(v)-1] // string terminating \x00
+ }
+ return v, true
+}
diff --git a/pkg/vminfo/vminfo.go b/pkg/vminfo/vminfo.go
index 2b94cec19..ee2541ad3 100644
--- a/pkg/vminfo/vminfo.go
+++ b/pkg/vminfo/vminfo.go
@@ -4,8 +4,10 @@
// Package vminfo extracts information about the target VM.
// The package itself runs on the host, which may be a different OS/arch.
// User of the package first requests set of files that needs to be fetched from the VM
-// (Checker.RequiredFiles), then fetches these files, and calls Checker.MachineInfo
-// to parse the files and extract information about the VM.
+// and set of test programs that needs to be executed in the VM (Checker.RequiredThings),
+// then fetches these files and executes test programs, and calls Checker.MachineInfo
+// to parse the files and extract information about the VM, and optionally calls
+// Checker.Check to obtain list of enabled/disabled syscalls.
// The information includes information about kernel modules and OS-specific info
// (for Linux that includes things like parsed /proc/cpuinfo).
package vminfo
@@ -21,19 +23,27 @@ import (
"github.com/google/syzkaller/pkg/host"
"github.com/google/syzkaller/pkg/mgrconfig"
+ "github.com/google/syzkaller/pkg/rpctype"
+ "github.com/google/syzkaller/prog"
"github.com/google/syzkaller/sys/targets"
)
type Checker struct {
checker
+ checkContext *checkContext
}
func New(cfg *mgrconfig.Config) *Checker {
+ var impl checker
switch {
case cfg.TargetOS == targets.Linux:
- return &Checker{new(linux)}
+ impl = new(linux)
default:
- return &Checker{new(stub)}
+ impl = new(stub)
+ }
+ return &Checker{
+ checker: impl,
+ checkContext: newCheckContext(cfg, impl),
}
}
@@ -62,12 +72,25 @@ func (checker *Checker) MachineInfo(fileInfos []host.FileInfo) ([]host.KernelMod
return modules, info.Bytes(), nil
}
+func (checker *Checker) StartCheck() ([]string, []rpctype.ExecutionRequest) {
+ return checker.checkFiles(), checker.checkContext.startCheck()
+}
+
+func (checker *Checker) FinishCheck(files []host.FileInfo, progs []rpctype.ExecutionResult) (
+ map[*prog.Syscall]bool, map[*prog.Syscall]string, error) {
+ ctx := checker.checkContext
+ checker.checkContext = nil
+ return ctx.finishCheck(files, progs)
+}
+
type machineInfoFunc func(files filesystem, w io.Writer) (string, error)
type checker interface {
RequiredFiles() []string
+ checkFiles() []string
parseModules(files filesystem) ([]host.KernelModule, error)
machineInfos() []machineInfoFunc
+ syscallCheck(*checkContext, *prog.Syscall) string
}
type filesystem map[string]host.FileInfo
@@ -121,6 +144,10 @@ func (stub) RequiredFiles() []string {
return nil
}
+func (stub) checkFiles() []string {
+ return nil
+}
+
func (stub) parseModules(files filesystem) ([]host.KernelModule, error) {
return nil, nil
}
@@ -128,3 +155,7 @@ func (stub) parseModules(files filesystem) ([]host.KernelModule, error) {
func (stub) machineInfos() []machineInfoFunc {
return nil
}
+
+func (stub) syscallCheck(*checkContext, *prog.Syscall) string {
+ return ""
+}
diff --git a/pkg/vminfo/vminfo_test.go b/pkg/vminfo/vminfo_test.go
index e4dc1bfb8..9ffb0dd31 100644
--- a/pkg/vminfo/vminfo_test.go
+++ b/pkg/vminfo/vminfo_test.go
@@ -9,11 +9,14 @@ import (
"testing"
"github.com/google/syzkaller/pkg/host"
+ "github.com/google/syzkaller/pkg/ipc"
"github.com/google/syzkaller/pkg/mgrconfig"
+ "github.com/google/syzkaller/prog"
+ "github.com/google/syzkaller/sys/targets"
)
func TestHostMachineInfo(t *testing.T) {
- checker, files := hostChecker()
+ checker, files := hostChecker(t)
dups := make(map[string]bool)
for _, file := range files {
if file.Name[0] != '/' || file.Name[len(file.Name)-1] == '/' || strings.Contains(file.Name, "\\") {
@@ -38,15 +41,32 @@ func TestHostMachineInfo(t *testing.T) {
}
}
-func hostChecker() (*Checker, []host.FileInfo) {
+func hostChecker(t *testing.T) (*Checker, []host.FileInfo) {
+ cfg := testConfig(t, runtime.GOOS, runtime.GOARCH)
+ checker := New(cfg)
+ files := host.ReadFiles(checker.RequiredFiles())
+ return checker, files
+}
+
+func testConfig(t *testing.T, OS, arch string) *mgrconfig.Config {
+ target, err := prog.GetTarget(OS, arch)
+ if err != nil {
+ t.Fatal(err)
+ }
cfg := &mgrconfig.Config{
+ Sandbox: ipc.FlagsToSandbox(0),
Derived: mgrconfig.Derived{
- TargetOS: runtime.GOOS,
- TargetArch: runtime.GOARCH,
- TargetVMArch: runtime.GOARCH,
+ TargetOS: OS,
+ TargetArch: arch,
+ TargetVMArch: arch,
+ Target: target,
+ SysTarget: targets.Get(OS, arch),
},
}
- checker := New(cfg)
- files := host.ReadFiles(checker.RequiredFiles())
- return checker, files
+ for id := range target.Syscalls {
+ if !target.Syscalls[id].Attrs.Disabled {
+ cfg.Syscalls = append(cfg.Syscalls, id)
+ }
+ }
+ return cfg
}