mirror of
https://github.com/openai/codex.git
synced 2026-05-24 13:04:29 +00:00
**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
222 lines
7.2 KiB
Rust
222 lines
7.2 KiB
Rust
use std::ffi::CStr;
|
|
use std::ffi::CString;
|
|
use std::fs::File;
|
|
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::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),
|
|
Bundled(BundledBwrapLauncher),
|
|
Unavailable,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct SystemBwrapLauncher {
|
|
program: AbsolutePathBuf,
|
|
supports_argv0: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
struct SystemBwrapCapabilities {
|
|
supports_argv0: bool,
|
|
supports_perms: bool,
|
|
}
|
|
|
|
pub(crate) fn exec_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
|
|
match preferred_bwrap_launcher() {
|
|
BubblewrapLauncher::System(launcher) => {
|
|
exec_system_bwrap(&launcher.program, 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(|| {
|
|
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 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 system_bwrap_launcher_for_path_with_probe(
|
|
system_bwrap_path: &Path,
|
|
system_bwrap_capabilities: impl FnOnce(&Path) -> Option<SystemBwrapCapabilities>,
|
|
) -> Option<SystemBwrapLauncher> {
|
|
if !system_bwrap_path.is_file() {
|
|
return None;
|
|
}
|
|
|
|
let Some(SystemBwrapCapabilities {
|
|
supports_argv0,
|
|
supports_perms: true,
|
|
}) = system_bwrap_capabilities(system_bwrap_path)
|
|
else {
|
|
return None;
|
|
};
|
|
let system_bwrap_path = match AbsolutePathBuf::from_absolute_path(system_bwrap_path) {
|
|
Ok(path) => path,
|
|
Err(err) => panic!(
|
|
"failed to normalize system bubblewrap path {}: {err}",
|
|
system_bwrap_path.display()
|
|
),
|
|
};
|
|
Some(SystemBwrapLauncher {
|
|
program: system_bwrap_path,
|
|
supports_argv0,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn preferred_bwrap_supports_argv0() -> bool {
|
|
match preferred_bwrap_launcher() {
|
|
BubblewrapLauncher::System(launcher) => launcher.supports_argv0,
|
|
BubblewrapLauncher::Bundled(_) | BubblewrapLauncher::Unavailable => true,
|
|
}
|
|
}
|
|
|
|
fn system_bwrap_capabilities(system_bwrap_path: &Path) -> Option<SystemBwrapCapabilities> {
|
|
// bubblewrap added `--argv0` in v0.9.0:
|
|
// https://github.com/containers/bubblewrap/releases/tag/v0.9.0
|
|
// Older distro packages (for example Ubuntu 20.04/22.04) ship builds that
|
|
// reject `--argv0`, so use the system binary's no-argv0 compatibility path
|
|
// in that case.
|
|
let output = match Command::new(system_bwrap_path).arg("--help").output() {
|
|
Ok(output) => output,
|
|
Err(_) => return None,
|
|
};
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
Some(SystemBwrapCapabilities {
|
|
supports_argv0: stdout.contains("--argv0") || stderr.contains("--argv0"),
|
|
supports_perms: stdout.contains("--perms") || stderr.contains("--perms"),
|
|
})
|
|
}
|
|
|
|
fn exec_system_bwrap(
|
|
program: &AbsolutePathBuf,
|
|
argv: Vec<String>,
|
|
preserved_files: Vec<File>,
|
|
) -> ! {
|
|
// System bwrap runs across an exec boundary, so preserved fds must survive exec.
|
|
make_files_inheritable(&preserved_files);
|
|
|
|
let program_path = program.as_path().display().to_string();
|
|
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(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
|
|
// the duration of the call. On success `execv` does not return.
|
|
unsafe {
|
|
libc::execv(program.as_ptr(), argv_ptrs.as_ptr());
|
|
}
|
|
let err = std::io::Error::last_os_error();
|
|
panic!("failed to exec system bubblewrap {program_path}: {err}");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[test]
|
|
fn prefers_system_bwrap_when_help_lists_argv0() {
|
|
let fake_bwrap = NamedTempFile::new().expect("temp file");
|
|
let fake_bwrap_path = fake_bwrap.path();
|
|
let expected = AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute");
|
|
|
|
assert_eq!(
|
|
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
|
|
Some(SystemBwrapCapabilities {
|
|
supports_argv0: true,
|
|
supports_perms: true,
|
|
})
|
|
}),
|
|
Some(SystemBwrapLauncher {
|
|
program: expected,
|
|
supports_argv0: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prefers_system_bwrap_when_system_bwrap_lacks_argv0() {
|
|
let fake_bwrap = NamedTempFile::new().expect("temp file");
|
|
let fake_bwrap_path = fake_bwrap.path();
|
|
|
|
assert_eq!(
|
|
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
|
|
Some(SystemBwrapCapabilities {
|
|
supports_argv0: false,
|
|
supports_perms: true,
|
|
})
|
|
}),
|
|
Some(SystemBwrapLauncher {
|
|
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
|
|
supports_argv0: false,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_system_bwrap_when_system_bwrap_lacks_perms() {
|
|
let fake_bwrap = NamedTempFile::new().expect("temp file");
|
|
|
|
assert_eq!(
|
|
system_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
|
|
Some(SystemBwrapCapabilities {
|
|
supports_argv0: false,
|
|
supports_perms: false,
|
|
})
|
|
}),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_system_bwrap_when_system_bwrap_is_missing() {
|
|
assert_eq!(
|
|
system_bwrap_launcher_for_path(Path::new("/definitely/not/a/bwrap")),
|
|
None
|
|
);
|
|
}
|
|
}
|