diff --git a/cmd/runprog/main.go b/cmd/runprog/main.go index 25354d4..7403029 100644 --- a/cmd/runprog/main.go +++ b/cmd/runprog/main.go @@ -218,6 +218,8 @@ func start() (*types.Result, error) { FileSize: outputLimit << 20, Stack: stackLimit << 20, } + debug("rlimit: ", rlims) + debug("defaultMount: ", mount.DefaultMounts) actionDefault := seccomp.ActionKill if showDetails { @@ -274,7 +276,7 @@ func start() (*types.Result, error) { return nil, fmt.Errorf("cannot make temp root for new namespace") } defer os.RemoveAll(root) - mounts, err := mount.NewBuilder().WithMounts(unshare.DefaultMounts).WithBind(root, "w", true).Build(true) + mounts, err := mount.NewBuilder().WithMounts(mount.DefaultMounts).WithBind(root, "w", true).Build(true) if err != nil { return nil, fmt.Errorf("cannot make rootfs mounts") } @@ -374,6 +376,7 @@ func debug(v ...interface{}) { } } +// Status defines uoj/run_program constants type Status int // UOJ run_program constants diff --git a/daemon/default.go b/daemon/default.go deleted file mode 100644 index 10a4bab..0000000 --- a/daemon/default.go +++ /dev/null @@ -1,55 +0,0 @@ -package daemon - -import ( - "github.com/criyle/go-sandbox/pkg/mount" - "golang.org/x/sys/unix" -) - -const ( - roBind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE | unix.MS_RDONLY - mFlag = unix.MS_NOSUID | unix.MS_NOATIME | unix.MS_NODEV -) - -// default parameters. I was tend to reuse the configs but it is hard since there are some -// cross device symbolic -var ( - DefaultPath = "PATH=/usr/local/bin:/usr/bin:/bin" - - // rootfs created by bind mounting - DefaultMounts = []mount.Mount{ - { - Source: "/usr", - Target: "usr", - Flags: roBind, - }, - { - Source: "/lib", - Target: "lib", - Flags: roBind, - }, - { - Source: "/lib64", - Target: "lib64", - Flags: roBind, - }, - { - Source: "/bin", - Target: "bin", - Flags: roBind, - }, - // work dir at /w - { - Source: "tmpfs", - Target: "w", - FsType: "tmpfs", - Flags: mFlag, - }, - // tmpfs at /tmp - { - Source: "tmpfs", - Target: "tmp", - FsType: "tmpfs", - Flags: mFlag, - }, - } -) diff --git a/daemon/master.go b/daemon/master.go index 89f17e0..d22335a 100644 --- a/daemon/master.go +++ b/daemon/master.go @@ -13,6 +13,9 @@ import ( "golang.org/x/sys/unix" ) +// DefaultPath defines path evironment variable for container init process +const DefaultPath = "PATH=/usr/local/bin:/usr/bin:/bin" + // Builder builds instance of container masters type Builder struct { // Root is container root mount path, empty uses current work path @@ -56,7 +59,11 @@ func (b *Builder) Build() (*Master, error) { // container mount points mounts := b.Mounts if len(mounts) == 0 { - if mounts, err = mount.NewBuilder().WithMounts(DefaultMounts).Build(true); err != nil { + if mounts, err = mount.NewBuilder(). + WithMounts(mount.DefaultMounts). + WithTmpfs("w", ""). // work dir + WithTmpfs("tmp", ""). // tmp + Build(true); err != nil { return nil, fmt.Errorf("daemon: failed to build rootfs mount %v", err) } } diff --git a/pkg/mount/default.go b/pkg/mount/default.go new file mode 100644 index 0000000..28f3d93 --- /dev/null +++ b/pkg/mount/default.go @@ -0,0 +1,34 @@ +package mount + +import "golang.org/x/sys/unix" + +// RoBind is read-only bind mount flags +const ( + RoBind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE | unix.MS_RDONLY +) + +var ( + // DefaultMounts is a example of minimal rootfs + DefaultMounts = []Mount{ + { + Source: "/usr", + Target: "usr", + Flags: RoBind, + }, + { + Source: "/lib", + Target: "lib", + Flags: RoBind, + }, + { + Source: "/lib64", + Target: "lib64", + Flags: RoBind, + }, + { + Source: "/bin", + Target: "bin", + Flags: RoBind, + }, + } +) diff --git a/pkg/mount/mount_linux.go b/pkg/mount/mount_linux.go index 85dda6c..3e7388f 100644 --- a/pkg/mount/mount_linux.go +++ b/pkg/mount/mount_linux.go @@ -26,9 +26,9 @@ func (m *Mount) Mount() error { func (m Mount) String() string { switch { - case m.Flags|syscall.MS_BIND == syscall.MS_BIND: + case m.Flags&syscall.MS_BIND == syscall.MS_BIND: flag := "rw" - if m.Flags|syscall.MS_RDONLY == syscall.MS_RDONLY { + if m.Flags&syscall.MS_RDONLY == syscall.MS_RDONLY { flag = "ro" } return fmt.Sprintf("bind[%s:%s:%s]", m.Source, m.Target, flag) diff --git a/ptracer/tracer_track_linux.go b/ptracer/tracer_track_linux.go index 38ab7e0..11d73e0 100644 --- a/ptracer/tracer_track_linux.go +++ b/ptracer/tracer_track_linux.go @@ -2,6 +2,7 @@ package ptracer import ( "context" + "fmt" "runtime" "time" @@ -14,31 +15,19 @@ import ( // Trace starts new goroutine and trace runner with ptrace func (t *Tracer) Trace(c context.Context) <-chan types.Result { result := make(chan types.Result, 1) - start := make(chan struct{}) - finish := make(chan struct{}) - - // run go func() { - defer close(finish) - ret, err := t.TraceRun(c.Done(), start) - ret.Error = err.Error() - result <- ret + result <- t.TraceRun(c) }() - - select { - case <-start: - case <-finish: - } return result } // TraceRun start and traces all child process by runner in the calling goroutine // parameter done used to cancel work, start is used notify child starts -func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result types.Result, err error) { +func (t *Tracer) TraceRun(c context.Context) (result types.Result) { var ( + status = types.StatusNormal wstatus unix.WaitStatus // wait4 wait status rusage unix.Rusage // wait4 rusage - tle bool // whether the timmer triggered due to timeout traced = make(map[int]bool) // store all process that have set ptrace options execved = false // store whether the runner process have successfully execvd pid int // store pid of wait4 result @@ -56,32 +45,26 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t if err != nil { t.Handler.Debug("start tracee failed: ", err) result.Status = types.StatusRunnerError - return result, err + result.Error = err.Error() + return } - close(start) - finish := make(chan struct{}) - defer close(finish) + cc, cancel := context.WithCancel(c) + defer cancel() // handle cancelation go func() { - select { - case <-done: - tle = true - killAll(pgid) - case <-finish: - } + <-cc.Done() + killAll(pgid) }() // handler potential panic and tle // also ensure processes was well terminated defer func() { - if tle { - err = types.StatusTimeLimitExceeded - } - if err2 := recover(); err2 != nil { - t.Handler.Debug(err2) - err = types.StatusRunnerError + if err := recover(); err != nil { + t.Handler.Debug("panic: ", err) + result.Status = types.StatusRunnerError + result.Error = fmt.Sprintf("%v", err) } // kill all tracee upon return killAll(pgid) @@ -90,7 +73,7 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t result.RunningTime = time.Since(fTime) }() - // trace unixs + // ptrace pool loop for { if execved { // Wait for all child in the process group @@ -101,11 +84,12 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t } if err != nil { t.Handler.Debug("wait4 failed: ", err) - return result, types.StatusRunnerError + result.Status = types.StatusRunnerError + result.Error = err.Error() + return } t.Handler.Debug("------ ", pid, " ------") - status := types.StatusNormal if pid == pgid { // update resource usage and check against limits userTime := time.Duration(rusage.Utime.Nano()) // ns @@ -124,7 +108,7 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t Memory: userMem, } if status != types.StatusNormal { - return result, status + return } } @@ -136,10 +120,16 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t if pid == pgid { if execved { result.ExitStatus = wstatus.ExitStatus() - return result, nil + if result.ExitStatus == 0 { + result.Status = types.StatusNormal + } else { + result.Status = types.StatusNonzeroExitStatus + } + return } result.Status = types.StatusRunnerError - return result, types.StatusRunnerError + result.Error = "child process exit before execve" + return } case wstatus.Signaled(): @@ -159,20 +149,21 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t } result.Status = status result.ExitStatus = int(sig) - return result, status + return } unix.PtraceCont(pid, int(sig)) case wstatus.Stopped(): // Set option if the process is newly forked if !traced[pid] { - t.Handler.Debug("set ptrace option") + t.Handler.Debug("set ptrace option for", pid) traced[pid] = true // Ptrace set option valid if the tracee is stopped err = setPtraceOption(pid) if err != nil { result.Status = types.StatusRunnerError - return result, err + result.Error = err.Error() + return } } @@ -185,7 +176,8 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t err := t.handleTrap(pid) if err != nil { result.Status = types.StatusDisallowedSyscall - return result, err + result.Error = err.Error() + return } } else { t.Handler.Debug("ptrace seccomp before execve (should be the execve syscall)") @@ -219,7 +211,7 @@ func (t *Tracer) TraceRun(done <-chan struct{}, start chan<- struct{}) (result t } if status != types.StatusNormal { result.Status = status - return result, status + return } // Likely encountered SIGSEGV (segment violation) // Or compiler child exited diff --git a/runner/unshare/default.go b/runner/unshare/default.go deleted file mode 100644 index f0d123b..0000000 --- a/runner/unshare/default.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build linux - -package unshare - -import ( - "golang.org/x/sys/unix" - - "github.com/criyle/go-sandbox/pkg/mount" -) - -const roBind = unix.MS_BIND | unix.MS_NOSUID | unix.MS_PRIVATE | unix.MS_RDONLY - -var ( - // DefaultMounts is a example of minimal rootfs - DefaultMounts = []mount.Mount{ - { - Source: "/usr", - Target: "usr", - Flags: roBind, - }, - { - Source: "/lib", - Target: "lib", - Flags: roBind, - }, - { - Source: "/lib64", - Target: "lib64", - Flags: roBind, - }, - { - Source: "/bin", - Target: "bin", - Flags: roBind, - }, - } -) diff --git a/runner/unshare/run.go b/runner/unshare/run.go index ce0051c..7e0096f 100644 --- a/runner/unshare/run.go +++ b/runner/unshare/run.go @@ -40,31 +40,18 @@ func (r *Runner) Run(c context.Context) <-chan types.Result { } result := make(chan types.Result, 1) - start := make(chan struct{}) - finish := make(chan struct{}) - - // run go func() { - defer close(finish) - ret, err2 := r.Trace(c.Done(), start, ch) - ret.Error = err2.Error() - result <- ret + result <- r.trace(c, ch) }() - select { - case <-start: - case <-finish: - } return result } // Trace tracks child processes -func (r *Runner) Trace(done <-chan struct{}, start chan<- struct{}, - runner *forkexec.Runner) (result types.Result, err error) { +func (r *Runner) trace(c context.Context, runner *forkexec.Runner) (result types.Result) { var ( wstatus unix.WaitStatus // wait4 wait status rusage unix.Rusage // wait4 rusage - tle = false status = types.StatusNormal sTime = time.Now() // start time fTime time.Time // finish time for setup @@ -75,28 +62,21 @@ func (r *Runner) Trace(done <-chan struct{}, start chan<- struct{}, r.println("Starts: ", pgid, err) if err != nil { result.Status = types.StatusRunnerError - return result, err + result.Error = err.Error() + return } - close(start) - finish := make(chan struct{}) - defer close(finish) + cc, cancel := context.WithCancel(c) + defer cancel() // handle cancel go func() { - select { - case <-done: - tle = true - killAll(pgid) - case <-finish: - } + <-cc.Done() + killAll(pgid) }() + // kill all tracee upon return defer func() { - if tle { - err = types.StatusTimeLimitExceeded - } - // kill all tracee upon return killAll(pgid) collectZombie(pgid) result.SetUpTime = fTime.Sub(sTime) @@ -104,17 +84,18 @@ func (r *Runner) Trace(done <-chan struct{}, start chan<- struct{}, }() fTime = time.Now() -loop: for { _, err := unix.Wait4(pgid, &wstatus, 0, &rusage) r.println("wait4: ", wstatus) if err != nil { - return result, types.StatusRunnerError + result.Status = types.StatusRunnerError + result.Error = err.Error() + return } // update resource usage and check against limits userTime := time.Duration(rusage.Utime.Nano()) // ns - userMem := types.Size(rusage.Maxrss << 10) // bytes // kb + userMem := types.Size(rusage.Maxrss << 10) // bytes // check tle / mle if userTime > r.Limit.TimeLimit { @@ -129,13 +110,15 @@ loop: Memory: userMem, } if status != types.StatusNormal { - return result, status + return } switch { case wstatus.Exited(): + result.Status = types.StatusNormal result.ExitStatus = wstatus.ExitStatus() - return result, nil + return + case wstatus.Signaled(): sig := wstatus.Signal() switch sig { @@ -150,10 +133,9 @@ loop: } result.Status = status result.ExitStatus = int(sig) - break loop + return } } - return result, status } // kill all tracee according to pids @@ -164,7 +146,6 @@ func killAll(pgid int) { // collect died child processes func collectZombie(pgid int) { var wstatus unix.WaitStatus - // collect zombies for { if _, err := unix.Wait4(-pgid, &wstatus, unix.WALL|unix.WNOHANG, nil); err != nil { break