diff options
| -rw-r--r-- | README.md | 9 | ||||
| -rw-r--r-- | config/config.go | 20 | ||||
| -rw-r--r-- | executor/executor.cc | 91 | ||||
| -rw-r--r-- | ipc/ipc.go | 31 | ||||
| -rw-r--r-- | syz-fuzzer/fuzzer.go | 5 | ||||
| -rw-r--r-- | syz-manager/manager.go | 4 | ||||
| -rw-r--r-- | tools/syz-execprog/execprog.go | 5 | ||||
| -rw-r--r-- | tools/syz-stress/stress.go | 5 |
8 files changed, 130 insertions, 40 deletions
@@ -97,9 +97,12 @@ following keys in its top-level object: the virtual machine. - `cpu`: Number of CPUs to simulate in the VM (*not currently used*). - `mem`: Amount of memory (in MiB) for the VM; this is passed as the `-m` option to `qemu-system-x86_64`. - - `dropprivs` : Whether the executor program should try to use namespaces to drop privileges - before executing (requires a kernel built with `CONFIG_NAMESPACES`, `CONFIG_UTS_NS`, - `CONFIG_USER_NS`, `CONFIG_PID_NS` and `CONFIG_NET_NS`). + - `sandbox` : Sandboxing mode, one of "none", "setuid", "namespace". + "none": don't do anything special (has false positives, e.g. due to killing init) + "setuid": impersonate into user nobody (65534), default + "namespace": use namespaces to drop privileges, + (requires a kernel built with `CONFIG_NAMESPACES`, `CONFIG_UTS_NS`, + `CONFIG_USER_NS`, `CONFIG_PID_NS` and `CONFIG_NET_NS`). - `enable_syscalls`: List of syscalls to test (optional). - `disable_syscalls`: List of system calls that should be treated as disabled (optional). - `suppressions`: List of regexps for known bugs. diff --git a/config/config.go b/config/config.go index 7142139d2..ce5b8553b 100644 --- a/config/config.go +++ b/config/config.go @@ -37,9 +37,14 @@ type Config struct { Count int // number of VMs Procs int // number of parallel processes inside of every VM - Cover bool // use kcov coverage (default: true) - DropPrivs bool // drop privileges during fuzzing (default: true) - Leak bool // do memory leak checking + Sandbox string // type of sandbox to use during fuzzing: + // "none": don't do anything special (has false positives, e.g. due to killing init) + // "setuid": impersonate into user nobody (65534), default + // "namespace": create a new namespace for fuzzer using CLONE_NEWNS/CLONE_NEWNET/CLONE_NEWPID/etc, + // requires building kernel with CONFIG_NAMESPACES, CONFIG_UTS_NS, CONFIG_USER_NS, CONFIG_PID_NS and CONFIG_NET_NS. + + Cover bool // use kcov coverage (default: true) + Leak bool // do memory leak checking ConsoleDev string // console device for adb vm @@ -69,7 +74,7 @@ func parse(data []byte) (*Config, map[int]bool, []*regexp.Regexp, error) { } cfg := new(Config) cfg.Cover = true - cfg.DropPrivs = true + cfg.Sandbox = "setuid" if err := json.Unmarshal(data, cfg); err != nil { return nil, nil, nil, fmt.Errorf("failed to parse config file: %v", err) } @@ -109,6 +114,11 @@ func parse(data []byte) (*Config, map[int]bool, []*regexp.Regexp, error) { default: return nil, nil, nil, fmt.Errorf("config param output must contain one of none/stdout/dmesg/file") } + switch cfg.Sandbox { + case "none", "setuid", "namespace": + default: + return nil, nil, nil, fmt.Errorf("config param sandbox must contain one of none/setuid/namespace") + } syscalls, err := parseSyscalls(cfg) if err != nil { @@ -240,7 +250,7 @@ func checkUnknownFields(data []byte) (string, error) { "Count", "Procs", "Cover", - "DropPrivs", + "Sandbox", "Leak", "ConsoleDev", "Enable_Syscalls", diff --git a/executor/executor.cc b/executor/executor.cc index e25309257..52275b185 100644 --- a/executor/executor.cc +++ b/executor/executor.cc @@ -67,12 +67,19 @@ const int kRetryStatus = 69; // so good enough for our purposes. const uint64_t default_value = -1; +enum sandbox_type { + sandbox_none, + sandbox_setuid, + sandbox_namespace, +}; + bool flag_debug; bool flag_cover; bool flag_threaded; bool flag_collide; bool flag_deduplicate; -bool flag_drop_privs; +bool flag_sandbox_privs; +sandbox_type flag_sandbox; __attribute__((aligned(64 << 10))) char input_data[kMaxInput]; __attribute__((aligned(64 << 10))) char output_data[kMaxOutput]; @@ -117,7 +124,11 @@ __attribute__((noreturn)) void fail(const char* msg, ...); __attribute__((noreturn)) void error(const char* msg, ...); __attribute__((noreturn)) void exitf(const char* msg, ...); void debug(const char* msg, ...); -int sandbox(void* arg); +int sandbox_proc(void* arg); +int do_sandbox_none(); +int do_sandbox_setuid(); +int do_sandbox_namespace(); +void sandbox_common(); void loop(); void execute_one(); uint64_t read_input(uint64_t** input_posp, bool peek = false); @@ -165,7 +176,11 @@ int main(int argc, char** argv) flag_threaded = flags & (1 << 2); flag_collide = flags & (1 << 3); flag_deduplicate = flags & (1 << 4); - flag_drop_privs = flags & (1 << 5); + flag_sandbox = sandbox_none; + if (flags & (1 << 5)) + flag_sandbox = sandbox_setuid; + else if (flags & (1 << 6)) + flag_sandbox = sandbox_namespace; if (!flag_threaded) flag_collide = false; @@ -181,17 +196,18 @@ int main(int argc, char** argv) syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8); int pid = -1; - if (flag_drop_privs) { - real_uid = getuid(); - real_gid = getgid(); - pid = clone(sandbox, &sandbox_stack[sizeof(sandbox_stack) - 8], - CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNET, NULL); - } else { - pid = fork(); - if (pid == 0) { - loop(); - exit(1); - } + switch (flag_sandbox) { + case sandbox_none: + pid = do_sandbox_none(); + break; + case sandbox_setuid: + pid = do_sandbox_setuid(); + break; + case sandbox_namespace: + pid = do_sandbox_namespace(); + break; + default: + fail("unknown sandbox type"); } if (pid < 0) fail("clone failed"); @@ -282,7 +298,46 @@ void loop() } } -int sandbox(void* arg) +int do_sandbox_none() +{ + int pid = fork(); + if (pid) + return pid; + loop(); + exit(1); +} + +int do_sandbox_setuid() +{ + int pid = fork(); + if (pid) + return pid; + + sandbox_common(); + + const int nobody = 65534; + if (setgroups(0, NULL)) + fail("failed to setgroups"); + // glibc versions do not we want -- they force all threads to setuid. + // We want to preserve the thread above as root. + if (syscall(SYS_setresgid, nobody, nobody, nobody)) + fail("failed to setresgid"); + if (syscall(SYS_setresuid, nobody, nobody, nobody)) + fail("failed to setresuid"); + + loop(); + exit(1); +} + +int do_sandbox_namespace() +{ + real_uid = getuid(); + real_gid = getgid(); + return clone(sandbox_proc, &sandbox_stack[sizeof(sandbox_stack) - 8], + CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNET, NULL); +} + +void sandbox_common() { prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); setpgrp(); @@ -299,8 +354,14 @@ int sandbox(void* arg) setrlimit(RLIMIT_CORE, &rlim); // CLONE_NEWIPC/CLONE_IO cause EINVAL on some systems, so we do them separately of clone. + unshare(CLONE_NEWNS); unshare(CLONE_NEWIPC); unshare(CLONE_IO); +} + +int sandbox_proc(void* arg) +{ + sandbox_common(); // /proc/self/setgroups is not present on some systems, ignore error. write_file("/proc/self/setgroups", "deny"); diff --git a/ipc/ipc.go b/ipc/ipc.go index 8301d0f78..2d4686bfc 100644 --- a/ipc/ipc.go +++ b/ipc/ipc.go @@ -37,19 +37,20 @@ type Env struct { } const ( - FlagDebug = uint64(1) << iota // debug output from executor - FlagCover // collect coverage - FlagThreaded // use multiple threads to mitigate blocked syscalls - FlagCollide // collide syscalls to provoke data races - FlagDedupCover // deduplicate coverage in executor - FlagDropPrivs // impersonate nobody user + FlagDebug = uint64(1) << iota // debug output from executor + FlagCover // collect coverage + FlagThreaded // use multiple threads to mitigate blocked syscalls + FlagCollide // collide syscalls to provoke data races + FlagDedupCover // deduplicate coverage in executor + FlagSandboxSetuid // impersonate nobody user + FlagSandboxNamespace // use namespaces for sandboxing ) var ( flagThreaded = flag.Bool("threaded", true, "use threaded mode in executor") flagCollide = flag.Bool("collide", true, "collide syscalls to provoke data races") flagCover = flag.Bool("cover", true, "collect coverage") - flagNobody = flag.Bool("nobody", true, "impersonate into nobody") + flagSandbox = flag.String("sandbox", "setuid", "sandbox for fuzzing (none/setuid/namespace)") flagDebug = flag.Bool("debug", false, "debug output from executor") // Executor protects against most hangs, so we use quite large timeout here. // Executor can be slow due to global locks in namespaces and other things, @@ -57,7 +58,7 @@ var ( flagTimeout = flag.Duration("timeout", 1*time.Minute, "execution timeout") ) -func DefaultFlags() (uint64, time.Duration) { +func DefaultFlags() (uint64, time.Duration, error) { var flags uint64 if *flagThreaded { flags |= FlagThreaded @@ -69,13 +70,19 @@ func DefaultFlags() (uint64, time.Duration) { flags |= FlagCover flags |= FlagDedupCover } - if *flagNobody { - flags |= FlagDropPrivs + switch *flagSandbox { + case "none": + case "setuid": + flags |= FlagSandboxSetuid + case "namespace": + flags |= FlagSandboxNamespace + default: + return 0, 0, fmt.Errorf("flag sandbox must contain one of none/setuid/namespace") } if *flagDebug { flags |= FlagDebug } - return flags, *flagTimeout + return flags, *flagTimeout, nil } func MakeEnv(bin string, timeout time.Duration, flags uint64) (*Env, error) { @@ -311,7 +318,7 @@ func makeCommand(bin []string, timeout time.Duration, flags uint64, inFile *os.F } }() - if flags&FlagDropPrivs != 0 { + if flags&(FlagSandboxSetuid|FlagSandboxNamespace) != 0 { if err := os.Chmod(dir, 0777); err != nil { return nil, fmt.Errorf("failed to chmod temp dir: %v", err) } diff --git a/syz-fuzzer/fuzzer.go b/syz-fuzzer/fuzzer.go index f1a459b45..cc7182af7 100644 --- a/syz-fuzzer/fuzzer.go +++ b/syz-fuzzer/fuzzer.go @@ -119,7 +119,10 @@ func main() { kmemleakInit() - flags, timeout := ipc.DefaultFlags() + flags, timeout, err := ipc.DefaultFlags() + if err != nil { + panic(err) + } noCover = flags&ipc.FlagCover == 0 if !noCover { fd, err := syscall.Open("/sys/kernel/debug/kcov", syscall.O_RDWR, 0) diff --git a/syz-manager/manager.go b/syz-manager/manager.go index 5afbe7972..35e9fd8f6 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -237,8 +237,8 @@ func (mgr *Manager) runInstance(vmCfg *vm.Config, first bool) bool { leak := first && mgr.cfg.Leak // Run the fuzzer binary. - outputC, errorC, err := inst.Run(time.Hour, fmt.Sprintf("%v -executor %v -name %v -manager %v -output=%v -procs %v -leak=%v -cover=%v -nobody=%v -v %d", - fuzzerBin, executorBin, vmCfg.Name, fwdAddr, mgr.cfg.Output, mgr.cfg.Procs, leak, mgr.cfg.Cover, mgr.cfg.DropPrivs, *flagV)) + outputC, errorC, err := inst.Run(time.Hour, fmt.Sprintf("%v -executor %v -name %v -manager %v -output=%v -procs %v -leak=%v -cover=%v -sandbox=%v -v %d", + fuzzerBin, executorBin, vmCfg.Name, fwdAddr, mgr.cfg.Output, mgr.cfg.Procs, leak, mgr.cfg.Cover, mgr.cfg.Sandbox, *flagV)) if err != nil { logf(0, "failed to run fuzzer: %v", err) return false diff --git a/tools/syz-execprog/execprog.go b/tools/syz-execprog/execprog.go index fdba0f258..5b7610754 100644 --- a/tools/syz-execprog/execprog.go +++ b/tools/syz-execprog/execprog.go @@ -55,7 +55,10 @@ func main() { return } - flags, timeout := ipc.DefaultFlags() + flags, timeout, err := ipc.DefaultFlags() + if err != nil { + log.Fatalf("%v", err) + } if *flagCoverFile != "" { flags |= ipc.FlagCover flags &= ^ipc.FlagDedupCover diff --git a/tools/syz-stress/stress.go b/tools/syz-stress/stress.go index de5f8ddf8..bb2352f76 100644 --- a/tools/syz-stress/stress.go +++ b/tools/syz-stress/stress.go @@ -47,7 +47,10 @@ func main() { prios := prog.CalculatePriorities(corpus) ct := prog.BuildChoiceTable(prios, calls) - flags, timeout := ipc.DefaultFlags() + flags, timeout, err := ipc.DefaultFlags() + if err != nil { + failf("%v", err) + } gate = ipc.NewGate(2**flagProcs, nil) for pid := 0; pid < *flagProcs; pid++ { pid := pid |
