mirror of
https://github.com/openai/codex.git
synced 2026-05-18 02:02:30 +00:00
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:
committed by
Channing Conger
parent
3fba7a6be9
commit
7c79a18688
16
.github/workflows/bazel.yml
vendored
16
.github/workflows/bazel.yml
vendored
@@ -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
12
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"agent-graph-store",
|
||||
"agent-identity",
|
||||
"backend-client",
|
||||
"bwrap",
|
||||
"ansi-escape",
|
||||
"async-utils",
|
||||
"app-server",
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
35
codex-rs/bwrap/BUILD.bazel
Normal file
35
codex-rs/bwrap/BUILD.bazel
Normal 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
19
codex-rs/bwrap/Cargo.toml
Normal 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
99
codex-rs/bwrap/build.rs
Normal 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
1
codex-rs/bwrap/config.h
Normal file
@@ -0,0 +1 @@
|
||||
#define PACKAGE_STRING "bubblewrap built for Codex"
|
||||
45
codex-rs/bwrap/src/main.rs
Normal file
45
codex-rs/bwrap/src/main.rs
Normal 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");
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define PACKAGE_STRING "bubblewrap built at codex build-time"
|
||||
68
codex-rs/linux-sandbox/src/bazel_bwrap.rs
Normal file
68
codex-rs/linux-sandbox/src/bazel_bwrap.rs
Normal 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
|
||||
}
|
||||
276
codex-rs/linux-sandbox/src/bundled_bwrap.rs
Normal file
276
codex-rs/linux-sandbox/src/bundled_bwrap.rs
Normal 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(¤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_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");
|
||||
}
|
||||
}
|
||||
77
codex-rs/linux-sandbox/src/exec_util.rs
Normal file
77
codex-rs/linux-sandbox/src/exec_util.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() -> ! {
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.";
|
||||
|
||||
Reference in New Issue
Block a user