mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
b0b383bea3
## Summary The Linux sandbox should find bundled `bwrap` through the same package-layout abstraction as the rest of the runtime, instead of maintaining a separate standalone-specific lookup path. This adds an `InstallContext` helper for bundled resources and updates `codex-linux-sandbox` to ask the current install context for `codex-resources/bwrap` before falling back to the old executable-relative probes. The tests cover npm-style, standalone, and canonical package layouts so `bwrap` lookup follows the package structure introduced earlier in the stack. ## Test plan - `cargo test -p codex-install-context` - `cargo test -p codex-linux-sandbox --lib` - `just fix -p codex-install-context -p codex-linux-sandbox` - `just bazel-lock-check` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23634). * #23638 * #23637 * #23636 * #23635 * __->__ #23634
317 lines
11 KiB
Rust
317 lines
11 KiB
Rust
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_install_context::InstallContext;
|
|
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_install_context(InstallContext::current())
|
|
.or_else(|| find_legacy_for_exe(¤t_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_install_context(context: &InstallContext) -> Option<AbsolutePathBuf> {
|
|
context
|
|
.bundled_resource("bwrap")
|
|
.filter(|path| is_executable_file(path))
|
|
}
|
|
|
|
fn find_legacy_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
|
|
legacy_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 legacy_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 codex_install_context::CodexPackageLayout;
|
|
use codex_install_context::InstallContext;
|
|
use codex_install_context::InstallMethod;
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs;
|
|
use tempfile::NamedTempFile;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn finds_package_layout_bwrap_from_install_context() {
|
|
let temp_dir = tempdir().expect("temp dir");
|
|
let package_dir = temp_dir.path();
|
|
let bin_dir = package_dir.join("bin");
|
|
let resources_dir = package_dir.join("codex-resources");
|
|
let expected_bwrap = resources_dir.join("bwrap");
|
|
fs::create_dir_all(&bin_dir).expect("create bin dir");
|
|
write_executable(&expected_bwrap);
|
|
|
|
let context = InstallContext {
|
|
method: InstallMethod::Other,
|
|
package_layout: Some(CodexPackageLayout {
|
|
package_dir: AbsolutePathBuf::from_absolute_path(package_dir).expect("absolute"),
|
|
bin_dir: AbsolutePathBuf::from_absolute_path(&bin_dir).expect("absolute"),
|
|
resources_dir: Some(
|
|
AbsolutePathBuf::from_absolute_path(&resources_dir).expect("absolute"),
|
|
),
|
|
path_dir: None,
|
|
}),
|
|
};
|
|
|
|
assert_eq!(
|
|
find_for_install_context(&context),
|
|
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn finds_legacy_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_legacy_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_legacy_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_legacy_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");
|
|
}
|
|
}
|