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"); }}