mirror of
https://github.com/criyle/go-sandbox.git
synced 2025-11-04 14:49:53 +08:00
try to fix unshare user namespace
This commit is contained in:
parent
c126eecfc7
commit
4a120f9a3d
@ -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] <args>\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)
|
||||
|
||||
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
42
forkexec/userns.go
Normal file
42
forkexec/userns.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
70
rununshared/default.go
Normal file
70
rununshared/default.go
Normal file
@ -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
|
||||
}
|
||||
158
rununshared/run.go
Normal file
158
rununshared/run.go
Normal file
@ -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...)
|
||||
}
|
||||
}
|
||||
38
rununshared/rununshared.go
Normal file
38
rununshared/rununshared.go
Normal file
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user