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 Channing Conger
parent 3fba7a6be9
commit 7c79a18688
27 changed files with 751 additions and 338 deletions

View File

@@ -371,6 +371,22 @@ jobs:
-- \
"${bazel_targets[@]}"
- name: Verify Bazel builds bwrap
if: runner.os == 'Linux'
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
shell: bash
run: |
./.github/scripts/run-bazel-ci.sh \
--remote-download-toplevel \
--print-failed-action-summary \
-- \
build \
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
--build_metadata=TAG_job=verify-bwrap \
-- \
//codex-rs/bwrap:bwrap
- name: Upload Bazel execution logs
if: always() && !cancelled()
continue-on-error: true

12
codex-rs/Cargo.lock generated
View File

@@ -2125,6 +2125,15 @@ dependencies = [
"serde_with",
]
[[package]]
name = "codex-bwrap"
version = "0.0.0"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "codex-chatgpt"
version = "0.0.0"
@@ -2905,7 +2914,6 @@ dependencies = [
name = "codex-linux-sandbox"
version = "0.0.0"
dependencies = [
"cc",
"clap",
"codex-core",
"codex-process-hardening",
@@ -2915,11 +2923,11 @@ dependencies = [
"globset",
"landlock",
"libc",
"pkg-config",
"pretty_assertions",
"seccompiler",
"serde",
"serde_json",
"sha2",
"tempfile",
"tokio",
"url",

View File

@@ -5,6 +5,7 @@ members = [
"agent-graph-store",
"agent-identity",
"backend-client",
"bwrap",
"ansi-escape",
"async-utils",
"app-server",

View File

@@ -14,5 +14,8 @@ codex_rust_crate(
"app-server-all-test": 16,
"app-server-unit-tests": 8,
},
extra_binaries = [
"//codex-rs/bwrap:bwrap",
],
test_tags = ["no-sandbox"],
)

View File

@@ -0,0 +1,35 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "bwrap",
crate_name = "codex_bwrap",
# Bazel wires vendored bubblewrap + libcap via :bwrap-ffi below and sets
# bwrap_available explicitly, so we skip Cargo's build.rs in Bazel builds.
build_script_enabled = False,
deps_extra = select({
"@platforms//os:linux": [":bwrap-ffi"],
"//conditions:default": [],
}),
rustc_flags_extra = select({
"@platforms//os:linux": ["--cfg=bwrap_available"],
"//conditions:default": [],
}),
)
cc_library(
name = "bwrap-ffi",
srcs = ["//codex-rs/vendor:bubblewrap_c_sources"],
hdrs = [
"config.h",
"//codex-rs/vendor:bubblewrap_headers",
],
copts = [
"-D_GNU_SOURCE",
"-Dmain=bwrap_main",
],
includes = ["."],
deps = ["@libcap//:libcap"],
target_compatible_with = ["@platforms//os:linux"],
visibility = ["//visibility:private"],
)

19
codex-rs/bwrap/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "codex-bwrap"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "bwrap"
path = "src/main.rs"
[lints]
workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
libc = { workspace = true }
[build-dependencies]
cc = "1"
pkg-config = "0.3"

99
codex-rs/bwrap/build.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
fn main() {
println!("cargo:rustc-check-cfg=cfg(bwrap_available)");
println!("cargo:rerun-if-env-changed=CODEX_BWRAP_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR");
println!("cargo:rerun-if-env-changed=CODEX_SKIP_BWRAP_BUILD");
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
for source in ["bubblewrap.c", "bind-mount.c", "network.c", "utils.c"] {
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join(source).display()
);
}
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" || env::var_os("CODEX_SKIP_BWRAP_BUILD").is_some() {
return;
}
if let Err(err) = try_build_bwrap() {
panic!("failed to compile bubblewrap for Linux target: {err}");
}
}
fn try_build_bwrap() -> Result<(), String> {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|err| err.to_string())?);
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|err| err.to_string())?);
let src_dir = resolve_bwrap_source_dir(&manifest_dir)?;
let libcap = pkg_config::Config::new()
.probe("libcap")
.map_err(|err| format!("libcap not available via pkg-config: {err}"))?;
let config_h = out_dir.join("config.h");
std::fs::write(
&config_h,
r#"#pragma once
#define PACKAGE_STRING "bubblewrap built for Codex"
"#,
)
.map_err(|err| format!("failed to write {}: {err}", config_h.display()))?;
let mut build = cc::Build::new();
build
.file(src_dir.join("bubblewrap.c"))
.file(src_dir.join("bind-mount.c"))
.file(src_dir.join("network.c"))
.file(src_dir.join("utils.c"))
.include(&out_dir)
.include(&src_dir)
.define("_GNU_SOURCE", None)
// Rename `main` so the Rust wrapper can expose the Cargo-built binary.
.define("main", Some("bwrap_main"));
for include_path in libcap.include_paths {
// Use -idirafter so target sysroot headers win (musl cross builds),
// while still allowing libcap headers from the host toolchain.
build.flag(format!("-idirafter{}", include_path.display()));
}
build.compile("standalone_bwrap");
println!("cargo:rustc-cfg=bwrap_available");
Ok(())
}
/// Resolve the bubblewrap source directory used for build-time compilation.
///
/// Priority:
/// 1. `CODEX_BWRAP_SOURCE_DIR` points at an existing bubblewrap checkout.
/// 2. The vendored bubblewrap tree under `codex-rs/vendor/bubblewrap`.
fn resolve_bwrap_source_dir(manifest_dir: &Path) -> Result<PathBuf, String> {
if let Ok(path) = env::var("CODEX_BWRAP_SOURCE_DIR") {
let src_dir = PathBuf::from(path);
if src_dir.exists() {
return Ok(src_dir);
}
return Err(format!(
"CODEX_BWRAP_SOURCE_DIR was set but does not exist: {}",
src_dir.display()
));
}
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
if vendor_dir.exists() {
return Ok(vendor_dir);
}
Err(format!(
"expected vendored bubblewrap at {}, but it was not found.\n\
Set CODEX_BWRAP_SOURCE_DIR to an existing checkout or vendor bubblewrap under codex-rs/vendor.",
vendor_dir.display()
))
}

