decoupling rununshared and tracer

This commit is contained in:
criyle 2019-07-20 15:20:42 -07:00
parent 3ed7b771c8
commit b8c180569f
18 changed files with 149 additions and 151 deletions

View File

@ -22,6 +22,7 @@ New Features:
2. More architectures (arm32, arm64, x86)
3. Allow multiple traced programs in different threads
4. Allow pipes as input / output files
5. Use Linux Namespace to isolate file access (elimilate ptrace)
Default file access action:
@ -34,13 +35,15 @@ Default file access action:
Packages:
- seccomp: provides utility function that wrappers libseccomp
- mount: provides utility function that wrappers mount syscall
- rlimit: provides utility function that defines rlimit syscall
- forkexec: fork-exec provides mount, unshare, ptrace, seccomp, capset before exec
- tracer: ptrace tracer and provides syscall trap filter context
- runprogram: wrapper to call forkexec and trecer
- rununshared: wrapper to call forkexec and unshared namespaces
- runconfig: defines arch & language specified trace condition
- runconfig: defines arch & language specified trace condition for seccomp and ptrace
- types: general runtime specs
- mount: provides utility function that wrappers mount syscall
- rlimit: provides utility function that defines rlimit syscall
- specs: provides general res / result data structures
Executable:
@ -52,5 +55,4 @@ Configuations:
TODO:
1. Use Linux Namespace to isolate file access (elimilate ptrace)
2. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4 rusage)
1. Use Linux Control Groups to limit & acct CPU & memory (elimilate wait4 rusage)

View File

