Kernel exec confinement on Linux is a Landlock layer inside the envelope

Amended 2026-10-06. The parent builds the ruleset, by raw syscalls in place of the landlock crate, which can neither hand a ruleset over nor adopt one, and lends it to the payload at a fixed descriptor; the payload probes nothing and enters what it is given. ral’s own binary is admitted by its boot-pinned descriptor, the inode, never by name. Finding 2 stands with one change of hands: Refer on / is the one rule the payload adds before entering, because the envelope’s root exists only inside, and granting Refer on every mounted handle instead would leave /tmp, /dev/shm and the cwd stand-in, tmpfs the parent never sees, under EXDEV. The floor fails closed: a grant that limits which programs may run is refused on a kernel without Landlock, and a payload promised a ruleset that never arrived refuses to run; only an unrestricted exec projection launches with nothing to enter. The rename test reads the inode, not mv’s exit: uutils mv (Fedora 42 and later) hides EXDEV by copying and unlinking, so a refused rename passes any test that asks only whether the file moved.

Amended 2026-10-06: blocks hold in the kernel. Finding 1 no longer leaves a deny inside an admit to the gate. The parent reads the kernel list back as a table (ExecRules::from_kernel) and admits each allowed directory with nothing blocked beneath it as one hierarchy rule; one holding a deny dir, a deny file or a walked veto is listed along the spine to each block only, every entry judged by Table::verdict, so a deny by another spelling holds as in the guard, and each subdirectory in turn. That is a snapshot: a program added to an expanded directory after launch is denied until the next launch, where Seatbelt admits it at once, the fail-closed side of a declared asymmetry; a directory with no block beneath it, target/ under exec: cwd:/, is never expanded. A veto is walked only where no trusted write reaches (write_reach), since under a trusted admit the child can author a renamed copy and the veto is advisory (a-veto-freezes-what-a-write-covers); walking the user’s own tree per launch would buy nothing for it. What stays with the gate: those vetoes, programs added after launch in an expanded directory, and anything beneath a directory the walk cannot read, which is admitted nowhere. A hard link aliases an inode, as Seatbelt’s literal does.

Closes linux-exec-confinement. A confined payload now enters a Landlock domain of its own (core/src/sandbox/linux/landlock.rs) rendering ExecProjection::Restricted, so a descendant’s sh -c or find -exec is gated by the kernel and not only by ral’s dispatch.

The layer goes on inside bwrap, entered by the payload itself. A Landlock domain that handles any filesystem right forbids mount(2), so a layer applied before the envelope kills bwrap at its first act (measured: bwrap: Failed to make / slave). The order is therefore bwrap, then the payload’s own restrict_self, and never the reverse.

Four findings shape it.

  1. Landlock is allow-list only. It cannot express “everything but this”, so the filesystem half stays with bwrap’s mounts, and on the exec half a deny outside every admit is already absence, while a deny inside an admit is rendered by subtraction, the admit’s entries the table still allows (see the second amendment). Where Seatbelt carries a deny into the kernel as a rule, Landlock carries it as an absence over the tree at launch.

  2. A layer restricts only what it handles — except Refer. Any domain refuses every cross-directory rename and link with EXDEV unless it both handles LANDLOCK_ACCESS_FS_REFER and grants it, which under ABI 1 no domain can, the right not existing there to grant. From ABI 2 it does, so the layer handles Execute | Refer and grants Refer on / to restore them — restored, not restricted, though non-escalation still gives EXDEV for a rename or link that would gain the file rights its new parent has and its old one did not, such as moving an unadmitted file into an Execute-admitted directory.

  3. execve of a dynamic binary needs Execute on the binary and its PT_INTERP only — and of a #! script, on its interpreter, which exec-carriers admits as a carrier. The shared libraries the loader then maps need no right from this layer. The platform base is therefore the linker files themselves — ld*.so* under the /lib and /usr/lib variants and one level down for Debian multiarch — and never a command directory, since /usr/bin in the base would be a layer that denies nothing. A missing linker shows up as EACCES on 7.1, not ENOENT. Admitting the loaders bounds what the layer is: a gate on which paths may be execved, never on which code a process runs. An admitted loader handed an unadmitted binary as an argument (ld-linux-x86-64.so.2 ./payload), an admitted interpreter handed a script by path, and mmap(PROT_EXEC) of file contents all run code the layer never sees, and only an fs layer handling ReadFile — costed and declined below — would see them.

  4. The signal scope is an envelope invariant, not a projection. LANDLOCK_SCOPE_SIGNAL (ABI 6, Linux 6.12) is applied on every launch, including an unrestricted exec projection, and gives EPERM when a confined child signals a host process. That closes the same-uid kill hole on hosts where the container runtime denies a pid namespace, with no namespace needed.

Two floors, both probed by the syscall rather than read off uname: ABI 1 for exec confinement — where a confined payload also loses cross-directory rename and link, Refer being ungrantable there — and ABI 6 for the scope. A kernel with no Landlock at all (ENOSYS, EOPNOTSUPP) enters nothing when exec is unrestricted, HostEnvelope recording each invariant as unheld, and refuses a grant that limits which programs may run (plan); any other errno from the probe is a host that cannot be read rather than one without the feature, and refuses the launch with confinement_unavailable.

A caution for anyone reconsidering an fs layer here: once any layer handles ReadFile, execve needs ReadFile on the binary and the loader needs it on the whole library closure (measured) — a far larger rule set than exec alone. Cost is not the obstacle: rules run about 1 µs each with no ceiling observed at 20 000.

See also core, grant, two enforcers.