// Copyright 2015 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 (
	"bytes"
	"fmt"
	"os"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/prog"
	"github.com/google/syzkaller/sys/targets"
	"golang.org/x/sys/unix"
)

func isSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if strings.HasPrefix(c.CallName, "syz_") {
		return isSupportedSyzkall(c, target, sandbox)
	}
	if reason := isSupportedLSM(c); reason != "" {
		return false, reason
	}
	if strings.HasPrefix(c.Name, "socket$") ||
		strings.HasPrefix(c.Name, "socketpair$") {
		return isSupportedSocket(c)
	}
	if strings.HasPrefix(c.Name, "openat$") {
		return isSupportedOpenAt(c)
	}
	if strings.HasPrefix(c.Name, "mount$") {
		return isSupportedMount(c, sandbox)
	}
	if c.CallName == "pkey_alloc" {
		return isSyzPkeySetSupported(c, target, sandbox)
	}
	if c.Name == "ioctl$EXT4_IOC_SHUTDOWN" && sandbox == "none" {
		// Don't shutdown root filesystem.
		return false, "unsafe with sandbox=none"
	}
	return isSupportedSyscall(c, target)
}

func isSupportedSyscall(c *prog.Syscall, target *prog.Target) (bool, string) {
	// There are 3 possible strategies for detecting supported syscalls:
	// 1. Executes all syscalls with presumably invalid arguments and check for ENOprog.
	//    But not all syscalls are safe to execute. For example, pause will hang,
	//    while setpgrp will push the process into own process group.
	// 2. Check presence of /sys/kernel/debug/tracing/events/syscalls/sys_enter_* files.
	//    This requires root and CONFIG_FTRACE_SYSCALLS. Also it lies for some syscalls.
	//    For example, on x86_64 it says that sendfile is not present (only sendfile64).
	// 3. Check sys_syscallname in /proc/kallsyms.
	//    Requires CONFIG_KALLSYMS.
	// Kallsyms seems to be the most reliable and fast. That's what we use first.
	// If kallsyms is not present, we fallback to execution of syscalls.
	kallsymsOnce.Do(func() {
		kallsyms, _ := os.ReadFile("/proc/kallsyms")
		if len(kallsyms) == 0 {
			return
		}
		kallsymsSyscallSet = parseKallsyms(kallsyms, target.Arch)
	})
	if !testFallback && len(kallsymsSyscallSet) != 0 {
		r, v := isSupportedKallsyms(c)
		return r, v
	}
	return isSupportedTrial(c)
}

func isSupportedSyscallName(name string, target *prog.Target) (bool, string) {
	syscall := target.SyscallMap[name]
	if syscall == nil {
		return false, fmt.Sprintf("sys_%v is not present in the target", name)
	}
	return isSupportedSyscall(syscall, target)
}

func parseKallsyms(kallsyms []byte, arch string) map[string]bool {
	set := make(map[string]bool)
	var re *regexp.Regexp
	switch arch {
	case targets.I386, targets.AMD64:
		re = regexp.MustCompile(` T (__ia32_|__x64_)?sys_([^\n]+)\n`)
	case targets.ARM, targets.ARM64:
		re = regexp.MustCompile(` T (__arm64_)?sys_([^\n]+)\n`)
	case targets.PPC64LE:
		re = regexp.MustCompile(` T ()?sys_([^\n]+)\n`)
	case targets.MIPS64LE:
		re = regexp.MustCompile(` T sys_(mips_)?([^\n]+)\n`)
	case targets.S390x:
		re = regexp.MustCompile(` T (__s390_|__s390x_)?sys_([^\n]+)\n`)
	case targets.RiscV64:
		re = regexp.MustCompile(` T sys_(riscv_)?([^\n]+)\n`)
	default:
		panic("unsupported arch for kallsyms parsing")
	}
	matches := re.FindAllSubmatch(kallsyms, -1)
	for _, m := range matches {
		name := string(m[2])
		set[name] = true
	}
	return set
}

func isSupportedKallsyms(c *prog.Syscall) (bool, string) {
	name := c.CallName
	if newname := kallsymsRenameMap[name]; newname != "" {
		name = newname
	}
	if !kallsymsSyscallSet[name] {
		return false, fmt.Sprintf("sys_%v is not enabled in the kernel (not present in /proc/kallsyms)", name)
	}
	return true, ""
}

func isSupportedTrial(c *prog.Syscall) (bool, string) {
	switch c.CallName {
	// These known to cause hangs.
	case "exit", "pause":
		return true, ""
	}
	reason := fmt.Sprintf("sys_%v is not enabled in the kernel (returns ENOSYS)", c.CallName)
	trialMu.Lock()
	defer trialMu.Unlock()
	if res, ok := trialSupported[c.NR]; ok {
		return res, reason
	}
	cmd := osutil.Command(os.Args[0])
	cmd.Env = []string{fmt.Sprintf("SYZ_TRIAL_TEST=%v", c.NR)}
	_, err := osutil.Run(10*time.Second, cmd)
	res := err != nil
	trialSupported[c.NR] = res
	return res, reason
}

