mirror of
https://github.com/criyle/go-sandbox.git
synced 2025-11-04 14:49:53 +08:00
291 lines
7.1 KiB
Go
291 lines
7.1 KiB
Go
package tracer
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
unix "golang.org/x/sys/unix"
|
|
)
|
|
|
|
// MsgDisallow, Msghandle defines the action needed when traped by
|
|
// seccomp filter
|
|
const (
|
|
MsgDisallow int16 = iota + 1
|
|
MsgHandle
|
|
)
|
|
|
|
var (
|
|
// ShowDetails is switch to trun on / off whether to show log message
|
|
ShowDetails bool
|
|
// Unsafe determines whether to terminate tracing when bad syscall caught
|
|
Unsafe bool
|
|
)
|
|
|
|
// 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, runners []Runner, limits ResLimit, timeout int64) (results []TraceResult, err error) {
|
|
var (
|
|
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 = make(map[int]bool) // store whether a runner process have successfully execvd
|
|
pidmap = make(map[int]int) // pid -> index map
|
|
running = len(runners) // total number of remained runner process
|
|
)
|
|
results = make([]TraceResult, len(runners))
|
|
|
|
// make this thread exit after trace, ensure no process escaped
|
|
runtime.LockOSThread()
|
|
|
|
// Starts all runners
|
|
for i, r := range runners {
|
|
pid, err := r.Start()
|
|
println("tracer started: ", pid, err)
|
|
if err != nil {
|
|
results[i].TraceStatus = TraceCodeRE
|
|
return results, err
|
|
}
|
|
pidmap[pid] = i
|
|
}
|
|
|
|
// Set real time limit, kill process after it
|
|
timer := time.AfterFunc(time.Duration(timeout), func() {
|
|
tle = true
|
|
killAll(traced)
|
|
})
|
|
|
|
// handler potential panic and tle
|
|
// also ensure processes was well terminated
|
|
defer func() {
|
|
timer.Stop()
|
|
if tle {
|
|
err = TraceCodeTLE
|
|
}
|
|
if err2 := recover(); err2 != nil {
|
|
println(err2)
|
|
err = TraceCodeFatal
|
|
}
|
|
// kill all tracee upon return
|
|
killAll(traced)
|
|
collectZombie()
|
|
}()
|
|
|
|
// trace unixs
|
|
for {
|
|
// Wait for all child
|
|
pid, err := unix.Wait4(-1, &wstatus, unix.WALL, &rusage)
|
|
if err != nil {
|
|
println("wait4 failed: ", err)
|
|
return results, TraceCodeFatal
|
|
}
|
|
println("------ ", pid, " ------")
|
|
// Set option if the process is newly forked
|
|
if !traced[pid] {
|
|
println("set ptrace option")
|
|
traced[pid] = true
|
|
// Ptrace set option valid if the tracee is stopped
|
|
err = setPtraceOption(pid)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
}
|
|
|
|
// update resource usage and check against limits
|
|
userTime := uint(rusage.Utime.Sec*1e3 + rusage.Utime.Usec/1e3) // ms
|
|
userMem := uint(rusage.Maxrss) // kb
|
|
idx, inside := pidmap[pid] // index
|
|
status := TraceCodeNormal // check limit
|
|
|
|
// check tle / mle
|
|
if userTime > limits.TimeLimit {
|
|
status = TraceCodeTLE
|
|
}
|
|
if userMem > limits.MemoryLimit {
|
|
status = TraceCodeMLE
|
|
}
|
|
if inside {
|
|
results[idx] = TraceResult{
|
|
UserTime: userTime,
|
|
UserMem: userMem,
|
|
TraceStatus: status,
|
|
}
|
|
}
|
|
if status != TraceCodeNormal {
|
|
return results, status
|
|
}
|
|
|
|
// check process status
|
|
switch {
|
|
case wstatus.Exited():
|
|
delete(traced, pid)
|
|
println("process exited: ", pid, wstatus.ExitStatus())
|
|
if inside {
|
|
if execved[pid] {
|
|
results[idx].ExitCode = wstatus.ExitStatus()
|
|
if running--; running == 0 {
|
|
return results, nil
|
|
}
|
|
} else {
|
|
results[idx].TraceStatus = TraceCodeFatal
|
|
return results, TraceCodeFatal
|
|
}
|
|
}
|
|
|
|
case wstatus.Signaled():
|
|
sig := wstatus.Signal()
|
|
println("ptrace signaled: ", sig)
|
|
if inside {
|
|
switch sig {
|
|
case unix.SIGXCPU:
|
|
status = TraceCodeTLE
|
|
case unix.SIGXFSZ:
|
|
status = TraceCodeOLE
|
|
case unix.SIGSYS:
|
|
status = TraceCodeBan
|
|
default:
|
|
status = TraceCodeRE
|
|
}
|
|
results[idx].TraceStatus = status
|
|
return results, status
|
|
}
|
|
delete(traced, pid)
|
|
|
|
case wstatus.Stopped():
|
|
if stopSig := wstatus.StopSignal(); stopSig == unix.SIGTRAP {
|
|
switch trapCause := wstatus.TrapCause(); trapCause {
|
|
case unix.PTRACE_EVENT_SECCOMP:
|
|
if !inside || execved[pid] {
|
|
// give the customized handle for syscall
|
|
err := handleTrap(handler, pid)
|
|
if err != nil {
|
|
if inside {
|
|
results[idx].TraceStatus = TraceCodeBan
|
|
}
|
|
return results, err
|
|
}
|
|
} else {
|
|
println("ptrace seccomp before execve (should be the execve syscall)")
|
|
}
|
|
|
|
case unix.PTRACE_EVENT_CLONE:
|
|
println("ptrace stop clone")
|
|
case unix.PTRACE_EVENT_VFORK:
|
|
println("ptrace stop vfork")
|
|
case unix.PTRACE_EVENT_FORK:
|
|
println("ptrace stop fork")
|
|
case unix.PTRACE_EVENT_EXEC:
|
|
// forked tracee have successfully called execve
|
|
execved[pid] = true
|
|
println("ptrace stop exec")
|
|
|
|
default:
|
|
println("ptrace unexpected trap cause: ", trapCause)
|
|
}
|
|
} else {
|
|
// Likely encountered SIGSEGV (segment violation)
|
|
if stopSig != unix.SIGSTOP {
|
|
println("ptrace unexpected stop signal: ", stopSig)
|
|
if inside {
|
|
results[idx].TraceStatus = TraceCodeRE
|
|
}
|
|
return results, TraceCodeRE
|
|
}
|
|
println("ptrace stopped")
|
|
}
|
|
unix.PtraceCont(pid, 0)
|
|
|
|
default:
|
|
// should never happen
|
|
println("unexpected wait status: ", wstatus)
|
|
unix.PtraceCont(pid, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleTrap handles the seccomp trap including the custom handle
|
|
func handleTrap(handler Handler, pid int) error {
|
|
println("seccomp traced")
|
|
msg, err := unix.PtraceGetEventMsg(pid)
|
|
if err != nil {
|
|
println(err)
|
|
return err
|
|
}
|
|
switch int16(msg) {
|
|
case MsgDisallow:
|
|
ctx, err := getTrapContext(pid)
|
|
if err != nil {
|
|
println(err)
|
|
return err
|
|
}
|
|
if ShowDetails {
|
|
syscallName, err := handler.GetSyscallName(ctx)
|
|
println("disallowed syscall: ", ctx.SyscallNo(), syscallName, err)
|
|
}
|
|
if !Unsafe {
|
|
return TraceCodeBan
|
|
}
|
|
|
|
case MsgHandle:
|
|
if handler != nil {
|
|
ctx, err := getTrapContext(pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
act := handler.Handle(ctx)
|
|
|
|
switch act {
|
|
case TraceBan:
|
|
// Set the syscallno to -1 and return value into register. https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt
|
|
return ctx.skipSyscall()
|
|
|
|
case TraceKill:
|
|
return TraceCodeBan
|
|
}
|
|
}
|
|
|
|
default:
|
|
// undefined seccomp message, possible set up filter wrong
|
|
println("unknown seccomp trap message: ", msg)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// set Ptrace option that set up seccomp, exit kill and all mult-process actions
|
|
func setPtraceOption(pid int) error {
|
|
return unix.PtraceSetOptions(pid, unix.PTRACE_O_TRACESECCOMP|unix.PTRACE_O_EXITKILL|
|
|
unix.PTRACE_O_TRACEFORK|unix.PTRACE_O_TRACECLONE|unix.PTRACE_O_TRACEEXEC|unix.PTRACE_O_TRACEVFORK)
|
|
}
|
|
|
|
// println only print when debug flag is on
|
|
func println(v ...interface{}) {
|
|
if ShowDetails {
|
|
fmt.Fprintln(os.Stderr, v...)
|
|
}
|
|
}
|
|
|
|
// kill all tracee according to pids
|
|
func killAll(pids map[int]bool) {
|
|
for p := range pids {
|
|
println("kill: ", p)
|
|
unix.Kill(p, unix.SIGKILL)
|
|
}
|
|
}
|
|
|
|
// collect died child processes
|
|
func collectZombie() {
|
|
// collect zombies
|
|
for {
|
|
var wstatus unix.WaitStatus
|
|
if p, err := unix.Wait4(-1, &wstatus, unix.WALL|unix.WNOWAIT, nil); err != nil {
|
|
break
|
|
} else {
|
|
println("collect: ", p)
|
|
}
|
|
}
|
|
}
|