diff --git a/forkexec/fork.go b/forkexec/fork.go index ad28acb..58070dd 100644 --- a/forkexec/fork.go +++ b/forkexec/fork.go @@ -53,6 +53,7 @@ func (r *Runner) Start() (int, error) { return 0, nil } + // pipe p used to notify child the uid / gid mapping have been setup if unshareUser { err = unix.Pipe2(p[:], unix.O_CLOEXEC) if err != nil { @@ -60,8 +61,19 @@ func (r *Runner) Start() (int, error) { } } + // socketpair p2 used to sync with parent before final execve + p2, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0) + if err != nil { + if p[0] > 0 { // avoid fd leak + unix.Close(p[0]) + unix.Close(p[1]) + } + return 0, err + } + // similar to exec_linux, avoid side effect by shuffling around fd, nextfd := prepareFds(r.Files) + pipe := p2[1] // Acquire the fork lock so that no other threads // create new fds that are not yet close-on-exec @@ -90,6 +102,24 @@ func (r *Runner) Start() (int, error) { unix.Close(p[1]) } + // sync with child + unix.Close(p2[1]) + _, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(p2[0]), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2))) + if err1 != 0 { + unix.Close(p2[0]) + return int(pid), syscall.Errno(err1) + } + + if r.SyncFunc != nil { + err = r.SyncFunc(int(pid)) + } + if err == nil { + err1 = 0 + syscall.RawSyscall(syscall.SYS_WRITE, uintptr(p2[0]), uintptr(unsafe.Pointer(&err1)), uintptr(unsafe.Sizeof((err1)))) + } + + unix.Close(p2[0]) + if err1 != 0 { return int(pid), syscall.Errno(err1) } @@ -134,14 +164,21 @@ func (r *Runner) Start() (int, error) { // Pass 1 & pass 2 assigns fds for child process // Pass 1: fd[i] < i => nextfd + if pipe < nextfd { + _, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(pipe), uintptr(nextfd), syscall.FD_CLOEXEC) + if err1 != 0 { + goto childerror + } + pipe = nextfd + nextfd++ + } for i := 0; i < len(fd); i++ { if fd[i] >= 0 && fd[i] < int(i) { - _, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), 0) + _, _, err1 = syscall.RawSyscall(syscall.SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), syscall.FD_CLOEXEC) if err1 != 0 { goto childerror } // Set up close on exec - syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(nextfd), syscall.F_SETFD, syscall.FD_CLOEXEC) fd[i] = nextfd nextfd++ } @@ -229,29 +266,38 @@ func (r *Runner) Start() (int, error) { // pivit_root if pivotRoot != nil { - // mkdir(root/old_root) + // mkdir("old_root") _, _, err1 = syscall.RawSyscall(syscall.SYS_MKDIRAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(oldRoot)), 0755) if err1 != 0 { goto childerror } - // pivot_root(root, root/old_root) + // pivot_root(root, "old_root") _, _, err1 = syscall.RawSyscall(syscall.SYS_PIVOT_ROOT, uintptr(unsafe.Pointer(pivotRoot)), uintptr(unsafe.Pointer(oldRoot)), 0) if err1 != 0 { goto childerror } - // umount(root/old, MNT_DETACH) + // umount("old_root", MNT_DETACH) _, _, err1 = syscall.RawSyscall(syscall.SYS_UMOUNT2, uintptr(unsafe.Pointer(oldRoot)), syscall.MNT_DETACH, 0) if err1 != 0 { goto childerror } - // rmdir(root/old_root) + // rmdir("old_root") _, _, err1 = syscall.RawSyscall(syscall.SYS_UNLINKAT, uintptr(_AT_FDCWD), uintptr(unsafe.Pointer(oldRoot)), uintptr(unix.AT_REMOVEDIR)) if err1 != 0 { goto childerror } + + // mount("tmpfs", "/", "tmpfs", MS_BIND | MS_REMOUNT | MS_RDONLY | MS_NOATIME | MS_NOSUID, nil) + _, _, err1 = syscall.RawSyscall6(syscall.SYS_MOUNT, uintptr(unsafe.Pointer(&tmpfs[0])), + uintptr(unsafe.Pointer(&slash[0])), uintptr(unsafe.Pointer(&tmpfs[0])), + uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NOSUID), + uintptr(unsafe.Pointer(&empty[0])), 0) + if err1 != 0 { + goto childerror + } } // chdir for child @@ -288,7 +334,7 @@ func (r *Runner) Start() (int, error) { } // Enable Ptrace - if r.Ptrace { + if r.Ptrace && r.Seccomp != nil { _, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0) if err1 != 0 { goto childerror @@ -320,6 +366,26 @@ func (r *Runner) Start() (int, error) { } } + // Before exec, sync with parent through pipe (configured as close_on_exec) + err2 = 0 + r1, _, err1 = syscall.RawSyscall(syscall.SYS_WRITE, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2))) + if r1 == 0 || err1 != 0 { + goto childerror + } + + r1, _, err1 = syscall.RawSyscall(syscall.SYS_READ, uintptr(pipe), uintptr(unsafe.Pointer(&err2)), uintptr(unsafe.Sizeof(err2))) + if r1 == 0 || err1 != 0 { + goto childerror + } + + // Enable ptrace if no seccomp is needed + if r.Ptrace && r.Seccomp == nil { + _, _, err1 = syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 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 c4b5bed..3de1cff 100644 --- a/forkexec/runner.go +++ b/forkexec/runner.go @@ -51,22 +51,31 @@ 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 + // if pivot root is provided, relative target is better for chdir-mount meta // and pivot root will mount as tmpfs before any mount Mounts []*mount.Mount - // pivot_root defines the new root after unshare mount namespace - // root need to be a mount point (e.g. defined in Mounts) and it should - // be a absolute path - // It will call: - // mkdir(root/old_root) - // pivot_root(root, root/old_root) - // umount(root/old, MNT_DETACH) - // rmdir(root/old_root) + // pivot_root defines a readonly new root after unshare mount namespace + // it should be a directory in absolute path + // Before mounts, tt will call: + // mount("tmpfs", root, "tmpfs", 0, nil) + // chdir(root) + // After mounts, it will call: + // mkdir("old_root") + // pivot_root(root, "old_root") + // umount("old_root", MNT_DETACH) + // rmdir("old_root") + // mount("tmpfs", "/", "tmpfs", MS_BIND | MS_REMOUNT | MS_RDONLY | MS_NOATIME | MS_NOSUID, nil) PivotRoot string // drop_caps calls cap_set(self, 0) to drop all capabilities // from effective, permitted, inheritable capability sets before execve - // it should avoid calls to set + // it should avoid calls to set ambient capabilities DropCaps bool + + // Parent and child process with sync sataus through a socket pair. + // SyncFunc will invoke with the child pid. If SyncFunc return some error, + // parent will signal child to stop and report the error + // SyncFunc is called right before execve, thus it could track cpu more percisely + SyncFunc func(int) error } diff --git a/rununshared/run.go b/rununshared/run.go index d532115..7d2dfc8 100644 --- a/rununshared/run.go +++ b/rununshared/run.go @@ -14,9 +14,8 @@ import ( ) 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 + // UnshareFlags is flags used to create namespaces except NET and IPC + UnshareFlags = unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS | unix.CLONE_NEWCGROUP ) // Start starts the unshared process