func init() {
	str := os.Getenv("SYZ_TRIAL_TEST")
	if str == "" {
		return
	}
	nr, err := strconv.Atoi(str)
	if err != nil {
		panic(err)
	}
	arg := ^uintptr(0) - 1e4 // something as invalid as possible
	_, _, err = syscall.Syscall6(uintptr(nr), arg, arg, arg, arg, arg, arg)
	if err == syscall.ENOSYS {
		os.Exit(0)
	}
	os.Exit(1)
}

// Some syscall names diverge in __NR_* consts and kallsyms.
// umount2 is renamed to umount in arch/x86/entry/syscalls/syscall_64.tbl.
// Where umount is renamed to oldumount is unclear.
var (
	kallsymsOnce       sync.Once
	kallsymsSyscallSet map[string]bool
	kallsymsRenameMap  = map[string]string{
		"umount":  "oldumount",
		"umount2": "umount",
		"stat":    "newstat",
	}
	trialMu         sync.Mutex
	trialSupported  = make(map[uint64]bool)
	filesystems     []byte
	filesystemsOnce sync.Once
	lsmOnce         sync.Once
	lsmError        error
	lsmDisabled     map[string]bool
)

func isSyzUsbSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	reason := checkUSBEmulation()
	return reason == "", reason
}

func alwaysSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	return true, ""
}

func isNetInjectionSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	reason := checkNetInjection()
	return reason == "", reason
}

func isVhciInjectionSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	reason := checkVhciInjection()
	return reason == "", reason
}

func isWifiEmulationSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	reason := checkWifiEmulation()
	return reason == "", reason
}

func isSyzKvmSetupCPUSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	switch c.Name {
	case "syz_kvm_setup_cpu$x86":
		if runtime.GOARCH == targets.AMD64 || runtime.GOARCH == targets.I386 {
			return true, ""
		}
	case "syz_kvm_setup_cpu$arm64":
		if runtime.GOARCH == targets.ARM64 {
			return true, ""
		}
	case "syz_kvm_setup_cpu$ppc64":
		if runtime.GOARCH == targets.PPC64LE {
			return true, ""
		}
	}
	return false, "unsupported arch"
}

func isSyzOpenDevSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	return isSupportedSyzOpenDev(sandbox, c)
}

func isSyzInitNetSocketSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if ok, reason := onlySandboxNone(sandbox); !ok {
		return false, reason
	}
	return isSupportedSocket(c)
}

func isSyzGenetlinkGetFamilyIDSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_GENERIC)
	if fd == -1 {
		return false, fmt.Sprintf("socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC) failed: %v", err)
	}
	// TODO: try to obtain actual family ID here. It will disable whole sets of sendmsg syscalls.
	syscall.Close(fd)
	return true, ""
}

func isSyzMountImageSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if ok, reason := onlySandboxNone(sandbox); !ok {
		return ok, reason
	}
	fstype, ok := extractStringConst(c.Args[0].Type)
	if !ok {
		panic("syz_mount_image arg is not string")
	}
	return isSupportedFilesystem(fstype)
}

func isSyzReadPartTableSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	return onlySandboxNone(sandbox)
}

func isSyzIoUringSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	return isSupportedSyscallName("io_uring_setup", target)
}

func isSyzMemcpySupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	ret, msg := isSyzIoUringSupported(c, target, sandbox)
	if ret {
		return ret, msg
	}
	return isSyzKvmSetupCPUSupported(c, target, sandbox)
}

func isBtfVmlinuxSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if err := osutil.IsAccessible("/sys/kernel/btf/vmlinux"); err != nil {
		return false, err.Error()
	}
	return onlySandboxNone(sandbox)
}

func isSyzFuseSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if ok, reason := isSupportedFilesystem("fuse"); !ok {
		return ok, reason
	}
	if ok, reason := onlySandboxNoneOrNamespace(sandbox); !ok {
		return false, reason
	}
	return true, ""
}

func isSyzUsbIPSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if err := osutil.IsWritable("/sys/devices/platform/vhci_hcd.0/attach"); err != nil {
		return false, err.Error()
	}
	return onlySandboxNoneOrNamespace(sandbox)
}

