Verify bundled bwrap in Bazel builds (#38494)

## Why

Bazel builds did not embed the SHA-256 digest used to verify the bundled
`bwrap` binary at launch.

## What changed

- Strip the Bazel-built `bwrap`, generate its SHA-256 digest, and pass that
  digest to `codex-linux-sandbox` through a compiler environment file.
- Report digest verification failures with exit code `8` instead of panicking.
- Add a Bazel integration test that accepts the original bundled binary and
  rejects a tampered copy.

GitOrigin-RevId: 224eeb6ae3d1bfb100e048b296f75fd8ba109581
This commit is contained in:
Adam Perry @ OpenAI
2026-08-14 04:53:01 +00:00
committed by copyberry
parent 45c9c74e29
commit 636e505c5c
7 changed files with 113 additions and 3 deletions

View File

@@ -12,11 +12,29 @@ codex_rust_crate(
"//conditions:default": [],
}),
rustc_flags_extra = select({
"@platforms//os:linux": ["--cfg=bwrap_available"],
"@platforms//os:linux": [
"--cfg=bwrap_available",
# TODO(anp) Extract bwrap symbols before stripping.
"-Cstrip=symbols",
],
"//conditions:default": [],
}),
)
genrule(
name = "bwrap-sha256-env",
srcs = [":bwrap"],
outs = ["bwrap.sha256.env"],
cmd = " && ".join([
'$(execpath @bazel_tools//tools/build_defs/hash:sha256) $(execpath :bwrap) "$@"',
'digest=$$(<"$@")',
'printf "CODEX_BWRAP_SHA256=%s\\n" "$$digest" > "$@"',
]),
target_compatible_with = ["@platforms//os:linux"],
tools = ["@bazel_tools//tools/build_defs/hash:sha256"],
visibility = ["//codex-rs/linux-sandbox:__pkg__"],
)
cc_library(
name = "bwrap-ffi",
srcs = ["//codex-rs/vendor:bubblewrap_c_sources"],

View File

@@ -6,4 +6,8 @@ codex_rust_crate(
extra_binaries = [
"//codex-rs/bwrap:bwrap",
],
rustc_env_files = select({
"@platforms//os:linux": ["//codex-rs/bwrap:bwrap-sha256-env"],
"//conditions:default": [],
}),
)

View File

@@ -40,8 +40,10 @@ impl BundledBwrapLauncher {
self.program.as_path().display()
)
});
verify_digest(&bwrap_file, expected_sha256(), self.program.as_path())
.unwrap_or_else(|err| panic!("{err}"));
if let Err(err) = verify_digest(&bwrap_file, expected_sha256(), self.program.as_path()) {
eprintln!("{err}");
std::process::exit(crate::BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE);
}
make_files_inheritable(&preserved_files);

View File

@@ -22,6 +22,10 @@ mod proxy_lifecycle;
#[cfg(target_os = "linux")]
mod proxy_routing;
/// Exit status returned when bundled bubblewrap fails digest verification.
#[cfg(target_os = "linux")]
pub const BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE: i32 = 8;
#[cfg(target_os = "linux")]
pub fn run_main() -> ! {
linux_run_main::run_main();

View File

@@ -0,0 +1,78 @@
#![cfg(target_os = "linux")]
use codex_linux_sandbox::BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE;
use codex_protocol::models::PermissionProfile;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use std::process::Command;
#[test]
fn bazel_build_rejects_tampered_bundled_bwrap() {
// Cargo embeds the bundled bwrap digest only in release builds.
if option_env!("BAZEL_PACKAGE").is_none() {
return;
}
let bwrap_runfile =
std::env::var("CARGO_BIN_EXE_bwrap").expect("Bazel should provide the bwrap runfile");
let runfiles_dir =
std::env::var_os("TEST_SRCDIR").expect("Bazel should provide its runfiles directory");
let bwrap_binary = PathBuf::from(runfiles_dir).join(bwrap_runfile);
let original_bwrap_bytes =
std::fs::read(&bwrap_binary).expect("built bwrap should be readable");
let package = tempfile::tempdir().expect("package directory should be created");
let resources = package.path().join("codex-resources");
std::fs::create_dir(&resources).expect("package resource directory should be created");
let sandbox_binary = package.path().join("codex-linux-sandbox");
std::fs::copy(env!("CARGO_BIN_EXE_codex-linux-sandbox"), &sandbox_binary)
.expect("sandbox binary should be copied into the package");
let bundled_bwrap = resources.join("bwrap");
std::fs::copy(&bwrap_binary, &bundled_bwrap)
.expect("built bwrap should be copied into the package");
let permission_profile = serde_json::to_string(&PermissionProfile::read_only())
.expect("read-only permission profile should serialize");
let run_sandbox = || {
Command::new(&sandbox_binary)
.arg("--sandbox-policy-cwd")
.arg(package.path())
.arg("--permission-profile")
.arg(&permission_profile)
.arg("--no-proc")
.arg("--")
.arg("/bin/true")
.env_clear()
.env("PATH", package.path())
.output()
.expect("sandbox binary should start")
};
let original_output = run_sandbox();
assert_ne!(
original_output.status.code(),
Some(BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE),
"sandbox should accept the digest of the unmodified bundled bwrap"
);
std::fs::remove_file(&bundled_bwrap).expect("read-only bundled bwrap should be replaceable");
let mut tampered_bwrap_bytes = original_bwrap_bytes;
tampered_bwrap_bytes.push(0);
std::fs::write(&bundled_bwrap, &tampered_bwrap_bytes)
.expect("modified bwrap should be copied into the package");
let bwrap_permissions = std::fs::metadata(&bwrap_binary)
.expect("built bwrap metadata should be readable")
.permissions();
std::fs::set_permissions(&bundled_bwrap, bwrap_permissions)
.expect("bundled bwrap should remain executable");
let tampered_output = run_sandbox();
assert_eq!(
tampered_output.status.code(),
Some(BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE),
"sandbox should reject the tampered bundled bwrap"
);
}

View File

@@ -1,3 +1,4 @@
// Aggregates all former standalone integration tests as modules.
mod bundled_bwrap;
mod landlock;
mod managed_proxy;

View File

@@ -193,6 +193,7 @@ def codex_rust_crate(
rustc_flags_extra = [],
binary_rustc_flags_extra = {},
rustc_env = {},
rustc_env_files = [],
deps_extra = [],
integration_compile_data_extra = [],
integration_test_args = [],
@@ -234,6 +235,7 @@ def codex_rust_crate(
binary_rustc_flags_extra: Mapping from binary names to extra rustc
flags for those binary targets.
rustc_env: Extra rustc_env entries to merge with defaults.
rustc_env_files: Generated compiler environment files for the library target.
deps_extra: Extra normal deps beyond @crates resolution.
Typically only needed when features add additional deps.
integration_compile_data_extra: Extra compile_data for integration tests.
@@ -325,6 +327,7 @@ def codex_rust_crate(
edition = crate_edition,
rustc_flags = rustc_flags_extra,
rustc_env = rustc_env,
rustc_env_files = rustc_env_files,
visibility = ["//visibility:public"],
)