aboutsummaryrefslogtreecommitdiffstats
// 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 {
			// Execute plain syscall (rather than a variation with $) to make test program
			// deduplication effective. However, if the plain syscall does not exist take
			// the first variant for this syscall, this still allows to dedup all variants.
			// This works b/c in syscall test we only check for ENOSYS result.
			name := call.CallName
			if ctx.target.SyscallMap[name] == nil {
				for _, call1 := range ctx.target.Syscalls {
					if name == call1.CallName {
						name = call1.Name
					}
				}
			}
			return ctx.supportedSyscalls([]string{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":                        supportedOpenat,
	"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":             linuxSyzKvmSupported,
	"syz_kvm_vgic_v3_setup":         linuxSyzSupportedOnArm64,
	"syz_kvm_setup_syzos_vm":        linuxSyzKvmSupported,
	"syz_kvm_add_vcpu":              linuxSyzKvmSupported,
	"syz_kvm_assert_syzos_uexit":    linuxSyzKvmSupported,
	"syz_kvm_assert_syzos_kvm_exit": linuxSyzKvmSupported,
	"syz_kvm_assert_reg":            linuxSyzSupportedOnArm64,
	"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,
	"syz_create_resource":           alwaysSupported,
	"syz_kfuzztest_run":             alwaysSupported,
}

func linuxSyzOpenDevSupported(ctx *checkContext, call *prog.Syscall) string {
	if _, ok := call.Args[0].Type.(*prog.ConstType); ok || call.Attrs.Automatic {
		// This is for syz_open_dev$char/block.
		// second operand for when we have an automatically generated description
		return ""
	}
	fname, ok := extractStringConst(call.Args[0].Type, call.Attrs.Automatic)
	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) {
				if ctx.rootCanOpen("/dev/raw-gadget") == "" {
					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")
}

const unsupportedArch = "unsupported arch"

func linuxSyzKvmSupported(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_syzos_vm$x86", "syz_kvm_add_vcpu$x86", "syz_kvm_assert_syzos_uexit$x86",
		"syz_kvm_assert_syzos_kvm_exit$x86":
		if ctx.target.Arch == targets.AMD64 {
			return ""
		}
	case "syz_kvm_setup_cpu$arm64", "syz_kvm_setup_syzos_vm$arm64", "syz_kvm_add_vcpu$arm64",
		"syz_kvm_assert_syzos_uexit$arm64", "syz_kvm_assert_syzos_kvm_exit$arm64":
		if ctx.target.Arch == targets.ARM64 {
			return ""
		}
	case "syz_kvm_setup_cpu$riscv64":
		if ctx.target.Arch == targets.RiscV64 {
			return ""
		}
	case "syz_kvm_setup_cpu$ppc64":
		if ctx.target.Arch == targets.PPC64LE {
			return ""
		}
	}
	return unsupportedArch
}

func linuxSyzSupportedOnArm64(ctx *checkContext, call *prog.Syscall) string {
	if ctx.target.Arch == targets.ARM64 {
		return ""
	}
	return unsupportedArch
}

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 {
	if call.Attrs.Automatic {
		return ""
	}
	fstype, ok := extractStringConst(call.Args[fsarg].Type, call.Attrs.Automatic)
	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" || call.Attrs.Automatic {
		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
}