var syzkallSupport = map[string]func(*prog.Syscall, *prog.Target, string) (bool, string){
	"syz_open_dev":                isSyzOpenDevSupported,
	"syz_open_procfs":             isSyzOpenProcfsSupported,
	"syz_open_pts":                alwaysSupported,
	"syz_execute_func":            alwaysSupported,
	"syz_emit_ethernet":           isNetInjectionSupported,
	"syz_extract_tcp_res":         isNetInjectionSupported,
	"syz_usb_connect":             isSyzUsbSupported,
	"syz_usb_connect_ath9k":       isSyzUsbSupported,
	"syz_usb_disconnect":          isSyzUsbSupported,
	"syz_usb_control_io":          isSyzUsbSupported,
	"syz_usb_ep_write":            isSyzUsbSupported,
	"syz_usb_ep_read":             isSyzUsbSupported,
	"syz_kvm_setup_cpu":           isSyzKvmSetupCPUSupported,
	"syz_emit_vhci":               isVhciInjectionSupported,
	"syz_init_net_socket":         isSyzInitNetSocketSupported,
	"syz_genetlink_get_family_id": isSyzGenetlinkGetFamilyIDSupported,
	"syz_mount_image":             isSyzMountImageSupported,
	"syz_read_part_table":         isSyzReadPartTableSupported,
	"syz_io_uring_submit":         isSyzIoUringSupported,
	"syz_io_uring_complete":       isSyzIoUringSupported,
	"syz_io_uring_setup":          isSyzIoUringSupported,
	"syz_memcpy_off":              isSyzMemcpySupported,
	"syz_btf_id_by_name":          isBtfVmlinuxSupported,
	"syz_fuse_handle_req":         isSyzFuseSupported,
	"syz_80211_inject_frame":      isWifiEmulationSupported,
	"syz_80211_join_ibss":         isWifiEmulationSupported,
	"syz_usbip_server_init":       isSyzUsbIPSupported,
	"syz_clone":                   alwaysSupported,
	"syz_clone3":                  alwaysSupported,
	"syz_pkey_set":                isSyzPkeySetSupported,
}

func isSupportedSyzkall(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	sysTarget := targets.Get(target.OS, target.Arch)
	for _, depCall := range sysTarget.PseudoSyscallDeps[c.CallName] {
		if ok, reason := isSupportedSyscallName(depCall, target); !ok {
			return ok, reason
		}
	}
	if strings.HasPrefix(c.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.
		if syzkallSupport[c.CallName] != nil {
			panic("syz_ext_ prefix is reserved for non-mainline pseudo-syscalls")
		}
		return true, ""
	}
	if isSupported, ok := syzkallSupport[c.CallName]; ok {
		return isSupported(c, target, sandbox)
	}
	panic("unknown syzkall: " + c.Name)
}

func isSupportedSyzOpenDev(sandbox string, c *prog.Syscall) (bool, string) {
	if _, ok := c.Args[0].Type.(*prog.ConstType); ok {
		// This is for syz_open_dev$char/block.
		return true, ""
	}
	fname, ok := extractStringConst(c.Args[0].Type)
	if !ok {
		panic("first open arg is not a pointer to string const")
	}
	if strings.Contains(fname, "/dev/raw/raw#") {
		// For syz_open_dev$char_raw, these files don't exist initially.
		return true, ""
	}
	if !strings.Contains(fname, "#") {
		panic(fmt.Sprintf("%v does not contain # in the file name (should be openat)", c.Name))
	}
	if checkUSBEmulation() == "" {
		// These entries might not be available at boot time,
		// but will be created by connected USB devices.
		USBDevicePrefixes := []string{
			"/dev/hidraw", "/dev/usb/hiddev", "/dev/input/",
		}
		for _, prefix := range USBDevicePrefixes {
			if strings.HasPrefix(fname, prefix) {
				return true, ""
			}
		}
	}
	var check func(dev string) bool
	check = func(dev string) bool {
		if !strings.Contains(dev, "#") {
			// Note: don't try to open them all, some can hang (e.g. /dev/snd/pcmC#D#p).
			return osutil.IsExist(dev)
		}
		for i := 0; i < 10; i++ {
			if check(strings.Replace(dev, "#", strconv.Itoa(i), 1)) {
				return true
			}
		}
		return false
	}
	if !check(fname) {
		return false, fmt.Sprintf("file %v does not exist", fname)
	}
	return onlySandboxNoneOrNamespace(sandbox)
}

func isSupportedLSM(c *prog.Syscall) string {
	lsmOnce.Do(func() {
		data, err := os.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) {
				lsmError = err
			}
			return
		}
		lsmDisabled = make(map[string]bool)
		for _, lsm := range []string{"selinux", "apparmor", "smack"} {
			if !strings.Contains(string(data), lsm) {
				lsmDisabled[lsm] = true
			}
		}
	})
	if lsmError != nil {
		return lsmError.Error()
	}
	for lsm := range lsmDisabled {
		if strings.Contains(strings.ToLower(c.Name), lsm) {
			return fmt.Sprintf("LSM %v is not enabled", lsm)
		}
	}
	return ""
}