1
codex-rs/bwrap/config.h Normal file
View File

@@ -0,0 +1 @@
#define PACKAGE_STRING "bubblewrap built for Codex"

View File

@@ -0,0 +1,45 @@
#[cfg(all(target_os = "linux", bwrap_available))]
fn main() {
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
use std::os::unix::ffi::OsStrExt;
unsafe extern "C" {
fn bwrap_main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
}
let cstrings = std::env::args_os()
.map(|arg| {
CString::new(arg.as_os_str().as_bytes())
.unwrap_or_else(|err| panic!("failed to convert argv to CString: {err}"))
})
.collect::<Vec<_>>();
let mut argv_ptrs = cstrings
.iter()
.map(CString::as_c_str)
.map(CStr::as_ptr)
.collect::<Vec<*const c_char>>();
argv_ptrs.push(std::ptr::null());
// SAFETY: We provide a null-terminated argv vector whose pointers remain
// valid for the duration of the call.
let exit_code = unsafe { bwrap_main(cstrings.len() as libc::c_int, argv_ptrs.as_ptr()) };
std::process::exit(exit_code);
}
#[cfg(all(target_os = "linux", not(bwrap_available)))]
fn main() {
panic!(
r#"bubblewrap is not available in this build.
Notes:
- ensure the target OS is Linux
- libcap headers must be available via pkg-config
- bubblewrap sources expected at codex-rs/vendor/bubblewrap (default)"#
);
}
#[cfg(not(target_os = "linux"))]
fn main() {
panic!("bwrap is only supported on Linux");
}

View File

