fix: overhaul how we spawn commands under seccomp/landlock on Linux (#1086)
Historically, we spawned the Seatbelt and Landlock sandboxes in
substantially different ways:
For **Seatbelt**, we would run `/usr/bin/sandbox-exec` with our policy
specified as an arg followed by the original command:
https://github.com/openai/codex/blob/d1de7bb383552e8fadd94be79d65d188e00fd562/codex-rs/core/src/exec.rs#L147-L219
For **Landlock/Seccomp**, we would do
`tokio::runtime::Builder::new_current_thread()`, _invoke
Landlock/Seccomp APIs to modify the permissions of that new thread_, and
then spawn the command:
https://github.com/openai/codex/blob/d1de7bb383552e8fadd94be79d65d188e00fd562/codex-rs/core/src/exec_linux.rs#L28-L49
While it is neat that Landlock/Seccomp supports applying a policy to
only one thread without having to apply it to the entire process, it
requires us to maintain two different codepaths and is a bit harder to
reason about. The tipping point was
https://github.com/openai/codex/pull/1061, in which we had to start
building up the `env` in an unexpected way for the existing
Landlock/Seccomp approach to continue to work.
This PR overhauls things so that we do similar things for Mac and Linux.
It turned out that we were already building our own "helper binary"
comparable to Mac's `sandbox-exec` as part of the `cli` crate:
https://github.com/openai/codex/blob/d1de7bb383552e8fadd94be79d65d188e00fd562/codex-rs/cli/Cargo.toml#L10-L12
We originally created this to build a small binary to include with the
Node.js version of the Codex CLI to provide support for Linux
sandboxing.
Though the sticky bit is that, at this point, we still want to deploy
the Rust version of Codex as a single, standalone binary rather than a
CLI and a supporting sandboxing binary. To satisfy this goal, we use
"the arg0 trick," in which we:
* use `std::env::current_exe()` to get the path to the CLI that is
currently running
* use the CLI as the `program` for the `Command`
* set `"codex-linux-sandbox"` as arg0 for the `Command`
A CLI that supports sandboxing should check arg0 at the start of the
program. If it is `"codex-linux-sandbox"`, it must invoke
`codex_linux_sandbox::run_main()`, which runs the CLI as if it were
`codex-linux-sandbox`. When acting as `codex-linux-sandbox`, we make the
appropriate Landlock/Seccomp API calls and then use `execvp(3)` to spawn
the original command, so do _replace_ the process rather than spawn a
subprocess. Incidentally, we do this before starting the Tokio runtime,
so the process should only have one thread when `execvp(3)` is called.
Because the `core` crate that needs to spawn the Linux sandboxing is not
a CLI in its own right, this means that every CLI that includes `core`
and relies on this behavior has to (1) implement it and (2) provide the
path to the sandboxing executable. While the path is almost always
`std::env::current_exe()`, we needed to make this configurable for
integration tests, so `Config` now has a `codex_linux_sandbox_exe:
Option<PathBuf>` property to facilitate threading this through,
introduced in https://github.com/openai/codex/pull/1089.
This common pattern is now captured in
`codex_linux_sandbox::run_with_sandbox()` and all of the `main.rs`
functions that should use it have been updated as part of this PR.
The `codex-linux-sandbox` crate added to the Cargo workspace as part of
this PR now has the bulk of the Landlock/Seccomp logic, which makes
`core` a bit simpler. Indeed, `core/src/exec_linux.rs` and
`core/src/landlock.rs` were removed/ported as part of this PR. I also
moved the unit tests for this code into an integration test,
`linux-sandbox/tests/landlock.rs`, in which I use
`env!("CARGO_BIN_EXE_codex-linux-sandbox")` as the value for
`codex_linux_sandbox_exe` since `std::env::current_exe()` is not
appropriate in that case.
2025-05-23 11:37:07 -07:00
|
|
|
# codex-linux-sandbox
|
|
|
|
|
|
|
|
|
|
This crate is responsible for producing:
|
|
|
|
|
|
|
|
|
|
- a `codex-linux-sandbox` standalone executable for Linux that is bundled with the Node.js version of the Codex CLI
|
|
|
|
|
- a lib crate that exposes the business logic of the executable as `run_main()` so that
|
|
|
|
|
- the `codex-exec` CLI can check if its arg0 is `codex-linux-sandbox` and, if so, execute as if it were `codex-linux-sandbox`
|
|
|
|
|
- this should also be true of the `codex` multitool CLI
|
2026-02-04 11:13:17 -08:00
|
|
|
|
|
|
|
|
On Linux, the bubblewrap pipeline uses the vendored bubblewrap path compiled
|
|
|
|
|
into this binary.
|
|
|
|
|
|
|
|
|
|
**Current Behavior**
|
|
|
|
|
- Legacy Landlock + mount protections remain available as the legacy pipeline.
|
|
|
|
|
- The bubblewrap pipeline is standardized on the vendored path.
|
|
|
|
|
- During rollout, the bubblewrap pipeline is gated by the temporary feature
|
|
|
|
|
flag `use_linux_sandbox_bwrap` (CLI `-c` alias for
|
|
|
|
|
`features.use_linux_sandbox_bwrap`; legacy remains default when off).
|
|
|
|
|
- When enabled, the bubblewrap pipeline applies `PR_SET_NO_NEW_PRIVS` and a
|
|
|
|
|
seccomp network filter in-process.
|
|
|
|
|
- When enabled, the filesystem is read-only by default via `--ro-bind / /`.
|
|
|
|
|
- When enabled, writable roots are layered with `--bind <root> <root>`.
|
|
|
|
|
- When enabled, protected subpaths under writable roots (for example `.git`,
|
|
|
|
|
resolved `gitdir:`, and `.codex`) are re-applied as read-only via `--ro-bind`.
|
|
|
|
|
- When enabled, symlink-in-path and non-existent protected paths inside
|
|
|
|
|
writable roots are blocked by mounting `/dev/null` on the symlink or first
|
|
|
|
|
missing component.
|
|
|
|
|
- When enabled, the helper isolates the PID namespace via `--unshare-pid`.
|
2026-02-09 23:44:21 -08:00
|
|
|
- When enabled and network is restricted without proxy routing, the helper also
|
|
|
|
|
isolates the network namespace via `--unshare-net`.
|
2026-02-04 11:13:17 -08:00
|
|
|
- When enabled, it mounts a fresh `/proc` via `--proc /proc` by default, but
|
|
|
|
|
you can skip this in restrictive container environments with `--no-proc`.
|
|
|
|
|
|
|
|
|
|
**Notes**
|
|
|
|
|
- The CLI surface still uses legacy names like `codex debug landlock`.
|