Amended 2026-10-06 (one-table-two-instances):
kernel()enters each carrier into a copy of the table as anExecScope::Carrierallow, rankedDir < Carrier < Exact < Name, and asks the table’s ownverdictfor each file once, so an authored exact deny or a veto beats a carrier by rank alone. A carrier holds a file by mutual containment, as aFilerule does.
The kernel exec set is one law on every platform
A confined command’s descendants may execve exactly what
the grant’s rules ∪ their carriers ∪ the platform base
admit, on macOS and Linux alike. ExecRules::kernel emits the list, ordered
by Rank so that last-match-wins equals the in-process guard’s
highest-rank-wins (exec-rules); Seatbelt and
Landlock render it, and no renderer adds anything else. A grant therefore
means one thing on both kernels and in the guard
(two enforcers).
The platform base is the loader and ral. Linux needs Execute on the ELF
loader of every dynamic binary; macOS needs no process-exec on dyld
(measured) and keeps only /Library/Apple/usr, Rosetta’s runtime. Amended
2026-10-17: Rosetta needs none either (measured, Darwin 25.5); the clause is
gone and the runtime is only read. ral’s own
binary is the trampoline and every bundled tool.
A carrier is what execve forwards to before the program’s own code runs:
a #! script’s interpreter, up to four hops, and on macOS the system’s
root-owned shims (/bin/sh to what /private/var/select/sh names, a
/usr/bin xcrun stub to its tool in the developer directory read from the
root-owned xcode_select_link, never DEVELOPER_DIR). Both kernels check a
script’s interpreter (measured), so without carriers a script the guard admits
would die in the kernel. The parent computes them once, at spawn, over the
table’s allowed files and the admitted program
(core/src/sandbox/carriers.rs); the guard never consults them; on Linux the
envelope binds each read-only.
env is not followed. #!/usr/bin/env X carries env; X needs its own
grant entry. Following it would read PATH, which the confined code controls.
A carrier is admitted only on bytes the session’s uid cannot author.
trusted_real walks a path from / with path::walk::walk_vetted, the
handle-relative walk that splices links itself, vetting each object it passes:
every directory and the final file must be neither
owned nor writable by the euid, and every symlink not owned by it. The program
that names a hop and the hop itself must both pass. Otherwise
#!/any/binary written into an admitted script would admit any binary. A
user-writable symlink to a system binary fails too: its owner can repoint it.
Root owns everything, so it trusts nothing.
The shebang grammar is the kernels’, narrowed. The #! line must end
within the first 256 bytes (Linux) or 512 (macOS); macOS also ends it at a
#. Leading blanks are skipped, the interpreter runs to the next blank, and
it must be absolute. Only the interpreter is kept. Any other shape carries
nothing, and the trampoline’s EACCES hint names the interpreter to grant.
Program behaviour is not forwarding. cc → cc1, clang → ld and git’s
helpers are execs a program chooses, which no header names. Toolchains are
grant data: the system: sigil carries each platform’s tool and helper roots,
visible in the grant.
Renderers. Seatbelt emits one form per rule in order: an allow admits
file-read* process-exec, a deny or veto process-exec only — exec denies do
not deny reads, which are fs’s. Landlock is allow-list only: it admits the
base, ral, every allowed file, and every allowed directory less what the
list, read back as a table, blocks beneath it; each file being emitted once at
its final verdict, that table judges as the guard does. Exec rules
carry RealPaths to the backends, and render_real, the one way from one to
a kernel spelling, adds only the firmlink twin and never reads the disk, so a
frozen grant cannot widen to a symlink’s new target. Landlock judges inside
the bwrap envelope, whose names bwrap minted by following host symlinks, so
the parent opens each admit in the host with RESOLVE_NO_SYMLINKS and builds
the ruleset itself; the payload only enters it.
Declared limits
- The layer gates which paths may be
execved, never which code runs. An allowed interpreter runs any script it can read. On Linux the admitted ELF loader runs any readable binary handed to it; macOS’sdyldcannot be executed directly (measured) (landlock-exec-layer). - Landlock subtracts inside an allowed directory only over the tree at launch: a program added afterwards to a directory holding a block is denied until the next launch, and a veto under a trusted write holds only at the in-process guard (landlock-exec-layer).
- The kernel cannot see argv, so
Onlyrenders as an allow. - Windows has no kernel exec layer.