Linux: build the view, then seccomp, then exec
The same read: Mac returns Operation not permitted, Linux returns No such file or directory. Linux first swaps the file tree the process can see, then tightens which syscalls it can make.
- Read the permission profile and preflight WSL1manager.rs L413
- Build helper argv and attach the profile JSONlandlock.rs L23
- Outer run_main picks bwrap or the shortcutlinux_run_main.rs L152
- Build the inner command with --apply-seccomp-then-execlinux_run_main.rs L1520
- bwrap does ro-bind, bind, unshare in orderbwrap.rs L306
- Insert --as-pid-1 when execing bwraplauncher.rs L39
- Inner capget, confirm capabilities are zerolinux_run_main.rs L216
- Set PR_SET_NO_NEW_PRIVS, then install seccomplandlock.rs L61
- Fork the user command; the parent waitpid reaps orphanslinux_run_main.rs L243
- execvp takes over the process imagelinux_run_main.rs L1565
A teammate runs Codex on a Mac; the agent reads ~/.ssh/id_rsa; Seatbelt returns Operation not permitted at once. Put the same repo on a Linux laptop and the usual copy becomes No such file or directory. The path is often not in the mounts at all.
If you set PR_SET_NO_NEW_PRIVS first, then call the distro’s own bwrap, the setuid binary on many distros will not even start. The sandbox fails on the machine that has everything installed. seccomp is a kernel filter that says which syscalls the process may still make; no_new_privs is its precondition, and it also blocks setuid privilege gain.
The default path enters the same helper twice. The outer pass only builds bubblewrap, swaps the filesystem to default read-only, then layers writable roots; .git, .agents, and .codex stay read-only even when they sit inside a writable root. The inner pass then sets no_new_privs, installs network seccomp, forks, and hands the process image to the user command.
The function header writes the order as three steps:
/// Entry point for the Linux sandbox helper.
///
/// The sequence is:
/// 1. When needed, wrap the command with bubblewrap to construct the
/// filesystem view.
/// 2. Apply in-process restrictions (no_new_privs + seccomp).
/// 3. `execvp` into the final command.
openai/codex repo; verified file codex-rs/linux-sandbox/src/linux_run_main.rs, commit 4f39251a01, verified 2026-08-22. The code block keeps the original source; these three steps are the default-path contract.A field comment puts the reason on the flag: bubblewrap may depend on setuid, so build the view first, then tighten.Source: codex-rs/linux-sandbox/src/linux_run_main.rs lines 117–121;codex-rs/linux-sandbox/src/landlock.rs lines 57–65
What you can see, and which syscalls you can make, are two walls. Swap the world first, then tighten calls. Rewrite it in another language: as long as distros still ship a setuid wrapper, this order still holds.
bubblewrap and seccomp must happen in the process that is about to become the user command. If the main CLI unshares and execs itself, a failure takes the whole session with it, and you cannot leave a PID-1 reaper in the PID namespace. Timeouts and cancels leave sleeping processes behind.
Linux looks at the basename of argv[0]. If the filename is already codex-linux-sandbox, keep it; otherwise rewrite it to that alias and steer the same binary into the helper. The outer exec is not ls; it is the helper itself, with --apply-seccomp-then-exec. The launcher also inserts --as-pid-1 so bubblewrap itself is PID 1 in the namespace.
The inner pass starts with capget. If effective or permitted is nonzero, it panics at once; the command never runs. Only after that check does it fork. The child execvps; the parent waitpid(-1) reaps descendants and forwards the exit code as-is.Source: codex-rs/linux-sandbox/src/linux_run_main.rs lines 216–221、lines 242–243;codex-rs/linux-sandbox/src/launcher.rs lines 38–39
A dedicated helper plus wrap-then-tighten is the same family of self-invocation across platforms. Windows looks at a hidden flag; Linux looks at arg0. When the wrapper fails, a fixed exit code plus a fixed stderr prefix is easier to attribute automatically.
The type is still named LandlockCommand, and the crate directory is still linux-sandbox, so plenty of write-ups say Codex uses Landlock on Linux. The current default filesystem sandbox is already bubblewrap. Landlock is an explicit legacy fallback; the feature flag is marked Deprecated and off by default. Legacy cannot express restricted read, and cannot express nested read-only that was split out.
If bwrap fails and silently switches to Landlock, the model plans as if “I am locked in a view,” while it can still see the host file tree. The next step forks.
The outer pass writes never falls back. bubblewrap failure is failure. On WSL1, when bubblewrap is required, transform returns Wsl1UnsupportedForBubblewrap and the command never enters the helper. When the disk is fully writable and no proxy is used, bwrap is not entered at all, so WSL1 can pass. A user namespace that cannot be built becomes a startup warning; it does not switch to a weaker backend.Source: codex-rs/linux-sandbox/src/linux_run_main.rs lines 290–294;codex-rs/sandboxing/src/manager.rs lines 413–420、lines 696–710;codex-rs/features/src/lib.rs lines 1064–1069
When platforms are unequal, the easy mistake is covering every Linux with “we have a Linux sandbox.” A laptop with a new bwrap, a CI container that banned user namespaces, a teammate still on WSL1 — three machines, not one wall. Product copy and the model brief must be written against the layer that is actually on.
DeepSeek Harness: if the probe passes, shift gears, fuse into one beat
DSH’s Linux chain is hard-wired as bwrap then landlock. The bwrap tier is view plus exec, no seccomp. Only if the probe fails does it take landlock-run: install rules on itself, then exec the target. Filesystem limits and exec share one main. That path has no setuid bwrap, so the filter does not need another beat.
Both sides use a dedicated helper. DSH launcher failure is 125, and you also need the fatal landlock-run: line — easier to attribute. Codex helper failure is a panic or the wait status as-is. If both chains fail the probe, DSH refuses to run naked. It may shift to a probed second tier; after the shift it still fails closed. Codex’s default path does not fall back to Landlock.Source: packages/sandbox/sandbox-local/src/index.ts lines 159–166;native/landlock-run/packages/entry/src/main.c lines 254–261
Grok: three beats hit the agent; failure may fall back
Grok’s order shares Codex’s engineering reason: let a possibly-setuid bwrap finish, then tighten inside a process that is already in the namespace. The subject differs. Grok lands the three beats on the agent process; commands only inherit the result. The view is a patchy --bind / / then deny paths on top; the host tree is still there. When a child spawns, if the network must be limited, seccomp is installed in pre_exec.
If bwrap exec fails and read-deny is not required, it warns once and falls back to Landlock. A built-in Profile apply failure is a warning and continue; is_active() is the real state. Codex’s outer pass writes never falls back.Source: crates/codegen/xai-grok-shell/src/config/mod.rs lines 1293–1320;crates/codegen/xai-grok-sandbox/src/lib.rs lines 181–192
The same read — after you flip switches, where does it stick
Leave the probe on reading /etc/shadow, then turn on WSL1. Write your prediction: which layer it stops at, and what the copy says. Then turn WSL1 off, turn on legacy Landlock, and see whether restricted read becomes UnsupportedOperation.
Advanced: switch the probe to connect outbound. Why does the default path walk through the view and stop at seccomp, with the copy changing from No such file to EPERM?