diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index aed3f7a1b3..79b49ddb10 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -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 diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 824c8a70f6..9bdf2e7182 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -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", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9f05af29f7..8675fab924 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -5,6 +5,7 @@ members = [ "agent-graph-store", "agent-identity", "backend-client", + "bwrap", "ansi-escape", "async-utils", "app-server", diff --git a/codex-rs/app-server/BUILD.bazel b/codex-rs/app-server/BUILD.bazel index b7ff5b1695..6765141bdc 100644 --- a/codex-rs/app-server/BUILD.bazel +++ b/codex-rs/app-server/BUILD.bazel @@ -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"], ) diff --git a/codex-rs/bwrap/BUILD.bazel b/codex-rs/bwrap/BUILD.bazel new file mode 100644 index 0000000000..3d0b89b966 --- /dev/null +++ b/codex-rs/bwrap/BUILD.bazel @@ -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"], +) diff --git a/codex-rs/bwrap/Cargo.toml b/codex-rs/bwrap/Cargo.toml new file mode 100644 index 0000000000..ed7010c8fd --- /dev/null +++ b/codex-rs/bwrap/Cargo.toml @@ -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" diff --git a/codex-rs/bwrap/build.rs b/codex-rs/bwrap/build.rs new file mode 100644 index 0000000000..40c2712839 --- /dev/null +++ b/codex-rs/bwrap/build.rs @@ -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 { + 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() + )) +} diff --git a/codex-rs/bwrap/config.h b/codex-rs/bwrap/config.h new file mode 100644 index 0000000000..f73932a0f8 --- /dev/null +++ b/codex-rs/bwrap/config.h @@ -0,0 +1 @@ +#define PACKAGE_STRING "bubblewrap built for Codex" diff --git a/codex-rs/bwrap/src/main.rs b/codex-rs/bwrap/src/main.rs new file mode 100644 index 0000000000..09c624aa9e --- /dev/null +++ b/codex-rs/bwrap/src/main.rs @@ -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::>(); + let mut argv_ptrs = cstrings + .iter() + .map(CString::as_c_str) + .map(CStr::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. + 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"); +} diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index dbca9ab63a..c78750576b 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -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", diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index be222a1673..3283ba2c3e 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -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 diff --git a/codex-rs/exec-server/BUILD.bazel b/codex-rs/exec-server/BUILD.bazel index 57ebe041f8..5f3efe6211 100644 --- a/codex-rs/exec-server/BUILD.bazel +++ b/codex-rs/exec-server/BUILD.bazel @@ -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"], ) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 8f084a50e9..76b0f22b2b 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -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( diff --git a/codex-rs/linux-sandbox/BUILD.bazel b/codex-rs/linux-sandbox/BUILD.bazel index 87ca8ba066..2770b97e09 100644 --- a/codex-rs/linux-sandbox/BUILD.bazel +++ b/codex-rs/linux-sandbox/BUILD.bazel @@ -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"], ) diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 05967661e2..aaacee27fa 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -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" diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index 5745f4816c..1b4c1e5aa7 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -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 diff --git a/codex-rs/linux-sandbox/build.rs b/codex-rs/linux-sandbox/build.rs index a2b4ca86ca..968cfc7e67 100644 --- a/codex-rs/linux-sandbox/build.rs +++ b/codex-rs/linux-sandbox/build.rs @@ -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 { - 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"); } diff --git a/codex-rs/linux-sandbox/config.h b/codex-rs/linux-sandbox/config.h deleted file mode 100644 index f08aa6fcee..0000000000 --- a/codex-rs/linux-sandbox/config.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#define PACKAGE_STRING "bubblewrap built at codex build-time" diff --git a/codex-rs/linux-sandbox/src/bazel_bwrap.rs b/codex-rs/linux-sandbox/src/bazel_bwrap.rs new file mode 100644 index 0000000000..90e41c3849 --- /dev/null +++ b/codex-rs/linux-sandbox/src/bazel_bwrap.rs @@ -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 { + 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 { + 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 { + 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 +} diff --git a/codex-rs/linux-sandbox/src/bundled_bwrap.rs b/codex-rs/linux-sandbox/src/bundled_bwrap.rs new file mode 100644 index 0000000000..505377907f --- /dev/null +++ b/codex-rs/linux-sandbox/src/bundled_bwrap.rs @@ -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 { + 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, preserved_files: Vec) -> ! { + 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 { + 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 { + 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> = 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"); + } +} diff --git a/codex-rs/linux-sandbox/src/exec_util.rs b/codex-rs/linux-sandbox/src/exec_util.rs new file mode 100644 index 0000000000..594c7a725d --- /dev/null +++ b/codex-rs/linux-sandbox/src/exec_util.rs @@ -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 { + let mut cstrings: Vec = 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 + } +} diff --git a/codex-rs/linux-sandbox/src/launcher.rs b/codex-rs/linux-sandbox/src/launcher.rs index 577ef52405..9b80075531 100644 --- a/codex-rs/linux-sandbox/src/launcher.rs +++ b/codex-rs/linux-sandbox/src/launcher.rs @@ -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, preserved_files: Vec) -> ! { 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 = 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 { + 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, -) -> BubblewrapLauncher { +) -> Option { 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 { - let mut cstrings: Vec = 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 - } } diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 900287c99d..478cd6c379 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -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() -> ! { diff --git a/codex-rs/linux-sandbox/src/vendored_bwrap.rs b/codex-rs/linux-sandbox/src/vendored_bwrap.rs deleted file mode 100644 index a2da14db05..0000000000 --- a/codex-rs/linux-sandbox/src/vendored_bwrap.rs +++ /dev/null @@ -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 { - let mut cstrings: Vec = 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, preserved_files: Vec) -> ! { - 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, _preserved_files: Vec) -> ! { - let _ = run_vendored_bwrap_main(&[], &[]); - unreachable!("run_vendored_bwrap_main should always panic in this configuration") - } -} - -pub(crate) use imp::exec_vendored_bwrap; diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 31021fb9ce..87e4ce68ae 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -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 { let policy = ShellEnvironmentPolicy::default(); diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index 932b7981d3..d1aa6856c4 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -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 { 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(); diff --git a/codex-rs/sandboxing/src/bwrap.rs b/codex-rs/sandboxing/src/bwrap.rs index 840bb3e62d..e0eee177fb 100644 --- a/codex-rs/sandboxing/src/bwrap.rs +++ b/codex-rs/sandboxing/src/bwrap.rs @@ -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.";