//go:build linux package platform import ( "fmt" "os" "runtime" "unsafe" "github.com/landlock-lsm/go-landlock/landlock" "golang.org/x/sys/unix" ) // RunLandlockShim is the inside-user-namespace entry point. It is invoked by // the helper as a direct child forked with CLONE_NEWUSER + uid map 0->host. // As uid-0-in-ns with CAP_SYS_ADMIN, it: // // 1. Loads the serialised landlockExecPolicy from policyFile. // 2. Installs the seccomp-notify filter WITHOUT PR_SET_NO_NEW_PRIVS. This is // the whole point of the user-ns indirection: without NNP, subsequent // execve(2)s in the target tree do NOT reset the dumpable flag to 0, so // the helper can keep opening /proc//mem for descendants and resolve // openat(2) path arguments. // 3. Sends the notify fd back to the helper over a socketpair on // notifySocketFd (fd number preserved via cmd.ExtraFiles). // 4. Applies Landlock restrictions. // 5. execve(2)s the target binary. The seccomp filter survives execve // (filters are inherited) and applies to the target and all descendants. // // Returns an error only if the shim fails before execve. On success the // shim process is replaced by the target and this function does not return. func RunLandlockShim(policyFile string, notifySocketFd int, args []string) error { if len(args) == 0 { return fmt.Errorf("shim: no target command") } // Without TSYNC the filter applies only to this thread; we must also // execve from this same thread so the target inherits it. runtime.LockOSThread() // The policy file is owned by the helper; shim doesn't delete it. policy, err := readLandlockPolicyFromFile(policyFile) if err != nil { return fmt.Errorf("shim: read policy: %w", err) } interceptOpen := len(policy.DenyPaths) > 0 notifyFd, err := shimInstallSeccomp(interceptOpen) if err != nil { return fmt.Errorf("shim: install seccomp: %w", err) } if err := sendFdToSocket(notifySocketFd, notifyFd); err != nil { return fmt.Errorf("shim: send notify fd: %w", err) } // Helper owns the notify fd now; kernel routes notifications via the // shared file description. _ = unix.Close(notifyFd) _ = unix.Close(notifySocketFd) var rules []landlock.Rule for _, r := range policy.FilesystemRules { access := landlockAdjustAccessForPath(r.Path, r.Access) rules = append(rules, landlock.PathAccess( landlock.AccessFSSet(access), r.Path, ).IgnoreIfMissing()) } cfg := landlockSelectConfig(policy) if err := cfg.BestEffort().RestrictPaths(rules...); err != nil { return fmt.Errorf("shim: landlock restrict: %w", err) } target := args[0] env := os.Environ() if len(policy.Env) > 0 { env = policy.Env } if err := unix.Exec(target, args, env); err != nil { return fmt.Errorf("shim: exec %s: %w", target, err) } return nil // unreachable } // shimInstallSeccomp installs the seccomp-notify filter WITHOUT // PR_SET_NO_NEW_PRIVS. The kernel accepts this only when the caller has // CAP_SYS_ADMIN in its user namespace; the helper arranges that by cloning // us with CLONE_NEWUSER + uid/gid mapping that makes us uid 0 in the new ns. func shimInstallSeccomp(interceptOpen bool) (int, error) { var filter []unix.SockFilter if interceptOpen { filter = []unix.SockFilter{ {Code: unix.BPF_LD | unix.BPF_W | unix.BPF_ABS, K: 0}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 4, Jf: 0, K: uint32(unix.SYS_OPENAT)}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 3, Jf: 0, K: uint32(unix.SYS_OPENAT2)}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 2, Jf: 0, K: uint32(unix.SYS_EXECVE)}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 1, Jf: 0, K: uint32(unix.SYS_EXECVEAT)}, {Code: unix.BPF_RET | unix.BPF_K, K: unix.SECCOMP_RET_ALLOW}, {Code: unix.BPF_RET | unix.BPF_K, K: unix.SECCOMP_RET_USER_NOTIF}, } } else { filter = []unix.SockFilter{ {Code: unix.BPF_LD | unix.BPF_W | unix.BPF_ABS, K: 0}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 2, Jf: 0, K: uint32(unix.SYS_EXECVE)}, {Code: unix.BPF_JMP | unix.BPF_JEQ | unix.BPF_K, Jt: 1, Jf: 0, K: uint32(unix.SYS_EXECVEAT)}, {Code: unix.BPF_RET | unix.BPF_K, K: unix.SECCOMP_RET_ALLOW}, {Code: unix.BPF_RET | unix.BPF_K, K: unix.SECCOMP_RET_USER_NOTIF}, } } prog := unix.SockFprog{Len: uint16(len(filter)), Filter: &filter[0]} flags := uintptr(unix.SECCOMP_FILTER_FLAG_NEW_LISTENER) fd, _, errno := unix.Syscall( unix.SYS_SECCOMP, unix.SECCOMP_SET_MODE_FILTER, flags, uintptr(unsafe.Pointer(&prog)), ) runtime.KeepAlive(&filter) runtime.KeepAlive(&prog) if errno != 0 { return -1, fmt.Errorf("SECCOMP_SET_MODE_FILTER without NNP (user-ns CAP_SYS_ADMIN required): %w", errno) } return int(fd), nil } // sendFdToSocket sends `fd` over a connected unix-domain socket using // SCM_RIGHTS. This transfers the fd to the peer process atomically. func sendFdToSocket(sockFd, fd int) error { rights := unix.UnixRights(fd) buf := []byte{0} iov := unix.Iovec{Base: &buf[0], Len: 1} msg := unix.Msghdr{Iov: &iov, Iovlen: 1, Control: &rights[0]} msg.SetControllen(len(rights)) _, _, errno := unix.Syscall( unix.SYS_SENDMSG, uintptr(sockFd), uintptr(unsafe.Pointer(&msg)), 0, ) runtime.KeepAlive(&buf) runtime.KeepAlive(&iov) runtime.KeepAlive(&rights) runtime.KeepAlive(&msg) if errno != 0 { return fmt.Errorf("sendmsg: %w", errno) } return nil }