func onlySandboxNone(sandbox string) (bool, string) {
	if syscall.Getuid() != 0 || sandbox != "none" {
		return false, "only supported under root with sandbox=none"
	}
	return true, ""
}

func onlySandboxNoneOrNamespace(sandbox string) (bool, string) {
	if syscall.Getuid() != 0 || sandbox == "setuid" {
		return false, "only supported under root with sandbox=none/namespace"
	}
	return true, ""
}

func isSupportedSocket(c *prog.Syscall) (bool, string) {
	af, ok := c.Args[0].Type.(*prog.ConstType)
	if !ok {
		panic("socket family is not const")
	}
	fd, err := syscall.Socket(int(af.Val), 0, 0)
	if fd != -1 {
		syscall.Close(fd)
	}
	if err == syscall.ENOSYS {
		return false, "socket syscall returns ENOSYS"
	}
	if err == syscall.EAFNOSUPPORT {
		return false, "socket family is not supported (EAFNOSUPPORT)"
	}
	proto, ok := c.Args[2].Type.(*prog.ConstType)
	if !ok {
		return true, ""
	}
	var typ uint64
	if arg, ok := c.Args[1].Type.(*prog.ConstType); ok {
		typ = arg.Val
	} else if arg, ok := c.Args[1].Type.(*prog.FlagsType); ok {
		typ = arg.Vals[0]
	} else {
		return true, ""
	}
	fd, err = syscall.Socket(int(af.Val), int(typ), int(proto.Val))
	if fd != -1 {
		syscall.Close(fd)
		return true, ""
	}
	return false, err.Error()
}

func isSyzOpenProcfsSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	return isSupportedOpenFile(c, 1, nil)
}

func isSupportedOpenAt(c *prog.Syscall) (bool, string) {
	// Attempt to extract flags from the syscall description.
	var modes []int
	if mode, ok := c.Args[2].Type.(*prog.ConstType); ok {
		modes = []int{int(mode.Val)}
	}
	return isSupportedOpenFile(c, 1, modes)
}

func isSupportedOpenFile(c *prog.Syscall, filenameArg int, modes []int) (bool, string) {
	fname, ok := extractStringConst(c.Args[filenameArg].Type)
	if !ok || fname == "" || fname[0] != '/' {
		return true, ""
	}
	if len(modes) == 0 {
		modes = []int{syscall.O_RDONLY, syscall.O_WRONLY, syscall.O_RDWR, syscall.O_RDONLY | syscall.O_NONBLOCK}
	}
	var err error
	for _, mode := range modes {
		var fd int
		fd, err = syscall.Open(fname, mode, 0)
		if fd != -1 {
			syscall.Close(fd)
		}
		if err == nil {
			return true, ""
		}
	}
	return false, fmt.Sprintf("open(%v) failed: %v", fname, err)
}

func isSupportedMount(c *prog.Syscall, sandbox string) (bool, string) {
	fstype, ok := extractStringConst(c.Args[2].Type)
	if !ok {
		panic(fmt.Sprintf("%v: filesystem is not string const", c.Name))
	}
	if ok, reason := isSupportedFilesystem(fstype); !ok {
		return ok, reason
	}
	switch fstype {
	case "fuse", "fuseblk":
		if err := osutil.IsAccessible("/dev/fuse"); err != nil {
			return false, err.Error()
		}
		return onlySandboxNoneOrNamespace(sandbox)
	default:
		return onlySandboxNone(sandbox)
	}
}

func isSupportedFilesystem(fstype string) (bool, string) {
	filesystemsOnce.Do(func() {
		filesystems, _ = os.ReadFile("/proc/filesystems")
	})
	if !bytes.Contains(filesystems, []byte("\t"+fstype+"\n")) {
		return false, fmt.Sprintf("/proc/filesystems does not contain %v", fstype)
	}
	return true, ""
}

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
}

func isSyzPkeySetSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) {
	if target.Arch != targets.AMD64 && target.Arch != targets.I386 {
		return false, "unsupported arch"
	}
	key, _, err := syscall.Syscall6(unix.SYS_PKEY_ALLOC, 0, 0, 0, 0, 0, 0)
	if err != 0 {
		return false, fmt.Sprintf("pkey_alloc failed: %v", err)
	}
	_, _, err = syscall.Syscall6(unix.SYS_PKEY_FREE, key, 0, 0, 0, 0, 0)
	if err != 0 {
		return false, fmt.Sprintf("pkey_free failed: %v", err)
	}
	return true, ""
}
