diff --git a/README.md b/README.md index 6cde773..049b88f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,17 @@ Configuations: - run_program/config.go: all configs toward running specs +Benchmarks for unshare: + +- 1ms: fork, unshare pid / user / cgroup +- 50ms: unshare ipc / mount +- 100ms: unshare pid & user & cgroup & mount & pivot root +- 400ms: unshare net +- 800ms: unshare all +- 880ms: unshare all & pivot root + +It seems unshare net or ipc takes time, maybe limits action by seccomp instead. + TODO: 1. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4 rusage) diff --git a/forkexec/bench_test.go b/forkexec/bench_test.go new file mode 100644 index 0000000..d57b6f2 --- /dev/null +++ b/forkexec/bench_test.go @@ -0,0 +1,188 @@ +package forkexec + +import ( + "io/ioutil" + "os" + "syscall" + "testing" + + "github.com/criyle/go-judger/types/mount" + "golang.org/x/sys/unix" +) + +// All testing data were from docker env on amd64 arch + +const ( + roBind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE | unix.MS_RDONLY +) + +var ( + defaultBind = []string{"/usr", "/lib", "/lib64", "/bin"} +) + +// BenchmarkSimpleFork is about 0.70ms/op +func BenchmarkSimpleFork(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + benchmarkRun(r, b) +} + +// BenchmarkUnsharePid is about 0.79ms/op +func BenchmarkUnsharePid(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWPID + benchmarkRun(r, b) +} + +// BenchmarkUnshareUser is about 0.84ms/op +func BenchmarkUnshareUser(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWUSER + benchmarkRun(r, b) +} + +// BenchmarkUnshareUts is about 0.78ms/op +func BenchmarkUnshareUts(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWUTS + benchmarkRun(r, b) +} + +// BenchmarkUnshareCgroup is about 0.85ms/op +func BenchmarkUnshareCgroup(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWCGROUP + benchmarkRun(r, b) +} + +// BenchmarkUnshareIpc is about 51ms/op +func BenchmarkUnshareIpc(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWIPC + benchmarkRun(r, b) +} + +// BenchmarkUnshareMount is about 51ms/op +func BenchmarkUnshareMount(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWNS + benchmarkRun(r, b) +} + +// BenchmarkUnshareNet is about 426ms/op +func BenchmarkUnshareNet(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWNET + benchmarkRun(r, b) +} + +// BenchmarkFastUnshareMountPivot is about 104ms/op +func BenchmarkFastUnshareMountPivot(b *testing.B) { + root, err := ioutil.TempDir("", "ns") + if err != nil { + b.Errorf("failed to create temp dir") + } + defer os.RemoveAll(root) + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS | unix.CLONE_NEWCGROUP + r.PivotRoot = root + r.NoNewPrivs = true + r.DropCaps = true + r.Mounts = getMounts(defaultBind) + benchmarkRun(r, b) +} + +// BenchmarkUnshareAll is about 800ms/op +func BenchmarkUnshareAll(b *testing.B) { + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = UnshareFlags + r.NoNewPrivs = true + r.DropCaps = true + benchmarkRun(r, b) +} + +// BenchmarkUnshareMountPivot is about 880ms/op +func BenchmarkUnshareMountPivot(b *testing.B) { + root, err := ioutil.TempDir("", "ns") + if err != nil { + b.Errorf("failed to create temp dir") + } + defer os.RemoveAll(root) + r, f := getRunner(b) + defer f.Close() + r.UnshareFlags = UnshareFlags + r.PivotRoot = root + r.NoNewPrivs = true + r.DropCaps = true + r.Mounts = getMounts(defaultBind) + benchmarkRun(r, b) +} + +func getRunner(b *testing.B) (*Runner, *os.File) { + f := openNull(b) + return &Runner{ + Args: []string{"/bin/echo"}, + Env: []string{"PATH=/bin"}, + Files: []uintptr{f.Fd(), f.Fd(), f.Fd()}, + WorkDir: "/bin", + }, f +} + +func benchmarkRun(r *Runner, b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + pid, err := r.Start() + if err != nil { + b.Fail() + } + wait4(pid, b) + } +} + +func getMounts(dirs []string) []*mount.Mount { + ret := make([]*mount.Mount, 0, len(dirs)) + for _, d := range dirs { + if _, err := os.Stat(d); !os.IsNotExist(err) { + ret = append(ret, getMount(d)) + } + } + return ret +} + +func getMount(dir string) *mount.Mount { + return &mount.Mount{ + Source: dir, + Target: dir[1:], + Flags: roBind, + } +} + +func openNull(b *testing.B) *os.File { + f, err := os.OpenFile("/dev/null", os.O_RDWR, 0666) + if err != nil { + b.Errorf("Failed to open %v", err) + } + return f +} + +func wait4(pid int, b *testing.B) { + var wstat syscall.WaitStatus + for { + syscall.Wait4(pid, &wstat, 0, nil) + if wstat.Exited() { + if s := wstat.ExitStatus(); s != 0 { + b.Errorf("Exited: %d", s) + } + break + } + } +} diff --git a/go.mod b/go.mod index 173c4bb..e4777e5 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.12 require ( github.com/seccomp/libseccomp-golang v0.9.1 - golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 ) diff --git a/go.sum b/go.sum index f4f8ecc..9d5689a 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46r github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/seccomp/seccomp_test.go b/seccomp/seccomp_test.go index ea7e4df..3433b52 100644 --- a/seccomp/seccomp_test.go +++ b/seccomp/seccomp_test.go @@ -6,6 +6,20 @@ import ( libseccomp "github.com/seccomp/libseccomp-golang" ) +var ( + defaultSyscallAllows = []string{ + "read", "write", "readv", "writev", "close", "fstat", "lseek", "dup", "dup2", "dup3", "ioctl", "fcntl", "fadvise64", + "mmap", "mprotect", "munmap", "brk", "mremap", "msync", "mincore", "madvise", + "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "rt_sigpending", "sigaltstack", + "getcwd", "exit", "exit_group", "arch_prctl", + "gettimeofday", "getrlimit", "getrusage", "times", "time", "clock_gettime", "restart_syscall", + } + + defaultSyscallTraces = []string{ + "execve", "open", "openat", "unlink", "unlinkat", "readlink", "readlinkat", "lstat", "stat", "access", "faccessat", + } +) + func TestBuildFilter(t *testing.T) { defaultAction := libseccomp.ActKill filter, err := buildFilterMock(defaultAction) @@ -64,6 +78,15 @@ func TestAddActionFail(t *testing.T) { } } +// BenchmarkBuildDefaultFilter is about 0.2ms/op +func BenchmarkBuildDefaultFilter(b *testing.B) { + for i := 0; i < b.N; i++ { + filter, _ := BuildFilter(libseccomp.ActKill, libseccomp.ActTrace, defaultSyscallAllows, defaultSyscallTraces) + _, _ = FilterToBPF(filter) + filter.Release() + } +} + func buildFilterMock(d libseccomp.ScmpAction) (*libseccomp.ScmpFilter, error) { defaultTrace := libseccomp.ActTrace allow := []string{"fork"}