@@ -52,6 +52,7 @@ codex_rust_crate(
test_tags = ["no-sandbox"],
unit_test_timeout = "long",
extra_binaries = [
"//codex-rs/bwrap:bwrap",
"//codex-rs/linux-sandbox:codex-linux-sandbox",
"//codex-rs/rmcp-client:test_stdio_server",
"//codex-rs/rmcp-client:test_streamable_http_server",

View File

@@ -39,14 +39,14 @@ The Linux sandbox helper prefers the first `bwrap` found on `PATH` outside the
current working directory whenever it is available. If `bwrap` is present but
too old to support `--argv0`, the helper keeps using system bubblewrap and
switches to a no-`--argv0` compatibility path for the inner re-exec. If
`bwrap` is missing, it falls back to the vendored bubblewrap path compiled into
the binary and Codex surfaces a startup warning through its normal notification
path instead of printing directly from the sandbox helper. Codex also surfaces
a startup warning when bubblewrap cannot create user namespaces. WSL2 uses the
normal Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing
because it cannot create the required user namespaces, so Codex rejects
sandboxed shell commands that would enter the bubblewrap path before invoking
`bwrap`.
`bwrap` is missing, it falls back to the bundled `codex-resources/bwrap`
binary shipped with Codex and Codex surfaces a startup warning through its
normal notification path instead of printing directly from the sandbox helper.
Codex also surfaces a startup warning when bubblewrap cannot create user
namespaces. WSL2 uses the normal Linux bubblewrap path. WSL1 is not supported
for bubblewrap sandboxing because it cannot create the required user
namespaces, so Codex rejects sandboxed shell commands that would enter the
bubblewrap path before invoking `bwrap`.
### Windows

View File

@@ -7,5 +7,8 @@ codex_rust_crate(
# they install process-global test-binary dispatch state, and the remote
# exec-server cases already rely on serialization around the full CLI path.
integration_test_args = ["--test-threads=1"],
extra_binaries = [
"//codex-rs/bwrap:bwrap",
],
test_tags = ["no-sandbox"],
)

View File

@@ -29,6 +29,15 @@ use crate::rpc::internal_error;
use crate::rpc::invalid_request;
const FS_HELPER_ENV_ALLOWLIST: &[&str] = &["PATH", "TMPDIR", "TMP", "TEMP"];
#[cfg(debug_assertions)]
const FS_HELPER_BAZEL_BWRAP_ENV_ALLOWLIST: &[&str] = &[
"CARGO_BIN_EXE_bwrap",
"RUNFILES_DIR",
"RUNFILES_MANIFEST_FILE",
"RUNFILES_MANIFEST_ONLY",
"TEST_SRCDIR",
"TEST_WORKSPACE",
];
#[derive(Clone, Debug)]
pub(crate) struct FileSystemSandboxRunner {
@@ -220,7 +229,19 @@ fn helper_env_from_vars(
}
fn helper_env_key_is_allowed(key: &str) -> bool {
FS_HELPER_ENV_ALLOWLIST.contains(&key) || (cfg!(windows) && key.eq_ignore_ascii_case("PATH"))
FS_HELPER_ENV_ALLOWLIST.contains(&key)
|| bazel_bwrap_env_key_is_allowed(key)
|| (cfg!(windows) && key.eq_ignore_ascii_case("PATH"))
}
#[cfg(debug_assertions)]
fn bazel_bwrap_env_key_is_allowed(key: &str) -> bool {
option_env!("BAZEL_PACKAGE").is_some() && FS_HELPER_BAZEL_BWRAP_ENV_ALLOWLIST.contains(&key)
}
#[cfg(not(debug_assertions))]
fn bazel_bwrap_env_key_is_allowed(_key: &str) -> bool {
false
}
async fn run_command(

View File

@@ -1,36 +1,9 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "linux-sandbox",
crate_name = "codex_linux_sandbox",
# Bazel wires vendored bubblewrap + libcap via :vendored-bwrap-ffi below
# and sets vendored_bwrap_available explicitly, so we skip Cargo's
# build.rs in Bazel builds.
build_script_enabled = False,
deps_extra = select({
"@platforms//os:linux": [":vendored-bwrap-ffi"],
"//conditions:default": [],
}),
rustc_flags_extra = select({
"@platforms//os:linux": ["--cfg=vendored_bwrap_available"],
"//conditions:default": [],
}),
)
cc_library(
name = "vendored-bwrap-ffi",
srcs = ["//codex-rs/vendor:bubblewrap_c_sources"],
hdrs = [
"config.h",
"//codex-rs/vendor:bubblewrap_headers",
extra_binaries = [
"//codex-rs/bwrap:bwrap",
],
copts = [
"-D_GNU_SOURCE",
"-Dmain=bwrap_main",
],
includes = ["."],
deps = ["@libcap//:libcap"],
target_compatible_with = ["@platforms//os:linux"],
visibility = ["//visibility:private"],
)

View File

@@ -27,6 +27,7 @@ libc = { workspace = true }
seccompiler = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
url = { workspace = true }
[target.'cfg(target_os = "linux")'.dev-dependencies]
@@ -40,7 +41,3 @@ tokio = { workspace = true, features = [
"rt-multi-thread",
"signal",
] }
[build-dependencies]
cc = "1"
pkg-config = "0.3"

View File

@@ -12,10 +12,10 @@ outside the current working directory whenever it is available. If `bwrap` is
present but too old to support
`--argv0`, the helper keeps using system bubblewrap and switches to a
no-`--argv0` compatibility path for the inner re-exec. If `bwrap` is missing,
the helper falls back to the vendored bubblewrap path compiled into this
binary.
the helper falls back to the bundled `codex-resources/bwrap` binary shipped
with Codex.
Codex also surfaces a startup warning when `bwrap` is missing so users know it
is falling back to the vendored helper. Codex surfaces the same startup warning
is falling back to the bundled helper. Codex surfaces the same startup warning
path when bubblewrap cannot create user namespaces. WSL2 follows the normal
Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing because
it cannot create the required user namespaces, so Codex rejects sandboxed shell
@@ -28,8 +28,8 @@ commands that would enter the bubblewrap path.
helper uses it.
- If `bwrap` is present but too old to support `--argv0`, the helper uses a
no-`--argv0` compatibility path for the inner re-exec.
- If `bwrap` is missing, the helper falls back to the vendored bubblewrap
path.
- If `bwrap` is missing, the helper falls back to the bundled
`codex-resources/bwrap` path.
- If `bwrap` is missing, Codex also surfaces a startup warning instead of
printing directly from the sandbox helper.
- If bubblewrap cannot create user namespaces, Codex surfaces a startup warning

View File

@@ -1,111 +1,3 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
fn main() {
// Tell rustc/clippy that this is an expected cfg value.
println!("cargo:rustc-check-cfg=cfg(vendored_bwrap_available)");
println!("cargo:rerun-if-env-changed=CODEX_BWRAP_SOURCE_DIR");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_ALLOW_CROSS");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR");
println!("cargo:rerun-if-env-changed=CODEX_SKIP_VENDORED_BWRAP");
// Rebuild if the vendored bwrap sources change.
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_default());
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("bubblewrap.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("bind-mount.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("network.c").display()
);
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join("utils.c").display()
);
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" || env::var_os("CODEX_SKIP_VENDORED_BWRAP").is_some() {
return;
}
if let Err(err) = try_build_vendored_bwrap() {
panic!("failed to compile vendored bubblewrap for Linux target: {err}");
}
}
fn try_build_vendored_bwrap() -> Result<(), String> {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|err| err.to_string())?);
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|err| err.to_string())?);
let src_dir = resolve_bwrap_source_dir(&manifest_dir)?;
let libcap = pkg_config::Config::new()
.probe("libcap")
.map_err(|err| format!("libcap not available via pkg-config: {err}"))?;
let config_h = out_dir.join("config.h");
std::fs::write(
&config_h,
r#"#pragma once
#define PACKAGE_STRING "bubblewrap built at codex build-time"
"#,
)
.map_err(|err| format!("failed to write {}: {err}", config_h.display()))?;
let mut build = cc::Build::new();
build
.file(src_dir.join("bubblewrap.c"))
.file(src_dir.join("bind-mount.c"))
.file(src_dir.join("network.c"))
.file(src_dir.join("utils.c"))
.include(&out_dir)
.include(&src_dir)
.define("_GNU_SOURCE", None)
// Rename `main` so we can call it via FFI.
.define("main", Some("bwrap_main"));
for include_path in libcap.include_paths {
// Use -idirafter so target sysroot headers win (musl cross builds),
// while still allowing libcap headers from the host toolchain.
build.flag(format!("-idirafter{}", include_path.display()));
}
build.compile("build_time_bwrap");
println!("cargo:rustc-cfg=vendored_bwrap_available");
Ok(())
}
/// Resolve the bubblewrap source directory used for build-time compilation.
///
/// Priority:
/// 1. `CODEX_BWRAP_SOURCE_DIR` points at an existing bubblewrap checkout.
/// 2. The vendored bubblewrap tree under `codex-rs/vendor/bubblewrap`.
fn resolve_bwrap_source_dir(manifest_dir: &Path) -> Result<PathBuf, String> {
if let Ok(path) = env::var("CODEX_BWRAP_SOURCE_DIR") {
let src_dir = PathBuf::from(path);
if src_dir.exists() {
return Ok(src_dir);
}
return Err(format!(
"CODEX_BWRAP_SOURCE_DIR was set but does not exist: {}",
src_dir.display()
));
}
let vendor_dir = manifest_dir.join("../vendor/bubblewrap");
if vendor_dir.exists() {
return Ok(vendor_dir);
}
Err(format!(
"expected vendored bubblewrap at {}, but it was not found.\n\
Set CODEX_BWRAP_SOURCE_DIR to an existing checkout or vendor bubblewrap under codex-rs/vendor.",
vendor_dir.display()
))
println!("cargo:rerun-if-env-changed=CODEX_BWRAP_SHA256");
}

