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
|
|
|
|
fix(linux-sandbox): prefer system /usr/bin/bwrap when available (#14963)
## Problem
Ubuntu/AppArmor hosts started failing in the default Linux sandbox path
after the switch to vendored/default bubblewrap in `0.115.0`.
The clearest report is in
[#14919](https://github.com/openai/codex/issues/14919), especially [this
investigation
comment](https://github.com/openai/codex/issues/14919#issuecomment-4076504751):
on affected Ubuntu systems, `/usr/bin/bwrap` works, but a copied or
vendored `bwrap` binary fails with errors like `bwrap: setting up uid
map: Permission denied` or `bwrap: loopback: Failed RTM_NEWADDR:
Operation not permitted`.
The root cause is Ubuntu's `/etc/apparmor.d/bwrap-userns-restrict`
profile, which grants `userns` access specifically to `/usr/bin/bwrap`.
Once Codex started using a vendored/internal bubblewrap path, that path
was no longer covered by the distro AppArmor exception, so sandbox
namespace setup could fail even when user namespaces were otherwise
enabled and `uidmap` was installed.
## What this PR changes
- prefer system `/usr/bin/bwrap` whenever it is available
- keep vendored bubblewrap as the fallback when `/usr/bin/bwrap` is
missing
- when `/usr/bin/bwrap` is missing, surface a Codex startup warning
through the app-server/TUI warning path instead of printing directly
from the sandbox helper with `eprintln!`
- use the same launcher decision for both the main sandbox execution
path and the `/proc` preflight path
- document the updated Linux bubblewrap behavior in the Linux sandbox
and core READMEs
## Why this fix
This still fixes the Ubuntu/AppArmor regression from
[#14919](https://github.com/openai/codex/issues/14919), but it keeps the
runtime rule simple and platform-agnostic: if the standard system
bubblewrap is installed, use it; otherwise fall back to the vendored
helper.
The warning now follows that same simple rule. If Codex cannot find
`/usr/bin/bwrap`, it tells the user that it is falling back to the
vendored helper, and it does so through the existing startup warning
plumbing that reaches the TUI and app-server instead of low-level
sandbox stderr.
## Testing
- `cargo test -p codex-linux-sandbox`
- `cargo test -p codex-app-server --lib`
- `cargo test -p codex-tui-app-server
tests::embedded_app_server_start_failure_is_returned`
- `cargo clippy -p codex-linux-sandbox --all-targets`
- `cargo clippy -p codex-app-server --all-targets`
- `cargo clippy -p codex-tui-app-server --all-targets`
2026-03-17 16:05:34 -07:00
|
|
|
On Linux, the bubblewrap pipeline prefers the system `/usr/bin/bwrap` whenever
|
|
|
|
|
it is available. If `/usr/bin/bwrap` is missing, the helper still falls back to
|
|
|
|
|
the vendored bubblewrap path compiled into this binary.
|
|
|
|
|
Codex also surfaces a startup warning when `/usr/bin/bwrap` is missing so users
|
|
|
|
|
know it is falling back to the vendored helper.
|
2026-02-04 11:13:17 -08:00
|
|
|
|
|
|
|
|
**Current Behavior**
|
2026-03-12 18:36:06 -07:00
|
|
|
- Legacy `SandboxPolicy` / `sandbox_mode` configs remain supported.
|
fix(linux-sandbox): prefer system /usr/bin/bwrap when available (#14963)
## Problem
Ubuntu/AppArmor hosts started failing in the default Linux sandbox path
after the switch to vendored/default bubblewrap in `0.115.0`.
The clearest report is in
[#14919](https://github.com/openai/codex/issues/14919), especially [this
investigation
comment](https://github.com/openai/codex/issues/14919#issuecomment-4076504751):
on affected Ubuntu systems, `/usr/bin/bwrap` works, but a copied or
vendored `bwrap` binary fails with errors like `bwrap: setting up uid
map: Permission denied` or `bwrap: loopback: Failed RTM_NEWADDR:
Operation not permitted`.
The root cause is Ubuntu's `/etc/apparmor.d/bwrap-userns-restrict`
profile, which grants `userns` access specifically to `/usr/bin/bwrap`.
Once Codex started using a vendored/internal bubblewrap path, that path
was no longer covered by the distro AppArmor exception, so sandbox
namespace setup could fail even when user namespaces were otherwise
enabled and `uidmap` was installed.
## What this PR changes
- prefer system `/usr/bin/bwrap` whenever it is available
- keep vendored bubblewrap as the fallback when `/usr/bin/bwrap` is
missing
- when `/usr/bin/bwrap` is missing, surface a Codex startup warning
through the app-server/TUI warning path instead of printing directly
from the sandbox helper with `eprintln!`
- use the same launcher decision for both the main sandbox execution
path and the `/proc` preflight path
- document the updated Linux bubblewrap behavior in the Linux sandbox
and core READMEs
## Why this fix
This still fixes the Ubuntu/AppArmor regression from
[#14919](https://github.com/openai/codex/issues/14919), but it keeps the
runtime rule simple and platform-agnostic: if the standard system
bubblewrap is installed, use it; otherwise fall back to the vendored
helper.
The warning now follows that same simple rule. If Codex cannot find
`/usr/bin/bwrap`, it tells the user that it is falling back to the
vendored helper, and it does so through the existing startup warning
plumbing that reaches the TUI and app-server instead of low-level
sandbox stderr.
## Testing
- `cargo test -p codex-linux-sandbox`
- `cargo test -p codex-app-server --lib`
- `cargo test -p codex-tui-app-server
tests::embedded_app_server_start_failure_is_returned`
- `cargo clippy -p codex-linux-sandbox --all-targets`
- `cargo clippy -p codex-app-server --all-targets`
- `cargo clippy -p codex-tui-app-server --all-targets`
2026-03-17 16:05:34 -07:00
|
|
|
- Bubblewrap is the default filesystem sandbox pipeline.
|
|
|
|
|
- If `/usr/bin/bwrap` is present, the helper uses it.
|
|
|
|
|
- If `/usr/bin/bwrap` is missing, the helper falls back to the vendored
|
|
|
|
|
bubblewrap path.
|
|
|
|
|
- If `/usr/bin/bwrap` is missing, Codex also surfaces a startup warning instead
|
|
|
|
|
of printing directly from the sandbox helper.
|
2026-03-12 10:56:32 -07:00
|
|
|
- Legacy Landlock + mount protections remain available as an explicit legacy
|
|
|
|
|
fallback path.
|
2026-03-11 23:31:18 -07:00
|
|
|
- Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`)
|
|
|
|
|
to force the legacy Landlock fallback.
|
2026-03-12 18:36:06 -07:00
|
|
|
- The legacy Landlock fallback is used only when the split filesystem policy is
|
|
|
|
|
sandbox-equivalent to the legacy model after `cwd` resolution.
|
2026-03-12 10:56:32 -07:00
|
|
|
- Split-only filesystem policies that do not round-trip through the legacy
|
|
|
|
|
`SandboxPolicy` model stay on bubblewrap so nested read-only or denied
|
|
|
|
|
carveouts are preserved.
|
|
|
|
|
- When the default bubblewrap pipeline is active, the helper applies `PR_SET_NO_NEW_PRIVS` and a
|
2026-02-04 11:13:17 -08:00
|
|
|
seccomp network filter in-process.
|
2026-03-11 23:31:18 -07:00
|
|
|
- When the default bubblewrap pipeline is active, the filesystem is read-only by default via `--ro-bind / /`.
|
|
|
|
|
- When the default bubblewrap pipeline is active, writable roots are layered with `--bind <root> <root>`.
|
2026-03-12 10:56:32 -07:00
|
|
|
- When the default bubblewrap pipeline is active, protected subpaths under writable roots (for
|
|
|
|
|
example `.git`,
|
2026-02-04 11:13:17 -08:00
|
|
|
resolved `gitdir:`, and `.codex`) are re-applied as read-only via `--ro-bind`.
|
2026-03-12 18:36:06 -07:00
|
|
|
- When the default bubblewrap pipeline is active, overlapping split-policy
|
|
|
|
|
entries are applied in path-specificity order so narrower writable children
|
|
|
|
|
can reopen broader read-only or denied parents while narrower denied subpaths
|
|
|
|
|
still win. For example, `/repo = write`, `/repo/a = none`, `/repo/a/b = write`
|
|
|
|
|
keeps `/repo` writable, denies `/repo/a`, and reopens `/repo/a/b` as
|
|
|
|
|
writable again.
|
2026-03-11 23:31:18 -07:00
|
|
|
- When the default bubblewrap pipeline is active, symlink-in-path and non-existent protected paths inside
|
2026-02-04 11:13:17 -08:00
|
|
|
writable roots are blocked by mounting `/dev/null` on the symlink or first
|
|
|
|
|
missing component.
|
2026-03-11 23:31:18 -07:00
|
|
|
- When the default bubblewrap pipeline is active, the helper explicitly isolates the user namespace via
|
2026-03-05 13:57:40 -08:00
|
|
|
`--unshare-user` and the PID namespace via `--unshare-pid`.
|
2026-03-11 23:31:18 -07:00
|
|
|
- When the default bubblewrap pipeline is active and network is restricted without proxy routing, the helper also
|
2026-02-09 23:44:21 -08:00
|
|
|
isolates the network namespace via `--unshare-net`.
|
2026-02-21 10:16:34 -08:00
|
|
|
- In managed proxy mode, the helper uses `--unshare-net` plus an internal
|
|
|
|
|
TCP->UDS->TCP routing bridge so tool traffic reaches only configured proxy
|
|
|
|
|
endpoints.
|
|
|
|
|
- In managed proxy mode, after the bridge is live, seccomp blocks new
|
|
|
|
|
AF_UNIX/socketpair creation for the user command.
|
2026-03-11 23:31:18 -07:00
|
|
|
- When the default bubblewrap pipeline is active, it mounts a fresh `/proc` via `--proc /proc` by default, but
|
2026-02-04 11:13:17 -08:00
|
|
|
you can skip this in restrictive container environments with `--no-proc`.
|
|
|
|
|
|
|
|
|
|
**Notes**
|
|
|
|
|
- The CLI surface still uses legacy names like `codex debug landlock`.
|