Remove types pkg & update documents

- move types pkg into runner
- add documentations for cgroup & container
- add benchmakr for cgroup & container
This commit is contained in:
criyle 2020-03-02 03:14:13 -05:00
parent e8260dde37
commit 4f2257a187
28 changed files with 469 additions and 249 deletions

View File

@ -43,7 +43,7 @@ Default file access syscall check:
2. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4.rusage) 2. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4.rusage)
3. Container tech with execveat memfd, sethostname, setdomainname 3. Container tech with execveat memfd, sethostname, setdomainname
## Design (in progress) ## Design
### Result Status ### Result Status
@ -86,7 +86,7 @@ Configured runner to run the program. `Context` is used to cancel (control time
``` go ``` go
type Runner interface { type Runner interface {
Run(context.Context) <-chan types.Result Run(context.Context) <-chan runner.Result
} }
``` ```
@ -144,7 +144,7 @@ type Environment interface {
Open([]OpenCmd) ([]*os.File, error) Open([]OpenCmd) ([]*os.File, error)
Delete(p string) error Delete(p string) error
Reset() error Reset() error
Execve(context.Context, ExecveParam) <-chan types.Result Execve(context.Context, ExecveParam) <-chan runner.Result
Destroy() error Destroy() error
} }
``` ```
@ -170,7 +170,6 @@ type Environment interface {
- filehandler: an example implementation of UOJ file set - filehandler: an example implementation of UOJ file set
- unshare: wrapper to call forkexec and unshared namespaces - unshare: wrapper to call forkexec and unshared namespaces
- ptracer: ptrace tracer and provides syscall trap filter context - ptracer: ptrace tracer and provides syscall trap filter context
- types: provides general res / result data structures
## Executable ## Executable
@ -180,35 +179,50 @@ type Environment interface {
- config/config.go: all configs toward running specs (similar to UOJ) - config/config.go: all configs toward running specs (similar to UOJ)
## Benchmarks (MacOS docker amd64 / native arm64) ## Benchmarks
- 1ms / 2ms: fork, unshare pid / user / cgroup ### ForkExec
- 4ms / 8ms: run inside pre-forked container
- 50ms / 25ms: unshare ipc / mount
- 100ms / 44ms: unshare pid & user & cgroup & mount & pivot root
- 400ms / 63ms: unshare net
- 800ms / 170ms: unshare all
- 880ms / 170ms: unshare all & pivot root
It seems unshare net or ipc takes time, maybe limits action by seccomp instead.
Pre-forked container also saves time for container creation / cleanup.
```bash ```bash
$ go test -bench . -benchtime 10s $ go test -bench . -benchtime 10s
goos: linux goos: linux
goarch: amd64 goarch: amd64
pkg: github.com/criyle/go-sandbox/pkg/forkexec pkg: github.com/criyle/go-sandbox/pkg/forkexec
BenchmarkSimpleFork-4 12789 870486 ns/op BenchmarkSimpleFork-4 12409 996096 ns/op
BenchmarkUnsharePid-4 13172 917304 ns/op BenchmarkUnsharePid-4 10000 1065168 ns/op
BenchmarkUnshareUser-4 13148 927952 ns/op BenchmarkUnshareUser-4 10000 1061770 ns/op
BenchmarkUnshareUts-4 13170 884606 ns/op BenchmarkUnshareUts-4 10000 1056558 ns/op
BenchmarkUnshareCgroup-4 13650 895186 ns/op BenchmarkUnshareCgroup-4 10000 1049446 ns/op
BenchmarkUnshareIpc-4 196 66418708 ns/op BenchmarkUnshareIpc-4 709 16114052 ns/op
BenchmarkUnshareMount-4 243 46957682 ns/op BenchmarkUnshareMount-4 745 16207754 ns/op
BenchmarkUnshareNet-4 100 411869776 ns/op BenchmarkUnshareNet-4 3643 3492924 ns/op
BenchmarkFastUnshareMountPivot-4 120 107310917 ns/op BenchmarkFastUnshareMountPivot-4 612 20967318 ns/op
BenchmarkUnshareAll-4 100 837352275 ns/op BenchmarkUnshareAll-4 837 14047995 ns/op
BenchmarkUnshareMountPivot-4 12 913099234 ns/op BenchmarkUnshareMountPivot-4 488 24198331 ns/op
PASS PASS
ok github.com/criyle/go-sandbox/pkg/forkexec 300.744s ok github.com/criyle/go-sandbox/pkg/forkexec 147.186s
```
### Container
```bash
$ go test -bench . -benchtime 10s
goos: linux
goarch: amd64
pkg: github.com/criyle/go-sandbox/container
BenchmarkContainer-4 5907 2062070 ns/op
PASS
ok github.com/criyle/go-sandbox/container 21.763s
```
### Cgroup
```bash
$ go test -bench . -benchtime 10s
goos: linux
goarch: amd64
pkg: github.com/criyle/go-sandbox/pkg/cgroup
BenchmarkCgroup-4 50283 245094 ns/op
PASS
ok github.com/criyle/go-sandbox/pkg/cgroup 14.744s
``` ```

View File

@ -1,3 +1,4 @@
// Command runprog executes program defined restricted environment including seccomp-ptraced, namespaced and containerized.
package main package main
import ( import (
@ -21,7 +22,6 @@ import (
"github.com/criyle/go-sandbox/runner/ptrace" "github.com/criyle/go-sandbox/runner/ptrace"
"github.com/criyle/go-sandbox/runner/ptrace/filehandler" "github.com/criyle/go-sandbox/runner/ptrace/filehandler"
"github.com/criyle/go-sandbox/runner/unshare" "github.com/criyle/go-sandbox/runner/unshare"
"github.com/criyle/go-sandbox/types"
) )
const ( const (
@ -108,24 +108,24 @@ func main() {
rt, err := start() rt, err := start()
if rt == nil { if rt == nil {
rt = &types.Result{ rt = &runner.Result{
Status: types.StatusRunnerError, Status: runner.StatusRunnerError,
} }
} }
if err == nil && rt.Status != types.StatusNormal { if err == nil && rt.Status != runner.StatusNormal {
err = rt.Status err = rt.Status
} }
debug("setupTime: ", rt.SetUpTime) debug("setupTime: ", rt.SetUpTime)
debug("runningTime: ", rt.RunningTime) debug("runningTime: ", rt.RunningTime)
if err != nil { if err != nil {
debug(err) debug(err)
c, ok := err.(types.Status) c, ok := err.(runner.Status)
if !ok { if !ok {
c = types.StatusRunnerError c = runner.StatusRunnerError
} }
// Handle fatal error from trace // Handle fatal error from trace
fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c), int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus) fmt.Fprintf(f, "%d %d %d %d\n", getStatus(c), int(rt.Time/time.Millisecond), uint64(rt.Memory)>>10, rt.ExitStatus)
if c == types.StatusRunnerError { if c == runner.StatusRunnerError {
os.Exit(1) os.Exit(1)
} }
} else { } else {
@ -138,17 +138,17 @@ type containerRunner struct {
container.ExecveParam container.ExecveParam
} }
func (r *containerRunner) Run(c context.Context) <-chan types.Result { func (r *containerRunner) Run(c context.Context) <-chan runner.Result {
return r.Environment.Execve(c, r.ExecveParam) return r.Environment.Execve(c, r.ExecveParam)
} }
func start() (*types.Result, error) { func start() (*runner.Result, error) {
var ( var (
runner runner.Runner r runner.Runner
cg *cgroup.CGroup cg *cgroup.Cgroup
err error err error
execFile uintptr execFile uintptr
rt types.Result rt runner.Result
) )
addRead := filehandler.GetExtraSet(addReadable, addRawReadable) addRead := filehandler.GetExtraSet(addReadable, addRawReadable)
@ -225,9 +225,9 @@ func start() (*types.Result, error) {
actionDefault = seccomp.ActionTrace.WithReturnCode(seccomp.MsgDisallow) actionDefault = seccomp.ActionTrace.WithReturnCode(seccomp.MsgDisallow)
} }
limit := types.Limit{ limit := runner.Limit{
TimeLimit: time.Duration(timeLimit) * time.Second, TimeLimit: time.Duration(timeLimit) * time.Second,
MemoryLimit: types.Size(memoryLimit << 20), MemoryLimit: runner.Size(memoryLimit << 20),
} }
if runt == "container" { if runt == "container" {
@ -250,12 +250,12 @@ func start() (*types.Result, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to ping container: %v", err) return nil, fmt.Errorf("failed to ping container: %v", err)
} }
runner = &containerRunner{ r = &containerRunner{
Environment: m, Environment: m,
ExecveParam: container.ExecveParam{ ExecveParam: container.ExecveParam{
Args: args, Args: args,
Env: []string{pathEnv}, Env: []string{pathEnv},
Fds: fds, Files: fds,
ExecFile: execFile, ExecFile: execFile,
RLimits: rlims.PrepareRLimit(), RLimits: rlims.PrepareRLimit(),
SyncFunc: syncFunc, SyncFunc: syncFunc,
@ -279,7 +279,7 @@ func start() (*types.Result, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot make rootfs mounts") return nil, fmt.Errorf("cannot make rootfs mounts")
} }
runner = &unshare.Runner{ r = &unshare.Runner{
Args: args, Args: args,
Env: []string{pathEnv}, Env: []string{pathEnv},
ExecFile: execFile, ExecFile: execFile,
@ -305,7 +305,7 @@ func start() (*types.Result, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create seccomp filter %v", err) return nil, fmt.Errorf("failed to create seccomp filter %v", err)
} }
runner = &ptrace.Runner{ r = &ptrace.Runner{
Args: args, Args: args,
Env: []string{pathEnv}, Env: []string{pathEnv},
ExecFile: execFile, ExecFile: execFile,
@ -332,14 +332,14 @@ func start() (*types.Result, error) {
c, cancel := context.WithTimeout(context.Background(), time.Duration(int64(realTimeLimit)*int64(time.Second))) c, cancel := context.WithTimeout(context.Background(), time.Duration(int64(realTimeLimit)*int64(time.Second)))
defer cancel() defer cancel()
s := runner.Run(c) s := r.Run(c)
rTime := time.Now() rTime := time.Now()
select { select {
case <-sig: case <-sig:
cancel() cancel()
rt = <-s rt = <-s
rt.Status = types.StatusRunnerError rt.Status = runner.StatusRunnerError
case rt = <-s: case rt = <-s:
} }
@ -367,7 +367,7 @@ func start() (*types.Result, error) {
} }
debug("cgroup: cpu: ", cpu, " memory: ", memory, "cache: ", cache) debug("cgroup: cpu: ", cpu, " memory: ", memory, "cache: ", cache)
rt.Time = time.Duration(cpu) rt.Time = time.Duration(cpu)
rt.Memory = types.Size(memory - cache) rt.Memory = runner.Size(memory - cache)
debug("cgroup:", rt) debug("cgroup:", rt)
} }
return &rt, nil return &rt, nil
@ -394,21 +394,21 @@ const (
StatusFatal // 7 StatusFatal // 7
) )
func getStatus(s types.Status) int { func getStatus(s runner.Status) int {
switch s { switch s {
case types.StatusNormal: case runner.StatusNormal:
return int(StatusNormal) return int(StatusNormal)
case types.StatusInvalid: case runner.StatusInvalid:
return int(StatusInvalid) return int(StatusInvalid)
case types.StatusTimeLimitExceeded: case runner.StatusTimeLimitExceeded:
return int(StatusTLE) return int(StatusTLE)
case types.StatusMemoryLimitExceeded: case runner.StatusMemoryLimitExceeded:
return int(StatusMLE) return int(StatusMLE)
case types.StatusOutputLimitExceeded: case runner.StatusOutputLimitExceeded:
return int(StatusOLE) return int(StatusOLE)
case types.StatusDisallowedSyscall: case runner.StatusDisallowedSyscall:
return int(StatusBan) return int(StatusBan)
case types.StatusSignalled, types.StatusNonzeroExitStatus: case runner.StatusSignalled, runner.StatusNonzeroExitStatus:
return int(StatusRE) return int(StatusRE)
default: default:
return int(StatusFatal) return int(StatusFatal)

115
container/benchmark_test.go Normal file
View File

@ -0,0 +1,115 @@
package container
import (
"context"
"errors"
"io/ioutil"
"testing"
"github.com/criyle/go-sandbox/runner"
)
func init() {
Init()
}
func BenchmarkContainer(b *testing.B) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
b.Error(err)
return
}
builder := &Builder{
Root: tmpDir,
}
m, err := builder.Build()
if err != nil {
b.Error(err)
return
}
b.Cleanup(func() {
m.Destroy()
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
rt := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/echo"},
Env: []string{"PATH=/bin"},
})
r := <-rt
if r.Status != runner.StatusNormal {
b.Error(r.Status, r.Error)
return
}
}
}
func TestContainerSuccess(t *testing.T) {
m := getEnv(t)
if m == nil {
return
}
rt := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/echo"},
Env: []string{"PATH=/bin"},
})
r := <-rt
if r.Status != runner.StatusNormal {
t.Error(r.Status, r.Error)
return
}
}
func TestContainerNotExists(t *testing.T) {
m := getEnv(t)
if m == nil {
return
}
rt := m.Execve(context.TODO(), ExecveParam{
Args: []string{"not_exists"},
Env: []string{"PATH=/bin"},
})
r := <-rt
if r.Status != runner.StatusRunnerError {
t.Error(r.Status, r.Error)
return
}
}
func TestContainerSyncFuncFail(t *testing.T) {
m := getEnv(t)
if m == nil {
return
}
err := errors.New("test error")
rt := m.Execve(context.TODO(), ExecveParam{
Args: []string{"/bin/echo"},
Env: []string{"PATH=/bin"},
SyncFunc: func(pid int) error {
return err
},
})
r := <-rt
if r.Status != runner.StatusRunnerError {
t.Error(r.Status, r.Error)
return
}
}
func getEnv(t *testing.T) Environment {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Error(err)
return nil
}
builder := &Builder{
Root: tmpDir,
}
m, err := builder.Build()
if err != nil {
t.Error(err)
return nil
}
return m
}

View File

@ -7,7 +7,7 @@ import (
"github.com/criyle/go-sandbox/pkg/forkexec" "github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/pkg/unixsocket" "github.com/criyle/go-sandbox/pkg/unixsocket"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
func (c *containerServer) handleExecve(cmd *execCmd, msg *unixsocket.Msg) error { func (c *containerServer) handleExecve(cmd *execCmd, msg *unixsocket.Msg) error {
@ -118,14 +118,14 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg *unixsocket.Msg) error
if err != nil { if err != nil {
c.sendErrorReply("execve: wait4 %v", err) c.sendErrorReply("execve: wait4 %v", err)
} else { } else {
status := types.StatusNormal status := runner.StatusNormal
userTime := time.Duration(rusage.Utime.Nano()) // ns userTime := time.Duration(rusage.Utime.Nano()) // ns
userMem := types.Size(rusage.Maxrss << 10) // bytes userMem := runner.Size(rusage.Maxrss << 10) // bytes
switch { switch {
case wstatus.Exited(): case wstatus.Exited():
exitStatus := wstatus.ExitStatus() exitStatus := wstatus.ExitStatus()
if exitStatus != 0 { if exitStatus != 0 {
status = types.StatusNonzeroExitStatus status = runner.StatusNonzeroExitStatus
} }
c.sendReply(&reply{ c.sendReply(&reply{
ExecReply: &execReply{ ExecReply: &execReply{
@ -140,13 +140,13 @@ func (c *containerServer) handleExecve(cmd *execCmd, msg *unixsocket.Msg) error
switch wstatus.Signal() { switch wstatus.Signal() {
// kill signal treats as TLE // kill signal treats as TLE
case syscall.SIGXCPU, syscall.SIGKILL: case syscall.SIGXCPU, syscall.SIGKILL:
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
case syscall.SIGXFSZ: case syscall.SIGXFSZ:
status = types.StatusOutputLimitExceeded status = runner.StatusOutputLimitExceeded
case syscall.SIGSYS: case syscall.SIGSYS:
status = types.StatusDisallowedSyscall status = runner.StatusDisallowedSyscall
default: default:
status = types.StatusSignalled status = runner.StatusSignalled
} }
c.sendReply(&reply{ c.sendReply(&reply{
ExecReply: &execReply{ ExecReply: &execReply{

View File

@ -12,33 +12,33 @@
// Host to container communication protocol is single threaded and always initiated by // Host to container communication protocol is single threaded and always initiated by
// the host: // the host:
// //
// - ping (alive check): // - ping (alive check):
// - reply: pong // - reply: pong
// //
// - conf (set configuration): // - conf (set configuration):
// - reply pong // - reply pong
// //
// - open (open files in given mode inside container): // - open (open files in given mode inside container):
// - send: []OpenCmd // - send: []OpenCmd
// - reply: "success", file fds / "error" // - reply: "success", file fds / "error"
// //
// - delete (unlink file / rmdir dir inside container): // - delete (unlink file / rmdir dir inside container):
// - send: path // - send: path
// - reply: "finished" / "error" // - reply: "finished" / "error"
// //
// - reset (clean up container for later use (clear workdir / tmp)): // - reset (clean up container for later use (clear workdir / tmp)):
// - send: // - send:
// - reply: "success" // - reply: "success"
// //
// - execve: (execute file inside container): // - execve: (execute file inside container):
// - send: argv, env, rLimits, fds // - send: argv, env, rLimits, fds
// - reply: // - reply:
// - success: "success", pid // - success: "success", pid
// - failed: "failed" // - failed: "failed"
// - send (success): "init_finished" (as cmd) // - send (success): "init_finished" (as cmd)
// - reply: "finished" / send: "kill" (as cmd) // - reply: "finished" / send: "kill" (as cmd)
// - send: "kill" (as cmd) / reply: "finished" // - send: "kill" (as cmd) / reply: "finished"
// - reply: // - reply:
// //
// Any socket related error will cause the container exit with all process inside container // Any socket related error will cause the container exit with all process inside container
package container package container

View File

@ -8,10 +8,9 @@ import (
"syscall" "syscall"
"github.com/criyle/go-sandbox/pkg/forkexec" "github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/pkg/memfd"
"github.com/criyle/go-sandbox/pkg/mount" "github.com/criyle/go-sandbox/pkg/mount"
"github.com/criyle/go-sandbox/pkg/unixsocket" "github.com/criyle/go-sandbox/pkg/unixsocket"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -49,7 +48,7 @@ type Environment interface {
Open([]OpenCmd) ([]*os.File, error) Open([]OpenCmd) ([]*os.File, error)
Delete(p string) error Delete(p string) error
Reset() error Reset() error
Execve(context.Context, ExecveParam) <-chan types.Result Execve(context.Context, ExecveParam) <-chan runner.Result
Destroy() error Destroy() error
} }
@ -196,21 +195,12 @@ func (b *Builder) exec() (*os.File, error) {
return OpenCurrentExec() return OpenCurrentExec()
} }
// OpenCurrentExec opens current executable and dup and seal it to memfd // OpenCurrentExec opens current executable (/proc/self/exe)
func OpenCurrentExec() (*os.File, error) { func OpenCurrentExec() (*os.File, error) {
self, err := os.Open(currentExec) return os.Open(currentExec)
if err != nil {
return nil, fmt.Errorf("failed to open %v: %v", currentExec, err)
}
defer self.Close()
execFile, err := memfd.DupToMemfd("init", self)
if err != nil {
return nil, fmt.Errorf("failed to create memfd: %v", err)
}
return execFile, nil
} }
// newPassCredSocketPair creates socket pair and let the first socket to receive credential information
func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) { func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) {
ins, outs, err := unixsocket.NewSocketPair() ins, outs, err := unixsocket.NewSocketPair()
if err != nil { if err != nil {
@ -221,11 +211,6 @@ func newPassCredSocketPair() (*unixsocket.Socket, *unixsocket.Socket, error) {
outs.Close() outs.Close()
return nil, nil, err return nil, nil, err
} }
if err = outs.SetPassCred(1); err != nil {
ins.Close()
outs.Close()
return nil, nil, err
}
return ins, outs, nil return ins, outs, nil
} }

View File

@ -7,37 +7,42 @@ import (
"github.com/criyle/go-sandbox/pkg/rlimit" "github.com/criyle/go-sandbox/pkg/rlimit"
"github.com/criyle/go-sandbox/pkg/unixsocket" "github.com/criyle/go-sandbox/pkg/unixsocket"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// ExecveParam is parameters to run process inside container // ExecveParam is parameters to run process inside container
type ExecveParam struct { type ExecveParam struct {
// Args holds command line arguments
Args []string Args []string
Env []string
Fds []uintptr
// fd parameter of fexecve // Env specifies the environment of the process
Env []string
// Files specifies file descriptors for the child process
Files []uintptr
// ExecFile specifies file descriptor for executable file using fexecve
ExecFile uintptr ExecFile uintptr
// POSIX Resource limit set by set rlimit // RLimits specifies POSIX Resource limit through setrlimit
RLimits []rlimit.RLimit RLimits []rlimit.RLimit
// SyncFunc called with pid before execve (for adding the process to cgroups) // SyncFunc calls with pid just before execve (for attach the process to cgroups)
SyncFunc func(pid int) error SyncFunc func(pid int) error
} }
// Execve runs process inside container. It accepts context cancelation as time limit exceeded. // Execve runs process inside container. It accepts context cancelation as time limit exceeded.
func (c *container) Execve(ctx context.Context, param ExecveParam) <-chan types.Result { func (c *container) Execve(ctx context.Context, param ExecveParam) <-chan runner.Result {
c.mu.Lock() c.mu.Lock()
sTime := time.Now() sTime := time.Now()
// make sure goroutine not leaked (blocked) even if result is not consumed // make sure goroutine not leaked (blocked) even if result is not consumed
result := make(chan types.Result, 1) result := make(chan runner.Result, 1)
errResult := func(f string, v ...interface{}) <-chan types.Result { errResult := func(f string, v ...interface{}) <-chan runner.Result {
result <- types.Result{ result <- runner.Result{
Status: types.StatusRunnerError, Status: runner.StatusRunnerError,
Error: fmt.Sprintf(f, v...), Error: fmt.Sprintf(f, v...),
} }
return result return result
@ -48,7 +53,7 @@ func (c *container) Execve(ctx context.Context, param ExecveParam) <-chan types.
if param.ExecFile > 0 { if param.ExecFile > 0 {
files = append(files, int(param.ExecFile)) files = append(files, int(param.ExecFile))
} }
files = append(files, uintptrSliceToInt(param.Fds)...) files = append(files, uintptrSliceToInt(param.Files)...)
msg := &unixsocket.Msg{ msg := &unixsocket.Msg{
Fds: files, Fds: files,
} }
@ -110,28 +115,28 @@ func (c *container) Execve(ctx context.Context, param ExecveParam) <-chan types.
// handle potential error // handle potential error
if err != nil { if err != nil {
result <- types.Result{ result <- runner.Result{
Status: types.StatusRunnerError, Status: runner.StatusRunnerError,
Error: err.Error(), Error: err.Error(),
} }
return return
} }
if reply2.Error != nil { if reply2.Error != nil {
result <- types.Result{ result <- runner.Result{
Status: types.StatusRunnerError, Status: runner.StatusRunnerError,
Error: reply2.Error.Error(), Error: reply2.Error.Error(),
} }
return return
} }
if reply2.ExecReply == nil { if reply2.ExecReply == nil {
result <- types.Result{ result <- runner.Result{
Status: types.StatusRunnerError, Status: runner.StatusRunnerError,
Error: "execve: no reply received", Error: "execve: no reply received",
} }
return return
} }
// emit result after all communication finish // emit result after all communication finish
result <- types.Result{ result <- runner.Result{
Status: reply2.ExecReply.Status, Status: reply2.ExecReply.Status,
ExitStatus: reply2.ExecReply.ExitStatus, ExitStatus: reply2.ExecReply.ExitStatus,
Time: reply2.ExecReply.Time, Time: reply2.ExecReply.Time,

View File

@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/criyle/go-sandbox/pkg/rlimit" "github.com/criyle/go-sandbox/pkg/rlimit"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// cmd is the control message send into container // cmd is the control message send into container
@ -64,9 +64,9 @@ type errorReply struct {
// execReply stores execve result // execReply stores execve result
type execReply struct { type execReply struct {
ExitStatus int // waitpid exit status ExitStatus int // waitpid exit status
Status types.Status // return status Status runner.Status // return status
Time time.Duration // waitpid user CPU (ns) Time time.Duration // waitpid user CPU (ns)
Memory types.Size // waitpid user memory (byte) Memory runner.Size // waitpid user memory (byte)
} }
func (e *errorReply) Error() string { func (e *errorReply) Error() string {

View File

@ -0,0 +1,66 @@
package cgroup
import "testing"
func BenchmarkCgroup(b *testing.B) {
builder, err := NewBuilder("benchmark").WithCPUAcct().WithMemory().WithPids().FilterByEnv()
if err != nil {
b.Error(err)
return
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
cg, err := builder.Build()
if err != nil {
b.Error(err)
return
}
if err := cg.SetMemoryLimitInBytes(4096); err != nil {
b.Error(err)
return
}
if err := cg.SetPidsMax(1); err != nil {
b.Error(err)
return
}
if _, err := cg.CpuacctUsage(); err != nil {
b.Error(err)
return
}
if _, err := cg.MemoryMaxUsageInBytes(); err != nil {
b.Error(err)
return
}
cg.Destroy()
}
}
func TestCgroup(t *testing.T) {
builder, err := NewBuilder("test").WithCPUAcct().WithMemory().WithPids().FilterByEnv()
if err != nil {
t.Error(err)
return
}
cg, err := builder.Build()
if err != nil {
t.Error(err)
return
}
if err := cg.SetMemoryLimitInBytes(4096); err != nil {
t.Error(err)
return
}
if err := cg.SetPidsMax(1); err != nil {
t.Error(err)
return
}
if _, err := cg.CpuacctUsage(); err != nil {
t.Error(err)
return
}
if _, err := cg.MemoryMaxUsageInBytes(); err != nil {
t.Error(err)
return
}
cg.Destroy()
}

View File

@ -6,20 +6,14 @@ import (
"os" "os"
) )
// additional ideas: // Cgroup is the combination of sub-cgroups
// cpu share(not used): cpu.share type Cgroup struct {
// reclaim pages from old process: memory.force_empty
// (tasks kill are managed out of cgroup as freeze takes some time)
// freeze: freezer.state
// CGroup is the combination of sub-cgroups
type CGroup struct {
prefix string prefix string
cpuacct, memory, pids *SubCGroup cpuacct, memory, pids *SubCgroup
} }
// Build creates new cgrouup directories // Build creates new cgrouup directories
func (b *Builder) Build() (cg *CGroup, err error) { func (b *Builder) Build() (cg *Cgroup, err error) {
var ( var (
cpuacctPath, memoryPath, pidsPath string cpuacctPath, memoryPath, pidsPath string
) )
@ -32,31 +26,31 @@ func (b *Builder) Build() (cg *CGroup, err error) {
} }
}() }()
if b.CPUAcct { if b.CPUAcct {
if cpuacctPath, err = CreateSubCGroupPath("cpuacct", b.Prefix); err != nil { if cpuacctPath, err = CreateSubCgroupPath("cpuacct", b.Prefix); err != nil {
return return
} }
} }
if b.Memory { if b.Memory {
if memoryPath, err = CreateSubCGroupPath("memory", b.Prefix); err != nil { if memoryPath, err = CreateSubCgroupPath("memory", b.Prefix); err != nil {
return return
} }
} }
if b.Pids { if b.Pids {
if pidsPath, err = CreateSubCGroupPath("pids", b.Prefix); err != nil { if pidsPath, err = CreateSubCgroupPath("pids", b.Prefix); err != nil {
return return
} }
} }
return &CGroup{ return &Cgroup{
prefix: b.Prefix, prefix: b.Prefix,
cpuacct: NewSubCGroup(cpuacctPath), cpuacct: NewSubCgroup(cpuacctPath),
memory: NewSubCGroup(memoryPath), memory: NewSubCgroup(memoryPath),
pids: NewSubCGroup(pidsPath), pids: NewSubCgroup(pidsPath),
}, nil }, nil
} }
// AddProc writes cgroup.procs to all sub-cgroup // AddProc writes cgroup.procs to all sub-cgroup
func (c *CGroup) AddProc(pid int) error { func (c *Cgroup) AddProc(pid int) error {
if err := c.cpuacct.WriteUint(cgroupProcs, uint64(pid)); err != nil { if err := c.cpuacct.WriteUint(cgroupProcs, uint64(pid)); err != nil {
return err return err
} }
@ -69,8 +63,8 @@ func (c *CGroup) AddProc(pid int) error {
return nil return nil
} }
// Destroy removes dir for sub-cggroup, errors are ignored if remove one failed // Destroy removes dir for sub-cgroup, errors are ignored if remove one failed
func (c *CGroup) Destroy() error { func (c *Cgroup) Destroy() error {
var err1 error var err1 error
if err := remove(c.cpuacct.path); err != nil { if err := remove(c.cpuacct.path); err != nil {
err1 = err err1 = err
@ -85,37 +79,37 @@ func (c *CGroup) Destroy() error {
} }
// CpuacctUsage read cpuacct.usage in ns // CpuacctUsage read cpuacct.usage in ns
func (c *CGroup) CpuacctUsage() (uint64, error) { func (c *Cgroup) CpuacctUsage() (uint64, error) {
return c.cpuacct.ReadUint("cpuacct.usage") return c.cpuacct.ReadUint("cpuacct.usage")
} }
// MemoryMaxUsageInBytes read memory.max_usage_in_bytes // MemoryMaxUsageInBytes read memory.max_usage_in_bytes
func (c *CGroup) MemoryMaxUsageInBytes() (uint64, error) { func (c *Cgroup) MemoryMaxUsageInBytes() (uint64, error) {
return c.memory.ReadUint("memory.max_usage_in_bytes") return c.memory.ReadUint("memory.max_usage_in_bytes")
} }
// SetMemoryLimitInBytes write memory.limit_in_bytes // SetMemoryLimitInBytes write memory.limit_in_bytes
func (c *CGroup) SetMemoryLimitInBytes(i uint64) error { func (c *Cgroup) SetMemoryLimitInBytes(i uint64) error {
return c.memory.WriteUint("memory.limit_in_bytes", i) return c.memory.WriteUint("memory.limit_in_bytes", i)
} }
// SetPidsMax write pids.max // SetPidsMax write pids.max
func (c *CGroup) SetPidsMax(i uint64) error { func (c *Cgroup) SetPidsMax(i uint64) error {
return c.pids.WriteUint("pids.max", i) return c.pids.WriteUint("pids.max", i)
} }
// SetCpuacctUsage write cpuacct.usage in ns // SetCpuacctUsage write cpuacct.usage in ns
func (c *CGroup) SetCpuacctUsage(i uint64) error { func (c *Cgroup) SetCpuacctUsage(i uint64) error {
return c.cpuacct.WriteUint("cpuacct.usage", i) return c.cpuacct.WriteUint("cpuacct.usage", i)
} }
// SetMemoryMaxUsageInBytes write cpuacct.usage in ns // SetMemoryMaxUsageInBytes write cpuacct.usage in ns
func (c *CGroup) SetMemoryMaxUsageInBytes(i uint64) error { func (c *Cgroup) SetMemoryMaxUsageInBytes(i uint64) error {
return c.memory.WriteUint("memory.max_usage_in_bytes", i) return c.memory.WriteUint("memory.max_usage_in_bytes", i)
} }
// FindMemoryStatProperty find certain property from memory.stat // FindMemoryStatProperty find certain property from memory.stat
func (c *CGroup) FindMemoryStatProperty(prop string) (uint64, error) { func (c *Cgroup) FindMemoryStatProperty(prop string) (uint64, error) {
content, err := c.memory.ReadFile("memory.stat") content, err := c.memory.ReadFile("memory.stat")
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -1,6 +1,17 @@
// Package cgroup provices builder to create multiple different cgroup-v1 sub groups // Package cgroup provices builder to create multiple different cgroup-v1 sub groups
// under systemd defined path (i.e. /sys/fs/cgroup). // under systemd defined mount path (i.e.,sys/fs/cgroup).
// //
// current available cgroups are cpuacct, memory, pids // Current available:
// not available: cpu, cpuset, devices, freezer, net_cls, blkio, perf_event, net_prio, huge_tlb, rdma // cpuacct
// memory
// pids
//
// Current not avaliable: cpu, cpuset, devices, freezer, net_cls, blkio, perf_event, net_prio, huge_tlb, rdma
//
// Additional ideas:
//
// cpu share(not used): cpu.share
// reclaim pages from old process: memory.force_empty
// (tasks kill are managed out of cgroup as freeze takes some time)
// freeze: freezer.state
package cgroup package cgroup

View File

@ -9,31 +9,29 @@ import (
"syscall" "syscall"
) )
// SubCGroup is the sub-cgroup // SubCgroup is the accessor for single cgroup resource with given path
type SubCGroup struct { type SubCgroup struct {
path string path string
} }
// ErrNotInitialized returned when trying to read from not initialized cgroup // ErrNotInitialized returned when trying to read from not initialized cgroup
var ErrNotInitialized = errors.New("cgroup was not initialized") var ErrNotInitialized = errors.New("cgroup was not initialized")
// NewSubCGroup creates a sug CGroup // NewSubCgroup creates a cgroup accessor with given path (path needs to be created in advance)
func NewSubCGroup(p string) *SubCGroup { func NewSubCgroup(p string) *SubCgroup {
return &SubCGroup{ return &SubCgroup{path: p}
path: p,
}
} }
// WriteUint writes uint64 into given file // WriteUint writes uint64 into given file
func (c *SubCGroup) WriteUint(filename string, i uint64) error { func (c *SubCgroup) WriteUint(filename string, i uint64) error {
if c.path == "" { if c.path == "" {
return nil return nil
} }
return c.WriteFile(filename, []byte(strconv.FormatUint(i, 10))) return c.WriteFile(filename, []byte(strconv.FormatUint(i, 10)))
} }
// ReadUint read uint64 into given file // ReadUint read uint64 from given file
func (c *SubCGroup) ReadUint(filename string) (uint64, error) { func (c *SubCgroup) ReadUint(filename string) (uint64, error) {
if c.path == "" { if c.path == "" {
return 0, ErrNotInitialized return 0, ErrNotInitialized
} }
@ -50,7 +48,7 @@ func (c *SubCGroup) ReadUint(filename string) (uint64, error) {
// WriteFile writes cgroup file and handles potential EINTR error while writes to // WriteFile writes cgroup file and handles potential EINTR error while writes to
// the slow device (cgroup) // the slow device (cgroup)
func (c *SubCGroup) WriteFile(name string, content []byte) error { func (c *SubCgroup) WriteFile(name string, content []byte) error {
p := path.Join(c.path, name) p := path.Join(c.path, name)
err := ioutil.WriteFile(p, content, 0664) err := ioutil.WriteFile(p, content, 0664)
for err != nil && errors.Is(err, syscall.EINTR) { for err != nil && errors.Is(err, syscall.EINTR) {
@ -61,7 +59,7 @@ func (c *SubCGroup) WriteFile(name string, content []byte) error {
// ReadFile reads cgroup file and handles potential EINTR error while read to // ReadFile reads cgroup file and handles potential EINTR error while read to
// the slow device (cgroup) // the slow device (cgroup)
func (c *SubCGroup) ReadFile(name string) ([]byte, error) { func (c *SubCgroup) ReadFile(name string) ([]byte, error) {
p := path.Join(c.path, name) p := path.Join(c.path, name)
data, err := ioutil.ReadFile(p) data, err := ioutil.ReadFile(p)
for err != nil && errors.Is(err, syscall.EINTR) { for err != nil && errors.Is(err, syscall.EINTR) {

View File

@ -8,7 +8,7 @@ import (
"strings" "strings"
) )
// EnsureDirExists creates dir if not exists // EnsureDirExists creates directories if the path not exists
func EnsureDirExists(path string) error { func EnsureDirExists(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return os.Mkdir(path, os.ModePerm) return os.Mkdir(path, os.ModePerm)
@ -16,8 +16,8 @@ func EnsureDirExists(path string) error {
return nil return nil
} }
// CreateSubCGroupPath creates path for sub-cgroup // CreateSubCgroupPath creates path for sub-cgroup with given group and prefix
func CreateSubCGroupPath(group, prefix string) (string, error) { func CreateSubCgroupPath(group, prefix string) (string, error) {
base := path.Join(basePath, group, prefix) base := path.Join(basePath, group, prefix)
EnsureDirExists(base) EnsureDirExists(base)
return ioutil.TempDir(base, "") return ioutil.TempDir(base, "")

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// RLimits defines the rlimit applied by setrlimit syscall to traced process // RLimits defines the rlimit applied by setrlimit syscall to traced process
@ -88,7 +88,7 @@ func (r RLimit) String() string {
case syscall.RLIMIT_AS: case syscall.RLIMIT_AS:
t = "AddressSpace" t = "AddressSpace"
} }
return fmt.Sprintf("%s[%v:%v]", t, types.Size(r.Rlim.Cur), types.Size(r.Rlim.Max)) return fmt.Sprintf("%s[%v:%v]", t, runner.Size(r.Rlim.Cur), runner.Size(r.Rlim.Max))
} }
func (r RLimits) String() string { func (r RLimits) String() string {

View File

@ -1,6 +1,6 @@
package ptracer package ptracer
import "github.com/criyle/go-sandbox/types" import "github.com/criyle/go-sandbox/runner"
// TraceAction defines the action returned by TraceHandle // TraceAction defines the action returned by TraceHandle
type TraceAction int type TraceAction int
@ -18,7 +18,7 @@ const (
type Tracer struct { type Tracer struct {
Handler Handler
Runner Runner
types.Limit runner.Limit
} }
// Runner represents the process runner // Runner represents the process runner

View File

@ -9,12 +9,12 @@ import (
unix "golang.org/x/sys/unix" unix "golang.org/x/sys/unix"
"github.com/criyle/go-sandbox/pkg/seccomp" "github.com/criyle/go-sandbox/pkg/seccomp"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// Trace starts new goroutine and trace runner with ptrace // Trace starts new goroutine and trace runner with ptrace
func (t *Tracer) Trace(c context.Context) <-chan types.Result { func (t *Tracer) Trace(c context.Context) <-chan runner.Result {
result := make(chan types.Result, 1) result := make(chan runner.Result, 1)
go func() { go func() {
result <- t.TraceRun(c) result <- t.TraceRun(c)
}() }()
@ -23,9 +23,9 @@ func (t *Tracer) Trace(c context.Context) <-chan types.Result {
// TraceRun start and traces all child process by runner in the calling goroutine // 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 // parameter done used to cancel work, start is used notify child starts
func (t *Tracer) TraceRun(c context.Context) (result types.Result) { func (t *Tracer) TraceRun(c context.Context) (result runner.Result) {
var ( var (
status = types.StatusNormal status = runner.StatusNormal
wstatus unix.WaitStatus // wait4 wait status wstatus unix.WaitStatus // wait4 wait status
rusage unix.Rusage // wait4 rusage rusage unix.Rusage // wait4 rusage
traced = make(map[int]bool) // store all process that have set ptrace options traced = make(map[int]bool) // store all process that have set ptrace options
@ -44,7 +44,7 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
t.Handler.Debug("tracer started: ", pgid, err) t.Handler.Debug("tracer started: ", pgid, err)
if err != nil { if err != nil {
t.Handler.Debug("start tracee failed: ", err) t.Handler.Debug("start tracee failed: ", err)
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = err.Error() result.Error = err.Error()
return return
} }
@ -63,7 +63,7 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
t.Handler.Debug("panic: ", err) t.Handler.Debug("panic: ", err)
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = fmt.Sprintf("%v", err) result.Error = fmt.Sprintf("%v", err)
} }
// kill all tracee upon return // kill all tracee upon return
@ -88,7 +88,7 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
} }
if err != nil { if err != nil {
t.Handler.Debug("wait4 failed: ", err) t.Handler.Debug("wait4 failed: ", err)
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = err.Error() result.Error = err.Error()
return return
} }
@ -97,21 +97,21 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
if pid == pgid { if pid == pgid {
// update resource usage and check against limits // update resource usage and check against limits
userTime := time.Duration(rusage.Utime.Nano()) // ns userTime := time.Duration(rusage.Utime.Nano()) // ns
userMem := types.Size(rusage.Maxrss << 10) // bytes userMem := runner.Size(rusage.Maxrss << 10) // bytes
// check tle / mle // check tle / mle
if userTime > t.Limit.TimeLimit { if userTime > t.Limit.TimeLimit {
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
} }
if userMem > t.Limit.MemoryLimit { if userMem > t.Limit.MemoryLimit {
status = types.StatusMemoryLimitExceeded status = runner.StatusMemoryLimitExceeded
} }
result = types.Result{ result = runner.Result{
Status: status, Status: status,
Time: userTime, Time: userTime,
Memory: userMem, Memory: userMem,
} }
if status != types.StatusNormal { if status != runner.StatusNormal {
return return
} }
} }
@ -125,13 +125,13 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
if execved { if execved {
result.ExitStatus = wstatus.ExitStatus() result.ExitStatus = wstatus.ExitStatus()
if result.ExitStatus == 0 { if result.ExitStatus == 0 {
result.Status = types.StatusNormal result.Status = runner.StatusNormal
} else { } else {
result.Status = types.StatusNonzeroExitStatus result.Status = runner.StatusNonzeroExitStatus
} }
return return
} }
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = "child process exit before execve" result.Error = "child process exit before execve"
return return
} }
@ -143,13 +143,13 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
delete(traced, pid) delete(traced, pid)
switch sig { switch sig {
case unix.SIGXCPU, unix.SIGKILL: case unix.SIGXCPU, unix.SIGKILL:
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
case unix.SIGXFSZ: case unix.SIGXFSZ:
status = types.StatusOutputLimitExceeded status = runner.StatusOutputLimitExceeded
case unix.SIGSYS: case unix.SIGSYS:
status = types.StatusDisallowedSyscall status = runner.StatusDisallowedSyscall
default: default:
status = types.StatusSignalled status = runner.StatusSignalled
} }
result.Status = status result.Status = status
result.ExitStatus = int(sig) result.ExitStatus = int(sig)
@ -165,7 +165,7 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
// Ptrace set option valid if the tracee is stopped // Ptrace set option valid if the tracee is stopped
err = setPtraceOption(pid) err = setPtraceOption(pid)
if err != nil { if err != nil {
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = err.Error() result.Error = err.Error()
return return
} }
@ -179,7 +179,7 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
// give the customized handle for syscall // give the customized handle for syscall
err := t.handleTrap(pid) err := t.handleTrap(pid)
if err != nil { if err != nil {
result.Status = types.StatusDisallowedSyscall result.Status = runner.StatusDisallowedSyscall
result.Error = err.Error() result.Error = err.Error()
return return
} }
@ -209,11 +209,11 @@ func (t *Tracer) TraceRun(c context.Context) (result types.Result) {
// check if cpu rlimit hit // check if cpu rlimit hit
switch stopSig { switch stopSig {
case unix.SIGXCPU: case unix.SIGXCPU:
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
case unix.SIGXFSZ: case unix.SIGXFSZ:
status = types.StatusOutputLimitExceeded status = runner.StatusOutputLimitExceeded
} }
if status != types.StatusNormal { if status != runner.StatusNormal {
result.Status = status result.Status = status
return return
} }
@ -263,7 +263,7 @@ func (t *Tracer) handleTrap(pid int) error {
return ctx.skipSyscall() return ctx.skipSyscall()
case TraceKill: case TraceKill:
return types.StatusDisallowedSyscall return runner.StatusDisallowedSyscall
} }
} }

33
runner/doc.go Normal file
View File

@ -0,0 +1,33 @@
// Package runner provides common interface for program runner together with
// common types including Result, Limit, Size and Status.
//
// Status
//
// Status defines the program running result status including
// Normal
// Program Error
// Resource Limit Exceeded (Time / Memory / Output)
// Unauthorized Access (Disallowed Syscall)
// Runtime Error (Signaled / Nonzero Exit Status)
// Program Runner Error
//
// Size
//
// Size defines size in bytes, underlying type is uint64 so it
// is effective to store up to EiB of size
//
// Limit
//
// Limit defines Time & Memory restriction on program runner
//
// Result
//
// Result defines program running result including
// Status, ExitStatus, Detailed Error, Time, Memory,
// SetupTime and RunningTime (in real clock)
//
// Runner
//
// General interface to run a program, including a context
// for canclation
package runner

View File

@ -1,4 +1,4 @@
package types package runner
import ( import (
"fmt" "fmt"

View File

@ -8,7 +8,7 @@ import (
"github.com/criyle/go-sandbox/pkg/seccomp/libseccomp" "github.com/criyle/go-sandbox/pkg/seccomp/libseccomp"
"github.com/criyle/go-sandbox/ptracer" "github.com/criyle/go-sandbox/ptracer"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
type tracerHandler struct { type tracerHandler struct {
@ -127,7 +127,7 @@ func (h *tracerHandler) GetSyscallName(ctx *ptracer.Context) (string, error) {
func (h *tracerHandler) HandlerDisallow(name string) error { func (h *tracerHandler) HandlerDisallow(name string) error {
if !h.Unsafe { if !h.Unsafe {
return types.StatusDisallowedSyscall return runner.StatusDisallowedSyscall
} }
return nil return nil
} }

View File

@ -5,11 +5,11 @@ import (
"github.com/criyle/go-sandbox/pkg/forkexec" "github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/ptracer" "github.com/criyle/go-sandbox/ptracer"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// Run starts the tracing process // Run starts the tracing process
func (r *Runner) Run(c context.Context) <-chan types.Result { func (r *Runner) Run(c context.Context) <-chan runner.Result {
ch := &forkexec.Runner{ ch := &forkexec.Runner{
Args: r.Args, Args: r.Args,
Env: r.Env, Env: r.Env,
@ -33,6 +33,5 @@ func (r *Runner) Run(c context.Context) <-chan types.Result {
Runner: ch, Runner: ch,
Limit: r.Limit, Limit: r.Limit,
} }
rt := tracer.Trace(c) return tracer.Trace(c)
return rt
} }

View File

@ -6,7 +6,7 @@ import (
"github.com/criyle/go-sandbox/pkg/rlimit" "github.com/criyle/go-sandbox/pkg/rlimit"
"github.com/criyle/go-sandbox/pkg/seccomp" "github.com/criyle/go-sandbox/pkg/seccomp"
"github.com/criyle/go-sandbox/ptracer" "github.com/criyle/go-sandbox/ptracer"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// Runner defines the spec to run a program safely by ptracer // Runner defines the spec to run a program safely by ptracer
@ -27,7 +27,7 @@ type Runner struct {
RLimits []rlimit.RLimit RLimits []rlimit.RLimit
// Res limit enforced by tracer // Res limit enforced by tracer
Limit types.Limit Limit runner.Limit
// Defines seccomp filter for the ptrace runner // Defines seccomp filter for the ptrace runner
// file access syscalls need to set as ActionTrace // file access syscalls need to set as ActionTrace

View File

@ -1,4 +1,4 @@
package types package runner
import ( import (
"fmt" "fmt"

View File

@ -2,11 +2,9 @@ package runner
import ( import (
"context" "context"
"github.com/criyle/go-sandbox/types"
) )
// Runner interface defines method to start running // Runner interface defines method to start running
type Runner interface { type Runner interface {
Run(context.Context) <-chan types.Result Run(context.Context) <-chan Result
} }

View File

@ -1,4 +1,4 @@
package types package runner
import "fmt" import "fmt"

View File

@ -1,4 +1,4 @@
package types package runner
// Status is the result Status // Status is the result Status
type Status int type Status int

3
runner/unshare/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package unshare implements runner that uses Linux unshare syscall & mount namespace & rlimit
// to restrict program access
package unshare

View File

@ -11,7 +11,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"github.com/criyle/go-sandbox/pkg/forkexec" "github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
const ( const (
@ -20,7 +20,16 @@ const (
) )
// Run starts the unshared process // Run starts the unshared process
func (r *Runner) Run(c context.Context) <-chan types.Result { func (r *Runner) Run(c context.Context) <-chan runner.Result {
result := make(chan runner.Result, 1)
go func() {
result <- r.trace(c)
}()
return result
}
// Trace tracks child processes
func (r *Runner) trace(c context.Context) (result runner.Result) {
ch := &forkexec.Runner{ ch := &forkexec.Runner{
Args: r.Args, Args: r.Args,
Env: r.Env, Env: r.Env,
@ -39,39 +48,29 @@ func (r *Runner) Run(c context.Context) <-chan types.Result {
SyncFunc: r.SyncFunc, SyncFunc: r.SyncFunc,
} }
result := make(chan types.Result, 1)
go func() {
result <- r.trace(c, ch)
}()
return result
}
// Trace tracks child processes
func (r *Runner) trace(c context.Context, runner *forkexec.Runner) (result types.Result) {
var ( var (
wstatus unix.WaitStatus // wait4 wait status wstatus unix.WaitStatus // wait4 wait status
rusage unix.Rusage // wait4 rusage rusage unix.Rusage // wait4 rusage
status = types.StatusNormal status = runner.StatusNormal
sTime = time.Now() // start time sTime = time.Now() // start time
fTime time.Time // finish time for setup fTime time.Time // finish time for setup
) )
// Start the runner // Start the runner
pgid, err := runner.Start() pgid, err := ch.Start()
r.println("Starts: ", pgid, err) r.println("Starts: ", pgid, err)
if err != nil { if err != nil {
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = err.Error() result.Error = err.Error()
return return
} }
cc, cancel := context.WithCancel(c) ctx, cancel := context.WithCancel(c)
defer cancel() defer cancel()
// handle cancel // handle cancel
go func() { go func() {
<-cc.Done() <-ctx.Done()
killAll(pgid) killAll(pgid)
}() }()
@ -91,37 +90,37 @@ func (r *Runner) trace(c context.Context, runner *forkexec.Runner) (result types
} }
r.println("wait4: ", wstatus) r.println("wait4: ", wstatus)
if err != nil { if err != nil {
result.Status = types.StatusRunnerError result.Status = runner.StatusRunnerError
result.Error = err.Error() result.Error = err.Error()
return return
} }
// update resource usage and check against limits // update resource usage and check against limits
userTime := time.Duration(rusage.Utime.Nano()) // ns userTime := time.Duration(rusage.Utime.Nano()) // ns
userMem := types.Size(rusage.Maxrss << 10) // bytes userMem := runner.Size(rusage.Maxrss << 10) // bytes
// check tle / mle // check tle / mle
if userTime > r.Limit.TimeLimit { if userTime > r.Limit.TimeLimit {
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
} }
if userMem > r.Limit.MemoryLimit { if userMem > r.Limit.MemoryLimit {
status = types.StatusMemoryLimitExceeded status = runner.StatusMemoryLimitExceeded
} }
result = types.Result{ result = runner.Result{
Status: status, Status: status,
Time: userTime, Time: userTime,
Memory: userMem, Memory: userMem,
} }
if status != types.StatusNormal { if status != runner.StatusNormal {
return return
} }
switch { switch {
case wstatus.Exited(): case wstatus.Exited():
result.Status = types.StatusNormal result.Status = runner.StatusNormal
result.ExitStatus = wstatus.ExitStatus() result.ExitStatus = wstatus.ExitStatus()
if result.ExitStatus != 0 { if result.ExitStatus != 0 {
result.Status = types.StatusNonzeroExitStatus result.Status = runner.StatusNonzeroExitStatus
} }
return return
@ -129,13 +128,13 @@ func (r *Runner) trace(c context.Context, runner *forkexec.Runner) (result types
sig := wstatus.Signal() sig := wstatus.Signal()
switch sig { switch sig {
case unix.SIGXCPU, unix.SIGKILL: case unix.SIGXCPU, unix.SIGKILL:
status = types.StatusTimeLimitExceeded status = runner.StatusTimeLimitExceeded
case unix.SIGXFSZ: case unix.SIGXFSZ:
status = types.StatusOutputLimitExceeded status = runner.StatusOutputLimitExceeded
case unix.SIGSYS: case unix.SIGSYS:
status = types.StatusDisallowedSyscall status = runner.StatusDisallowedSyscall
default: default:
status = types.StatusSignalled status = runner.StatusSignalled
} }
result.Status = status result.Status = status
result.ExitStatus = int(sig) result.ExitStatus = int(sig)

View File

@ -4,7 +4,7 @@ import (
"github.com/criyle/go-sandbox/pkg/mount" "github.com/criyle/go-sandbox/pkg/mount"
"github.com/criyle/go-sandbox/pkg/rlimit" "github.com/criyle/go-sandbox/pkg/rlimit"
"github.com/criyle/go-sandbox/pkg/seccomp" "github.com/criyle/go-sandbox/pkg/seccomp"
"github.com/criyle/go-sandbox/types" "github.com/criyle/go-sandbox/runner"
) )
// Runner runs program in unshared namespaces // Runner runs program in unshared namespaces
@ -26,7 +26,7 @@ type Runner struct {
RLimits []rlimit.RLimit RLimits []rlimit.RLimit
// Resource limit enforced by tracer // Resource limit enforced by tracer
Limit types.Limit Limit runner.Limit
// Seccomp defines the seccomp filter attach to the process (should be whitelist only) // Seccomp defines the seccomp filter attach to the process (should be whitelist only)
Seccomp seccomp.Filter Seccomp seccomp.Filter