diff --git a/cmd/run_program/main.go b/cmd/run_program/main.go index ff52cf4..c54f19a 100644 --- a/cmd/run_program/main.go +++ b/cmd/run_program/main.go @@ -3,13 +3,25 @@ package main import ( "flag" "fmt" + "io/ioutil" "os" + "github.com/criyle/go-judger/rlimit" "github.com/criyle/go-judger/runconfig" "github.com/criyle/go-judger/runprogram" + "github.com/criyle/go-judger/rununshared" "github.com/criyle/go-judger/tracer" ) +const ( + pathEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +) + +// Runner can be ptraced runner or namespaced runner +type Runner interface { + Start() (tracer.TraceResult, error) +} + func printUsage() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] \n", os.Args[0]) flag.PrintDefaults() @@ -19,18 +31,19 @@ func printUsage() { func main() { var ( addReadable, addWritable, addRawReadable, addRawWritable arrayFlags - allowProc, unsafe, showDetails bool + allowProc, unsafe, showDetails, namespace bool pType, result string - timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint + timeLimit, realTimeLimit, memoryLimit, outputLimit, stackLimit uint64 inputFileName, outputFileName, errorFileName, workPath string + runner Runner ) flag.Usage = printUsage - flag.UintVar(&timeLimit, "tl", 1, "Set time limit (in second)") - flag.UintVar(&realTimeLimit, "rtl", 0, "Set real time limit (in second)") - flag.UintVar(&memoryLimit, "ml", 256, "Set memory limit (in mb)") - flag.UintVar(&outputLimit, "ol", 64, "Set output limit (in mb)") - flag.UintVar(&stackLimit, "sl", 1024, "Set stack limit (in mb)") + flag.Uint64Var(&timeLimit, "tl", 1, "Set time limit (in second)") + flag.Uint64Var(&realTimeLimit, "rtl", 0, "Set real time limit (in second)") + flag.Uint64Var(&memoryLimit, "ml", 256, "Set memory limit (in mb)") + flag.Uint64Var(&outputLimit, "ol", 64, "Set output limit (in mb)") + flag.Uint64Var(&stackLimit, "sl", 1024, "Set stack limit (in mb)") flag.StringVar(&inputFileName, "in", "", "Set input file name") flag.StringVar(&outputFileName, "out", "", "Set output file name") flag.StringVar(&errorFileName, "err", "", "Set error file name") @@ -44,6 +57,7 @@ func main() { flag.BoolVar(&allowProc, "allow-proc", false, "Allow fork, exec... etc.") flag.Var(&addRawReadable, "add-readable-raw", "Add a readable file (don't transform to its real path)") flag.Var(&addRawWritable, "add-writable-raw", "Add a writable file (don't transform to its real path)") + flag.BoolVar(&namespace, "ns", false, "Use namespace to restrict file accesses") flag.Parse() args := flag.Args() @@ -89,27 +103,58 @@ func main() { } } - runner := &runprogram.RunProgram{ - Args: h.Args, - Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, - WorkDir: workPath, - RLimits: runprogram.RLimits{ - CPU: timeLimit, - CPUHard: realTimeLimit, - FileSize: outputLimit, - Stack: stackLimit, - }, - TraceLimit: runprogram.TraceLimit{ - TimeLimit: uint64(timeLimit * 1e3), - RealTimeLimit: uint64(realTimeLimit * 1e3), - MemoryLimit: uint64(memoryLimit << 10), - }, - Files: fds, - SyscallAllowed: h.SyscallAllow, - SyscallTraced: h.SyscallTrace, - ShowDetails: showDetails, - Unsafe: unsafe, - Handler: h, + rlims := rlimit.RLimits{ + CPU: timeLimit, + CPUHard: realTimeLimit, + FileSize: outputLimit, + Stack: stackLimit, + } + + if namespace { + h.SyscallAllow = append(h.SyscallAllow, h.SyscallTrace...) + root, err := ioutil.TempDir("", "ns") + if err != nil { + panic("cannot make temp root for new namespace") + } + runner = &rununshared.RunUnshared{ + Args: h.Args, + Env: []string{pathEnv}, + WorkDir: "/w", + Files: fds, + RLimits: rlims, + ResLimits: tracer.ResLimit{ + TimeLimit: timeLimit * 1e3, + RealTimeLimit: realTimeLimit * 1e3, + MemoryLimit: memoryLimit << 10, + }, + SyscallAllowed: h.SyscallAllow, + Root: root, + Mounts: rununshared.GetDefaultMounts(root, []rununshared.AddBind{ + { + Source: workPath, + Target: "w", + }, + }), + ShowDetails: true, + } + } else { + runner = &runprogram.RunProgram{ + Args: h.Args, + Env: []string{pathEnv}, + WorkDir: workPath, + RLimits: rlims, + TraceLimit: runprogram.TraceLimit{ + TimeLimit: timeLimit * 1e3, + RealTimeLimit: realTimeLimit * 1e3, + MemoryLimit: memoryLimit << 10, + }, + Files: fds, + SyscallAllowed: h.SyscallAllow, + SyscallTraced: h.SyscallTrace, + ShowDetails: showDetails, + Unsafe: unsafe, + Handler: h, + } } var f *os.File @@ -129,6 +174,7 @@ func main() { // Run tracer rt, err := runner.Start() println("used process_vm_readv: ", tracer.UseVMReadv) + println("results:", rt, err) if err != nil { c, ok := err.(tracer.TraceCode) diff --git a/forkexec/consts.go b/forkexec/consts.go index 513512f..5c70e0e 100644 --- a/forkexec/consts.go +++ b/forkexec/consts.go @@ -13,16 +13,33 @@ const ( // Unshare flags UnshareFlags = unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS | unix.CLONE_NEWCGROUP + + // Read-only bind mount need to be remounted + bindRo = unix.MS_BIND | unix.MS_RDONLY ) // used by unshare remount / to private var ( none = [...]byte{'n', 'o', 'n', 'e', 0} slash = [...]byte{'/', 0} + empty = [...]byte{0} + tmpfs = [...]byte{'t', 'm', 'p', 'f', 's', 0} // tmp dir made by pivot_root OldRoot = "old_root" // go does not allow constant uintptr to be negative... _AT_FDCWD = unix.AT_FDCWD + + // Drop all capabilities + dropCapHeader = unix.CapUserHeader{ + Version: unix.LINUX_CAPABILITY_VERSION_3, + Pid: 0, + } + + dropCapData = unix.CapUserData{ + Effective: 0, + Permitted: 0, + Inheritable: 0, + } ) diff --git a/forkexec/fork.go b/forkexec/fork.go index 498827d..6155247 100644 --- a/forkexec/fork.go +++ b/forkexec/fork.go @@ -25,6 +25,7 @@ func afterForkInChild() func (r *Runner) Start() (int, error) { var ( err1 syscall.Errno + r1 uintptr ) argv0, argv, envv, err := prepareExec(r.Args, r.Env) @@ -50,6 +51,9 @@ func (r *Runner) Start() (int, error) { return 0, nil } + // prepare set uid / gid map files + files := prepareIDMap(r.UnshareFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER) + // similar to exec_linux, avoid side effect by shuffling around fd, nextfd := prepareFds(r.Files) @@ -137,8 +141,41 @@ func (r *Runner) Start() (int, error) { } } - // chdir to new root before performing mounts + // If usernamespace is unshared, uid map and gid map is required to create folders + // and files + // Notice: This is not working right now since unshare user namespace drops all + // capabilities, thus this operation will fail to do this + // Thus, we need parent to setup uid_map / gid_map for us + // At the same time, socket pair / pipe sychronization is required as well + for _, f := range files { + r1, _, err1 = syscall.RawSyscall6(syscall.SYS_OPENAT, uintptr(_AT_FDCWD), + uintptr(unsafe.Pointer(f.fileName)), uintptr(fileOption), uintptr(filePerm), 0, 0) + if err1 == syscall.ENOENT { // Kernel > 3.19 for setgroups + continue + } else if err1 != 0 { + goto childerror + } + _, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, r1, uintptr(unsafe.Pointer(&f.fileContent[0])), + uintptr(len(f.fileContent))) + if err1 != 0 { + goto childerror + } + _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, r1, 0, 0) + if err1 != 0 { + goto childerror + } + } + + // mount tmpfs & chdir to new root before performing mounts if pivotRoot != nil { + // mount("tmpfs", root, "tmpfs", 0, "") + _, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&tmpfs[0])), + uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(&tmpfs[0])), 0, + uintptr(unsafe.Pointer(&empty[0])), 0) + if err1 != 0 { + goto childerror + } + _, _, err1 = syscall.RawSyscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(pivotRoot)), 0, 0) if err1 != 0 { goto childerror @@ -161,6 +198,15 @@ func (r *Runner) Start() (int, error) { if err1 != 0 { goto childerror } + // bind mount is not respect ro flag so that read-only bind mount needs remount + if m.Flags&bindRo == bindRo { + _, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&empty[0])), + uintptr(unsafe.Pointer(m.Target)), uintptr(unsafe.Pointer(m.FsType)), + uintptr(m.Flags|syscall.MS_REMOUNT), uintptr(unsafe.Pointer(m.Data)), 0) + if err1 != 0 { + goto childerror + } + } } // pivit_root @@ -215,6 +261,14 @@ func (r *Runner) Start() (int, error) { } } + // Drop all capabilities + if r.DropCaps { + _, _, err1 = syscall.RawSyscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&dropCapHeader)), uintptr(unsafe.Pointer(&dropCapData)), 0) + if err1 != 0 { + goto childerror + } + } + // Enable Ptrace if r.Ptrace { _, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0) diff --git a/forkexec/fork_util.go b/forkexec/fork_util.go index 7e7bbfa..94ef237 100644 --- a/forkexec/fork_util.go +++ b/forkexec/fork_util.go @@ -1,7 +1,6 @@ package forkexec import ( - "path" "syscall" "github.com/criyle/go-judger/mount" @@ -54,12 +53,11 @@ func preparePivotRoot(r string) (*byte, *byte, error) { if r == "" { return nil, nil, nil } - or := path.Join(r, OldRoot) root, err := syscall.BytePtrFromString(r) if err != nil { return nil, nil, err } - oldRoot, err := syscall.BytePtrFromString(or) + oldRoot, err := syscall.BytePtrFromString(OldRoot) if err != nil { return nil, nil, err } @@ -76,7 +74,7 @@ func prepareMounts(ms []*mount.Mount) ([]*mount.SyscallParams, [][]*byte, error) pathsToCreate := make([][]*byte, 0, len(ms)) for _, m := range ms { prefix := pathPrefix(m.Target) - paths, err := syscall.SlicePtrFromStrings(prefix) + paths, err := arrayPtrFromStrings(prefix) if err != nil { return nil, nil, err } @@ -96,3 +94,16 @@ func pathPrefix(path string) []string { ret = append(ret, path) return ret } + +// arrayPtrFromStrings convers srings to c style strings +func arrayPtrFromStrings(strs []string) ([]*byte, error) { + bytes := make([]*byte, 0, len(strs)) + for _, s := range strs { + b, err := syscall.BytePtrFromString(s) + if err != nil { + return nil, err + } + bytes = append(bytes, b) + } + return bytes, nil +} diff --git a/forkexec/runner.go b/forkexec/runner.go index 47813e2..a76f103 100644 --- a/forkexec/runner.go +++ b/forkexec/runner.go @@ -55,6 +55,7 @@ type Runner struct { // mounts defines the mount syscalls after unshare mount namespace // need CAP_ADMIN inside the namespace (e.g. unshare user namespace) // if pivot root is provided, relative target will based on PivotRoot directory + // and pivot root will mount as tmpfs before any mount Mounts []*mount.Mount // pivot_root defines the new root after unshare mount namespace diff --git a/forkexec/userns.go b/forkexec/userns.go new file mode 100644 index 0000000..a3c485f --- /dev/null +++ b/forkexec/userns.go @@ -0,0 +1,42 @@ +package forkexec + +import ( + "strconv" + + "golang.org/x/sys/unix" +) + +const ( + fileOption = unix.O_RDWR + filePerm = 0755 +) + +var ( + uidMap = [...]byte{'/', 'p', 'r', 'o', 'c', '/', 's', 'e', 'l', 'f', '/', 'u', 'i', 'd', '_', 'm', 'a', 'p', 0} + gidMap = [...]byte{'/', 'p', 'r', 'o', 'c', '/', 's', 'e', 'l', 'f', '/', 'g', 'i', 'd', '_', 'm', 'a', 'p', 0} + setGroups = [...]byte{'/', 'p', 'r', 'o', 'c', '/', 's', 'e', 'l', 'f', '/', 's', 'e', 't', 'g', 'r', 'o', 'u', 'p', 's', 0} +) + +type fileWriteSyscall struct { + fileName *byte + fileContent []byte +} + +func prepareIDMap(userNs bool) []fileWriteSyscall { + ret := make([]fileWriteSyscall, 0, 3) + if userNs { + ret = append(ret, fileWriteSyscall{ + fileName: &uidMap[0], + fileContent: []byte("0 " + strconv.Itoa(unix.Geteuid()) + " 1"), + }) + ret = append(ret, fileWriteSyscall{ + fileName: &gidMap[0], + fileContent: []byte("0 " + strconv.Itoa(unix.Getegid()) + " 1"), + }) + ret = append(ret, fileWriteSyscall{ + fileName: &setGroups[0], + fileContent: []byte("deny"), + }) + } + return ret +} diff --git a/mount/mount.go b/mount/mount.go index 92574ac..a85c6e0 100644 --- a/mount/mount.go +++ b/mount/mount.go @@ -49,11 +49,20 @@ func (m *Mount) ToSyscall() (*SyscallParams, error) { // Mount calls mount syscall func (m *Mount) Mount() error { - err := os.MkdirAll(m.Target, 0755) - if err != nil { + if err := os.MkdirAll(m.Target, 0755); err != nil { return err } - return syscall.Mount(m.Source, m.Target, m.FsType, m.Flags, m.Data) + if err := syscall.Mount(m.Source, m.Target, m.FsType, m.Flags, m.Data); err != nil { + return err + } + // Read-only bind mount need to be remounted + const bindRo = syscall.MS_BIND | syscall.MS_RDONLY + if m.Flags&bindRo == bindRo { + if err := syscall.Mount("", m.Target, m.FsType, m.Flags|syscall.MS_REMOUNT, m.Data); err != nil { + return err + } + } + return nil } // ToSyscalls converts arrays of Mounts into SyscallParams diff --git a/runprogram/rlimit.go b/rlimit/rlimit.go similarity index 59% rename from runprogram/rlimit.go rename to rlimit/rlimit.go index 1c71f37..4c7ed62 100644 --- a/runprogram/rlimit.go +++ b/rlimit/rlimit.go @@ -1,4 +1,4 @@ -package runprogram +package rlimit import ( "syscall" @@ -8,21 +8,21 @@ import ( // RLimits defines the rlimit applied by setrlimit syscall to traced process type RLimits struct { - CPU uint // in s - CPUHard uint // in s - Data uint // in kb - FileSize uint // in kb - Stack uint // in kb - AddressSpace uint // in kb + CPU uint64 // in s + CPUHard uint64 // in s + Data uint64 // in kb + FileSize uint64 // in kb + Stack uint64 // in kb + AddressSpace uint64 // in kb } func getRlimit(cur, max uint64) syscall.Rlimit { return syscall.Rlimit{Cur: uint64(cur), Max: uint64(max)} } -// prepareRLimit creates rlimit structures for tracee +// PrepareRLimit creates rlimit structures for tracee // TimeLimit in s, SizeLimit in byte -func (r *RLimits) prepareRLimit() []forkexec.RLimit { +func (r *RLimits) PrepareRLimit() []forkexec.RLimit { var ret []forkexec.RLimit if r.CPU > 0 { cpuHard := r.CPUHard @@ -32,31 +32,31 @@ func (r *RLimits) prepareRLimit() []forkexec.RLimit { ret = append(ret, forkexec.RLimit{ Res: syscall.RLIMIT_CPU, - Rlim: getRlimit(uint64(r.CPU), uint64(cpuHard)), + Rlim: getRlimit(r.CPU, cpuHard), }) } if r.Data > 0 { ret = append(ret, forkexec.RLimit{ Res: syscall.RLIMIT_DATA, - Rlim: getRlimit(uint64(r.Data)<<10, uint64(r.Data)<<10), + Rlim: getRlimit(r.Data<<10, r.Data<<10), }) } if r.FileSize > 0 { ret = append(ret, forkexec.RLimit{ Res: syscall.RLIMIT_FSIZE, - Rlim: getRlimit(uint64(r.FileSize)<<10, uint64(r.FileSize)<<10), + Rlim: getRlimit(r.FileSize<<10, r.FileSize<<10), }) } if r.Stack > 0 { ret = append(ret, forkexec.RLimit{ Res: syscall.RLIMIT_STACK, - Rlim: getRlimit(uint64(r.Stack)<<10, uint64(r.Stack)<<10), + Rlim: getRlimit(r.Stack<<10, r.Stack<<10), }) } if r.AddressSpace > 0 { ret = append(ret, forkexec.RLimit{ Res: syscall.RLIMIT_AS, - Rlim: getRlimit(uint64(r.AddressSpace)<<10, uint64(r.AddressSpace)<<10), + Rlim: getRlimit(r.AddressSpace<<10, r.AddressSpace<<10), }) } return ret diff --git a/runprogram/runprogram.go b/runprogram/runprogram.go index 9c18620..88aaf87 100644 --- a/runprogram/runprogram.go +++ b/runprogram/runprogram.go @@ -3,6 +3,7 @@ package runprogram import ( "syscall" + "github.com/criyle/go-judger/rlimit" "github.com/criyle/go-judger/tracer" ) @@ -18,7 +19,7 @@ type RunProgram struct { Files []uintptr // Resource limit set by set rlimit - RLimits RLimits + RLimits rlimit.RLimits // Res limit enforced by tracer TraceLimit TraceLimit diff --git a/runprogram/runprogram_run.go b/runprogram/runprogram_run.go index 04389e6..651346c 100644 --- a/runprogram/runprogram_run.go +++ b/runprogram/runprogram_run.go @@ -27,7 +27,7 @@ func (r *RunProgram) Start() (rt tracer.TraceResult, err error) { ch := &forkexec.Runner{ Args: r.Args, Env: r.Env, - RLimits: r.RLimits.prepareRLimit(), + RLimits: r.RLimits.PrepareRLimit(), Files: r.Files, WorkDir: r.WorkDir, Seccomp: bpf, diff --git a/rununshared/default.go b/rununshared/default.go new file mode 100644 index 0000000..4b2fd48 --- /dev/null +++ b/rununshared/default.go @@ -0,0 +1,70 @@ +package rununshared + +import ( + "github.com/criyle/go-judger/mount" + "golang.org/x/sys/unix" +) + +// AddBind is the additional bind mounts besides the default one +type AddBind struct { + Source, Target string + ReadOnly bool +} + +const ( + bind2 = unix.MS_BIND | unix.MS_NOSUID | unix.MS_NOATIME | unix.MS_NODEV | unix.MS_NODIRATIME + bind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE + roBind = bind | unix.MS_RDONLY + noExecRoBind = roBind | unix.MS_NOEXEC + remountRo = unix.MS_REMOUNT | unix.MS_RDONLY +) + +// default parameters. I was tend to reuse the configs but it is hard since there are some +// cross device symblics +var ( + DefaultMounts = []*mount.Mount{ + { + Source: "/usr/lib/locale", + Target: "usr/lib/locale", + Flags: roBind, + }, + { + Source: "/usr", + Target: "usr", + Flags: roBind, + }, + { + Source: "/lib", + Target: "lib", + Flags: roBind, + }, + { + Source: "/lib64", + Target: "lib64", + Flags: roBind, + }, + { + Source: "/bin", + Target: "bin", + Flags: roBind, + }, + } +) + +// GetDefaultMounts returns default mount parameters for given root +func GetDefaultMounts(root string, add []AddBind) []*mount.Mount { + mounts := make([]*mount.Mount, 0, len(DefaultMounts)+len(add)) + mounts = append(mounts, DefaultMounts...) + for _, m := range add { + flags := bind + if m.ReadOnly { + flags = roBind + } + mounts = append(mounts, &mount.Mount{ + Source: m.Source, + Target: m.Target, + Flags: uintptr(flags), + }) + } + return mounts +} diff --git a/rununshared/run.go b/rununshared/run.go new file mode 100644 index 0000000..83a54e7 --- /dev/null +++ b/rununshared/run.go @@ -0,0 +1,158 @@ +package rununshared + +import ( + "fmt" + "os" + "syscall" + "time" + + "github.com/criyle/go-judger/forkexec" + "github.com/criyle/go-judger/seccomp" + "github.com/criyle/go-judger/tracer" + libseccomp "github.com/seccomp/libseccomp-golang" + "golang.org/x/sys/unix" +) + +const ( + // UnshareFlags is flags used to create namespaces + UnshareFlags = unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | + unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS | unix.CLONE_NEWCGROUP +) + +// Start starts the unshared process +func (r *RunUnshared) Start() (rt tracer.TraceResult, err error) { + filter, err := seccomp.BuildFilter(libseccomp.ActKill, libseccomp.ActTrap, r.SyscallAllowed, []string{}) + if err != nil { + println(err) + return + } + defer filter.Release() + + bpf, err := seccomp.FilterToBPF(filter) + if err != nil { + println(err) + return + } + + ch := &forkexec.Runner{ + Args: r.Args, + Env: r.Env, + RLimits: r.RLimits.PrepareRLimit(), + Files: r.Files, + WorkDir: r.WorkDir, + Seccomp: bpf, + NoNewPrivs: true, + StopBeforeExec: true, + UnshareFlags: UnshareFlags, + Mounts: r.Mounts, + PivotRoot: r.Root, + DropCaps: true, + } + return r.Trace(ch) +} + +// Trace tracks child processes +func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult, err error) { + var ( + wstatus unix.WaitStatus // wait4 wait status + rusage unix.Rusage // wait4 rusage + tle = false + status = tracer.TraceCodeNormal + ) + + // Start the runner + pgid, err := runner.Start() + r.println("Starts: ", pgid, err) + if err != nil { + result.TraceStatus = tracer.TraceCodeRE + return result, err + } + // Set real time limit, kill process after it + timer := time.AfterFunc(time.Duration(int64(r.ResLimits.RealTimeLimit)*1e6), func() { + tle = true + killAll(pgid) + }) + + defer func() { + timer.Stop() + if tle { + err = tracer.TraceCodeTLE + } + // kill all tracee upon return + killAll(pgid) + collectZombie(pgid) + }() + + for { + pid, err := unix.Wait4(pgid, &wstatus, unix.WALL, &rusage) + r.println("wait4: ", wstatus) + if err != nil { + return result, tracer.TraceCodeFatal + } + + // update resource usage and check against limits + userTime := uint64(rusage.Utime.Sec*1e3 + rusage.Utime.Usec/1e3) // ms + userMem := uint64(rusage.Maxrss) // kb + + // check tle / mle + if userTime > r.ResLimits.TimeLimit { + status = tracer.TraceCodeTLE + } + if userMem > r.ResLimits.MemoryLimit { + status = tracer.TraceCodeMLE + } + result = tracer.TraceResult{ + UserTime: userTime, + UserMem: userMem, + TraceStatus: status, + } + if status != tracer.TraceCodeNormal { + return result, status + } + + switch { + case wstatus.Exited(): + result.ExitCode = wstatus.ExitStatus() + return result, nil + case wstatus.Signaled(): + sig := wstatus.Signal() + switch sig { + case unix.SIGXCPU: + status = tracer.TraceCodeTLE + case unix.SIGXFSZ: + status = tracer.TraceCodeOLE + case unix.SIGSYS: + status = tracer.TraceCodeBan + default: + status = tracer.TraceCodeRE + } + result.TraceStatus = status + return result, status + case wstatus.Stopped(): + unix.Kill(pid, syscall.SIGCONT) + } + } + return result, status +} + +// kill all tracee according to pids +func killAll(pgid int) { + unix.Kill(-pgid, unix.SIGKILL) +} + +// collect died child processes +func collectZombie(pgid int) { + // collect zombies + for { + var wstatus unix.WaitStatus + if _, err := unix.Wait4(-pgid, &wstatus, unix.WALL|unix.WNOWAIT, nil); err != nil { + break + } + } +} + +func (r *RunUnshared) println(v ...interface{}) { + if r.ShowDetails { + fmt.Fprintln(os.Stderr, v...) + } +} diff --git a/rununshared/rununshared.go b/rununshared/rununshared.go new file mode 100644 index 0000000..6985841 --- /dev/null +++ b/rununshared/rununshared.go @@ -0,0 +1,38 @@ +package rununshared + +import ( + "github.com/criyle/go-judger/mount" + "github.com/criyle/go-judger/rlimit" + "github.com/criyle/go-judger/tracer" +) + +// RunUnshared runs program in unshared namespaces +type RunUnshared struct { + // argv and env for the child process + Args []string + Env []string + + // workdir is the current dir after unshare mount namespaces + WorkDir string + + // file disriptors for new process, from 0 to len - 1 + Files []uintptr + + // Resource limit set by set rlimit + RLimits rlimit.RLimits + + // Resource limit enforced by tracer + ResLimits tracer.ResLimit + + // Allowed syscall names + SyscallAllowed []string + + // New root + Root string + + // Mount syscalls + Mounts []*mount.Mount + + // Show Details + ShowDetails bool +}