mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: Initial implementation of landlock based sandbox driver * fix: Handle seccom probe failure * fix: Remove unnecessary seccomp probe * fix: Use file based policy load * fix: Keep bpf filter in memory * fix: Use TSYNC for seccom filter * fix: Use TSYNC for seccom filter * fix: Update landlock translator * fix: Landlock sandbox implementation * fix: Landlock + seccomp based sandboxing on Linux * fix: Misc fixes * fix: Cleanup sandbox files * fix: Handle mandatory deny API change post merge * fix: Landlock write access translation * chore: Fix linter issues * ci: Use /tmp for npm cache for landlock
150 lines
5.3 KiB
Go
150 lines
5.3 KiB
Go
//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/<pid>/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
|
|
}
|