mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
feat(linux-sandbox): add build-time bubblewrap FFI path
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -35,3 +35,7 @@ tokio = { workspace = true, features = [
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
pkg-config = "0.3"
|
||||
|
||||
112
codex-rs/linux-sandbox/build.rs
Normal file
112
codex-rs/linux-sandbox/build.rs
Normal 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}"
|
||||
))
|
||||
}
|
||||
@@ -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}" \
|
||||
|
||||
@@ -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)?);
|
||||
|
||||
@@ -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() -> ! {
|
||||
|
||||
@@ -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,
|
||||
|
||||
50
codex-rs/linux-sandbox/src/vendored_bwrap.rs
Normal file
50
codex-rs/linux-sandbox/src/vendored_bwrap.rs
Normal 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;
|
||||
Reference in New Issue
Block a user