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
This commit is contained in:
Michael Bolin
2026-05-05 17:14:29 -07:00
committed by GitHub
parent 03d3403a41
commit 26f355b67b
27 changed files with 751 additions and 338 deletions
+68
View File
@@ -0,0 +1,68 @@
#[cfg(debug_assertions)]
use std::fs::File;
#[cfg(debug_assertions)]
use std::io::BufRead;
use std::path::PathBuf;
#[cfg(debug_assertions)]
const BAZEL_BWRAP_ENV_VAR: &str = "CARGO_BIN_EXE_bwrap";
#[cfg(debug_assertions)]
pub(crate) fn candidate() -> Option<PathBuf> {
if option_env!("BAZEL_PACKAGE").is_none() || !runfiles_env_present() {
return None;
}
let raw = PathBuf::from(std::env::var_os(BAZEL_BWRAP_ENV_VAR)?);
if raw.is_absolute() {
return Some(raw);
}
resolve_runfile(raw.to_str()?)
}
#[cfg(not(debug_assertions))]
pub(crate) fn candidate() -> Option<PathBuf> {
None
}
#[cfg(debug_assertions)]
fn runfiles_env_present() -> bool {
std::env::var_os("RUNFILES_DIR").is_some()
|| std::env::var_os("TEST_SRCDIR").is_some()
|| std::env::var_os("RUNFILES_MANIFEST_FILE").is_some()
}
#[cfg(debug_assertions)]
fn resolve_runfile(logical_path: &str) -> Option<PathBuf> {
let mut logical_paths = vec![logical_path.to_string()];
if let Ok(workspace) = std::env::var("TEST_WORKSPACE")
&& !workspace.is_empty()
{
logical_paths.push(format!("{workspace}/{logical_path}"));
}
for root_env in ["RUNFILES_DIR", "TEST_SRCDIR"] {
let Some(root) = std::env::var_os(root_env) else {
continue;
};
let root = PathBuf::from(root);
for logical_path in &logical_paths {
let candidate = root.join(logical_path);
if candidate.exists() {
return Some(candidate);
}
}
}
let manifest = PathBuf::from(std::env::var_os("RUNFILES_MANIFEST_FILE")?);
let file = File::open(manifest).ok()?;
for line in std::io::BufReader::new(file).lines().map_while(Result::ok) {
let Some((key, value)) = line.split_once(' ') else {
continue;
};
if logical_paths.iter().any(|logical_path| logical_path == key) {
return Some(PathBuf::from(value));
}
}
None
}
+276
View File
@@ -0,0 +1,276 @@
use std::ffi::CStr;
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::os::raw::c_char;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::bazel_bwrap;
use crate::exec_util::argv_to_cstrings;
use crate::exec_util::make_files_inheritable;
use codex_utils_absolute_path::AbsolutePathBuf;
use sha2::Digest as _;
use sha2::Sha256;
const SHA256_HEX_LEN: usize = 64;
const NULL_SHA256_DIGEST: [u8; 32] = [0; 32];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BundledBwrapLauncher {
program: AbsolutePathBuf,
}
pub(crate) fn launcher() -> Option<BundledBwrapLauncher> {
let current_exe = std::env::current_exe().ok()?;
find_for_exe(&current_exe).map(|program| BundledBwrapLauncher { program })
}
impl BundledBwrapLauncher {
pub(crate) fn exec(&self, argv: Vec<String>, preserved_files: Vec<File>) -> ! {
let bwrap_file = File::open(self.program.as_path()).unwrap_or_else(|err| {
panic!(
"failed to open bundled bubblewrap {}: {err}",
self.program.as_path().display()
)
});
verify_digest(&bwrap_file, expected_sha256(), self.program.as_path())
.unwrap_or_else(|err| panic!("{err}"));
make_files_inheritable(&preserved_files);
let fd_path = format!("/proc/self/fd/{}", bwrap_file.as_raw_fd());
let program_cstring = CString::new(fd_path.as_str())
.unwrap_or_else(|err| panic!("invalid bundled bubblewrap fd path: {err}"));
let cstrings = argv_to_cstrings(&argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings
.iter()
.map(CString::as_c_str)
.map(CStr::as_ptr)
.collect();
argv_ptrs.push(std::ptr::null());
// SAFETY: `program_cstring` and every entry in `argv_ptrs` are valid C
// strings for the duration of the call. On success `execv` does not return.
unsafe {
libc::execv(program_cstring.as_ptr(), argv_ptrs.as_ptr());
}
let err = std::io::Error::last_os_error();
panic!(
"failed to exec bundled bubblewrap {} via {fd_path}: {err}",
self.program.as_path().display()
);
}
}
fn find_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
candidates_for_exe(exe)
.into_iter()
.find(|candidate| is_executable_file(candidate))
.map(|path| {
AbsolutePathBuf::from_absolute_path(&path).unwrap_or_else(|err| {
panic!(
"failed to normalize bundled bubblewrap path {}: {err}",
path.display()
)
})
})
}
fn candidates_for_exe(exe: &Path) -> Vec<PathBuf> {
let Some(exe_dir) = exe.parent() else {
return Vec::new();
};
let mut candidates = Vec::new();
candidates.push(exe_dir.join("codex-resources").join("bwrap"));
if let Some(package_target_dir) = exe_dir.parent() {
candidates.push(package_target_dir.join("codex-resources").join("bwrap"));
}
candidates.push(exe_dir.join("bwrap"));
if let Some(path) = bazel_bwrap::candidate() {
candidates.push(path);
}
candidates
}
fn is_executable_file(path: &Path) -> bool {
let Ok(metadata) = path.metadata() else {
return false;
};
metadata.is_file() && metadata.permissions().mode() & 0o111 != 0
}
fn expected_sha256() -> Option<[u8; 32]> {
static EXPECTED: OnceLock<Option<[u8; 32]>> = OnceLock::new();
*EXPECTED.get_or_init(|| {
let raw_digest = option_env!("CODEX_BWRAP_SHA256")?;
let digest = parse_sha256_hex(raw_digest)
.unwrap_or_else(|err| panic!("invalid CODEX_BWRAP_SHA256 value: {err}"));
(digest != NULL_SHA256_DIGEST).then_some(digest)
})
}
fn verify_digest(file: &File, expected: Option<[u8; 32]>, path: &Path) -> Result<(), String> {
let Some(expected) = expected else {
return Ok(());
};
let mut file = file
.try_clone()
.map_err(|err| format!("failed to clone bundled bubblewrap fd: {err}"))?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 8192];
loop {
let read = file.read(&mut buffer).map_err(|err| {
format!(
"failed to read bundled bubblewrap {} for digest verification: {err}",
path.display()
)
})?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
let actual: [u8; 32] = hasher.finalize().into();
if actual == expected {
return Ok(());
}
Err(format!(
"bundled bubblewrap digest mismatch for {}: expected sha256:{}, got sha256:{}",
path.display(),
bytes_to_hex(&expected),
bytes_to_hex(&actual),
))
}
fn parse_sha256_hex(raw: &str) -> Result<[u8; 32], String> {
if raw.len() != SHA256_HEX_LEN {
return Err(format!(
"expected {SHA256_HEX_LEN} hex characters, got {}",
raw.len()
));
}
let mut digest = [0_u8; 32];
for (index, byte) in digest.iter_mut().enumerate() {
let start = index * 2;
*byte = u8::from_str_radix(&raw[start..start + 2], 16)
.map_err(|err| format!("invalid hex byte at offset {start}: {err}"))?;
}
Ok(digest)
}
fn bytes_to_hex(bytes: &[u8; 32]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hex = String::with_capacity(SHA256_HEX_LEN);
for byte in bytes {
hex.push(HEX[(byte >> 4) as usize] as char);
hex.push(HEX[(byte & 0x0f) as usize] as char);
}
hex
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::NamedTempFile;
use tempfile::tempdir;
#[test]
fn finds_standalone_bundled_bwrap_next_to_exe_resources() {
let temp_dir = tempdir().expect("temp dir");
let exe = temp_dir.path().join("codex");
let expected_bwrap = temp_dir.path().join("codex-resources").join("bwrap");
write_executable(&exe);
write_executable(&expected_bwrap);
assert_eq!(
find_for_exe(&exe),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
#[test]
fn finds_npm_bundled_bwrap_next_to_target_vendor_dir() {
let temp_dir = tempdir().expect("temp dir");
let target_dir = temp_dir.path().join("vendor/x86_64-unknown-linux-musl");
let exe = target_dir.join("codex").join("codex");
let expected_bwrap = target_dir.join("codex-resources").join("bwrap");
write_executable(&exe);
write_executable(&expected_bwrap);
assert_eq!(
find_for_exe(&exe),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
#[test]
fn finds_adjacent_dev_bwrap() {
let temp_dir = tempdir().expect("temp dir");
let exe = temp_dir.path().join("codex");
let expected_bwrap = temp_dir.path().join("bwrap");
write_executable(&exe);
write_executable(&expected_bwrap);
assert_eq!(
find_for_exe(&exe),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
#[test]
fn digest_verification_skips_missing_expected_digest() {
let file = NamedTempFile::new().expect("temp file");
fs::write(file.path(), b"contents").expect("write file");
verify_digest(file.as_file(), /*expected*/ None, file.path())
.expect("missing digest should skip verification");
}
#[test]
fn digest_verification_accepts_matching_digest() {
let file = NamedTempFile::new().expect("temp file");
fs::write(file.path(), b"contents").expect("write file");
let expected: [u8; 32] = Sha256::digest(b"contents").into();
verify_digest(file.as_file(), Some(expected), file.path())
.expect("matching digest should verify");
}
#[test]
fn digest_verification_rejects_mismatched_digest() {
let file = NamedTempFile::new().expect("temp file");
fs::write(file.path(), b"contents").expect("write file");
let err = verify_digest(file.as_file(), Some([0xab; 32]), file.path())
.expect_err("mismatched digest should fail");
assert!(err.contains("bundled bubblewrap digest mismatch"));
}
#[test]
fn parses_sha256_hex_digest() {
assert_eq!(parse_sha256_hex(&"ab".repeat(32)), Ok([0xab; 32]));
assert_eq!(parse_sha256_hex(&"00".repeat(32)), Ok(NULL_SHA256_DIGEST));
assert!(parse_sha256_hex("ab").is_err());
assert!(parse_sha256_hex(&format!("{}xx", "00".repeat(31))).is_err());
}
fn write_executable(path: &Path) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dir");
}
fs::write(path, b"").expect("write executable");
fs::set_permissions(path, fs::Permissions::from_mode(0o755))
.expect("set executable permissions");
}
}
+77
View File
@@ -0,0 +1,77 @@
use std::ffi::CString;
use std::fs::File;
use std::os::fd::AsRawFd;
pub(crate) fn argv_to_cstrings(argv: &[String]) -> Vec<CString> {
let mut cstrings: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv {
match CString::new(arg.as_str()) {
Ok(value) => cstrings.push(value),
Err(err) => panic!("failed to convert argv to CString: {err}"),
}
}
cstrings
}
pub(crate) fn make_files_inheritable(files: &[File]) {
for file in files {
clear_cloexec(file.as_raw_fd());
}
}
fn clear_cloexec(fd: libc::c_int) {
// SAFETY: `fd` is an owned descriptor kept alive by `files`.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for preserved bubblewrap file descriptor {fd}: {err}");
}
let cleared_flags = flags & !libc::FD_CLOEXEC;
if cleared_flags == flags {
return;
}
// SAFETY: `fd` is valid and we are only clearing FD_CLOEXEC.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, cleared_flags) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to clear CLOEXEC for preserved bubblewrap file descriptor {fd}: {err}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::NamedTempFile;
#[test]
fn preserved_files_are_made_inheritable() {
let file = NamedTempFile::new().expect("temp file");
set_cloexec(file.as_file().as_raw_fd());
make_files_inheritable(std::slice::from_ref(file.as_file()));
assert_eq!(fd_flags(file.as_file().as_raw_fd()) & libc::FD_CLOEXEC, 0);
}
fn set_cloexec(fd: libc::c_int) {
let flags = fd_flags(fd);
// SAFETY: `fd` is valid for the duration of the test.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to set CLOEXEC for test fd {fd}: {err}");
}
}
fn fd_flags(fd: libc::c_int) -> libc::c_int {
// SAFETY: `fd` is valid for the duration of the test.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for test fd {fd}: {err}");
}
flags
}
}
+48 -93
View File
@@ -1,20 +1,24 @@
use std::ffi::CStr;
use std::ffi::CString;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::raw::c_char;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
use crate::vendored_bwrap::exec_vendored_bwrap;
use crate::bundled_bwrap;
use crate::bundled_bwrap::BundledBwrapLauncher;
use crate::exec_util::argv_to_cstrings;
use crate::exec_util::make_files_inheritable;
use codex_sandboxing::find_system_bwrap_in_path;
use codex_utils_absolute_path::AbsolutePathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
enum BubblewrapLauncher {
System(SystemBwrapLauncher),
Vendored,
Bundled(BundledBwrapLauncher),
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -34,30 +38,44 @@ pub(crate) fn exec_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
BubblewrapLauncher::System(launcher) => {
exec_system_bwrap(&launcher.program, argv, preserved_files)
}
BubblewrapLauncher::Vendored => exec_vendored_bwrap(argv, preserved_files),
BubblewrapLauncher::Bundled(launcher) => launcher.exec(argv, preserved_files),
BubblewrapLauncher::Unavailable => {
panic!(
"bubblewrap is unavailable: no system bwrap was found on PATH and no bundled \
codex-resources/bwrap binary was found next to the Codex executable"
)
}
}
}
fn preferred_bwrap_launcher() -> BubblewrapLauncher {
static LAUNCHER: OnceLock<BubblewrapLauncher> = OnceLock::new();
LAUNCHER
.get_or_init(|| match find_system_bwrap_in_path() {
Some(path) => preferred_bwrap_launcher_for_path(&path),
None => BubblewrapLauncher::Vendored,
.get_or_init(|| {
if let Some(path) = find_system_bwrap_in_path()
&& let Some(launcher) = system_bwrap_launcher_for_path(&path)
{
return BubblewrapLauncher::System(launcher);
}
match bundled_bwrap::launcher() {
Some(launcher) => BubblewrapLauncher::Bundled(launcher),
None => BubblewrapLauncher::Unavailable,
}
})
.clone()
}
fn preferred_bwrap_launcher_for_path(system_bwrap_path: &Path) -> BubblewrapLauncher {
preferred_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_capabilities)
fn system_bwrap_launcher_for_path(system_bwrap_path: &Path) -> Option<SystemBwrapLauncher> {
system_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_capabilities)
}
fn preferred_bwrap_launcher_for_path_with_probe(
fn system_bwrap_launcher_for_path_with_probe(
system_bwrap_path: &Path,
system_bwrap_capabilities: impl FnOnce(&Path) -> Option<SystemBwrapCapabilities>,
) -> BubblewrapLauncher {
) -> Option<SystemBwrapLauncher> {
if !system_bwrap_path.is_file() {
return BubblewrapLauncher::Vendored;
return None;
}
let Some(SystemBwrapCapabilities {
@@ -65,7 +83,7 @@ fn preferred_bwrap_launcher_for_path_with_probe(
supports_perms: true,
}) = system_bwrap_capabilities(system_bwrap_path)
else {
return BubblewrapLauncher::Vendored;
return None;
};
let system_bwrap_path = match AbsolutePathBuf::from_absolute_path(system_bwrap_path) {
Ok(path) => path,
@@ -74,7 +92,7 @@ fn preferred_bwrap_launcher_for_path_with_probe(
system_bwrap_path.display()
),
};
BubblewrapLauncher::System(SystemBwrapLauncher {
Some(SystemBwrapLauncher {
program: system_bwrap_path,
supports_argv0,
})
@@ -83,7 +101,7 @@ fn preferred_bwrap_launcher_for_path_with_probe(
pub(crate) fn preferred_bwrap_supports_argv0() -> bool {
match preferred_bwrap_launcher() {
BubblewrapLauncher::System(launcher) => launcher.supports_argv0,
BubblewrapLauncher::Vendored => true,
BubblewrapLauncher::Bundled(_) | BubblewrapLauncher::Unavailable => true,
}
}
@@ -117,7 +135,11 @@ fn exec_system_bwrap(
let program = CString::new(program.as_path().as_os_str().as_bytes())
.unwrap_or_else(|err| panic!("invalid system bubblewrap path: {err}"));
let cstrings = argv_to_cstrings(&argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
let mut argv_ptrs: Vec<*const c_char> = cstrings
.iter()
.map(CString::as_c_str)
.map(CStr::as_ptr)
.collect();
argv_ptrs.push(std::ptr::null());
// SAFETY: `program` and every entry in `argv_ptrs` are valid C strings for
@@ -129,43 +151,6 @@ fn exec_system_bwrap(
panic!("failed to exec system bubblewrap {program_path}: {err}");
}
fn argv_to_cstrings(argv: &[String]) -> Vec<CString> {
let mut cstrings: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv {
match CString::new(arg.as_str()) {
Ok(value) => cstrings.push(value),
Err(err) => panic!("failed to convert argv to CString: {err}"),
}
}
cstrings
}
fn make_files_inheritable(files: &[File]) {
for file in files {
clear_cloexec(file.as_raw_fd());
}
}
fn clear_cloexec(fd: libc::c_int) {
// SAFETY: `fd` is an owned descriptor kept alive by `files`.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for preserved bubblewrap file descriptor {fd}: {err}");
}
let cleared_flags = flags & !libc::FD_CLOEXEC;
if cleared_flags == flags {
return;
}
// SAFETY: `fd` is valid and we are only clearing FD_CLOEXEC.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, cleared_flags) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to clear CLOEXEC for preserved bubblewrap file descriptor {fd}: {err}");
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -179,13 +164,13 @@ mod tests {
let expected = AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute");
assert_eq!(
preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
Some(SystemBwrapCapabilities {
supports_argv0: true,
supports_perms: true,
})
}),
BubblewrapLauncher::System(SystemBwrapLauncher {
Some(SystemBwrapLauncher {
program: expected,
supports_argv0: true,
})
@@ -198,13 +183,13 @@ mod tests {
let fake_bwrap_path = fake_bwrap.path();
assert_eq!(
preferred_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
Some(SystemBwrapCapabilities {
supports_argv0: false,
supports_perms: true,
})
}),
BubblewrapLauncher::System(SystemBwrapLauncher {
Some(SystemBwrapLauncher {
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
supports_argv0: false,
})
@@ -212,55 +197,25 @@ mod tests {
}
#[test]
fn falls_back_to_vendored_when_system_bwrap_lacks_perms() {
fn ignores_system_bwrap_when_system_bwrap_lacks_perms() {
let fake_bwrap = NamedTempFile::new().expect("temp file");
assert_eq!(
preferred_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
system_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
Some(SystemBwrapCapabilities {
supports_argv0: false,
supports_perms: false,
})
}),
BubblewrapLauncher::Vendored
None
);
}
#[test]
fn falls_back_to_vendored_when_system_bwrap_is_missing() {
fn ignores_system_bwrap_when_system_bwrap_is_missing() {
assert_eq!(
preferred_bwrap_launcher_for_path(Path::new("/definitely/not/a/bwrap")),
BubblewrapLauncher::Vendored
system_bwrap_launcher_for_path(Path::new("/definitely/not/a/bwrap")),
None
);
}
#[test]
fn preserved_files_are_made_inheritable_for_system_exec() {
let file = NamedTempFile::new().expect("temp file");
set_cloexec(file.as_file().as_raw_fd());
make_files_inheritable(std::slice::from_ref(file.as_file()));
assert_eq!(fd_flags(file.as_file().as_raw_fd()) & libc::FD_CLOEXEC, 0);
}
fn set_cloexec(fd: libc::c_int) {
let flags = fd_flags(fd);
// SAFETY: `fd` is valid for the duration of the test.
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
if result < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to set CLOEXEC for test fd {fd}: {err}");
}
}
fn fd_flags(fd: libc::c_int) -> libc::c_int {
// SAFETY: `fd` is valid for the duration of the test.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let err = std::io::Error::last_os_error();
panic!("failed to read fd flags for test fd {fd}: {err}");
}
flags
}
}
+6 -2
View File
@@ -4,8 +4,14 @@
//! - in-process restrictions (`no_new_privs` + seccomp), and
//! - bubblewrap for filesystem isolation.
#[cfg(target_os = "linux")]
mod bazel_bwrap;
#[cfg(target_os = "linux")]
mod bundled_bwrap;
#[cfg(target_os = "linux")]
mod bwrap;
#[cfg(target_os = "linux")]
mod exec_util;
#[cfg(target_os = "linux")]
mod landlock;
#[cfg(target_os = "linux")]
mod launcher;
@@ -13,8 +19,6 @@ mod launcher;
mod linux_run_main;
#[cfg(target_os = "linux")]
mod proxy_routing;
#[cfg(target_os = "linux")]
mod vendored_bwrap;
#[cfg(target_os = "linux")]
pub fn run_main() -> ! {
@@ -1,78 +0,0 @@
//! Build-time bubblewrap entrypoint.
//!
//! On Linux targets, the build script compiles bubblewrap's C sources and
//! exposes a `bwrap_main` symbol that we can call via FFI.
#[cfg(vendored_bwrap_available)]
mod imp {
use std::ffi::CString;
use std::fs::File;
use std::os::raw::c_char;
unsafe extern "C" {
fn bwrap_main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
}
fn argv_to_cstrings(argv: &[String]) -> Vec<CString> {
let mut cstrings: Vec<CString> = Vec::with_capacity(argv.len());
for arg in argv {
match CString::new(arg.as_str()) {
Ok(value) => cstrings.push(value),
Err(err) => panic!("failed to convert argv to CString: {err}"),
}
}
cstrings
}
/// Run the build-time bubblewrap `main` function and return its exit code.
///
/// On success, bubblewrap will `execve` into the target program and this
/// function will never return. A return value therefore implies failure.
pub(crate) fn run_vendored_bwrap_main(
argv: &[String],
_preserved_files: &[File],
) -> libc::c_int {
let cstrings = argv_to_cstrings(argv);
let mut argv_ptrs: Vec<*const c_char> = cstrings.iter().map(|arg| arg.as_ptr()).collect();
argv_ptrs.push(std::ptr::null());
// SAFETY: We provide a null-terminated argv vector whose pointers
// remain valid for the duration of the call.
unsafe { bwrap_main(cstrings.len() as libc::c_int, argv_ptrs.as_ptr()) }
}
/// Execute the build-time bubblewrap `main` function with the given argv.
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
let exit_code = run_vendored_bwrap_main(&argv, &preserved_files);
std::process::exit(exit_code);
}
}
#[cfg(not(vendored_bwrap_available))]
mod imp {
use std::fs::File;
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn run_vendored_bwrap_main(
_argv: &[String],
_preserved_files: &[File],
) -> libc::c_int {
panic!(
r#"build-time bubblewrap is not available in this build.
codex-linux-sandbox should always compile vendored bubblewrap on Linux targets.
Notes:
- ensure the target OS is Linux
- libcap headers must be available via pkg-config
- bubblewrap sources expected at codex-rs/vendor/bubblewrap (default)"#
);
}
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>, _preserved_files: Vec<File>) -> ! {
let _ = run_vendored_bwrap_main(&[], &[]);
unreachable!("run_vendored_bwrap_main should always panic in this configuration")
}
}
pub(crate) use imp::exec_vendored_bwrap;