diff --git a/runner/main.go b/runner/main.go index e7a268b..6bc2992 100644 --- a/runner/main.go +++ b/runner/main.go @@ -108,8 +108,12 @@ func main() { if err != nil { log.Fatal("Failed to create filter: ", err) } + r := NewProgramRunner() + r.Args = os.Args[1:] + r.Filter = filter + // run in restricted mode - pid, err := ForkAndLoadSeccomp(os.Args[1:], filter) + pid, err := r.StartChild() if err != nil { log.Fatal("Failed to fork: ", err) } @@ -124,7 +128,9 @@ func main() { defer timer.Stop() // Set trace seccomp - unix.PtraceSetOptions(pid, unix.PTRACE_O_TRACESECCOMP) + unix.PtraceSetOptions(pid, unix.PTRACE_O_TRACESECCOMP|unix.PTRACE_O_EXITKILL| + unix.PTRACE_O_TRACEFORK|unix.PTRACE_O_TRACECLONE|unix.PTRACE_O_TRACEEXEC| + unix.PTRACE_O_TRACEVFORK) log.Println("Strat trace pid: ", pid) // trace unixs @@ -144,16 +150,32 @@ func main() { log.Println("Signal", wstatus.Signal()) } if wstatus.Stopped() { - log.Println("Stopped") - if wstatus.TrapCause() == unix.PTRACE_EVENT_SECCOMP { + switch cause := wstatus.TrapCause(); cause { + case unix.PTRACE_EVENT_SECCOMP: log.Println("Seccomp Traced") msg, err := unix.PtraceGetEventMsg(pid) if err != nil { log.Fatalln(err) } log.Println("Ptrace Event: ", msg) - } else { - log.Println("Stop Cause: ", wstatus.TrapCause()) + + case unix.PTRACE_EVENT_CLONE: + log.Println("Ptrace stop clone") + + case unix.PTRACE_EVENT_VFORK: + log.Println("Ptrace stop vfork") + + case unix.PTRACE_EVENT_FORK: + log.Println("Ptrace stop fork") + + case unix.PTRACE_EVENT_EXEC: + log.Println("Ptrace stop exec") + + case -1: + log.Println("Ptrace stop signal: ", wstatus.StopSignal()) + + default: + log.Println("Ptrace trap cause: ", cause, wstatus) } } diff --git a/runner/run_child.go b/runner/run_child.go index 8351763..49571fc 100644 --- a/runner/run_child.go +++ b/runner/run_child.go @@ -1,10 +1,10 @@ package main import ( + "os" "syscall" "unsafe" // required for go:linkname. - libseccomp "github.com/seccomp/libseccomp-golang" "golang.org/x/sys/unix" ) @@ -17,35 +17,118 @@ func afterFork() //go:linkname afterForkInChild syscall.runtime_AfterForkInChild func afterForkInChild() -// ForkAndLoadSeccomp will fork, load seccomp and execv and being traced by ptrace +type rlimit struct { + resource int + rlim syscall.Rlimit +} + +// StartChild will fork, load seccomp and execv and being traced by ptrace // Reference to src/syscall/exec_linux.go // The runtime OS thread must be locked before calling this function //go:noinline //go:norace -func ForkAndLoadSeccomp(args []string, filter *libseccomp.ScmpFilter) (int, error) { +func (r *ProgramRunner) StartChild() (int, error) { var ( err1 syscall.Errno + bpf *syscall.SockFprog ) + // verify + r.verify() + // make exec args - argv0, err := syscall.BytePtrFromString(args[0]) + argv0, err := syscall.BytePtrFromString(r.Args[0]) if err != nil { return 0, err } - argv, err := syscall.SlicePtrFromStrings(args) + argv, err := syscall.SlicePtrFromStrings(r.Args) if err != nil { return 0, err } - envv, err := syscall.SlicePtrFromStrings([]string{""}) + // make env + envv, err := syscall.SlicePtrFromStrings(r.Env) if err != nil { return 0, err } // make bpf using libseccomp - bpf, err := FilterToBPF(filter) - if err != nil { - return 0, err + if r.Filter != nil { + bpf, err = FilterToBPF(r.Filter) + if err != nil { + return 0, err + } } + rlimits := []rlimit{ + // CPU limit + { + resource: syscall.RLIMIT_CPU, + rlim: syscall.Rlimit{ + Cur: r.TimeLimit, + Max: r.RealTimeLimit, + }, + }, + // File limit + { + resource: syscall.RLIMIT_FSIZE, + rlim: syscall.Rlimit{ + Cur: r.OutputLimit << 20, + Max: r.OutputLimit << 20, + }, + }, + // Stack limit + { + resource: syscall.RLIMIT_STACK, + rlim: syscall.Rlimit{ + Cur: r.StackLimit << 20, + Max: r.StackLimit << 20, + }, + }, + } + + // work dir + var dir *byte + if r.WorkPath != "" { + dir, err = syscall.BytePtrFromString(r.WorkPath) + if err != nil { + return 0, err + } + } + + // stdin, stdout, stderr + files := make([]*os.File, 3) + if r.InputFileName != "" { + files[0], err = os.OpenFile(r.InputFileName, os.O_RDONLY, 0755) + if err != nil { + return 0, err + } + defer files[0].Close() + } + if r.OutputFileName != "" { + files[1], err = os.OpenFile(r.OutputFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755) + if err != nil { + return 0, err + } + defer files[1].Close() + } + if r.ErrorFileName != "" { + files[2], err = os.OpenFile(r.ErrorFileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755) + if err != nil { + return 0, err + } + defer files[2].Close() + } + fds := make([]uintptr, 3) + for i, f := range files { + if f != nil { + fds[i] = f.Fd() + } + } + + // Acquire the fork lock so that no other threads + // create new fds that are not yet close-on-exec + // before we fork. + syscall.ForkLock.Lock() + // About to call fork. // No more allocation or calls of non-assembly functions. beforeFork() @@ -54,6 +137,8 @@ func ForkAndLoadSeccomp(args []string, filter *libseccomp.ScmpFilter) (int, erro if err1 != 0 || pid != 0 { // restore all signals afterFork() + syscall.ForkLock.Unlock() + if err1 != 0 { return int(pid), syscall.Errno(err1) } @@ -64,24 +149,53 @@ func ForkAndLoadSeccomp(args []string, filter *libseccomp.ScmpFilter) (int, erro afterForkInChild() // Notice: cannot call any functions beyond this point + // Set limit + for _, rlim := range rlimits { + _, _, err1 = syscall.RawSyscall(syscall.SYS_SETRLIMIT, uintptr(rlim.resource), uintptr(unsafe.Pointer(&rlim.rlim)), 0) + if err1 != 0 { + goto childerror + } + } + + // Chdir + if dir != nil { + _, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(dir)), 0, 0) + if err1 != 0 { + goto childerror + } + } + + // stdin, stdout, stderr + // the other file already marked as close on exec + for i, fd := range fds { + if fd != 0 { + _, _, err1 = syscall.RawSyscall(syscall.SYS_DUP, fd, uintptr(i), 0) + if err1 != 0 { + goto childerror + } + } + } + // Enable ptrace _, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0) if err1 != 0 { goto childerror } - // Check if support - // SECCOMP_SET_MODE_STRICT = 0, args = 1 for invalid operation - _, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 0, 1, 0) - if err1 != syscall.EINVAL { - goto childerror - } + if r.Filter != nil { + // Check if support + // SECCOMP_SET_MODE_STRICT = 0, args = 1 for invalid operation + _, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 0, 1, 0) + if err1 != syscall.EINVAL { + goto childerror + } - // Load the filter manually - // No new priv - _, _, err1 = syscall.Syscall6(syscall.SYS_PRCTL, unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0, 0) - if err1 != 0 { - goto childerror + // Load the filter manually + // No new priv + _, _, err1 = syscall.Syscall6(syscall.SYS_PRCTL, unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0, 0) + if err1 != 0 { + goto childerror + } } // Get pid of child @@ -96,13 +210,15 @@ func ForkAndLoadSeccomp(args []string, filter *libseccomp.ScmpFilter) (int, erro goto childerror } - // set seccomp - //_, _, err1 = syscall.Syscall6(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&bpf[0])), 0, 0, 0) - // SECCOMP_SET_MODE_FILTER = 1 - // SECCOMP_FILTER_FLAG_TSYNC = 1 - _, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 1, 1, uintptr(unsafe.Pointer(bpf))) - if err1 != 0 { - goto childerror + if r.Filter != nil { + // set seccomp + //_, _, err1 = syscall.Syscall6(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&bpf[0])), 0, 0, 0) + // SECCOMP_SET_MODE_FILTER = 1 + // SECCOMP_FILTER_FLAG_TSYNC = 1 + _, _, err1 = syscall.Syscall(unix.SYS_SECCOMP, 1, 1, uintptr(unsafe.Pointer(bpf))) + if err1 != 0 { + goto childerror + } } // time to exec diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..a17d277 --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,52 @@ +package main + +import ( + libseccomp "github.com/seccomp/libseccomp-golang" +) + +// ProgramRunner is the RunProgramConfig +// including the exec path, argv +// and resource limits +type ProgramRunner struct { + // Resource limit set by set rlimit + TimeLimit uint64 // second + RealTimeLimit uint64 // second + MemoryLimit uint64 // mb + OutputLimit uint64 // mb + StackLimit uint64 // mb + + // stdin, stdout, stderr file name. nil for default + InputFileName string + OutputFileName string + ErrorFileName string + + // work path + WorkPath string + + // argv and env for the child process + Args []string + Env []string + + // libseccomp Filter applied to child + Filter *libseccomp.ScmpFilter +} + +// NewProgramRunner creates program config with default setting +func NewProgramRunner() ProgramRunner { + return ProgramRunner{ + TimeLimit: 1, + RealTimeLimit: 0, + MemoryLimit: 256, + OutputLimit: 64, + StackLimit: 1024, + } +} + +func (r *ProgramRunner) verify() { + if r.RealTimeLimit < r.TimeLimit { + r.RealTimeLimit = r.TimeLimit + 2 + } + if r.StackLimit > r.MemoryLimit { + r.StackLimit = r.MemoryLimit + } +}