From 22ee48a2879809608f79cc23c914859fa2335d59 Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Thu, 25 Apr 2024 13:51:51 +0200 Subject: 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. --- pkg/rpctype/rpctype.go | 14 +- pkg/vminfo/linux.go | 9 ++ pkg/vminfo/linux_syscalls.go | 345 ++++++++++++++++++++++++++++++++++++++++ pkg/vminfo/linux_test.go | 65 +++++++- pkg/vminfo/syscalls.go | 370 +++++++++++++++++++++++++++++++++++++++++++ pkg/vminfo/vminfo.go | 39 ++++- pkg/vminfo/vminfo_test.go | 36 ++++- 7 files changed, 856 insertions(+), 22 deletions(-) create mode 100644 pkg/vminfo/linux_syscalls.go create mode 100644 pkg/vminfo/syscalls.go (limited to 'pkg') 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 } -- cgit mrf-deployment