Limitations¶
hallpass is a proof of concept. It mediates filesystem writes and the programs run (by path only), and mediates connect only when network mediation is enabled. By default, a sandboxed process can therefore reach a service that acts on its behalf outside the sandbox, and so write anywhere that service can, without a prompt. For example systemd-run --user, or docker/podman run talking to a daemon, or a process connecting directly to the systemd or D-Bus session socket. Running those clients is asked about, but an allowed interpreter can connect to the socket itself. Network mediation asks about those connections, but an always on such a socket allows the escape again; treat the current build as "ask me about honest mistakes", not "contain a hostile agent".
What is mediated¶
- Only writes (write-intent
open, create, delete, rename, link,truncate,mknod), program executions and, with network mediation, connections and sends are mediated: reading files, secrets included, is not restricted. chmod,chown, extended attributes and timestamps are not mediated yet.
Files¶
- Lite mode: allowed syscalls continue with
SECCOMP_USER_NOTIF_FLAG_CONTINUE, so a malicious multi-threaded process can change the path between the check and the kernel acting on it (TOCTOU). Strict mode (BPF-LSM) closes this. - Lite mode:
openat2passes its flags in memory (struct open_how), which the seccomp filter cannot read: everyopenat2reaches hallpass, which reads the flags and lets the read-only ones through. A malicious process can change the flags after the check, as it can change the path. Strict mode closes this too. - Lite mode: pre-existing hard links to protected files are not detected.
- Strict mode: sandboxed processes cannot mount overlayfs (for example rootless podman or buildah with the native overlay driver): overlayfs writes to its layers without going through the hooks hallpass checks. fuse-overlayfs works.
Programs¶
- An allowed interpreter (
python3,node, a shell) runs any code it is given, and the dynamic loader (ld.so /path/to/program) runs a program without executing it. - A program copied or downloaded to a path an allow covers runs. The project directory is writable, not executable by default.
- When several commands are one binary (busybox, uutils coreutils), installed as symlinks, allowing one allows all: rules match the resolved path, and telling the commands apart needs
argv[0]. Hard links and copies have paths of their own. - A program run from a file descriptor (
execveatwithAT_EMPTY_PATH, for example amemfd) gives lite mode no path to read: it asks about it as an unknown path ("once" only). Strict mode matches the rules on the file the descriptor is open on, without asking, and always denies a file with no path at all (amemfd). binfmt_mischandlers (qemu, Wine) are not seen by lite mode, which only reads#!lines: they run. Strict mode checks them in the kernel like#!interpreters, but never asks about them at seccomp time: they need an allow rule.- Lite mode: the path is read from memory after the check (TOCTOU), as for writes.
- In Kubernetes, the container's command (its
args[0]) is allowed for good, with any arguments and any number of times.
Network¶
- Sockets inherited from outside the sandbox, or received with
SCM_RIGHTS, are already connected: their traffic is not asked about. - Only IPv4, IPv6 and unix sockets are mediated; other families (netlink, packet, vsock...) are not.
- A UDP socket bound inside the sandbox is not a listener: sending to it is asked.
- Answers cover one destination; there are no answers for a subnet or a folder of sockets yet (use
--allow-connect). - Lite mode: the destination address is read from memory after the check (TOCTOU), and a listener inherited from outside the sandbox counts as started inside it.
- Strict mode: a TCP listener counts as started inside the sandbox as in lite mode (racy); the kernel then lets through only the address granted. A
sendmmsgto more than 4 addresses sends the first 4 and fails on the fifth withEACCES; resending the rest asks again. - Names: every
recvmsg, and everyrecvfromwith an address, is paused and continued, which slows UDP servers and other programs receiving many datagrams. A blocking DNS receive is not interrupted by a signal the sandbox catches. - Names: DNS queries sent with
sendmsgorsendmmsgwith the server's address, or with TCP fast open, are not redirected and name nothing. On a workstation, a session's forwarder listens on the host's loopback and is not denied to other hallpass sessions or other programs: one of them can send it queries, and the server's answers then name addresses in that session too (real answers, but not ones the sandbox asked for). On Kubernetes, a container restored after hallpass-node restarted has no names.
Strict mode¶
- The supervisor holds writable fds of the BPF maps, so the user running hallpass can put any process, including other users', under the policy or under signal protection. Choosing an escalation tool is the administrator's consent to that.
- If the supervisor dies, the programs are detached. Seccomp then fails the sandbox's mediated syscalls closed, but nothing else is enforced on processes left behind.
- When the helper runs without a recognised escalation tool in its environment (
--escalate=manual, orsu, which clearsSUDO_UID/PKEXEC_UID/DOAS_USER), it cannot tell which user invoked it and only checks that the target runs the same hallpass binary. kernel.yama.ptrace_scope=3prevents the helper from fetching the socket (pidfd_getfd); run hallpass as root instead.