aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--csource/common.go344
-rw-r--r--csource/csource.go112
-rw-r--r--csource/csource_test.go57
-rw-r--r--executor/common.h365
-rw-r--r--executor/executor.cc311
-rw-r--r--repro/repro.go344
-rw-r--r--tools/syz-prog2c/prog2c.go20
-rw-r--r--tools/syz-repro/repro.go241
9 files changed, 1235 insertions, 561 deletions
diff --git a/Makefile b/Makefile
index 69d4fd8dc..3696a5f88 100644
--- a/Makefile
+++ b/Makefile
@@ -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
}