Files
codex/codex-rs/sandboxing/src/bwrap.rs
T
Michael BolinandGitHub 26f355b67b linux-sandbox: use standalone bundled bwrap (#21255)
**Summary**
- Add `codex-bwrap`, a standalone `bwrap` binary built from the existing
vendored bubblewrap sources.
- Remove the linked vendored bwrap path from `codex-linux-sandbox`;
runtime now prefers system `bwrap` and falls back to bundled
`codex-resources/bwrap`.
- Add bundled SHA-256 verification with missing/all-zero digest as the
dev-mode skip value, then exec the verified file through
`/proc/self/fd`.
- Keep `launcher.rs` focused on choosing and dispatching the preferred
launcher. Bundled lookup, digest verification, and bundled exec now live
in `linux-sandbox/src/bundled_bwrap.rs`; Bazel runfiles lookup lives in
`linux-sandbox/src/bazel_bwrap.rs`; shared argv/fd exec helpers live in
`linux-sandbox/src/exec_util.rs`.
- Teach Bazel tests to surface the Bazel-built `//codex-rs/bwrap:bwrap`
through `CARGO_BIN_EXE_bwrap`; `codex-linux-sandbox` only honors that
fallback in debug Bazel runfiles environments so release/user runtime
lookup stays tied to `codex-resources/bwrap`.
- Allow `codex-exec-server` filesystem helpers to preserve just the
Bazel bwrap/runfiles variables they need in debug Bazel builds, since
those helpers intentionally rebuild a small environment before spawning
`codex-linux-sandbox`.
- Verify the Bazel bwrap target in Linux release CI with a build-only
check. Running `bwrap --version` is too strong for GitHub runners
because bubblewrap still attempts namespace setup there.

**Verification**
- Latest update: `cargo test -p codex-linux-sandbox`
- Latest update: `just fix -p codex-linux-sandbox`
- `cargo check --target x86_64-unknown-linux-gnu -p codex-linux-sandbox`
could not run locally because this macOS machine does not have
`x86_64-linux-gnu-gcc`; GitHub Linux Bazel CI is expected to cover the
Linux-only modules.
- Earlier in this PR: `cargo test -p codex-bwrap`
- Earlier in this PR: `cargo test -p codex-exec-server`
- Earlier in this PR: `cargo check --release -p codex-exec-server`
- Earlier in this PR: `just fix -p codex-linux-sandbox -p
codex-exec-server`
- Earlier in this PR: `bazel test --nobuild
//codex-rs/linux-sandbox:linux-sandbox-all-test
//codex-rs/core:core-all-test
//codex-rs/exec-server:exec-server-file_system-test
//codex-rs/app-server:app-server-all-test` (analysis completed; Bazel
then refuses to run tests under `--nobuild`)
- Earlier in this PR: `bazel build --nobuild //codex-rs/bwrap:bwrap`
- Prior to this update: `just bazel-lock-update`, `just
bazel-lock-check`, and YAML parse check for
`.github/workflows/bazel.yml`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/21255).
* #21257
* #21256
* __->__ #21255
2026-05-05 17:14:29 -07:00

195 lines
6.5 KiB
Rust

use crate::policy_transforms::should_require_platform_sandbox;
use codex_protocol::models::PermissionProfile;
use std::io::ErrorKind;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
use std::process::Stdio;
use std::thread;
use std::time::Duration;
use std::time::Instant;
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 bundled bubblewrap in the meantime.",
);
const USER_NAMESPACE_WARNING: &str =
"Codex's Linux sandbox uses bubblewrap and needs access to create user namespaces.";
pub(crate) const WSL1_BWRAP_WARNING: &str = concat!(
"Codex's Linux sandbox uses bubblewrap, which is not supported on WSL1 ",
"because WSL1 cannot create the required user namespaces. ",
"Use WSL2 for sandboxed shell commands."
);
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",
];
const SYSTEM_BWRAP_PROBE_TIMEOUT: Duration = Duration::from_millis(500);
const SYSTEM_BWRAP_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(50);
const SYSTEM_BWRAP_PROBE_STDERR_LIMIT_BYTES: u64 = 64 * 1024;
pub fn system_bwrap_warning(permission_profile: &PermissionProfile) -> Option<String> {
if !should_warn_about_system_bwrap(permission_profile) {
return None;
}
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(permission_profile: &PermissionProfile) -> bool {
let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
should_require_platform_sandbox(
&file_system_policy,
network_policy,
/*has_managed_network_requirements*/ false,
)
}
fn system_bwrap_warning_for_path(system_bwrap_path: Option<&Path>) -> Option<String> {
if is_wsl1() {
return Some(WSL1_BWRAP_WARNING.to_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, SYSTEM_BWRAP_PROBE_TIMEOUT) {
return Some(USER_NAMESPACE_WARNING.to_string());
}
None
}
fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path, timeout: Duration) -> bool {
let mut child = match Command::new(system_bwrap_path)
.args([
"--unshare-user",
"--unshare-net",
"--ro-bind",
"/",
"/",
"/bin/true",
])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(_) => return true,
};
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => {
let stderr = child.stderr.take().map_or_else(Vec::new, |stderr| {
let fd = stderr.as_raw_fd();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0
|| unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0
{
return Vec::new();
}
let mut bytes = Vec::new();
let mut stderr = stderr.take(SYSTEM_BWRAP_PROBE_STDERR_LIMIT_BYTES);
if let Err(err) = stderr.read_to_end(&mut bytes)
&& err.kind() != ErrorKind::WouldBlock
{
return bytes;
}
bytes
});
let output = Output {
status,
stdout: Vec::new(),
stderr,
};
return output.status.success() || !is_user_namespace_failure(&output);
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return true;
}
thread::sleep(SYSTEM_BWRAP_PROBE_POLL_INTERVAL);
}
Err(_) => {
let _ = child.kill();
let _ = child.wait();
return true;
}
}
}
}
pub(crate) fn is_wsl1() -> bool {
std::fs::read_to_string("/proc/version")
.is_ok_and(|proc_version| proc_version_indicates_wsl1(&proc_version))
}
fn proc_version_indicates_wsl1(proc_version: &str) -> bool {
let proc_version = proc_version.to_ascii_lowercase();
let mut remaining = proc_version.as_str();
while let Some(marker) = remaining.find("wsl") {
let version_start = marker + "wsl".len();
let version_digits: String = remaining[version_start..]
.chars()
.take_while(char::is_ascii_digit)
.collect();
if let Ok(version) = version_digits.parse::<u32>() {
return version == 1;
}
remaining = &remaining[version_start..];
}
proc_version.contains("microsoft") && !proc_version.contains("microsoft-standard")
}
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> {
let search_path = std::env::var_os("PATH")?;
let cwd = std::env::current_dir().ok()?;
find_system_bwrap_in_search_paths(std::env::split_paths(&search_path), &cwd)
}
fn find_system_bwrap_in_search_paths(
search_paths: impl IntoIterator<Item = PathBuf>,
cwd: &Path,
) -> Option<PathBuf> {
let search_path = std::env::join_paths(search_paths).ok()?;
let cwd = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
which::which_in_all(SYSTEM_BWRAP_PROGRAM, Some(search_path), &cwd)
.ok()?
.find_map(|path| {
let path = std::fs::canonicalize(path).ok()?;
if path.starts_with(&cwd) {
None
} else {
Some(path)
}
})
}
#[cfg(test)]
#[path = "bwrap_tests.rs"]
mod tests;