feat(linux-sandbox): add build-time bubblewrap FFI path

This commit is contained in:
viyatb-oai
2026-01-30 12:15:26 -08:00
parent 5063676691
commit fbcd5ad9a5
8 changed files with 250 additions and 8 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -1593,11 +1593,13 @@ dependencies = [
name = "codex-linux-sandbox"
version = "0.0.0"
dependencies = [
"cc",
"clap",
"codex-core",
"codex-utils-absolute-path",
"landlock",
"libc",
"pkg-config",
"pretty_assertions",
"seccompiler",
"serde_json",

View File

@@ -35,3 +35,7 @@ tokio = { workspace = true, features = [
"rt-multi-thread",
"signal",
] }
[build-dependencies]
cc = "1"
pkg-config = "0.3"

View File

@@ -0,0 +1,112 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
fn main() {
// Tell rustc/clippy that this is an expected cfg value.
println!("cargo:rustc-check-cfg=cfg(vendored_bwrap_available)");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" {
return;
}
// Opt-in: do not attempt to fetch/compile bwrap unless explicitly enabled.
let enable_ffi = matches!(env::var("CODEX_BWRAP_ENABLE_FFI"), Ok(value) if value == "1");
if !enable_ffi {
return;
}
if let Err(err) = try_build_vendored_bwrap() {
// Keep normal builds working even if the experiment fails.
println!("cargo:warning=build-time bubblewrap disabled: {err}");
}
}
fn try_build_vendored_bwrap() -> Result<(), String> {
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|err| err.to_string())?);
let src_dir = resolve_bwrap_source_dir(&out_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,
"#pragma once\n#define PACKAGE_STRING \"bubblewrap built at codex build-time\"\n",
)
.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 {
build.include(include_path);
}
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. `CODEX_BWRAP_FETCH=1` triggers a build-time shallow git clone into
/// `OUT_DIR`.
fn resolve_bwrap_source_dir(out_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 fetch = matches!(env::var("CODEX_BWRAP_FETCH"), Ok(value) if value == "1");
if !fetch {
return Err(
"no bwrap source available: set CODEX_BWRAP_SOURCE_DIR or CODEX_BWRAP_FETCH=1"
.to_string(),
);
}
let fetch_ref = env::var("CODEX_BWRAP_FETCH_REF").unwrap_or_else(|_| "v0.11.0".to_string());
let src_dir = out_dir.join("bubblewrap-src");
if src_dir.exists() {
return Ok(src_dir);
}
let status = Command::new("git")
.arg("clone")
.arg("--depth")
.arg("1")
.arg("--branch")
.arg(&fetch_ref)
.arg("https://github.com/containers/bubblewrap")
.arg(&src_dir)
.status()
.map_err(|err| format!("failed to spawn git clone: {err}"))?;
if status.success() {
return Ok(src_dir);
}
Err(format!(
"git clone bubblewrap ({fetch_ref}) failed with status: {status}"
))
}

View File

