Why now
Recent downstream dogfooding has repeatedly exposed the same assurance failure shape: the system proved the mechanism it intended to run rather than the postcondition it actually established.
batpak-saas repaired a witness rig whose frontier probe mutated the frontier it claimed merely to observe, and whose benchmark verdict was hardcoded green: batpak-saas PR #11.
- Its wasm verifier then closed a self-blessing provenance path so stale generated bytes could not notarize themselves as current: batpak-saas PR #12.
- Its repository-integrity judge also had to replace lexical/path assumptions with canonical containment and exact fixture-root checks: batpak-saas PR #17.
A fresh upstream audit found the same class in bvisor's Linux launcher.
The launcher says the Landlock ruleset descriptors are CLOEXEC-marked and therefore cannot enter the workload, but the operation that establishes that claim is explicitly best-effort:
build_confinement discovers the newly opened ruleset fds, calls sys::set_cloexec(fd) for each, ignores any result, and returns BuiltConfinement whose documentation says those fds are CLOEXEC-set.
set_cloexec ignores both F_GETFD and F_SETFD failure. Its rustdoc says that at worst the fd leaks.
allowlist deliberately exempts those ruleset fds from the child's scrub so restrict_self can use them.
- After a successful
fexecve, finish_child emits ConfinementPhaseResolved, ReadyToExec, and ExecSucceeded. It has no evidence that every transitional descriptor actually left the exec image.
- The
unsafe_ledger.yaml entry likewise blesses this security-boundary syscall as best-effort.
fcntl() is fallible and returns -1 with errno on error. The Linux kernel's own Landlock example does not rely on a best-effort FD_CLOEXEC promise after policy installation: it calls landlock_restrict_self() and then immediately closes ruleset_fd. Once restriction succeeds, the policy remains enforced on the thread and its descendants. Sources:
This is not a claim that the leaked ruleset fd is an immediate sandbox escape. It is a concrete violation of the launcher's no-ambient-fd and evidence-honesty contract: a descriptor not declared for the workload can survive while the transcript reports the hygiene/confinement phases as applied.
Exact failure sequence
Landlock ruleset created
-> ruleset fd discovered
-> F_GETFD or F_SETFD fails
-> failure ignored
-> ruleset fd exempted from scrub
-> child successfully restricts itself
-> child successfully fexecves
-> ruleset fd survives into target image
-> transcript reports Applied / ExecSucceeded
The implementation currently proves scheduled + built + exec succeeded. The public claim is stronger: the target entered with exactly the allowed descriptor authority.
Core law
A setup phase may report Applied only after every authority-bearing transition it promises has reached a checked postcondition.
Every descriptor present before fexecve must have one explicit lifecycle: close before exec, close-on-exec verified, or intentionally inherited under a declared descriptor role. There is no fourth bucket called best effort.
Proposal
1. Model descriptor lifecycle explicitly
Introduce a small BatPak-owned launcher model, exact spelling optional:
enum ExecFdDisposition {
CloseBeforePolicy,
CloseAfterPolicy,
VerifiedCloseOnExec,
DeclaredInherited(DescriptorRole),
}
struct ExecFdPlan {
fd: RawFd,
disposition: ExecFdDisposition,
origin: FdOrigin,
}
The coordinator should derive one complete descriptor plan from the verified launch body plus launcher-created descriptors. The scrub list and the post-policy close list become generated projections of that one plan rather than independently maintained allowlists.
At minimum classify stdio, target executable, control/error channels, userns rendezvous pipes, Landlock ruleset fds, cgroup fd, declared roots, and future setup descriptors. Unknown descriptors remain scrubbed. A descriptor required only during setup must not silently become exec-inherited.
2. Close Landlock ruleset fds immediately after successful restriction
Follow the kernel's reference lifecycle:
build ruleset in parent
-> child scrub preserving ruleset fd
-> landlock_restrict_self
-> checked raw close of every ruleset fd
-> install later mechanisms
-> fexecve
The close runs before seccomp. On a nonzero close result, capture errno, report through the existing error pipe, and _exit; never continue into the target with an uncertain descriptor set. Do not retry close blindly.
This fits the child branch's no-destructor discipline: after restriction the RulesetCreated value is never used again and the child always diverges through exec or _exit.
3. Make every required CLOEXEC transition typed and checked
For descriptors that legitimately must remain open until exec:
set_cloexec returns io::Result<()>;
F_GETFD failure is surfaced;
F_SETFD failure is surfaced;
- a second
F_GETFD verifies FD_CLOEXEC is present;
- failure before clone refuses/faults the launch;
- no type or documentation may claim
VerifiedCloseOnExec before the transition succeeds.
Prefer atomic creation flags such as O_CLOEXEC, MFD_CLOEXEC, pipe2(O_CLOEXEC), and F_DUPFD_CLOEXEC where available. The postcondition still belongs in the lifecycle model so future descriptor sources cannot bypass it.
4. Bind phase resolution to completed child transitions
ConfinementPhaseResolved and AmbientAuthorityPhaseResolved must mean more than "the coordinator built the objects."
The child's success path should prove through checked syscall completion before fexecve that scrub completed, setup descriptors were closed, Landlock installed, seccomp installed when scheduled, and every remaining descriptor was declared and verified for exec inheritance.
The existing error-pipe EOF can remain the compact success signal if every forbidden transition calls child_fail. Do not add allocation or a protocol encoder inside the post-clone window.
5. Make the unsafe ledger grade failure policy
Extend unsafe-ledger entries with a machine-readable failure policy, for example:
failure_policy: refuse_before_child | child_exit | best_effort_non_authority
For entries backing InheritedFds, Filesystem, ChildSpawn, confinement, identity, or evidence claims, best_effort_non_authority must be structurally forbidden unless a narrow waiver proves the operation cannot affect authority or reported success.
Plant a RED fixture that changes a checked fcntl or close into let _ = ... and prove structural-check bites.
6. Keep the proof physics-bounded
Do not make a noisy cloud stopwatch the acceptance gate. Measure:
- fds classified, scrubbed, and closed after policy;
fcntl, clone, and exec syscall counts;
- allocations before clone;
- zero allocations and zero locks after clone;
- startup latency distributions as supporting evidence only.
Sweep descriptor count and confinement-root count independently so a faster result cannot hide a smaller workload.
Required negative fixtures
- Force
F_GETFD failure on a ruleset fd before clone: launch faults/refuses, no target execution.
- Force
F_SETFD failure: launch faults/refuses, no target execution.
- Remove the post-
restrict_self close: an exec-target fd census catches the leaked descriptor.
- Force the post-restriction close syscall to fail: child reports failure and
_exits before target execution.
- Add an unknown coordinator fd after the initial snapshot: it is scrubbed or launch refuses.
- Duplicate a declared fd under another number: only the explicitly classified occurrence survives.
- Run with zero, one, and many Landlock roots and prove the exec-time fd set is identical except for declared descriptors.
- Combine Landlock, seccomp, userns, netns, and cgroup and prove the staged lifecycle remains exact.
- Execute a tiny target that enumerates
/proc/self/fd; compare the actual inherited set to the reference plan.
- Mutate one descriptor disposition from
CloseAfterPolicy to VerifiedCloseOnExec without establishing CLOEXEC and require refusal/test failure.
Validation gates
Reference fd-state machine
CoordinatorOpen
-> ChildInherited
-> Scrubbed
-> PolicyInstalled
-> TransitionalClosed
-> ExecInherited
Generate descriptor origins, roles, setup mechanisms, duplicates, syscall failures, and ordering changes. Compare the real launch disposition and transcript to the model.
Deterministic syscall-fault matrix
Add narrow test-only fault points around F_GETFD, F_SETFD, post-policy close, scrub close, restrict_self, seccomp install, and fexecve. Prepare hook state before clone and read it allocation-free in the child. Every planted fault must leave observable evidence that it fired.
Mutation seam
Kill mutations that ignore an fcntl return, skip post-policy close, omit a descriptor from classification, mark a phase applied early, collapse declared inheritance with temporary setup inheritance, or continue after an uncertain close.
Structural and unsafe qualification
- one source of truth for fd disposition;
- no independent hand-maintained scrub, allow, and post-close lists;
- every child-window syscall appears in the unsafe ledger;
- every authority-affecting syscall has a fail-closed policy;
- the post-clone path remains allocation-free, lock-free, and divergent.
Cross-kernel Linux lane
Run the fd-census and fault fixtures across the supported Landlock ABI floor and at least one newer ABI. Record kernel release, Landlock ABI, seccomp mode, namespace profile, and compiled launcher fingerprint.
Acceptance criteria
- A successful workload inherits exactly the descriptors declared for the exec image, with no launcher-temporary descriptors.
- Landlock ruleset fds are closed after successful
restrict_self and before seccomp/exec.
F_GETFD or F_SETFD failure cannot be reported as a successful hygiene/confinement phase.
- Every required CLOEXEC bit is established and verified, not assumed from intent.
- Every descriptor has one canonical lifecycle disposition.
- Unknown descriptors are scrubbed or cause refusal.
- Child cleanup failures call
child_fail and never execute the target.
ConfinementPhaseResolved, AmbientAuthorityPhaseResolved, and ExecSucceeded cannot over-claim the fd postcondition.
- The exec-target census matches the reference model under all supported mechanism combinations.
- Mutation tests kill removal of the checked transitions and phase binding.
- The unsafe ledger rejects best-effort policy for authority-bearing syscalls.
- No async runtime, heap allocation, lock, unwinding, or new general IPC protocol enters the child window.
- Normal-path overhead is reported by work counts plus latency distributions; no benchmark win may remove a descriptor check.
Relationship to existing issues
Non-goals
- no claim that a leaked Landlock ruleset fd is by itself a sandbox escape;
- no async executor or Tokio dependency;
- no heap allocation, lock, formatting, or channel use in the child window;
- no replacement of Landlock/seccomp with containers or a new runtime;
- no broad bvisor rewrite;
- no weakening the exact-fd contract because the leaked descriptor appears harmless today.
Bottom line
The launcher currently has a tiny honesty crack with a large semantic shadow: it calls descriptor cleanup best-effort, then reports the descriptor boundary as proven.
Close transitional authority explicitly, check every transition, and make the target's exec-time fd set a postcondition rather than a comment.
Why now
Recent downstream dogfooding has repeatedly exposed the same assurance failure shape: the system proved the mechanism it intended to run rather than the postcondition it actually established.
batpak-saasrepaired a witness rig whose frontier probe mutated the frontier it claimed merely to observe, and whose benchmark verdict was hardcoded green: batpak-saas PR #11.A fresh upstream audit found the same class in bvisor's Linux launcher.
The launcher says the Landlock ruleset descriptors are
CLOEXEC-marked and therefore cannot enter the workload, but the operation that establishes that claim is explicitly best-effort:build_confinementdiscovers the newly opened ruleset fds, callssys::set_cloexec(fd)for each, ignores any result, and returnsBuiltConfinementwhose documentation says those fds are CLOEXEC-set.set_cloexecignores bothF_GETFDandF_SETFDfailure. Its rustdoc says that at worst the fd leaks.allowlistdeliberately exempts those ruleset fds from the child's scrub sorestrict_selfcan use them.fexecve,finish_childemitsConfinementPhaseResolved,ReadyToExec, andExecSucceeded. It has no evidence that every transitional descriptor actually left the exec image.unsafe_ledger.yamlentry likewise blesses this security-boundary syscall as best-effort.fcntl()is fallible and returns-1witherrnoon error. The Linux kernel's own Landlock example does not rely on a best-effortFD_CLOEXECpromise after policy installation: it callslandlock_restrict_self()and then immediately closesruleset_fd. Once restriction succeeds, the policy remains enforced on the thread and its descendants. Sources:landlock_restrict_self(2)fcntl(2)return and error contractThis is not a claim that the leaked ruleset fd is an immediate sandbox escape. It is a concrete violation of the launcher's no-ambient-fd and evidence-honesty contract: a descriptor not declared for the workload can survive while the transcript reports the hygiene/confinement phases as applied.
Exact failure sequence
The implementation currently proves
scheduled + built + exec succeeded. The public claim is stronger:the target entered with exactly the allowed descriptor authority.Core law
Proposal
1. Model descriptor lifecycle explicitly
Introduce a small BatPak-owned launcher model, exact spelling optional:
The coordinator should derive one complete descriptor plan from the verified launch body plus launcher-created descriptors. The scrub list and the post-policy close list become generated projections of that one plan rather than independently maintained allowlists.
At minimum classify stdio, target executable, control/error channels, userns rendezvous pipes, Landlock ruleset fds, cgroup fd, declared roots, and future setup descriptors. Unknown descriptors remain scrubbed. A descriptor required only during setup must not silently become exec-inherited.
2. Close Landlock ruleset fds immediately after successful restriction
Follow the kernel's reference lifecycle:
The close runs before seccomp. On a nonzero close result, capture errno, report through the existing error pipe, and
_exit; never continue into the target with an uncertain descriptor set. Do not retrycloseblindly.This fits the child branch's no-destructor discipline: after restriction the
RulesetCreatedvalue is never used again and the child always diverges through exec or_exit.3. Make every required CLOEXEC transition typed and checked
For descriptors that legitimately must remain open until exec:
set_cloexecreturnsio::Result<()>;F_GETFDfailure is surfaced;F_SETFDfailure is surfaced;F_GETFDverifiesFD_CLOEXECis present;VerifiedCloseOnExecbefore the transition succeeds.Prefer atomic creation flags such as
O_CLOEXEC,MFD_CLOEXEC,pipe2(O_CLOEXEC), andF_DUPFD_CLOEXECwhere available. The postcondition still belongs in the lifecycle model so future descriptor sources cannot bypass it.4. Bind phase resolution to completed child transitions
ConfinementPhaseResolvedandAmbientAuthorityPhaseResolvedmust mean more than "the coordinator built the objects."The child's success path should prove through checked syscall completion before
fexecvethat scrub completed, setup descriptors were closed, Landlock installed, seccomp installed when scheduled, and every remaining descriptor was declared and verified for exec inheritance.The existing error-pipe EOF can remain the compact success signal if every forbidden transition calls
child_fail. Do not add allocation or a protocol encoder inside the post-clone window.5. Make the unsafe ledger grade failure policy
Extend unsafe-ledger entries with a machine-readable failure policy, for example:
For entries backing
InheritedFds,Filesystem,ChildSpawn, confinement, identity, or evidence claims,best_effort_non_authoritymust be structurally forbidden unless a narrow waiver proves the operation cannot affect authority or reported success.Plant a RED fixture that changes a checked
fcntlorcloseintolet _ = ...and provestructural-checkbites.6. Keep the proof physics-bounded
Do not make a noisy cloud stopwatch the acceptance gate. Measure:
fcntl, clone, and exec syscall counts;Sweep descriptor count and confinement-root count independently so a faster result cannot hide a smaller workload.
Required negative fixtures
F_GETFDfailure on a ruleset fd before clone: launch faults/refuses, no target execution.F_SETFDfailure: launch faults/refuses, no target execution.restrict_selfclose: an exec-target fd census catches the leaked descriptor._exits before target execution./proc/self/fd; compare the actual inherited set to the reference plan.CloseAfterPolicytoVerifiedCloseOnExecwithout establishing CLOEXEC and require refusal/test failure.Validation gates
Reference fd-state machine
Generate descriptor origins, roles, setup mechanisms, duplicates, syscall failures, and ordering changes. Compare the real launch disposition and transcript to the model.
Deterministic syscall-fault matrix
Add narrow test-only fault points around
F_GETFD,F_SETFD, post-policyclose, scrubclose,restrict_self, seccomp install, andfexecve. Prepare hook state before clone and read it allocation-free in the child. Every planted fault must leave observable evidence that it fired.Mutation seam
Kill mutations that ignore an
fcntlreturn, skip post-policy close, omit a descriptor from classification, mark a phase applied early, collapse declared inheritance with temporary setup inheritance, or continue after an uncertain close.Structural and unsafe qualification
Cross-kernel Linux lane
Run the fd-census and fault fixtures across the supported Landlock ABI floor and at least one newer ABI. Record kernel release, Landlock ABI, seccomp mode, namespace profile, and compiled launcher fingerprint.
Acceptance criteria
restrict_selfand before seccomp/exec.F_GETFDorF_SETFDfailure cannot be reported as a successful hygiene/confinement phase.child_failand never execute the target.ConfinementPhaseResolved,AmbientAuthorityPhaseResolved, andExecSucceededcannot over-claim the fd postcondition.Relationship to existing issues
Non-goals
Bottom line
The launcher currently has a tiny honesty crack with a large semantic shadow: it calls descriptor cleanup best-effort, then reports the descriptor boundary as proven.
Close transitional authority explicitly, check every transition, and make the target's exec-time fd set a postcondition rather than a comment.