diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | csource/common.go | 344 | ||||
| -rw-r--r-- | csource/csource.go | 112 | ||||
| -rw-r--r-- | csource/csource_test.go | 57 | ||||
| -rw-r--r-- | executor/common.h | 365 | ||||
| -rw-r--r-- | executor/executor.cc | 311 | ||||
| -rw-r--r-- | repro/repro.go | 344 | ||||
| -rw-r--r-- | tools/syz-prog2c/prog2c.go | 20 | ||||
| -rw-r--r-- | tools/syz-repro/repro.go | 241 |
9 files changed, 1235 insertions, 561 deletions
@@ -13,6 +13,7 @@ all: go install ./syz-manager ./syz-fuzzer $(MAKE) manager $(MAKE) fuzzer + $(MAKE) execprog $(MAKE) executor all-tools: execprog mutate prog2c stress repro upgrade @@ -62,6 +63,7 @@ presubmit: $(MAKE) generate go generate ./... $(MAKE) format + $(MAKE) executor ARCH=amd64 go install ./... ARCH=arm64 go install ./... ARCH=ppc64le go install ./... diff --git a/csource/common.go b/csource/common.go index 30b479b90..f935c8fbf 100644 --- a/csource/common.go +++ b/csource/common.go @@ -1,23 +1,86 @@ // AUTOGENERATED FROM executor/common.h package csource - var commonHeader = ` +#include <dirent.h> +#include <errno.h> #include <fcntl.h> +#include <grp.h> +#include <linux/capability.h> #include <pthread.h> #include <setjmp.h> #include <signal.h> +#include <stdarg.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/resource.h> #include <sys/stat.h> #include <sys/syscall.h> +#include <sys/time.h> #include <sys/types.h> +#include <sys/wait.h> #include <unistd.h> +const int kFailStatus = 67; +const int kErrorStatus = 68; +const int kRetryStatus = 69; + +__attribute__((noreturn)) void fail(const char* msg, ...) +{ + int e = errno; + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, " (errno %d)\n", e); + exit(kFailStatus); +} + +#if defined(SYZ_EXECUTOR) +__attribute__((noreturn)) void error(const char* msg, ...) +{ + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, "\n"); + exit(kErrorStatus); +} +#endif + +__attribute__((noreturn)) void exitf(const char* msg, ...) +{ + int e = errno; + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, " (errno %d)\n", e); + exit(kRetryStatus); +} + +static int flag_debug; + +void debug(const char* msg, ...) +{ + if (!flag_debug) + return; + va_list args; + va_start(args, msg); + vfprintf(stdout, msg, args); + va_end(args); + fflush(stdout); +} + __thread int skip_segv; __thread jmp_buf segv_env; @@ -147,4 +210,283 @@ static uintptr_t execute_syscall(int nr, uintptr_t a0, uintptr_t a1, uintptr_t a return syz_fuseblk_mount(a0, a1, a2, a3, a4, a5, a6, a7); } } + +static void setup_main_process() +{ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8); + syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8); + install_segv_handler(); + + char tmpdir_template[] = "./syzkaller.XXXXXX"; + char* tmpdir = mkdtemp(tmpdir_template); + if (!tmpdir) + fail("failed to mkdtemp"); + if (chdir(tmpdir)) + fail("failed to chdir"); +} + +static void loop(); + +static void sandbox_common() +{ + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + setpgrp(); + setsid(); + + struct rlimit rlim; + rlim.rlim_cur = rlim.rlim_max = 128 << 20; + setrlimit(RLIMIT_AS, &rlim); + rlim.rlim_cur = rlim.rlim_max = 1 << 20; + setrlimit(RLIMIT_FSIZE, &rlim); + rlim.rlim_cur = rlim.rlim_max = 1 << 20; + setrlimit(RLIMIT_STACK, &rlim); + rlim.rlim_cur = rlim.rlim_max = 0; + setrlimit(RLIMIT_CORE, &rlim); + + unshare(CLONE_NEWNS); + unshare(CLONE_NEWIPC); + unshare(CLONE_IO); +} + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_NONE) +static int do_sandbox_none() +{ + int pid = fork(); + if (pid) + return pid; + sandbox_common(); + loop(); + exit(1); +} +#endif + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_SETUID) +static 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"); + 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); +} +#endif + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_NAMESPACE) +static int real_uid; +static int real_gid; +static char sandbox_stack[1 << 20]; + +static bool write_file(const char* file, const char* what, ...) +{ + char buf[1024]; + va_list args; + va_start(args, what); + vsnprintf(buf, sizeof(buf), what, args); + va_end(args); + buf[sizeof(buf) - 1] = 0; + int len = strlen(buf); + + int fd = open(file, O_WRONLY | O_CLOEXEC); + if (fd == -1) + return false; + if (write(fd, buf, len) != len) { + close(fd); + return false; + } + close(fd); + return true; +} + +static int namespace_sandbox_proc(void* arg) +{ + sandbox_common(); + + write_file("/proc/self/setgroups", "deny"); + if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + fail("write of /proc/self/uid_map failed"); + if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + fail("write of /proc/self/gid_map failed"); + + if (mkdir("./syz-tmp", 0777)) + fail("mkdir(syz-tmp) failed"); + if (mount("", "./syz-tmp", "tmpfs", 0, NULL)) + fail("mount(tmpfs) failed"); + if (mkdir("./syz-tmp/newroot", 0777)) + fail("mkdir failed"); + if (mkdir("./syz-tmp/newroot/dev", 0700)) + fail("mkdir failed"); + if (mount("/dev", "./syz-tmp/newroot/dev", NULL, MS_BIND | MS_REC | MS_PRIVATE, NULL)) + fail("mount(dev) failed"); + if (mkdir("./syz-tmp/pivot", 0777)) + fail("mkdir failed"); + if (syscall(SYS_pivot_root, "./syz-tmp", "./syz-tmp/pivot")) { + debug("pivot_root failed"); + if (chdir("./syz-tmp")) + fail("chdir failed"); + } else { + if (chdir("/")) + fail("chdir failed"); + if (umount2("./pivot", MNT_DETACH)) + fail("umount failed"); + } + if (chroot("./newroot")) + fail("chroot failed"); + if (chdir("/")) + fail("chdir failed"); + + __user_cap_header_struct cap_hdr = {}; + __user_cap_data_struct cap_data[2] = {}; + cap_hdr.version = _LINUX_CAPABILITY_VERSION_3; + cap_hdr.pid = getpid(); + if (syscall(SYS_capget, &cap_hdr, &cap_data)) + fail("capget failed"); + cap_data[0].effective &= ~(1 << CAP_SYS_PTRACE); + cap_data[0].permitted &= ~(1 << CAP_SYS_PTRACE); + cap_data[0].inheritable &= ~(1 << CAP_SYS_PTRACE); + if (syscall(SYS_capset, &cap_hdr, &cap_data)) + fail("capset failed"); + + loop(); + exit(1); +} + +static int do_sandbox_namespace() +{ + real_uid = getuid(); + real_gid = getgid(); + return clone(namespace_sandbox_proc, &sandbox_stack[sizeof(sandbox_stack) - 8], + CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNET, NULL); +} +#endif + +static void remove_dir(const char* dir) +{ + DIR* dp; + struct dirent* ep; + int iter = 0; +retry: + dp = opendir(dir); + if (dp == NULL) { + if (errno == EMFILE) { + exitf("opendir(%s) failed due to NOFILE, exiting"); + } + exitf("opendir(%s) failed", dir); + } + while ((ep = readdir(dp))) { + if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0) + continue; + char filename[FILENAME_MAX]; + snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name); + struct stat st; + if (lstat(filename, &st)) + exitf("lstat(%s) failed", filename); + if (S_ISDIR(st.st_mode)) { + remove_dir(filename); + continue; + } + for (int i = 0;; i++) { + debug("unlink(%s)\n", filename); + if (unlink(filename) == 0) + break; + if (errno == EROFS) { + debug("ignoring EROFS\n"); + break; + } + if (errno != EBUSY || i > 100) + exitf("unlink(%s) failed", filename); + debug("umount(%s)\n", filename); + if (umount2(filename, MNT_DETACH)) + exitf("umount(%s) failed", filename); + } + } + closedir(dp); + for (int i = 0;; i++) { + debug("rmdir(%s)\n", dir); + if (rmdir(dir) == 0) + break; + if (i < 100) { + if (errno == EROFS) { + debug("ignoring EROFS\n"); + break; + } + if (errno == EBUSY) { + debug("umount(%s)\n", dir); + if (umount2(dir, MNT_DETACH)) + exitf("umount(%s) failed", dir); + continue; + } + if (errno == ENOTEMPTY) { + if (iter < 100) { + iter++; + goto retry; + } + } + } + exitf("rmdir(%s) failed", dir); + } +} + +static uint64_t current_time_ms() +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + fail("clock_gettime failed"); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} + +#if defined(SYZ_REPEAT) +static void test(); + +void loop() +{ + for (int iter = 0;; iter++) { + char cwdbuf[256]; + sprintf(cwdbuf, "./%d", iter); + if (mkdir(cwdbuf, 0777)) + fail("failed to mkdir"); + int pid = fork(); + if (pid < 0) + fail("clone failed"); + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + setpgrp(); + if (chdir(cwdbuf)) + fail("failed to chdir"); + test(); + exit(0); + } + int status = 0; + uint64_t start = current_time_ms(); + for (;;) { + int res = waitpid(pid, &status, __WALL | WNOHANG); + int errno0 = errno; + if (res == pid) + break; + usleep(1000); + if (current_time_ms() - start > 5 * 1000) { + kill(-pid, SIGKILL); + kill(pid, SIGKILL); + waitpid(pid, &status, __WALL); + break; + } + } + remove_dir(cwdbuf); + } +} +#endif ` diff --git a/csource/csource.go b/csource/csource.go index 9bd349b7c..49c15fe72 100644 --- a/csource/csource.go +++ b/csource/csource.go @@ -1,13 +1,14 @@ // 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. -//go:generate bash -c "echo -e '// AUTOGENERATED FROM executor/common.h\npackage csource\nvar commonHeader = `' > common.go; cat ../executor/common.h | egrep -v '^[ ]*//' | sed 's#[ ]*//.*##g' >> common.go; echo '`' >> common.go" +//go:generate bash -c "echo -e '// AUTOGENERATED FROM executor/common.h\npackage csource\nvar commonHeader = `' > common.go; cat ../executor/common.h | egrep -v '^[ ]*//' | sed '/^[ ]*\\/\\/.*/d' | sed 's#[ ]*//.*##g' >> common.go; echo '`' >> common.go" package csource import ( "bytes" "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -21,9 +22,13 @@ import ( type Options struct { Threaded bool Collide bool + Repeat bool + Procs int + Sandbox string + Repro bool // generate code for use with repro package } -func Write(p *prog.Prog, opts Options) []byte { +func Write(p *prog.Prog, opts Options) ([]byte, error) { exec := p.SerializeForExec() w := new(bytes.Buffer) @@ -45,23 +50,69 @@ func Write(p *prog.Prog, opts Options) []byte { } fmt.Fprintf(w, "\n") - fmt.Fprint(w, commonHeader) + hdr, err := preprocessCommonHeader(opts) + if err != nil { + return nil, err + } + fmt.Fprint(w, hdr) fmt.Fprint(w, "\n") calls, nvar := generateCalls(exec) fmt.Fprintf(w, "long r[%v];\n", nvar) + if !opts.Repeat { + generateTestFunc(w, opts, calls, "loop") + + fmt.Fprint(w, "int main()\n{\n") + fmt.Fprint(w, "\tsetup_main_process();\n") + fmt.Fprintf(w, "\tint pid = do_sandbox_%v();\n", opts.Sandbox) + fmt.Fprint(w, "\tint status = 0;\n") + fmt.Fprint(w, "\twhile (waitpid(pid, &status, __WALL) != pid) {}\n") + fmt.Fprint(w, "\treturn 0;\n}\n") + } else { + generateTestFunc(w, opts, calls, "test") + if opts.Procs <= 1 { + fmt.Fprint(w, "int main()\n{\n") + fmt.Fprint(w, "\tsetup_main_process();\n") + fmt.Fprintf(w, "\tint pid = do_sandbox_%v();\n", opts.Sandbox) + fmt.Fprint(w, "\tint status = 0;\n") + fmt.Fprint(w, "\twhile (waitpid(pid, &status, __WALL) != pid) {}\n") + fmt.Fprint(w, "\treturn 0;\n}\n") + } else { + fmt.Fprint(w, "int main()\n{\n") + fmt.Fprintf(w, "\tfor (int i = 0; i < %v; i++) {\n", opts.Procs) + fmt.Fprint(w, "\t\tif (fork() == 0) {\n") + fmt.Fprint(w, "\t\t\tsetup_main_process();\n") + fmt.Fprintf(w, "\t\t\tdo_sandbox_%v();\n", opts.Sandbox) + fmt.Fprint(w, "\t\t}\n") + fmt.Fprint(w, "\t}\n") + fmt.Fprint(w, "\tsleep(1000000);\n") + fmt.Fprint(w, "\treturn 0;\n}\n") + } + } + // Remove duplicate new lines. + out := w.Bytes() + for { + out1 := bytes.Replace(out, []byte{'\n', '\n', '\n'}, []byte{'\n', '\n'}, -1) + if len(out) == len(out1) { + break + } + out = out1 + } + return out, nil +} + +func generateTestFunc(w io.Writer, opts Options, calls []string, name string) { if !opts.Threaded && !opts.Collide { - fmt.Fprint(w, ` -int main() -{ - install_segv_handler(); - memset(r, -1, sizeof(r)); -`) + fmt.Fprintf(w, "void %v()\n{\n", name) + if opts.Repro { + fmt.Fprintf(w, "\twrite(1, \"executing program\\n\", strlen(\"executing program\\n\"));\n") + } + fmt.Fprintf(w, "\tmemset(r, -1, sizeof(r));\n") for _, c := range calls { fmt.Fprintf(w, "%s", c) } - fmt.Fprintf(w, "\treturn 0;\n}\n") + fmt.Fprintf(w, "}\n") } else { fmt.Fprintf(w, "void *thr(void *arg)\n{\n") fmt.Fprintf(w, "\tswitch ((long)arg) {\n") @@ -73,11 +124,13 @@ int main() fmt.Fprintf(w, "\t}\n") fmt.Fprintf(w, "\treturn 0;\n}\n\n") - fmt.Fprintf(w, "int main()\n{\n") + fmt.Fprintf(w, "void %v()\n{\n", name) fmt.Fprintf(w, "\tlong i;\n") fmt.Fprintf(w, "\tpthread_t th[%v];\n", 2*len(calls)) fmt.Fprintf(w, "\n") - fmt.Fprintf(w, "install_segv_handler();\n") + if opts.Repro { + fmt.Fprintf(w, "\twrite(1, \"executing program\\n\", strlen(\"executing program\\n\"));\n") + } fmt.Fprintf(w, "\tmemset(r, -1, sizeof(r));\n") fmt.Fprintf(w, "\tsrand(getpid());\n") fmt.Fprintf(w, "\tfor (i = 0; i < %v; i++) {\n", len(calls)) @@ -92,9 +145,8 @@ int main() fmt.Fprintf(w, "\t}\n") } fmt.Fprintf(w, "\tusleep(100000);\n") - fmt.Fprintf(w, "\treturn 0;\n}\n") + fmt.Fprintf(w, "}\n\n") } - return w.Bytes() } func generateCalls(exec []byte) ([]string, int) { @@ -198,6 +250,34 @@ loop: return calls, n } +func preprocessCommonHeader(opts Options) (string, error) { + cmd := exec.Command("cpp", "-nostdinc", "-undef", "-fdirectives-only", "-dDI", "-E", "-P", "-") + switch opts.Sandbox { + case "none": + cmd.Args = append(cmd.Args, "-DSYZ_SANDBOX_NONE") + case "setuid": + cmd.Args = append(cmd.Args, "-DSYZ_SANDBOX_SETUID") + case "namespace": + cmd.Args = append(cmd.Args, "-DSYZ_SANDBOX_NAMESPACE") + default: + return "", fmt.Errorf("unknown sandbox mode: %v", opts.Sandbox) + } + if opts.Repeat { + cmd.Args = append(cmd.Args, "-DSYZ_REPEAT") + } + cmd.Stdin = strings.NewReader(commonHeader) + stderr := new(bytes.Buffer) + stdout := new(bytes.Buffer) + cmd.Stderr = stderr + cmd.Stdout = stdout + if err := cmd.Run(); len(stdout.Bytes()) == 0 { + return "", fmt.Errorf("cpp failed: %v\n%v\n%v\n", err, stdout.String(), stderr.String()) + } + out := strings.Replace(stdout.String(), "#define __STDC__ 1\n", "", -1) + out = strings.Replace(out, "#define __STDC_HOSTED__ 1\n", "", -1) + return out, nil +} + // Build builds a C/C++ program from source file src // and returns name of the resulting binary. func Build(src string) (string, error) { @@ -206,7 +286,7 @@ func Build(src string) (string, error) { return "", fmt.Errorf("failed to create temp file: %v", err) } bin.Close() - out, err := exec.Command("gcc", "-x", "c++", "-std=gnu++11", src, "-o", bin.Name(), "-pthread", "-static", "-O1", "-g").CombinedOutput() + out, err := exec.Command("gcc", "-x", "c", "-std=gnu99", src, "-o", bin.Name(), "-pthread", "-static", "-O1", "-g").CombinedOutput() if err != nil { // Some distributions don't have static libraries. out, err = exec.Command("gcc", "-x", "c++", "-std=gnu++11", src, "-o", bin.Name(), "-pthread", "-O1", "-g").CombinedOutput() @@ -214,7 +294,7 @@ func Build(src string) (string, error) { if err != nil { os.Remove(bin.Name()) data, _ := ioutil.ReadFile(src) - return "", fmt.Errorf("failed to build program::\n%s\n%s", data, out) + return "", fmt.Errorf("failed to build program::\n%s\n%s", out, data) } return bin.Name(), nil } diff --git a/csource/csource_test.go b/csource/csource_test.go index c3e535cf5..ff35a2883 100644 --- a/csource/csource_test.go +++ b/csource/csource_test.go @@ -4,6 +4,7 @@ package csource import ( + "fmt" "math/rand" "os" "testing" @@ -14,9 +15,9 @@ import ( ) func initTest(t *testing.T) (rand.Source, int) { - iters := 1000 + iters := 10 if testing.Short() { - iters = 10 + iters = 1 } seed := int64(time.Now().UnixNano()) rs := rand.NewSource(seed) @@ -24,23 +25,53 @@ func initTest(t *testing.T) (rand.Source, int) { return rs, iters } +func allOptionsPermutations() []Options { + var options []Options + var opt Options + for _, opt.Threaded = range []bool{false, true} { + for _, opt.Collide = range []bool{false, true} { + for _, opt.Repeat = range []bool{false, true} { + for _, opt.Repro = range []bool{false, true} { + for _, opt.Procs = range []int{1, 4} { + for _, opt.Sandbox = range []string{"none", "setuid", "namespace"} { + if opt.Collide && !opt.Threaded { + continue + } + if !opt.Repeat && opt.Procs != 1 { + continue + } + if testing.Short() && opt.Procs != 1 { + continue + } + options = append(options, opt) + } + } + } + } + } + } + return options +} + func Test(t *testing.T) { rs, iters := initTest(t) - options := []Options{ - Options{}, - Options{Threaded: true}, - Options{Threaded: true, Collide: true}, - } - for i := 0; i < iters; i++ { - p := prog.Generate(rs, 10, nil) - for _, opts := range options { - testOne(t, p, opts) - } + for i, opts := range allOptionsPermutations() { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + t.Logf("opts: %+v", opts) + for i := 0; i < iters; i++ { + p := prog.Generate(rs, 10, nil) + testOne(t, p, opts) + } + }) } } func testOne(t *testing.T, p *prog.Prog, opts Options) { - src := Write(p, opts) + src, err := Write(p, opts) + if err != nil { + t.Logf("program:\n%s\n", p.Serialize()) + t.Fatalf("%v", err) + } srcf, err := fileutil.WriteTempFile(src) if err != nil { t.Logf("program:\n%s\n", p.Serialize()) diff --git a/executor/common.h b/executor/common.h index 52f2aeb9b..62462817f 100644 --- a/executor/common.h +++ b/executor/common.h @@ -2,21 +2,88 @@ // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. // This file is shared between executor and csource package. +#include <dirent.h> +#include <errno.h> #include <fcntl.h> +#include <grp.h> +#include <linux/capability.h> #include <pthread.h> #include <setjmp.h> #include <signal.h> +#include <stdarg.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/resource.h> #include <sys/stat.h> #include <sys/syscall.h> +#include <sys/time.h> #include <sys/types.h> +#include <sys/wait.h> #include <unistd.h> +const int kFailStatus = 67; +const int kErrorStatus = 68; +const int kRetryStatus = 69; + +// logical error (e.g. invalid input program) +__attribute__((noreturn)) void fail(const char* msg, ...) +{ + int e = errno; + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, " (errno %d)\n", e); + exit(kFailStatus); +} + +#if defined(SYZ_EXECUTOR) +// kernel error (e.g. wrong syscall return value) +__attribute__((noreturn)) void error(const char* msg, ...) +{ + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, "\n"); + exit(kErrorStatus); +} +#endif + +// just exit (e.g. due to temporal ENOMEM error) +__attribute__((noreturn)) void exitf(const char* msg, ...) +{ + int e = errno; + fflush(stdout); + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf(stderr, " (errno %d)\n", e); + exit(kRetryStatus); +} + +static int flag_debug; + +void debug(const char* msg, ...) +{ + if (!flag_debug) + return; + va_list args; + va_start(args, msg); + vfprintf(stdout, msg, args); + va_end(args); + fflush(stdout); +} + __thread int skip_segv; __thread jmp_buf segv_env; @@ -154,3 +221,301 @@ static uintptr_t execute_syscall(int nr, uintptr_t a0, uintptr_t a1, uintptr_t a return syz_fuseblk_mount(a0, a1, a2, a3, a4, a5, a6, a7); } } + +static void setup_main_process() +{ + // Don't need that SIGCANCEL/SIGSETXID glibc stuff. + // SIGCANCEL sent to main thread causes it to exit + // without bringing down the whole group. + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_IGN; + syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8); + syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8); + install_segv_handler(); + + char tmpdir_template[] = "./syzkaller.XXXXXX"; + char* tmpdir = mkdtemp(tmpdir_template); + if (!tmpdir) + fail("failed to mkdtemp"); + if (chdir(tmpdir)) + fail("failed to chdir"); +} + +static void loop(); + +static void sandbox_common() +{ + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + setpgrp(); + setsid(); + + struct rlimit rlim; + rlim.rlim_cur = rlim.rlim_max = 128 << 20; + setrlimit(RLIMIT_AS, &rlim); + rlim.rlim_cur = rlim.rlim_max = 1 << 20; + setrlimit(RLIMIT_FSIZE, &rlim); + rlim.rlim_cur = rlim.rlim_max = 1 << 20; + setrlimit(RLIMIT_STACK, &rlim); + rlim.rlim_cur = rlim.rlim_max = 0; + 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); +} + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_NONE) +static int do_sandbox_none() +{ + int pid = fork(); + if (pid) + return pid; + sandbox_common(); + loop(); + exit(1); +} +#endif + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_SETUID) +static 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); +} +#endif + +#if defined(SYZ_EXECUTOR) || defined(SYZ_SANDBOX_NAMESPACE) +static int real_uid; +static int real_gid; +static char sandbox_stack[1 << 20]; + +static bool write_file(const char* file, const char* what, ...) +{ + char buf[1024]; + va_list args; + va_start(args, what); + vsnprintf(buf, sizeof(buf), what, args); + va_end(args); + buf[sizeof(buf) - 1] = 0; + int len = strlen(buf); + + int fd = open(file, O_WRONLY | O_CLOEXEC); + if (fd == -1) + return false; + if (write(fd, buf, len) != len) { + close(fd); + return false; + } + close(fd); + return true; +} + +static int namespace_sandbox_proc(void* arg) +{ + sandbox_common(); + + // /proc/self/setgroups is not present on some systems, ignore error. + write_file("/proc/self/setgroups", "deny"); + if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + fail("write of /proc/self/uid_map failed"); + if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + fail("write of /proc/self/gid_map failed"); + + if (mkdir("./syz-tmp", 0777)) + fail("mkdir(syz-tmp) failed"); + if (mount("", "./syz-tmp", "tmpfs", 0, NULL)) + fail("mount(tmpfs) failed"); + if (mkdir("./syz-tmp/newroot", 0777)) + fail("mkdir failed"); + if (mkdir("./syz-tmp/newroot/dev", 0700)) + fail("mkdir failed"); + if (mount("/dev", "./syz-tmp/newroot/dev", NULL, MS_BIND | MS_REC | MS_PRIVATE, NULL)) + fail("mount(dev) failed"); + if (mkdir("./syz-tmp/pivot", 0777)) + fail("mkdir failed"); + if (syscall(SYS_pivot_root, "./syz-tmp", "./syz-tmp/pivot")) { + debug("pivot_root failed"); + if (chdir("./syz-tmp")) + fail("chdir failed"); + } else { + if (chdir("/")) + fail("chdir failed"); + if (umount2("./pivot", MNT_DETACH)) + fail("umount failed"); + } + if (chroot("./newroot")) + fail("chroot failed"); + if (chdir("/")) + fail("chdir failed"); + + // Drop CAP_SYS_PTRACE so that test processes can't attach to parent processes. + // Previously it lead to hangs because the loop process stopped due to SIGSTOP. + // Note that a process can always ptrace its direct children, which is enough + // for testing purposes. + __user_cap_header_struct cap_hdr = {}; + __user_cap_data_struct cap_data[2] = {}; + cap_hdr.version = _LINUX_CAPABILITY_VERSION_3; + cap_hdr.pid = getpid(); + if (syscall(SYS_capget, &cap_hdr, &cap_data)) + fail("capget failed"); + cap_data[0].effective &= ~(1 << CAP_SYS_PTRACE); + cap_data[0].permitted &= ~(1 << CAP_SYS_PTRACE); + cap_data[0].inheritable &= ~(1 << CAP_SYS_PTRACE); + if (syscall(SYS_capset, &cap_hdr, &cap_data)) + fail("capset failed"); + + loop(); + exit(1); +} + +static int do_sandbox_namespace() +{ + real_uid = getuid(); + real_gid = getgid(); + return clone(namespace_sandbox_proc, &sandbox_stack[sizeof(sandbox_stack) - 8], + CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNET, NULL); +} +#endif + +// One does not simply remove a directory. +// There can be mounts, so we need to try to umount. +// Moreover, a mount can be mounted several times, so we need to try to umount in a loop. +// Moreover, after umount a dir can become non-empty again, so we need another loop. +// Moreover, a mount can be re-mounted as read-only and then we will fail to make a dir empty. +static void remove_dir(const char* dir) +{ + DIR* dp; + struct dirent* ep; + int iter = 0; +retry: + dp = opendir(dir); + if (dp == NULL) { + if (errno == EMFILE) { + // This happens when the test process casts prlimit(NOFILE) on us. + // Ideally we somehow prevent test processes from messing with parent processes. + // But full sandboxing is expensive, so let's ignore this error for now. + exitf("opendir(%s) failed due to NOFILE, exiting"); + } + exitf("opendir(%s) failed", dir); + } + while ((ep = readdir(dp))) { + if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0) + continue; + char filename[FILENAME_MAX]; + snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name); + struct stat st; + if (lstat(filename, &st)) + exitf("lstat(%s) failed", filename); + if (S_ISDIR(st.st_mode)) { + remove_dir(filename); + continue; + } + for (int i = 0;; i++) { + debug("unlink(%s)\n", filename); + if (unlink(filename) == 0) + break; + if (errno == EROFS) { + debug("ignoring EROFS\n"); + break; + } + if (errno != EBUSY || i > 100) + exitf("unlink(%s) failed", filename); + debug("umount(%s)\n", filename); + if (umount2(filename, MNT_DETACH)) + exitf("umount(%s) failed", filename); + } + } + closedir(dp); + for (int i = 0;; i++) { + debug("rmdir(%s)\n", dir); + if (rmdir(dir) == 0) + break; + if (i < 100) { + if (errno == EROFS) { + debug("ignoring EROFS\n"); + break; + } + if (errno == EBUSY) { + debug("umount(%s)\n", dir); + if (umount2(dir, MNT_DETACH)) + exitf("umount(%s) failed", dir); + continue; + } + if (errno == ENOTEMPTY) { + if (iter < 100) { + iter++; + goto retry; + } + } + } + exitf("rmdir(%s) failed", dir); + } +} + +static uint64_t current_time_ms() +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + fail("clock_gettime failed"); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} + +#if defined(SYZ_REPEAT) +static void test(); + +void loop() +{ + for (int iter = 0;; iter++) { + char cwdbuf[256]; + sprintf(cwdbuf, "./%d", iter); + if (mkdir(cwdbuf, 0777)) + fail("failed to mkdir"); + int pid = fork(); + if (pid < 0) + fail("clone failed"); + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + setpgrp(); + if (chdir(cwdbuf)) + fail("failed to chdir"); + test(); + exit(0); + } + int status = 0; + uint64_t start = current_time_ms(); + for (;;) { + int res = waitpid(pid, &status, __WALL | WNOHANG); + int errno0 = errno; + if (res == pid) + break; + usleep(1000); + if (current_time_ms() - start > 5 * 1000) { + kill(-pid, SIGKILL); + kill(pid, SIGKILL); + waitpid(pid, &status, __WALL); + break; + } + } + remove_dir(cwdbuf); + } +} +#endif diff --git a/executor/executor.cc b/executor/executor.cc index 972a80d24..b155c578d 100644 --- a/executor/executor.cc +++ b/executor/executor.cc @@ -2,18 +2,14 @@ // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. #include <algorithm> -#include <dirent.h> #include <errno.h> #include <fcntl.h> -#include <grp.h> #include <limits.h> -#include <linux/capability.h> #include <linux/futex.h> #include <linux/reboot.h> #include <pthread.h> #include <setjmp.h> #include <signal.h> -#include <stdarg.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> @@ -21,10 +17,8 @@ #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> -#include <sys/mount.h> #include <sys/prctl.h> #include <sys/reboot.h> -#include <sys/resource.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/time.h> @@ -35,6 +29,7 @@ #include "syscalls.h" +#define SYZ_EXECUTOR #include "common.h" #define KCOV_INIT_TRACE _IOR('c', 1, unsigned long long) @@ -61,10 +56,6 @@ const uint64_t arg_const = 0; const uint64_t arg_result = 1; const uint64_t arg_data = 2; -const int kFailStatus = 67; -const int kErrorStatus = 68; -const int kRetryStatus = 69; - // We use the default value instead of results of failed syscalls. // -1 is an invalid fd and an invalid address and deterministic, // so good enough for our purposes. @@ -76,7 +67,6 @@ enum sandbox_type { sandbox_namespace, }; -bool flag_debug; bool flag_cover; bool flag_threaded; bool flag_collide; @@ -90,8 +80,6 @@ uint32_t* output_pos; int completed; int running; bool collide; -int real_uid; -int real_gid; struct res_t { bool executed; @@ -121,18 +109,7 @@ struct thread_t { }; thread_t threads[kMaxThreads]; -char sandbox_stack[1 << 20]; - -__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_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); uint64_t read_arg(uint64_t** input_posp); @@ -146,14 +123,11 @@ void handle_completion(thread_t* th); void thread_create(thread_t* th, int id); void* worker_thread(void* arg); bool write_file(const char* file, const char* what, ...); -void remove_dir(const char* dir); -uint64_t current_time_ms(); void cover_open(); void cover_enable(thread_t* th); void cover_reset(thread_t* th); uint64_t cover_read(thread_t* th); uint64_t cover_dedup(thread_t* th, uint64_t n); -void handle_segv(int sig, siginfo_t* info, void* uctx); int main(int argc, char** argv) { @@ -189,16 +163,7 @@ int main(int argc, char** argv) flag_collide = false; cover_open(); - - // Don't need that SIGCANCEL/SIGSETXID glibc stuff. - // SIGCANCEL sent to main thread causes it to exit - // without bringing down the whole group. - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = SIG_IGN; - syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8); - syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8); - install_segv_handler(); + setup_main_process(); int pid = -1; switch (flag_sandbox) { @@ -303,125 +268,6 @@ void loop() } } -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(); - setsid(); - - struct rlimit rlim; - rlim.rlim_cur = rlim.rlim_max = 128 << 20; - setrlimit(RLIMIT_AS, &rlim); - rlim.rlim_cur = rlim.rlim_max = 1 << 20; - setrlimit(RLIMIT_FSIZE, &rlim); - rlim.rlim_cur = rlim.rlim_max = 1 << 20; - setrlimit(RLIMIT_STACK, &rlim); - rlim.rlim_cur = rlim.rlim_max = 0; - 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"); - if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) - fail("write of /proc/self/uid_map failed"); - if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) - fail("write of /proc/self/gid_map failed"); - - if (mkdir("./syz-tmp", 0777)) - fail("mkdir(syz-tmp) failed"); - if (mount("", "./syz-tmp", "tmpfs", 0, NULL)) - fail("mount(tmpfs) failed"); - if (mkdir("./syz-tmp/newroot", 0777)) - fail("mkdir failed"); - if (mkdir("./syz-tmp/newroot/dev", 0700)) - fail("mkdir failed"); - if (mount("/dev", "./syz-tmp/newroot/dev", NULL, MS_BIND | MS_REC | MS_PRIVATE, NULL)) - fail("mount(dev) failed"); - if (mkdir("./syz-tmp/pivot", 0777)) - fail("mkdir failed"); - if (syscall(SYS_pivot_root, "./syz-tmp", "./syz-tmp/pivot")) { - debug("pivot_root failed\n"); - if (chdir("./syz-tmp")) - fail("chdir failed"); - } else { - if (chdir("/")) - fail("chdir failed"); - if (umount2("./pivot", MNT_DETACH)) - fail("umount failed"); - } - if (chroot("./newroot")) - fail("chroot failed"); - if (chdir("/")) - fail("chdir failed"); - - // Drop CAP_SYS_PTRACE so that test processes can't attach to parent processes. - // Previously it lead to hangs because the loop process stopped due to SIGSTOP. - // Note that a process can always ptrace its direct children, which is enough - // for testing purposes. - __user_cap_header_struct cap_hdr = {}; - __user_cap_data_struct cap_data[2] = {}; - cap_hdr.version = _LINUX_CAPABILITY_VERSION_3; - cap_hdr.pid = getpid(); - if (syscall(SYS_capget, &cap_hdr, &cap_data)) - fail("capget failed"); - cap_data[0].effective &= ~(1 << CAP_SYS_PTRACE); - cap_data[0].permitted &= ~(1 << CAP_SYS_PTRACE); - cap_data[0].inheritable &= ~(1 << CAP_SYS_PTRACE); - if (syscall(SYS_capset, &cap_hdr, &cap_data)) - fail("capset failed"); - - loop(); - exit(1); -} - void execute_one() { retry: @@ -831,154 +677,3 @@ void write_output(uint32_t v) fail("output overflow"); *output_pos++ = v; } - -bool write_file(const char* file, const char* what, ...) -{ - char buf[1024]; - va_list args; - va_start(args, what); - vsnprintf(buf, sizeof(buf), what, args); - va_end(args); - buf[sizeof(buf) - 1] = 0; - int len = strlen(buf); - - int fd = open(file, O_WRONLY | O_CLOEXEC); - if (fd == -1) - return false; - if (write(fd, buf, len) != len) { - close(fd); - return false; - } - close(fd); - return true; -} - -// One does not simply remove a directory. -// There can be mounts, so we need to try to umount. -// Moreover, a mount can be mounted several times, so we need to try to umount in a loop. -// Moreover, after umount a dir can become non-empty again, so we need another loop. -// Moreover, a mount can be re-mounted as read-only and then we will fail to make a dir empty. -void remove_dir(const char* dir) -{ - int iter = 0; -retry: - DIR* dp = opendir(dir); - if (dp == NULL) { - if (errno == EMFILE) { - // This happens when the test process casts prlimit(NOFILE) on us. - // Ideally we somehow prevent test processes from messing with parent processes. - // But full sandboxing is expensive, so let's ignore this error for now. - exitf("opendir(%s) failed due to NOFILE, exiting"); - } - exitf("opendir(%s) failed", dir); - } - while (dirent* ep = readdir(dp)) { - if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0) - continue; - char filename[FILENAME_MAX]; - snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name); - struct stat st; - if (lstat(filename, &st)) - exitf("lstat(%s) failed", filename); - if (S_ISDIR(st.st_mode)) { - remove_dir(filename); - continue; - } - for (int i = 0;; i++) { - debug("unlink(%s)\n", filename); - if (unlink(filename) == 0) - break; - if (errno == EROFS) { - debug("ignoring EROFS\n"); - break; - } - if (errno != EBUSY || i > 100) - exitf("unlink(%s) failed", filename); - debug("umount(%s)\n", filename); - if (umount2(filename, MNT_DETACH)) - exitf("umount(%s) failed", filename); - } - } - closedir(dp); - for (int i = 0;; i++) { - debug("rmdir(%s)\n", dir); - if (rmdir(dir) == 0) - break; - if (i < 100) { - if (errno == EROFS) { - debug("ignoring EROFS\n"); - break; - } - if (errno == EBUSY) { - debug("umount(%s)\n", dir); - if (umount2(dir, MNT_DETACH)) - exitf("umount(%s) failed", dir); - continue; - } - if (errno == ENOTEMPTY) { - if (iter < 100) { - iter++; - goto retry; - } - } - } - exitf("rmdir(%s) failed", dir); - } -} - -uint64_t current_time_ms() -{ - timespec ts; - if (clock_gettime(CLOCK_MONOTONIC, &ts)) - fail("clock_gettime failed"); - return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; -} - -// logical error (e.g. invalid input program) -void fail(const char* msg, ...) -{ - int e = errno; - fflush(stdout); - va_list args; - va_start(args, msg); - vfprintf(stderr, msg, args); - va_end(args); - fprintf(stderr, " (errno %d)\n", e); - exit(kFailStatus); -} - -// kernel error (e.g. wrong syscall return value) -void error(const char* msg, ...) -{ - fflush(stdout); - va_list args; - va_start(args, msg); - vfprintf(stderr, msg, args); - va_end(args); - fprintf(stderr, "\n"); - exit(kErrorStatus); -} - -// just exit (e.g. due to temporal ENOMEM error) -void exitf(const char* msg, ...) -{ - int e = errno; - fflush(stdout); - va_list args; - va_start(args, msg); - vfprintf(stderr, msg, args); - va_end(args); - fprintf(stderr, " (errno %d)\n", e); - exit(kRetryStatus); -} - -void debug(const char* msg, ...) -{ - if (!flag_debug) - return; - va_list args; - va_start(args, msg); - vfprintf(stdout, msg, args); - va_end(args); - fflush(stdout); -} diff --git a/repro/repro.go b/repro/repro.go new file mode 100644 index 000000000..cdb4fa696 --- /dev/null +++ b/repro/repro.go @@ -0,0 +1,344 @@ +// Copyright 2016 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 repro + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/google/syzkaller/config" + "github.com/google/syzkaller/csource" + "github.com/google/syzkaller/fileutil" + . "github.com/google/syzkaller/log" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/report" + "github.com/google/syzkaller/vm" +) + +type Result struct { + Prog *prog.Prog + Opts csource.Options + CRepro bool +} + +type context struct { + cfg *config.Config + crashDesc string + instances chan *instance + bootRequests chan int +} + +type instance struct { + vm.Instance + index int + execprogBin string + executorBin string +} + +func Run(crashLog []byte, cfg *config.Config, vmIndexes []int) (*Result, error) { + if len(vmIndexes) == 0 { + return nil, fmt.Errorf("no VMs provided") + } + if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-execprog")); err != nil { + return nil, fmt.Errorf("bin/syz-execprog is missing (run 'make execprog')") + } + entries := prog.ParseLog(crashLog) + if len(entries) == 0 { + return nil, fmt.Errorf("crash log does not contain any programs") + } + crashDesc, _, crashStart, _ := report.Parse(crashLog) + if crashDesc == "" { + crashStart = len(crashLog) // assuming VM hanged + crashDesc = "hang" + } + Logf(0, "reproducing crash '%v': %v programs, %v VMs", crashDesc, len(entries), len(vmIndexes)) + + ctx := &context{ + cfg: cfg, + crashDesc: crashDesc, + instances: make(chan *instance, len(vmIndexes)), + bootRequests: make(chan int, len(vmIndexes)), + } + var wg sync.WaitGroup + wg.Add(len(vmIndexes)) + for _, vmIndex := range vmIndexes { + ctx.bootRequests <- vmIndex + go func() { + defer wg.Done() + for vmIndex := range ctx.bootRequests { + var inst *instance + for try := 0; try < 3; try++ { + vmCfg, err := config.CreateVMConfig(cfg, vmIndex) + if err != nil { + Logf(0, "reproducing crash '%v': failed to create VM config: %v", crashDesc, err) + time.Sleep(10 * time.Second) + continue + } + vmInst, err := vm.Create(cfg.Type, vmCfg) + if err != nil { + Logf(0, "reproducing crash '%v': failed to create VM: %v", crashDesc, err) + time.Sleep(10 * time.Second) + continue + + } + execprogBin, err := vmInst.Copy(filepath.Join(cfg.Syzkaller, "bin/syz-execprog")) + if err != nil { + Logf(0, "reproducing crash '%v': failed to copy to VM: %v", crashDesc, err) + vmInst.Close() + time.Sleep(10 * time.Second) + continue + } + executorBin, err := vmInst.Copy(filepath.Join(cfg.Syzkaller, "bin/syz-executor")) + if err != nil { + Logf(0, "reproducing crash '%v': failed to copy to VM: %v", crashDesc, err) + vmInst.Close() + time.Sleep(10 * time.Second) + continue + } + inst = &instance{vmInst, vmIndex, execprogBin, executorBin} + break + } + if inst == nil { + break + } + ctx.instances <- inst + } + }() + } + go func() { + wg.Wait() + close(ctx.instances) + }() + + res, err := ctx.repro(entries, crashStart) + + close(ctx.bootRequests) + for inst := range ctx.instances { + inst.Close() + } + return res, err +} + +func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) { + // Cut programs that were executed after crash. + for i, ent := range entries { + if ent.Start > crashStart { + entries = entries[:i] + break + } + } + // Extract last program on every proc. + procs := make(map[int]int) + for i, ent := range entries { + procs[ent.Proc] = i + } + var indices []int + for _, idx := range procs { + indices = append(indices, idx) + } + sort.Ints(indices) + var suspected []*prog.LogEntry + for i := len(indices) - 1; i >= 0; i-- { + suspected = append(suspected, entries[indices[i]]) + } + Logf(2, "reproducing crash '%v': suspecting %v programs", ctx.crashDesc, len(suspected)) + opts := csource.Options{ + Threaded: true, + Collide: true, + Repeat: true, + Procs: ctx.cfg.Procs, + Sandbox: ctx.cfg.Sandbox, + Repro: true, + } + // Execute the suspected programs. + // We first try to execute each program for 10 seconds, that should detect simple crashes + // (i.e. no races and no hangs). Then we execute each program for 5 minutes + // to catch races and hangs. Note that the max duration must be larger than + // hang/no output detection duration in vm.MonitorExecution, which is currently set to 3 mins. + var res *Result + var duration time.Duration + for _, dur := range []time.Duration{10 * time.Second, 5 * time.Minute} { + for _, ent := range suspected { + crashed, err := ctx.testProg(ent.P, dur, opts, true) + if err != nil { + return nil, err + } + if crashed { + res = &Result{ + Prog: ent.P, + Opts: opts, + } + duration = dur * 3 / 2 + break + } + } + if res != nil { + break + } + } + if res == nil { + Logf(0, "reproducing crash '%v': no program crashed", ctx.crashDesc) + return nil, nil + } + + Logf(2, "reproducing crash '%v': minimizing guilty program", ctx.crashDesc) + res.Prog, _ = prog.Minimize(res.Prog, -1, func(p1 *prog.Prog, callIndex int) bool { + crashed, err := ctx.testProg(p1, duration, res.Opts, false) + if err != nil { + Logf(1, "reproducing crash '%v': minimization failed with %v", ctx.crashDesc, err) + return false + } + return crashed + }) + + // Try to "minimize" threaded/collide/sandbox/etc to find simpler reproducer. + opts = res.Opts + opts.Collide = false + crashed, err := ctx.testProg(res.Prog, duration, opts, false) + if err != nil { + return res, err + } + if crashed { + res.Opts = opts + opts.Threaded = false + crashed, err := ctx.testProg(res.Prog, duration, opts, false) + if err != nil { + return res, err + } + if crashed { + res.Opts = opts + } + } + if res.Opts.Sandbox == "namespace" { + opts = res.Opts + opts.Sandbox = "none" + crashed, err := ctx.testProg(res.Prog, duration, opts, false) + if err != nil { + return res, err + } + if crashed { + res.Opts = opts + } + } + if res.Opts.Procs > 1 { + opts = res.Opts + opts.Procs = 1 + crashed, err := ctx.testProg(res.Prog, duration, opts, false) + if err != nil { + return res, err + } + if crashed { + res.Opts = opts + } + } + if res.Opts.Repeat { + opts = res.Opts + opts.Repeat = false + crashed, err := ctx.testProg(res.Prog, duration, opts, false) + if err != nil { + return res, err + } + if crashed { + res.Opts = opts + } + } + + src, err := csource.Write(res.Prog, res.Opts) + if err != nil { + return res, err + } + srcf, err := fileutil.WriteTempFile(src) + if err != nil { + return res, err + } + bin, err := csource.Build(srcf) + if err != nil { + return res, err + } + defer os.Remove(bin) + crashed, err = ctx.testBin(bin, duration, false) + if err != nil { + return res, err + } + res.CRepro = crashed + return res, nil +} + +func (ctx *context) testProg(p *prog.Prog, duration time.Duration, opts csource.Options, reboot bool) (crashed bool, err error) { + inst := <-ctx.instances + if inst == nil { + return false, fmt.Errorf("all VMs failed to boot") + } + defer func() { + ctx.returnInstance(inst, reboot, crashed) + }() + + pstr := p.Serialize() + progFile, err := fileutil.WriteTempFile(pstr) + if err != nil { + return false, err + } + defer os.Remove(progFile) + vmProgFile, err := inst.Copy(progFile) + if err != nil { + return false, fmt.Errorf("failed to copy to VM: %v", err) + } + + repeat := "1" + if opts.Repeat { + repeat = "0" + } + command := fmt.Sprintf("%v -executor %v -cover=0 -procs=%v -repeat=%v -sandbox %v -threaded=%v -collide=%v %v", + inst.execprogBin, inst.executorBin, opts.Procs, repeat, opts.Sandbox, opts.Threaded, opts.Collide, vmProgFile) + Logf(2, "reproducing crash '%v': testing program (duration=%v, %+v): %s", + ctx.crashDesc, duration, opts, p) + return ctx.testImpl(inst, command, duration) +} + +func (ctx *context) testBin(bin string, duration time.Duration, reboot bool) (crashed bool, err error) { + inst := <-ctx.instances + if inst == nil { + return false, fmt.Errorf("all VMs failed to boot") + } + defer func() { + ctx.returnInstance(inst, reboot, crashed) + }() + + bin, err = inst.Copy(bin) + if err != nil { + return false, fmt.Errorf("failed to copy to VM: %v", err) + } + Logf(2, "reproducing crash '%v': testing compiled C program", ctx.crashDesc) + return ctx.testImpl(inst, bin, duration) +} + +func (ctx *context) testImpl(inst vm.Instance, command string, duration time.Duration) (crashed bool, err error) { + outc, errc, err := inst.Run(duration, command) + if err != nil { + return false, fmt.Errorf("failed to run command in VM: %v", err) + } + desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, false, false) + _, _, _ = text, output, timedout + if !crashed { + Logf(2, "reproducing crash '%v': program did not crash", ctx.crashDesc) + return false, nil + } + Logf(2, "reproducing crash '%v': program crashed: %v", ctx.crashDesc, desc) + return true, nil +} + +func (ctx *context) returnInstance(inst *instance, reboot, crashed bool) { + if reboot || crashed { + // The test crashed, discard the VM and issue another boot request. + ctx.bootRequests <- inst.index + inst.Close() + } else { + // The test did not crash, reuse the same VM in future. + ctx.instances <- inst + } +} diff --git a/tools/syz-prog2c/prog2c.go b/tools/syz-prog2c/prog2c.go index fd7796fc1..b11e37230 100644 --- a/tools/syz-prog2c/prog2c.go +++ b/tools/syz-prog2c/prog2c.go @@ -16,15 +16,19 @@ import ( var ( flagThreaded = flag.Bool("threaded", false, "create threaded program") flagCollide = flag.Bool("collide", false, "create collide program") + flagRepeat = flag.Bool("repeat", false, "repeat program infinitely or not") + flagProcs = flag.Int("procs", 4, "number of parallel processes") + flagSandbox = flag.String("sandbox", "none", "sandbox to use (none, setuid, namespace)") + flagProg = flag.String("prog", "", "file with program to convert (required)") ) func main() { flag.Parse() - if len(flag.Args()) != 1 { - fmt.Fprintf(os.Stderr, "usage: prog2c [-threaded [-collide]] prog_file\n") + if *flagProg == "" { + flag.PrintDefaults() os.Exit(1) } - data, err := ioutil.ReadFile(flag.Args()[0]) + data, err := ioutil.ReadFile(*flagProg) if err != nil { fmt.Fprintf(os.Stderr, "failed to read prog file: %v\n", err) os.Exit(1) @@ -37,8 +41,16 @@ func main() { opts := csource.Options{ Threaded: *flagThreaded, Collide: *flagCollide, + Repeat: *flagRepeat, + Procs: *flagProcs, + Sandbox: *flagSandbox, + Repro: false, + } + src, err := csource.Write(p, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to generate C spurce: %v\n", err) + os.Exit(1) } - src := csource.Write(p, opts) if formatted, err := csource.Format(src); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) } else { diff --git a/tools/syz-repro/repro.go b/tools/syz-repro/repro.go index 27e6c1cd9..fb9ad0ef6 100644 --- a/tools/syz-repro/repro.go +++ b/tools/syz-repro/repro.go @@ -9,17 +9,12 @@ import ( "io/ioutil" "os" "os/signal" - "path/filepath" - "sort" "syscall" - "time" "github.com/google/syzkaller/config" "github.com/google/syzkaller/csource" - "github.com/google/syzkaller/fileutil" . "github.com/google/syzkaller/log" - "github.com/google/syzkaller/prog" - "github.com/google/syzkaller/report" + "github.com/google/syzkaller/repro" "github.com/google/syzkaller/vm" _ "github.com/google/syzkaller/vm/adb" _ "github.com/google/syzkaller/vm/gce" @@ -30,20 +25,10 @@ import ( var ( flagConfig = flag.String("config", "", "configuration file") flagCount = flag.Int("count", 0, "number of VMs to use (overrides config count param)") - - instances chan VM - bootRequests chan int - shutdown = make(chan struct{}) ) -type VM struct { - vm.Instance - index int - execprogBin string - executorBin string -} - func main() { + os.Args = append(append([]string{}, os.Args[0], "-v=10"), os.Args[1:]...) flag.Parse() cfg, _, _, err := config.Parse(*flagConfig) if err != nil { @@ -52,10 +37,9 @@ func main() { if *flagCount > 0 { cfg.Count = *flagCount } - if _, err := os.Stat(filepath.Join(cfg.Syzkaller, "bin/syz-execprog")); err != nil { - Fatalf("bin/syz-execprog is missing (run 'make execprog')") + if cfg.Count > 4 { + cfg.Count = 4 } - if len(flag.Args()) != 1 { Fatalf("usage: syz-repro -config=config.file execution.log") } @@ -63,220 +47,39 @@ func main() { if err != nil { Fatalf("failed to open log file: %v", err) } - entries := prog.ParseLog(data) - Logf(0, "parsed %v programs", len(entries)) - - crashDesc, _, crashStart, _ := report.Parse(data) - if crashDesc == "" { - crashStart = len(data) // assuming VM hanged - } - - instances = make(chan VM, cfg.Count) - bootRequests = make(chan int, cfg.Count) - for i := 0; i < cfg.Count; i++ { - bootRequests <- i - go func() { - for index := range bootRequests { - vmCfg, err := config.CreateVMConfig(cfg, index) - if err != nil { - Fatalf("failed to create VM config: %v", err) - } - inst, err := vm.Create(cfg.Type, vmCfg) - if err != nil { - Fatalf("failed to create VM: %v", err) - } - execprogBin, err := inst.Copy(filepath.Join(cfg.Syzkaller, "bin/syz-execprog")) - if err != nil { - Fatalf("failed to copy to VM: %v", err) - } - executorBin, err := inst.Copy(filepath.Join(cfg.Syzkaller, "bin/syz-executor")) - if err != nil { - Fatalf("failed to copy to VM: %v", err) - } - instances <- VM{inst, index, execprogBin, executorBin} - } - }() + vmIndexes := make([]int, cfg.Count) + for i := range vmIndexes { + vmIndexes[i] = i } go func() { c := make(chan os.Signal, 2) signal.Notify(c, syscall.SIGINT) <-c - close(shutdown) + close(vm.Shutdown) Logf(-1, "shutting down...") <-c Fatalf("terminating") }() - repro(cfg, entries, crashStart) - exit() -} - -func exit() { - for { - select { - case inst := <-instances: - inst.Close() - default: - os.Exit(0) - } - } -} - -func repro(cfg *config.Config, entries []*prog.LogEntry, crashStart int) { - // Cut programs that were executed after crash. - for i, ent := range entries { - if ent.Start > crashStart { - entries = entries[:i] - break - } - } - // Extract last program on every proc. - procs := make(map[int]int) - for i, ent := range entries { - procs[ent.Proc] = i - } - var indices []int - for _, idx := range procs { - indices = append(indices, idx) - } - sort.Ints(indices) - var suspected []*prog.LogEntry - for i := len(indices) - 1; i >= 0; i-- { - suspected = append(suspected, entries[indices[i]]) - } - // Execute the suspected programs. - Logf(0, "the suspected programs are:") - for _, ent := range suspected { - Logf(0, "on proc %v:\n%s\n", ent.Proc, ent.P.Serialize()) - } - var p *prog.Prog - multiplier := 1 - for ; p == nil && multiplier <= 100; multiplier *= 10 { - for _, ent := range suspected { - if testProg(cfg, ent.P, multiplier, true, true) { - p = ent.P - break - } - } + res, err := repro.Run(data, cfg, vmIndexes) + if err != nil { + Logf(0, "reproduction failed: %v", err) } - if p == nil { - Logf(0, "no program crashed") + if res == nil { return } - Logf(0, "minimizing program") - - p, _ = prog.Minimize(p, -1, func(p1 *prog.Prog, callIndex int) bool { - return testProg(cfg, p1, multiplier, true, true) - }) - opts := csource.Options{ - Threaded: true, - Collide: true, - } - if testProg(cfg, p, multiplier, true, false) { - opts.Collide = false - if testProg(cfg, p, multiplier, false, false) { - opts.Threaded = false + fmt.Printf("opts: %+v crepro: %v\n\n", res.Opts, res.CRepro) + fmt.Printf("%s\n", res.Prog.Serialize()) + if res.CRepro { + src, err := csource.Write(res.Prog, res.Opts) + if err != nil { + Fatalf("failed to generate C repro: %v", err) } + if formatted, err := csource.Format(src); err == nil { + src = formatted + } + fmt.Printf("%s\n", src) } - - src := csource.Write(p, opts) - src, _ = csource.Format(src) - Logf(0, "C source:\n%s\n", src) - srcf, err := fileutil.WriteTempFile(src) - if err != nil { - Fatalf("%v", err) - } - bin, err := csource.Build(srcf) - if err != nil { - Fatalf("%v", err) - } - defer os.Remove(bin) - testBin(cfg, bin) -} - -func returnInstance(inst VM, res bool) { - if res { - // The test crashed, discard the VM and issue another boot request. - bootRequests <- inst.index - inst.Close() - } else { - // The test did not crash, reuse the same VM in future. - instances <- inst - } -} - -func testProg(cfg *config.Config, p *prog.Prog, multiplier int, threaded, collide bool) (res bool) { - Logf(0, "booting VM") - var inst VM - select { - case inst = <-instances: - case <-shutdown: - exit() - } - defer func() { - returnInstance(inst, res) - }() - - pstr := p.Serialize() - progFile, err := fileutil.WriteTempFile(pstr) - if err != nil { - Fatalf("%v", err) - } - defer os.Remove(progFile) - bin, err := inst.Copy(progFile) - if err != nil { - Fatalf("failed to copy to VM: %v", err) - } - - repeat := 100 - timeoutSec := 10 * repeat / cfg.Procs - if threaded { - repeat *= 10 - timeoutSec *= 1 - } - repeat *= multiplier - timeoutSec *= multiplier - timeout := time.Duration(timeoutSec) * time.Second - command := fmt.Sprintf("%v -executor %v -cover=0 -procs=%v -repeat=%v -sandbox %v -threaded=%v -collide=%v %v", - inst.execprogBin, inst.executorBin, cfg.Procs, repeat, cfg.Sandbox, threaded, collide, bin) - Logf(0, "testing program (threaded=%v, collide=%v, repeat=%v, timeout=%v):\n%s\n", - threaded, collide, repeat, timeout, pstr) - return testImpl(inst, command, timeout) -} - -func testBin(cfg *config.Config, bin string) (res bool) { - Logf(0, "booting VM") - var inst VM - select { - case inst = <-instances: - case <-shutdown: - exit() - } - defer func() { - returnInstance(inst, res) - }() - - bin, err := inst.Copy(bin) - if err != nil { - Fatalf("failed to copy to VM: %v", err) - } - Logf(0, "testing compiled C program") - return testImpl(inst, bin, 10*time.Second) -} - -func testImpl(inst vm.Instance, command string, timeout time.Duration) (res bool) { - outc, errc, err := inst.Run(timeout, command) - if err != nil { - Fatalf("failed to run command in VM: %v", err) - } - desc, text, output, crashed, timedout := vm.MonitorExecution(outc, errc, false, false) - _, _ = text, output - if crashed || timedout { - Logf(0, "program crashed with: %v", desc) - return true - } - Logf(0, "program did not crash") - return false } |