View File

@@ -1,3 +0,0 @@
#pragma once
#define PACKAGE_STRING "bubblewrap built at codex build-time"

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
}

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

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
}
}

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
}
}

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() -> ! {

View File

@@ -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;

View File

@@ -40,7 +40,7 @@ const NETWORK_TIMEOUT_MS: u64 = 10_000;
#[cfg(target_arch = "aarch64")]
const NETWORK_TIMEOUT_MS: u64 = 10_000;
const BWRAP_UNAVAILABLE_ERR: &str = "build-time bubblewrap is not available in this build.";
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found";
fn create_env_from_core_vars() -> HashMap<String, String> {
let policy = ShellEnvironmentPolicy::default();

View File

@@ -15,7 +15,7 @@ use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
const BWRAP_UNAVAILABLE_ERR: &str = "build-time bubblewrap is not available in this build.";
const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable: no system bwrap was found";
const NETWORK_TIMEOUT_MS: u64 = 4_000;
const MANAGED_PROXY_PERMISSION_ERR_SNIPPETS: &[&str] = &[
"loopback: Failed RTM_NEWADDR",
@@ -82,7 +82,7 @@ fn is_managed_proxy_permission_error(stderr: &str) -> bool {
async fn managed_proxy_skip_reason() -> Option<String> {
if should_skip_bwrap_tests().await {
return Some("vendored bwrap was not built in this environment".to_string());
return Some("bubblewrap is unavailable in this environment".to_string());
}
let mut env = create_env_from_core_vars();

View File

@@ -18,7 +18,7 @@ const MISSING_BWRAP_WARNING: &str = concat!(
"Install bubblewrap with your OS package manager. ",
"See the sandbox prerequisites: ",
"https://developers.openai.com/codex/concepts/sandboxing#prerequisites. ",
"Codex will use the vendored bubblewrap in the meantime.",
"Codex will use the bundled bubblewrap in the meantime.",
);
const USER_NAMESPACE_WARNING: &str =
"Codex's Linux sandbox uses bubblewrap and needs access to create user namespaces.";