@@ -18,6 +18,7 @@ set -euo pipefail
# CODEX_LINUX_SANDBOX_DEBUG=1 # default: 0 (pass debug env var through)
# CODEX_LINUX_SANDBOX_USE_BWRAP=1 # default: 1 (run the bwrap suite)
# CODEX_LINUX_SANDBOX_USE_LEGACY=1 # default: 1 (run the legacy suite)
# CODEX_LINUX_SANDBOX_USE_VENDORED=1 # default: 0 (use build-time bwrap FFI)
# CODEX_LINUX_SANDBOX_BWRAP_PATH # default: $(command -v bwrap)
if [[ "$(uname -s)" != "Linux" ]]; then
@@ -29,16 +30,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
CODEX_RS_DIR="${REPO_ROOT}/codex-rs"
BWRAP_PATH="${CODEX_LINUX_SANDBOX_BWRAP_PATH:-$(command -v bwrap || true)}"
if [[ -z "${BWRAP_PATH}" ]]; then
echo "bubblewrap (bwrap) is required but was not found on PATH." >&2
exit 1
fi
NO_PROC="${CODEX_LINUX_SANDBOX_NO_PROC:-1}"
DEBUG="${CODEX_LINUX_SANDBOX_DEBUG:-0}"
USE_BWRAP_SUITE="${CODEX_LINUX_SANDBOX_USE_BWRAP:-1}"
USE_LEGACY_SUITE="${CODEX_LINUX_SANDBOX_USE_LEGACY:-1}"
USE_VENDORED="${CODEX_LINUX_SANDBOX_USE_VENDORED:-0}"
BWRAP_PATH=""
if [[ "${USE_VENDORED}" != "1" ]]; then
BWRAP_PATH="${CODEX_LINUX_SANDBOX_BWRAP_PATH:-$(command -v bwrap || true)}"
if [[ -z "${BWRAP_PATH}" ]]; then
echo "bubblewrap (bwrap) is required but was not found on PATH." >&2
exit 1
fi
fi
SANDBOX_BIN="${CODEX_RS_DIR}/target/debug/codex-linux-sandbox"
tmp_root=""
@@ -71,7 +76,11 @@ run_sandbox() {
local bwrap_flag=()
if [[ "${use_bwrap}" == "1" ]]; then
bwrap_flag=(--bwrap-path "${BWRAP_PATH}")
if [[ "${USE_VENDORED}" == "1" ]]; then
bwrap_flag=(--use-vendored-bwrap)
else
bwrap_flag=(--bwrap-path "${BWRAP_PATH}")
fi
fi
"${debug_env[@]}" "${SANDBOX_BIN}" \

View File

@@ -68,6 +68,37 @@ pub(crate) fn create_bwrap_command_args(
let mut args = Vec::new();
args.push(path_to_string(&bwrap_path));
args.extend(create_bwrap_flags(command, sandbox_policy, cwd, options)?);
Ok(args)
}
/// Doc-hidden helper that builds bubblewrap arguments without a program path.
///
/// This is intended for experiments where we call a build-time bubblewrap
/// `main` symbol via FFI rather than exec'ing the `bwrap` binary. The caller
/// is responsible for providing a suitable `argv[0]`.
#[doc(hidden)]
pub(crate) fn create_bwrap_command_args_vendored(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
if sandbox_policy.has_full_disk_write_access() {
return Ok(command);
}
create_bwrap_flags(command, sandbox_policy, cwd, options)
}
/// Build the bubblewrap flags (everything after `argv[0]`).
fn create_bwrap_flags(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
options: BwrapOptions,
) -> Result<Vec<String>> {
let mut args = Vec::new();
args.push("--new-session".to_string());
args.push("--die-with-parent".to_string());
args.extend(create_filesystem_args(sandbox_policy, cwd)?);

View File

@@ -9,6 +9,8 @@ mod bwrap;
mod landlock;
#[cfg(target_os = "linux")]
mod linux_run_main;
#[cfg(target_os = "linux")]
mod vendored_bwrap;
#[cfg(target_os = "linux")]
pub fn run_main() -> ! {

View File

@@ -5,7 +5,9 @@ use std::path::PathBuf;
use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::bwrap::create_bwrap_command_args_vendored;
use crate::landlock::apply_sandbox_policy_to_current_thread;
use crate::vendored_bwrap::exec_vendored_bwrap;
#[derive(Debug, Parser)]
/// CLI surface for the Linux sandbox helper.
@@ -33,6 +35,12 @@ pub struct LandlockCommand {
#[arg(long = "bwrap-path", hide = true)]
pub bwrap_path: Option<PathBuf>,
/// Experimental: call a build-time bubblewrap `main()` via FFI.
///
/// This is opt-in and only works when the build script compiles bwrap.
#[arg(long = "use-vendored-bwrap", hide = true, default_value_t = false)]
pub use_vendored_bwrap: bool,
/// Internal: apply seccomp and `no_new_privs` in the already-sandboxed
/// process, then exec the user command.
///
@@ -65,11 +73,12 @@ pub fn run_main() -> ! {
sandbox_policy,
use_bwrap_sandbox,
bwrap_path,
use_vendored_bwrap,
apply_seccomp_then_exec,
no_proc,
command,
} = LandlockCommand::parse();
let use_bwrap_sandbox = use_bwrap_sandbox || bwrap_path.is_some();
let use_bwrap_sandbox = use_bwrap_sandbox || bwrap_path.is_some() || use_vendored_bwrap;
if command.is_empty() {
panic!("No command specified to execute.");
@@ -106,6 +115,29 @@ pub fn run_main() -> ! {
let options = BwrapOptions {
mount_proc: !no_proc,
};
if use_vendored_bwrap {
let mut argv0 = bwrap_path
.as_deref()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| "bwrap".to_string());
if argv0.is_empty() {
argv0 = "bwrap".to_string();
}
let mut argv = vec![argv0];
argv.extend(
create_bwrap_command_args_vendored(
inner,
&sandbox_policy,
&sandbox_policy_cwd,
options,
)
.unwrap_or_else(|err| {
panic!("error building build-time bubblewrap command: {err:?}")
}),
);
exec_vendored_bwrap(argv);
}
ensure_bwrap_available(bwrap_path.as_deref());
create_bwrap_command_args(
inner,

View File

@@ -0,0 +1,50 @@
//! Build-time bubblewrap entrypoint.
//!
//! This module is intentionally behind a build-time opt-in. When enabled, 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::os::raw::c_char;
unsafe extern "C" {
fn bwrap_main(argc: libc::c_int, argv: *const *const c_char) -> libc::c_int;
}
/// Execute the build-time bubblewrap `main` function with the given argv.
pub(crate) fn exec_vendored_bwrap(argv: Vec<String>) -> ! {
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}"),
}
}
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.
let exit_code = unsafe { bwrap_main(cstrings.len() as libc::c_int, argv_ptrs.as_ptr()) };
std::process::exit(exit_code);
}
}
#[cfg(not(vendored_bwrap_available))]
mod imp {
/// Panics with a clear error when the build-time bwrap path is not enabled.
pub(crate) fn exec_vendored_bwrap(_argv: Vec<String>) -> ! {
panic!(
"build-time bubblewrap is not available in this build.\n\
To enable it on Linux:\n\
- set CODEX_BWRAP_ENABLE_FFI=1\n\
- and either set CODEX_BWRAP_SOURCE_DIR to a bubblewrap checkout,\n\
or set CODEX_BWRAP_FETCH=1 (and optionally CODEX_BWRAP_FETCH_REF)."
);
}
}
pub(crate) use imp::exec_vendored_bwrap;