@ -6,11 +6,11 @@ import (
"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"
"github.com/criyle/go-judger/types/rlimit"
"github.com/criyle/go-judger/types/specs"
)
const (
@ -19,7 +19,7 @@ const (
// Runner can be ptraced runner or namespaced runner
type Runner interface {
Start() (tracer.TraceResult, error)
Start() (specs.TraceResult, error)
}
func printUsage() {
@ -122,7 +122,7 @@ func main() {
WorkDir: "/w",
Files: fds,
RLimits: rlims,
ResLimits: tracer.ResLimit{
ResLimits: specs.ResLimit{
TimeLimit: timeLimit * 1e3,
RealTimeLimit: realTimeLimit * 1e3,
MemoryLimit: memoryLimit << 10,
@ -143,7 +143,7 @@ func main() {
Env: []string{pathEnv},
WorkDir: workPath,
RLimits: rlims,
TraceLimit: runprogram.TraceLimit{
TraceLimit: specs.ResLimit{
TimeLimit: timeLimit * 1e3,
RealTimeLimit: realTimeLimit * 1e3,
MemoryLimit: memoryLimit << 10,
@ -173,17 +173,16 @@ 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)
c, ok := err.(specs.TraceCode)
if !ok {
c = tracer.TraceCodeFatal
c = specs.TraceCodeFatal
}
// Handle fatal error from trace
fmt.Fprintf(f, "%d %d %d %d\n", int(c), rt.UserTime, rt.UserMem, rt.ExitCode)
if c == tracer.TraceCodeFatal {
if c == specs.TraceCodeFatal {
os.Exit(1)
}
} else {

View File

@ -77,6 +77,7 @@ func (r *Runner) Start() (int, error) {
if err1 != 0 || pid != 0 {
// restore all signals
afterFork()
syscall.ForkLock.Unlock()
// synchronize with child for uid / gid map
if unshareUser {
@ -89,7 +90,6 @@ func (r *Runner) Start() (int, error) {
unix.Close(p[1])
}
syscall.ForkLock.Unlock()
if err1 != 0 {
return int(pid), syscall.Errno(err1)
}

View File

@ -3,7 +3,7 @@ package forkexec
import (
"syscall"
"github.com/criyle/go-judger/mount"
"github.com/criyle/go-judger/types/mount"
)
// prepareExec prepares execve parameters

View File

@ -1,11 +0,0 @@
package forkexec
import "syscall"
// RLimit is the resource limits defined by Linux setrlimit
type RLimit struct {
// Res is the resource type (e.g. syscall.RLIMIT_CPU)
Res int
// Rlim is the limit applied to that resource
Rlim syscall.Rlimit
}

View File

@ -5,7 +5,8 @@ package forkexec
import (
"syscall"
"github.com/criyle/go-judger/mount"
"github.com/criyle/go-judger/types/mount"
"github.com/criyle/go-judger/types/rlimit"
)
// Runner is the RunProgramConfig including the exec path, argv
@ -16,10 +17,10 @@ type Runner struct {
Args []string
Env []string
// Resource limit set by set rlimit
RLimits []RLimit
// POSIX Resource limit set by set rlimit
RLimits []rlimit.RLimit
// file disriptors for new process, from 0 to len - 1
// file disriptors map for new process, from 0 to len - 1
Files []uintptr
// work path set by chdir(dir) (current working directory for child)

View File

@ -26,7 +26,7 @@ func writeIDMaps(pid int) error {
// writeFile writes file
func writeFile(path string, content []byte) error {
fd, err := unix.Open(path, unix.O_RDWR, 0)
fd, err := unix.Open(path, unix.O_RDWR|unix.O_CLOEXEC, 0)
if err != nil {
return err
}

View File

@ -9,6 +9,7 @@ import (
libseccomp "github.com/seccomp/libseccomp-golang"
"github.com/criyle/go-judger/tracer"
"github.com/criyle/go-judger/types/specs"
)
type tracerHandler struct {
@ -121,7 +122,7 @@ func (h *tracerHandler) GetSyscallName(ctx *tracer.Context) (string, error) {
func (h *tracerHandler) HandlerDisallow(name string) error {
if !h.Unsafe {
return tracer.TraceCodeBan
return specs.TraceCodeBan
}
return nil
}

View File

@ -3,8 +3,8 @@ package runprogram
import (
"syscall"
"github.com/criyle/go-judger/rlimit"
"github.com/criyle/go-judger/tracer"
"github.com/criyle/go-judger/types/rlimit"
"github.com/criyle/go-judger/types/specs"
)
// RunProgram defines the spec to run a program safely
@ -22,7 +22,7 @@ type RunProgram struct {
RLimits rlimit.RLimits
// Res limit enforced by tracer
TraceLimit TraceLimit
TraceLimit specs.ResLimit
// Allowed / Traced syscall names
// Notice: file access syscalls should be traced
@ -38,14 +38,11 @@ type RunProgram struct {
ShowDetails, Unsafe bool
}
// TraceLimit defines the limits enforced by tracer
type TraceLimit tracer.ResLimit
// TraceAction defines action against a syscall check
type TraceAction int
// BanRet defines the return value for a syscall ban acction
var BanRet syscall.Errno = syscall.EACCES
var BanRet = syscall.EACCES
// TraceAllow allow the access, trace ban ignores the syscall and set the
// return value to BanRet, TraceKill stops the trace action

View File

@ -6,10 +6,11 @@ import (
"github.com/criyle/go-judger/forkexec"
"github.com/criyle/go-judger/seccomp"
"github.com/criyle/go-judger/tracer"
"github.com/criyle/go-judger/types/specs"
)
// Start starts the tracing process
func (r *RunProgram) Start() (rt tracer.TraceResult, err error) {
func (r *RunProgram) Start() (rt specs.TraceResult, err error) {
// build seccomp filter
filter, err := buildFilter(r.ShowDetails, r.SyscallAllowed, r.SyscallTraced)
if err != nil {
@ -39,7 +40,7 @@ func (r *RunProgram) Start() (rt tracer.TraceResult, err error) {
Unsafe: r.Unsafe,
Handler: r.Handler,
}
return tracer.Trace(th, ch, tracer.ResLimit(r.TraceLimit))
return tracer.Trace(th, ch, specs.ResLimit(r.TraceLimit))
}
// build filter builds the libseccomp filter according to the allow, trace and show details

View File

@ -3,7 +3,7 @@ package rununshared
import (
"os"
"github.com/criyle/go-judger/mount"
"github.com/criyle/go-judger/types/mount"
"golang.org/x/sys/unix"
)

View File

@ -8,7 +8,7 @@ import (
"github.com/criyle/go-judger/forkexec"
"github.com/criyle/go-judger/seccomp"
"github.com/criyle/go-judger/tracer"
"github.com/criyle/go-judger/types/specs"
libseccomp "github.com/seccomp/libseccomp-golang"
"golang.org/x/sys/unix"
)
@ -20,7 +20,7 @@ const (
)
// Start starts the unshared process
func (r *RunUnshared) Start() (rt tracer.TraceResult, err error) {
func (r *RunUnshared) Start() (rt specs.TraceResult, err error) {
filter, err := seccomp.BuildFilter(libseccomp.ActKill, libseccomp.ActTrap, r.SyscallAllowed, []string{})
if err != nil {
println(err)
@ -52,12 +52,12 @@ func (r *RunUnshared) Start() (rt tracer.TraceResult, err error) {
}
// Trace tracks child processes
func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult, err error) {
func (r *RunUnshared) Trace(runner *forkexec.Runner) (result specs.TraceResult, err error) {
var (
wstatus unix.WaitStatus // wait4 wait status
rusage unix.Rusage // wait4 rusage
tle = false
status = tracer.TraceCodeNormal
status = specs.TraceCodeNormal
sTime = time.Now().UnixNano() // start time
fTime int64 // finish time for setup
)
@ -66,7 +66,7 @@ func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult,
pgid, err := runner.Start()
r.println("Starts: ", pgid, err)
if err != nil {
result.TraceStatus = tracer.TraceCodeRE
result.TraceStatus = specs.TraceCodeRE
return result, err
}
// Set real time limit, kill process after it
@ -78,7 +78,7 @@ func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult,
defer func() {
timer.Stop()
if tle {
err = tracer.TraceCodeTLE
err = specs.TraceCodeTLE
}
// kill all tracee upon return
killAll(pgid)
@ -94,7 +94,7 @@ func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult,
pid, err := unix.Wait4(pgid, &wstatus, unix.WALL, &rusage)
r.println("wait4: ", wstatus)
if err != nil {
return result, tracer.TraceCodeFatal
return result, specs.TraceCodeFatal
}
// update resource usage and check against limits
@ -103,17 +103,17 @@ func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult,
// check tle / mle
if userTime > r.ResLimits.TimeLimit {
status = tracer.TraceCodeTLE
status = specs.TraceCodeTLE
}
if userMem > r.ResLimits.MemoryLimit {
status = tracer.TraceCodeMLE
status = specs.TraceCodeMLE
}
result = tracer.TraceResult{
result = specs.TraceResult{
UserTime: userTime,
UserMem: userMem,
TraceStatus: status,
}
if status != tracer.TraceCodeNormal {
if status != specs.TraceCodeNormal {
return result, status
}
@ -125,13 +125,13 @@ func (r *RunUnshared) Trace(runner *forkexec.Runner) (result tracer.TraceResult,
sig := wstatus.Signal()
switch sig {
case unix.SIGXCPU:
status = tracer.TraceCodeTLE
status = specs.TraceCodeTLE
case unix.SIGXFSZ:
status = tracer.TraceCodeOLE
status = specs.TraceCodeOLE
case unix.SIGSYS:
status = tracer.TraceCodeBan
status = specs.TraceCodeBan
default:
status = tracer.TraceCodeRE
status = specs.TraceCodeRE
}
result.TraceStatus = status
return result, status

View File

@ -1,9 +1,9 @@
package rununshared
import (
"github.com/criyle/go-judger/mount"
"github.com/criyle/go-judger/rlimit"
"github.com/criyle/go-judger/tracer"
"github.com/criyle/go-judger/types/mount"
"github.com/criyle/go-judger/types/rlimit"
"github.com/criyle/go-judger/types/specs"
)
// RunUnshared runs program in unshared namespaces
@ -22,7 +22,7 @@ type RunUnshared struct {
RLimits rlimit.RLimits
// Resource limit enforced by tracer
ResLimits tracer.ResLimit
ResLimits specs.ResLimit
// Allowed syscall names
SyscallAllowed []string

View File

@ -12,70 +12,12 @@ const (
TraceKill
)
// TraceCode is the error type
type TraceCode int
// Different end condtion
const (
TraceCodeNormal TraceCode = iota // 0
TraceCodeInvalid // 1
TraceCodeRE // 2
TraceCodeMLE // 3
TraceCodeTLE // 4
TraceCodeOLE // 5
TraceCodeBan // 6
TraceCodeFatal // 7
)
func (t TraceCode) Error() string {
switch t {
case TraceCodeNormal:
return ""
case TraceCodeRE:
return "runtime error"
case TraceCodeTLE:
return "time limit exceeded"
case TraceCodeMLE:
return "memory limit exceeded"
case TraceCodeOLE:
return "output limit exceeded"
case TraceCodeBan:
return "syscall banned"
case TraceCodeFatal:
return "handle failed"
default:
return "invalid"
}
}
// TraceStat is the time usages in ns
type TraceStat struct {
SetUpTime int64
RunningTime int64
}
// TraceResult is the result returned by strat trace
type TraceResult struct {
UserTime uint64 // used user CPU time (in ms)
UserMem uint64 // used user memory (in kb)
ExitCode int // exit code
TraceStatus TraceCode // the final status for the process
TraceStat // collects time for the process
}
// Runner represents the process runner
type Runner interface {
// Starts starts the child process and return pid and error if failed
Start() (int, error)
}
// ResLimit represents the resource limit for traced process
type ResLimit struct {
TimeLimit uint64 // user CPU time limit (in ms)
RealTimeLimit uint64 // sig_kill will force the process to exit after this limit (in ms)
MemoryLimit uint64 // user memory limit (in kB)
}
// Handler defines customized handler for traced syscall
type Handler interface {
Handle(*Context) TraceAction

View File

@ -4,6 +4,7 @@ import (
"runtime"
"time"
"github.com/criyle/go-judger/types/specs"
unix "golang.org/x/sys/unix"
)
@ -17,7 +18,7 @@ const (
// Trace traces all child process that created by runner
// this function should called only once and in the same thread that
// exec tracee
func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult, err error) {
func Trace(handler Handler, runner Runner, limits specs.ResLimit) (result specs.TraceResult, err error) {
var (
wstatus unix.WaitStatus // wait4 wait status
rusage unix.Rusage // wait4 rusage
@ -39,7 +40,7 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
handler.Debug("tracer started: ", pgid, err)
if err != nil {
handler.Debug("start tracee failed: ", err)
result.TraceStatus = TraceCodeRE
result.TraceStatus = specs.TraceCodeRE
return result, err
}
@ -54,11 +55,11 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
defer func() {
timer.Stop()
if tle {
err = TraceCodeTLE
err = specs.TraceCodeTLE
}
if err2 := recover(); err2 != nil {
handler.Debug(err2)
err = TraceCodeFatal
err = specs.TraceCodeFatal
}
// kill all tracee upon return
killAll(pgid)
@ -78,11 +79,11 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
}
if err != nil {
handler.Debug("wait4 failed: ", err)
return result, TraceCodeFatal
return result, specs.TraceCodeFatal
}
handler.Debug("------ ", pid, " ------")
status := TraceCodeNormal
status := specs.TraceCodeNormal
if pid == pgid {
if initMem == 0 {
initMem = uint64(rusage.Maxrss)
@ -94,17 +95,17 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
// check tle / mle
if userTime > limits.TimeLimit {
status = TraceCodeTLE
status = specs.TraceCodeTLE
}
if userMem > limits.MemoryLimit {
status = TraceCodeMLE
status = specs.TraceCodeMLE
}
result = TraceResult{
result = specs.TraceResult{
UserTime: userTime,
UserMem: userMem,
TraceStatus: status,
}
if status != TraceCodeNormal {
if status != specs.TraceCodeNormal {
return result, status
}
}
@ -119,8 +120,8 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
result.ExitCode = wstatus.ExitStatus()
return result, nil
}
result.TraceStatus = TraceCodeFatal
return result, TraceCodeFatal
result.TraceStatus = specs.TraceCodeFatal
return result, specs.TraceCodeFatal
}
case wstatus.Signaled():
@ -130,13 +131,13 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
delete(traced, pid)
switch sig {
case unix.SIGXCPU:
status = TraceCodeTLE
status = specs.TraceCodeTLE
case unix.SIGXFSZ:
status = TraceCodeOLE
status = specs.TraceCodeOLE
case unix.SIGSYS:
status = TraceCodeBan
status = specs.TraceCodeBan
default:
status = TraceCodeRE
status = specs.TraceCodeRE
}
result.TraceStatus = status
return result, status
@ -151,7 +152,7 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
// Ptrace set option valid if the tracee is stopped
err = setPtraceOption(pid)
if err != nil {
result.TraceStatus = TraceCodeFatal
result.TraceStatus = specs.TraceCodeFatal
return result, err
}
}
@ -164,7 +165,7 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
// give the customized handle for syscall
err := handleTrap(handler, pid)
if err != nil {
result.TraceStatus = TraceCodeBan
result.TraceStatus = specs.TraceCodeBan
return result, err
}
} else {
@ -193,11 +194,11 @@ func Trace(handler Handler, runner Runner, limits ResLimit) (result TraceResult,
// check if cpu rlimit hit
switch stopSig {
case unix.SIGXCPU:
status = TraceCodeTLE
status = specs.TraceCodeTLE
case unix.SIGXFSZ:
status = TraceCodeOLE
status = specs.TraceCodeOLE
}
if status != TraceCodeNormal {
if status != specs.TraceCodeNormal {
result.TraceStatus = status
return result, status
}
@ -246,7 +247,7 @@ func handleTrap(handler Handler, pid int) error {
return ctx.skipSyscall()
case TraceKill:
return TraceCodeBan
return specs.TraceCodeBan
}
}

View File

@ -2,8 +2,6 @@ package rlimit
import (
"syscall"
"github.com/criyle/go-judger/forkexec"
)
// RLimits defines the rlimit applied by setrlimit syscall to traced process
@ -16,45 +14,53 @@ type RLimits struct {
AddressSpace uint64 // in kb
}
// RLimit is the resource limits defined by Linux setrlimit
type RLimit struct {
// Res is the resource type (e.g. syscall.RLIMIT_CPU)
Res int
// Rlim is the limit applied to that resource
Rlim syscall.Rlimit
}
func getRlimit(cur, max uint64) syscall.Rlimit {
return syscall.Rlimit{Cur: uint64(cur), Max: uint64(max)}
}
// PrepareRLimit creates rlimit structures for tracee
// TimeLimit in s, SizeLimit in byte
func (r *RLimits) PrepareRLimit() []forkexec.RLimit {
var ret []forkexec.RLimit
func (r *RLimits) PrepareRLimit() []RLimit {
var ret []RLimit
if r.CPU > 0 {
cpuHard := r.CPUHard
if cpuHard < r.CPU {
cpuHard = r.CPU
}
ret = append(ret, forkexec.RLimit{
ret = append(ret, RLimit{
Res: syscall.RLIMIT_CPU,
Rlim: getRlimit(r.CPU, cpuHard),
})
}
if r.Data > 0 {
ret = append(ret, forkexec.RLimit{
ret = append(ret, RLimit{
Res: syscall.RLIMIT_DATA,
Rlim: getRlimit(r.Data<<10, r.Data<<10),
})
}
if r.FileSize > 0 {
ret = append(ret, forkexec.RLimit{
ret = append(ret, RLimit{
Res: syscall.RLIMIT_FSIZE,
Rlim: getRlimit(r.FileSize<<10, r.FileSize<<10),
})
}
if r.Stack > 0 {
ret = append(ret, forkexec.RLimit{
ret = append(ret, RLimit{
Res: syscall.RLIMIT_STACK,
Rlim: getRlimit(r.Stack<<10, r.Stack<<10),
})
}
if r.AddressSpace > 0 {
ret = append(ret, forkexec.RLimit{
ret = append(ret, RLimit{
Res: syscall.RLIMIT_AS,
Rlim: getRlimit(r.AddressSpace<<10, r.AddressSpace<<10),
})

59
types/specs/specs.go Normal file
View File

@ -0,0 +1,59 @@
package specs
// TraceCode is the error type
type TraceCode int
// Different end condtion
const (
TraceCodeNormal TraceCode = iota // 0
TraceCodeInvalid // 1
TraceCodeRE // 2
TraceCodeMLE // 3
TraceCodeTLE // 4
TraceCodeOLE // 5
TraceCodeBan // 6
TraceCodeFatal // 7
)
func (t TraceCode) Error() string {
switch t {
case TraceCodeNormal:
return ""
case TraceCodeRE:
return "runtime error"
case TraceCodeTLE:
return "time limit exceeded"
case TraceCodeMLE:
return "memory limit exceeded"
case TraceCodeOLE:
return "output limit exceeded"
case TraceCodeBan:
return "syscall banned"
case TraceCodeFatal:
return "handle failed"
default:
return "invalid"
}
}
// TraceStat is the time usages in ns
type TraceStat struct {
SetUpTime int64
RunningTime int64
}
// TraceResult is the result returned by strat trace
type TraceResult struct {
UserTime uint64 // used user CPU time (in ms)
UserMem uint64 // used user memory (in kb)
ExitCode int // exit code
TraceStatus TraceCode // the final status for the process
TraceStat // collects time for the process
}
// ResLimit represents the resource limit for traced process
type ResLimit struct {
TimeLimit uint64 // user CPU time limit (in ms)
RealTimeLimit uint64 // sig_kill will force the process to exit after this limit (in ms)
MemoryLimit uint64 // user memory limit (in kB)
}