try to fix unshare user namespace

This commit is contained in:
criyle 2019-07-14 02:11:23 -07:00
parent c126eecfc7
commit 4a120f9a3d
13 changed files with 499 additions and 52 deletions

View File

@ -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)

View File

@ -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,
}
)

View File

@ -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)

View File

@ -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
}

View File

@ -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
View 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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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...)
}
}

View 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
}