OpenAI Codex · Code Mode

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.

Course goalAfter this lesson you can explain three things. Why the Linux default path is two beats: bubblewrap builds the filesystem view first, then seccomp. Why a separate helper exists. Why the type is still named LandlockCommand while the default walks bubblewrap.
Try it first · One request through two layers
Swap probes, flip switches, see which layer stops the request
Probe
Switches
On the default path, reading /etc/shadow stops at the view. Switch to connect and the same road reaches the filter.
This request
open /etc/shadow
Current path
Default two-stage
Layer 1 · PreflightStandby
transform first asks whether the helper is there, whether this is WSL1, and whether a user namespace can be built. If it cannot enter, it stops here.
Layer 2 · ViewStandby
bubblewrap swaps the file tree first. Default is read-only; writable roots are layered on. A path not in the mounts later reports that the file does not exist.
Layer 3 · seccompStandby
Only after the view is built does it set no_new_privs and install the network filter. A hit returns EPERM.
Hit Play to watch this request walk through the two layers.
Logic trail · each animation step maps to a source span
  1. Read the permission profile and preflight WSL1manager.rs L413
  2. Build helper argv and attach the profile JSONlandlock.rs L23
  3. Outer run_main picks bwrap or the shortcutlinux_run_main.rs L152
  4. Build the inner command with --apply-seccomp-then-execlinux_run_main.rs L1520
  5. bwrap does ro-bind, bind, unshare in orderbwrap.rs L306
  6. Insert --as-pid-1 when execing bwraplauncher.rs L39
  7. Inner capget, confirm capabilities are zerolinux_run_main.rs L216
  8. Set PR_SET_NO_NEW_PRIVS, then install seccomplandlock.rs L61
  9. Fork the user command; the parent waitpid reaps orphanslinux_run_main.rs L243
  10. execvp takes over the process imagelinux_run_main.rs L1565
Hit Play to watch this request walk through the two layers.
The view blocks seeingIf the path is not in the mounts, the kernel returns No such file or directory. This layer has not reached the syscall table yet.
seccomp blocks callingThe filter allows by default; only hits on rules like connect, bind, ptrace return EPERM. Files already invisible are not its job.
If it cannot enter, say soOn WSL1 or a missing user namespace, the command stops in transform. A default-path failure does not quietly fall back to Landlock.
Teaching sketch: paths and probes are course fixtures. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Swap the world it can see, then tighten calls
What problem it solves

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.

What the idea is

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:

codex-rs/linux-sandbox/src/linux_run_main.rslines 152–158
/// 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.
Source snapshot note: based on the local 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

Outer helper Build bubblewrap flags --ro-bind then --bind Swap the file tree first Do not touch no_new_privs yet Inner helper capget confirms caps are zero no_new_privs plus seccomp Then tighten syscalls With --apply-seccomp-then-exec execvp User command Takes the process
Teaching diagram: the same binary enters twice, with a bubblewrap-built world in between.
Why it lasts

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.

Idea 2 · Start a helper, leave someone to reap orphans
What problem it solves

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.

What the idea is

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

SandboxManager Outer helper Build inner argv bubblewrap --as-pid-1 Inner fork of the command Child execvp Parent stays waitpid reaps orphans Cancel and timeout lean on the PID 1 that stayed
Teaching sequence: after the process is handed off, the reaper is still there.
Why it lasts

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.

Idea 3 · The default is not Landlock, and failure does not fall back
What problem it solves

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.

What the idea is

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

Local distro Has user namespace System bwrap or bundled binary Default two-stage CI container User namespace disabled No silent Landlock fallback Startup warning, hard fail WSL1 Cannot build a user namespace Commands that need bwrap cannot enter Refused at transform
Teaching diagram: they are all called a Linux sandbox; the layer that is actually on is not the same on three machines.
Seeing and calling are two walls. If it cannot enter, say so — do not synthesize “already sandboxed.”
Why it lasts

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.

Side-by-side · after the three beats split, what each side leaks

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

Checked DSH PLATFORM_CHAINS and landlock-run · 2026-08-22 · DSH · Sandbox

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

Checked Grok bwrap_reexec_for_profile and apply · 2026-08-22 · Grok · Five sandbox Profiles
Classroom Exercise
01

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?

Takeaway: The Linux default path is three beats: bubblewrap swaps the filesystem view, then seccomp is applied in a helper already inside the namespace, then exec. Reverse the order and a setuid system bwrap will not start. The type is still named Landlock — that is compatibility, not the current default. If it cannot enter, refuse before spawn; failure does not switch to a weaker backend.