diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 63cd3b6f36a..d8ea2ac9843 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -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 diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index d8af1959a40..340b1091017 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -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`) diff --git a/codex-rs/sandboxing/src/bwrap.rs b/codex-rs/sandboxing/src/bwrap.rs index aa73af1d3e5..01d53ebbe84 100644 --- a/codex-rs/sandboxing/src/bwrap.rs +++ b/codex-rs/sandboxing/src/bwrap.rs @@ -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 { 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) -> Option { - 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 { + 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 { diff --git a/codex-rs/sandboxing/src/bwrap_tests.rs b/codex-rs/sandboxing/src/bwrap_tests.rs index 43eddb39c42..b0303e2b78b 100644 --- a/codex-rs/sandboxing/src/bwrap_tests.rs +++ b/codex-rs/sandboxing/src/bwrap_tests.rs @@ -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_eq!( + system_bwrap_warning_for_path(/*system_bwrap_path*/ None), + Some(MISSING_BWRAP_WARNING.to_string()) + ); +} - assert!(warning.contains("could not find system bubblewrap")); +#[test] +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_too_old_system_bwrap() { +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") }