diff --git a/README.md b/README.md index 29519ec..9b94a44 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,11 @@ Packages: - seccomp: provides utility function that wrappers libseccomp - mount: provides utility function that wrappers mount syscall -- forkexec: fork-exec provides unshare, ptrace, seccomp before exec +- 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 Executable: diff --git a/forkexec/fork.go b/forkexec/fork.go index 6155247..bb70738 100644 --- a/forkexec/fork.go +++ b/forkexec/fork.go @@ -24,8 +24,10 @@ func afterForkInChild() //go:norace func (r *Runner) Start() (int, error) { var ( - err1 syscall.Errno - r1 uintptr + err1, err2 syscall.Errno + r1 uintptr + p [2]int + unshareUser = r.UnshareFlags&unix.CLONE_NEWUSER == unix.CLONE_NEWUSER ) argv0, argv, envv, err := prepareExec(r.Args, r.Env) @@ -51,8 +53,12 @@ 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) + if unshareUser { + err = unix.Pipe2(p[:], unix.O_CLOEXEC) + if err != nil { + return 0, err + } + } // similar to exec_linux, avoid side effect by shuffling around fd, nextfd := prepareFds(r.Files) @@ -71,8 +77,19 @@ 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 { + unix.Close(p[0]) + err = writeIDMaps(int(pid)) + if err != nil { + err2 = err.(syscall.Errno) + } + syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p[1]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2))) + unix.Close(p[1]) + } + + syscall.ForkLock.Unlock() if err1 != 0 { return int(pid), syscall.Errno(err1) } @@ -83,6 +100,32 @@ func (r *Runner) Start() (int, error) { afterForkInChild() // Notice: cannot call any GO functions beyond this point + // If usernamespace is unshared, uid map and gid map is required to create folders + // and files + // We need parent to setup uid_map / gid_map for us since we do not have capabilities + // in the original namespace + // At the same time, socket pair / pipe sychronization is required as well + if unshareUser { + if _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(p[1]), 0, 0); err1 != 0 { + goto childerror + } + r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(p[0]), uintptr(unsafe.Pointer(&err2)), unsafe.Sizeof(err2)) + if err1 != 0 { + goto childerror + } + if r1 != unsafe.Sizeof(err2) { + err1 = syscall.EINVAL + goto childerror + } + if err2 != 0 { + err1 = err2 + goto childerror + } + if _, _, err1 = syscall.RawSyscall(syscall.SYS_CLOSE, uintptr(p[0]), 0, 0); err1 != 0 { + goto childerror + } + } + // Get pid of child pid, _, err1 = syscall.RawSyscall(syscall.SYS_GETPID, 0, 0, 0) if err1 != 0 { @@ -141,31 +184,6 @@ func (r *Runner) Start() (int, error) { } } - // 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, "") @@ -301,14 +319,6 @@ func (r *Runner) Start() (int, error) { } } - // stop before execve syscall - if r.StopBeforeExec { - _, _, err1 = syscall.RawSyscall(syscall.SYS_KILL, pid, uintptr(syscall.SIGSTOP), 0) - if err1 != 0 { - goto childerror - } - } - // at this point, runner is successfully attached for seccomp trap filter // or execve traped without seccomp filter // time to exec diff --git a/forkexec/runner.go b/forkexec/runner.go index a76f103..e862138 100644 --- a/forkexec/runner.go +++ b/forkexec/runner.go @@ -41,13 +41,9 @@ type Runner struct { // right before the calls to seccomp. It is automatically enabled when seccomp // filter and ptrace are provided since kill might not be avaliable after // seccomp and execve might be traced by ptrace + // cannot stop after seccomp since kill might not be allowed by seccomp filter StopBeforeSeccomp bool - // stop before exec calls kill(getpid(), SIGSTOP) to wait for tracer to continue - // right before the final calls to execve. It acts as a synchronize - // for parent process - StopBeforeExec bool - // unshare flag to create linux namespace, effective when clone child // since unshare syscall does not join the new pid group UnshareFlags uintptr diff --git a/forkexec/userns.go b/forkexec/userns.go index a3c485f..0042e7a 100644 --- a/forkexec/userns.go +++ b/forkexec/userns.go @@ -6,37 +6,36 @@ import ( "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"), - }) +// writeUidGidMappings writes User ID and Group ID mappings for user namespaces +// for a process and it is called from the parent process. +func writeIDMaps(pid int) error { + pidStr := strconv.Itoa(pid) + uidStr := strconv.Itoa(unix.Geteuid()) + gidStr := strconv.Itoa(unix.Getegid()) + if err := writeFile("/proc/"+pidStr+"/uid_map", []byte("0 "+uidStr+" 1")); err != nil { + return err } - return ret + if err := writeFile("/proc/"+pidStr+"/setgroups", []byte("deny")); err != nil { + return err + } + if err := writeFile("/proc/"+pidStr+"/gid_map", []byte("0 "+gidStr+" 1")); err != nil { + return err + } + return nil +} + +// writeFile writes file +func writeFile(path string, content []byte) error { + fd, err := unix.Open(path, unix.O_RDWR, 0) + if err != nil { + return err + } + if _, err := unix.Write(fd, content); err != nil { + unix.Close(fd) + return err + } + if err := unix.Close(fd); err != nil { + return err + } + return nil } diff --git a/rununshared/run.go b/rununshared/run.go index 83a54e7..a8206a4 100644 --- a/rununshared/run.go +++ b/rununshared/run.go @@ -35,18 +35,18 @@ func (r *RunUnshared) Start() (rt tracer.TraceResult, err error) { } 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, + Args: r.Args, + Env: r.Env, + RLimits: r.RLimits.PrepareRLimit(), + Files: r.Files, + WorkDir: r.WorkDir, + Seccomp: bpf, + NoNewPrivs: true, + StopBeforeSeccomp: true, + UnshareFlags: UnshareFlags, + Mounts: r.Mounts, + PivotRoot: r.Root, + DropCaps: true, } return r.Trace(ch) }