fix: warn when bwrap cannot create user namespaces (#15893)

## Summary
- add a Linux startup warning when system `bwrap` is present but cannot
create user namespaces
- keep the Linux-specific probe, sandbox-policy gate, and stderr
matching in `codex-sandboxing`
- polish the missing-`bwrap` warning to point users at the sandbox
prerequisites and OS package-manager install path

## Details
- probes system `bwrap` with `--unshare-user`, `--unshare-net`, and a
minimal bind before command execution
- detects known bubblewrap setup failures for `RTM_NEWADDR`,
`RTM_NEWLINK`, uid-map permission denial, and `No permissions to create
a new namespace`
- preserves the existing suppression for sandbox-bypassed policies such
as `danger-full-access` and `external-sandbox`
- updates the Linux sandbox docs to call out the user-namespace
requirement

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-06 19:19:35 -07:00
committed by GitHub
Unverified
parent 5fe9ef06ce
commit 806e5f7c69
4 changed files with 87 additions and 25 deletions
+2 -1
View File
@@ -41,7 +41,8 @@ too old to support `--argv0`, the helper keeps using system bubblewrap and
switches to a no-`--argv0` compatibility path for the inner re-exec. If
`bwrap` is missing, it falls back to the vendored bubblewrap path compiled into
the binary and Codex surfaces a startup warning through its normal notification
path instead of printing directly from the sandbox helper.
path instead of printing directly from the sandbox helper. Codex also surfaces
a startup warning when bubblewrap cannot create user namespaces.
### Windows
+4 -1
View File
@@ -15,7 +15,8 @@ no-`--argv0` compatibility path for the inner re-exec. If `bwrap` is missing,
the helper falls back to the vendored bubblewrap path compiled into this
binary.
Codex also surfaces a startup warning when `bwrap` is missing so users know it
is falling back to the vendored helper.
is falling back to the vendored helper. Codex surfaces the same startup warning
path when bubblewrap cannot create user namespaces.
**Current Behavior**
- Legacy `SandboxPolicy` / `sandbox_mode` configs remain supported.
@@ -28,6 +29,8 @@ is falling back to the vendored helper.
path.
- If `bwrap` is missing, Codex also surfaces a startup warning instead of
printing directly from the sandbox helper.
- If bubblewrap cannot create user namespaces, Codex surfaces a startup warning
instead of waiting for a runtime sandbox failure.
- Legacy Landlock + mount protections remain available as an explicit legacy
fallback path.
- Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`)
+54 -8
View File
@@ -1,15 +1,33 @@
use codex_protocol::protocol::SandboxPolicy;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
const SYSTEM_BWRAP_PROGRAM: &str = "bwrap";
const MISSING_BWRAP_WARNING: &str = concat!(
"Codex could not find bubblewrap on PATH. ",
"Install bubblewrap with your OS package manager. ",
"See the sandbox prerequisites: ",
"https://developers.openai.com/codex/concepts/sandboxing#prerequisites. ",
"Codex will use the vendored bubblewrap in the meantime.",
);
const USER_NAMESPACE_WARNING: &str =
"Codex's Linux sandbox uses bubblewrap and needs access to create user namespaces.";
const USER_NAMESPACE_FAILURES: [&str; 4] = [
"loopback: Failed RTM_NEWADDR",
"loopback: Failed RTM_NEWLINK",
"setting up uid map: Permission denied",
"No permissions to create a new namespace",
];
pub fn system_bwrap_warning(sandbox_policy: &SandboxPolicy) -> Option<String> {
if !should_warn_about_system_bwrap(sandbox_policy) {
return None;
}
system_bwrap_warning_for_lookup(find_system_bwrap_in_path())
let system_bwrap_path = find_system_bwrap_in_path();
system_bwrap_warning_for_path(system_bwrap_path.as_deref())
}
fn should_warn_about_system_bwrap(sandbox_policy: &SandboxPolicy) -> bool {
@@ -19,14 +37,42 @@ fn should_warn_about_system_bwrap(sandbox_policy: &SandboxPolicy) -> bool {
)
}
fn system_bwrap_warning_for_lookup(system_bwrap_path: Option<PathBuf>) -> Option<String> {
match system_bwrap_path {
Some(_) => None,
None => Some(
"Codex could not find system bubblewrap on PATH. Please install bubblewrap with your package manager. Codex will use the vendored bubblewrap in the meantime."
.to_string(),
),
fn system_bwrap_warning_for_path(system_bwrap_path: Option<&Path>) -> Option<String> {
let Some(system_bwrap_path) = system_bwrap_path else {
return Some(MISSING_BWRAP_WARNING.to_string());
};
if !system_bwrap_has_user_namespace_access(system_bwrap_path) {
return Some(USER_NAMESPACE_WARNING.to_string());
}
None
}
fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path) -> bool {
let output = match Command::new(system_bwrap_path)
.args([
"--unshare-user",
"--unshare-net",
"--ro-bind",
"/",
"/",
"/bin/true",
])
.output()
{
Ok(output) => output,
Err(_) => return true,
};
output.status.success() || !is_user_namespace_failure(&output)
}
fn is_user_namespace_failure(output: &Output) -> bool {
let stderr = String::from_utf8_lossy(&output.stderr);
USER_NAMESPACE_FAILURES
.iter()
.any(|failure| stderr.contains(failure))
}
pub fn find_system_bwrap_in_path() -> Option<PathBuf> {
+27 -15
View File
@@ -6,30 +6,42 @@ use tempfile::tempdir;
#[test]
fn system_bwrap_warning_reports_missing_system_bwrap() {
let warning = system_bwrap_warning_for_lookup(/*system_bwrap_path*/ None)
.expect("missing system bwrap should emit a warning");
assert!(warning.contains("could not find system bubblewrap"));
assert_eq!(
system_bwrap_warning_for_path(/*system_bwrap_path*/ None),
Some(MISSING_BWRAP_WARNING.to_string())
);
}
#[test]
fn system_bwrap_warning_skips_too_old_system_bwrap() {
fn system_bwrap_warning_reports_user_namespace_failures() {
for failure in USER_NAMESPACE_FAILURES {
let fake_bwrap = write_fake_bwrap(&format!(
r#"#!/bin/sh
echo '{failure}' >&2
exit 1
"#
));
let fake_bwrap_path: &Path = fake_bwrap.as_ref();
assert_eq!(
system_bwrap_warning_for_path(Some(fake_bwrap_path)),
Some(USER_NAMESPACE_WARNING.to_string()),
"{failure}",
);
}
}
#[test]
fn system_bwrap_warning_skips_unrelated_bwrap_failures() {
let fake_bwrap = write_fake_bwrap(
r#"#!/bin/sh
if [ "$1" = "--help" ]; then
echo 'usage: bwrap [OPTION...] COMMAND'
exit 0
fi
echo 'bwrap: Unknown option --argv0' >&2
exit 1
"#,
);
let fake_bwrap_path: &Path = fake_bwrap.as_ref();
assert_eq!(
system_bwrap_warning_for_lookup(Some(fake_bwrap_path.to_path_buf())),
None,
"Do not warn even if bwrap does not support `--argv0`",
);
assert_eq!(system_bwrap_warning_for_path(Some(fake_bwrap_path)), None);
}
#[test]
@@ -102,5 +114,5 @@ fn write_named_fake_bwrap_in(dir: &Path) -> PathBuf {
fs::write(&path, "#!/bin/sh\n").expect("write fake bwrap");
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&path, permissions).expect("chmod fake bwrap");
path
fs::canonicalize(path).expect("canonicalize fake bwrap")
}