mirror of
https://github.com/openai/codex.git
synced 2026-04-30 03:12:20 +03:00
feat(linux-sandbox): vendor bubblewrap and wire it with FFI (#10413)
## Summary Vendor Bubblewrap into the repo and add minimal build plumbing in `codex-linux-sandbox` to compile/link it. ## Why We want to move Linux sandboxing toward Bubblewrap, but in a safe two-step rollout: 1) vendoring/build setup (this PR), 2) runtime integration (follow-up PR). ## Included - Add `codex-rs/vendor/bubblewrap` sources. - Add build-time FFI path in `codex-rs/linux-sandbox`. - Update `build.rs` rerun tracking for vendored files. - Small vendored compile warning fix (`sockaddr_nl` full init). follow up in https://github.com/openai/codex/pull/9938
This commit is contained in:
291
codex-rs/linux-sandbox/src/bwrap.rs
Normal file
291
codex-rs/linux-sandbox/src/bwrap.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
//! Bubblewrap-based filesystem sandboxing for Linux.
|
||||
//!
|
||||
//! This module mirrors the semantics used by the macOS Seatbelt sandbox:
|
||||
//! - the filesystem is read-only by default,
|
||||
//! - explicit writable roots are layered on top, and
|
||||
//! - sensitive subpaths such as `.git` and `.codex` remain read-only even when
|
||||
//! their parent root is writable.
|
||||
//!
|
||||
//! The overall Linux sandbox is composed of:
|
||||
//! - seccomp + `PR_SET_NO_NEW_PRIVS` applied in-process, and
|
||||
//! - bubblewrap used to construct the filesystem view before exec.
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::error::Result;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::WritableRoot;
|
||||
|
||||
/// Options that control how bubblewrap is invoked.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct BwrapOptions {
|
||||
/// Whether to mount a fresh `/proc` inside the PID namespace.
|
||||
///
|
||||
/// This is the secure default, but some restrictive container environments
|
||||
/// deny `--proc /proc` even when PID namespaces are available.
|
||||
pub mount_proc: bool,
|
||||
}
|
||||
|
||||
impl Default for BwrapOptions {
|
||||
fn default() -> Self {
|
||||
Self { mount_proc: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
|
||||
/// with explicit writable roots and read-only subpaths layered afterward.
|
||||
///
|
||||
/// When the policy grants full disk write access, this returns `command`
|
||||
/// unchanged so we avoid unnecessary sandboxing overhead.
|
||||
pub(crate) fn create_bwrap_command_args(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
options: BwrapOptions,
|
||||
bwrap_path: Option<&Path>,
|
||||
) -> Result<Vec<String>> {
|
||||
if sandbox_policy.has_full_disk_write_access() {
|
||||
return Ok(command);
|
||||
}
|
||||
|
||||
let bwrap_path = match bwrap_path {
|
||||
Some(path) => {
|
||||
if path.exists() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
return Err(CodexErr::UnsupportedOperation(format!(
|
||||
"bubblewrap (bwrap) not found at configured path: {}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => which::which("bwrap").map_err(|err| {
|
||||
CodexErr::UnsupportedOperation(format!("bubblewrap (bwrap) not found on PATH: {err}"))
|
||||
})?,
|
||||
};
|
||||
|
||||
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)?);
|
||||
// Isolate the PID namespace.
|
||||
args.push("--unshare-pid".to_string());
|
||||
// Mount a fresh /proc unless the caller explicitly disables it.
|
||||
if options.mount_proc {
|
||||
args.push("--proc".to_string());
|
||||
args.push("/proc".to_string());
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command);
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
/// Build the bubblewrap filesystem mounts for a given sandbox policy.
|
||||
///
|
||||
/// The mount order is important:
|
||||
/// 1. `--ro-bind / /` makes the entire filesystem read-only.
|
||||
/// 2. `--bind <root> <root>` re-enables writes for allowed roots.
|
||||
/// 3. `--ro-bind <subpath> <subpath>` re-applies read-only protections under
|
||||
/// those writable roots so protected subpaths win.
|
||||
/// 4. `--dev-bind /dev/null /dev/null` preserves the common sink even under a
|
||||
/// read-only root.
|
||||
fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result<Vec<String>> {
|
||||
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
ensure_mount_targets_exist(&writable_roots)?;
|
||||
|
||||
let mut args = Vec::new();
|
||||
|
||||
// Read-only root, then selectively re-enable writes.
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/".to_string());
|
||||
args.push("/".to_string());
|
||||
|
||||
for writable_root in &writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
args.push("--bind".to_string());
|
||||
args.push(path_to_string(root));
|
||||
args.push(path_to_string(root));
|
||||
}
|
||||
|
||||
// Re-apply read-only subpaths after the writable binds so they win.
|
||||
let allowed_write_paths: Vec<PathBuf> = writable_roots
|
||||
.iter()
|
||||
.map(|writable_root| writable_root.root.as_path().to_path_buf())
|
||||
.collect();
|
||||
|
||||
for subpath in collect_read_only_subpaths(&writable_roots) {
|
||||
if let Some(symlink_path) = find_symlink_in_path(&subpath, &allowed_write_paths) {
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&symlink_path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subpath.exists() {
|
||||
if let Some(first_missing) = find_first_non_existent_component(&subpath)
|
||||
&& is_within_allowed_write_paths(&first_missing, &allowed_write_paths)
|
||||
{
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push(path_to_string(&first_missing));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_within_allowed_write_paths(&subpath, &allowed_write_paths) {
|
||||
args.push("--ro-bind".to_string());
|
||||
args.push(path_to_string(&subpath));
|
||||
args.push(path_to_string(&subpath));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `/dev/null` remains usable regardless of the root bind.
|
||||
args.push("--dev-bind".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
args.push("/dev/null".to_string());
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
/// Collect unique read-only subpaths across all writable roots.
|
||||
fn collect_read_only_subpaths(writable_roots: &[WritableRoot]) -> Vec<PathBuf> {
|
||||
let mut subpaths: BTreeSet<PathBuf> = BTreeSet::new();
|
||||
for writable_root in writable_roots {
|
||||
for subpath in &writable_root.read_only_subpaths {
|
||||
subpaths.insert(subpath.as_path().to_path_buf());
|
||||
}
|
||||
}
|
||||
subpaths.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Validate that writable roots exist before constructing mounts.
|
||||
///
|
||||
/// Bubblewrap requires bind mount targets to exist. We fail fast with a clear
|
||||
/// error so callers can present an actionable message.
|
||||
fn ensure_mount_targets_exist(writable_roots: &[WritableRoot]) -> Result<()> {
|
||||
for writable_root in writable_roots {
|
||||
let root = writable_root.root.as_path();
|
||||
if !root.exists() {
|
||||
return Err(CodexErr::UnsupportedOperation(format!(
|
||||
"Sandbox expected writable root {root}, but it does not exist.",
|
||||
root = root.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_to_string(path: &Path) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Returns true when `path` is under any allowed writable root.
|
||||
fn is_within_allowed_write_paths(path: &Path, allowed_write_paths: &[PathBuf]) -> bool {
|
||||
allowed_write_paths
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root))
|
||||
}
|
||||
|
||||
/// Find the first symlink along `target_path` that is also under a writable root.
|
||||
///
|
||||
/// This blocks symlink replacement attacks where a protected path is a symlink
|
||||
/// inside a writable root (e.g., `.codex -> ./decoy`). In that case we mount
|
||||
/// `/dev/null` on the symlink itself to prevent rewiring it.
|
||||
fn find_symlink_in_path(target_path: &Path, allowed_write_paths: &[PathBuf]) -> Option<PathBuf> {
|
||||
let mut current = PathBuf::new();
|
||||
|
||||
for component in target_path.components() {
|
||||
use std::path::Component;
|
||||
match component {
|
||||
Component::RootDir => {
|
||||
current.push(Path::new("/"));
|
||||
continue;
|
||||
}
|
||||
Component::CurDir => continue,
|
||||
Component::ParentDir => {
|
||||
current.pop();
|
||||
continue;
|
||||
}
|
||||
Component::Normal(part) => current.push(part),
|
||||
Component::Prefix(_) => continue,
|
||||
}
|
||||
|
||||
let metadata = match std::fs::symlink_metadata(¤t) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink()
|
||||
&& is_within_allowed_write_paths(¤t, allowed_write_paths)
|
||||
{
|
||||
return Some(current);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the first missing path component while walking `target_path`.
|
||||
///
|
||||
/// Mounting `/dev/null` on the first missing component prevents the sandboxed
|
||||
/// process from creating the protected path hierarchy.
|
||||
fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
|
||||
let mut current = PathBuf::new();
|
||||
|
||||
for component in target_path.components() {
|
||||
use std::path::Component;
|
||||
match component {
|
||||
Component::RootDir => {
|
||||
current.push(Path::new("/"));
|
||||
continue;
|
||||
}
|
||||
Component::CurDir => continue,
|
||||
Component::ParentDir => {
|
||||
current.pop();
|
||||
continue;
|
||||
}
|
||||
Component::Normal(part) => current.push(part),
|
||||
Component::Prefix(_) => continue,
|
||||
}
|
||||
|
||||
if !current.exists() {
|
||||
return Some(current);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
//! Linux sandbox helper entry point.
|
||||
//!
|
||||
//! On Linux, `codex-linux-sandbox` applies:
|
||||
//! - in-process restrictions (`no_new_privs` + seccomp), and
|
||||
//! - bubblewrap for filesystem isolation.
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod landlock;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_run_main;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod mounts;
|
||||
mod vendored_bwrap;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn run_main() -> ! {
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
use clap::Parser;
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
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.
|
||||
///
|
||||
/// The type name remains `LandlockCommand` for compatibility with existing
|
||||
/// wiring, but the filesystem sandbox now uses bubblewrap.
|
||||
pub struct LandlockCommand {
|
||||
/// It is possible that the cwd used in the context of the sandbox policy
|
||||
/// is different from the cwd of the process to spawn.
|
||||
@@ -14,26 +23,179 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "sandbox-policy")]
|
||||
pub sandbox_policy: codex_core::protocol::SandboxPolicy,
|
||||
|
||||
/// Full command args to run under landlock.
|
||||
/// Opt-in: use the bubblewrap-based Linux sandbox pipeline.
|
||||
///
|
||||
/// When not set, we fall back to the legacy Landlock + mount pipeline.
|
||||
#[arg(long = "use-bwrap-sandbox", hide = true, default_value_t = false)]
|
||||
pub use_bwrap_sandbox: bool,
|
||||
|
||||
/// Optional explicit path to the `bwrap` binary to use.
|
||||
///
|
||||
/// When provided, this implies bubblewrap opt-in and avoids PATH lookups.
|
||||
#[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.
|
||||
///
|
||||
/// This exists so we can run bubblewrap first (which may rely on setuid)
|
||||
/// and only tighten with seccomp after the filesystem view is established.
|
||||
#[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)]
|
||||
pub apply_seccomp_then_exec: bool,
|
||||
|
||||
/// When set, skip mounting a fresh `/proc` even though PID isolation is
|
||||
/// still enabled. This is primarily intended for restrictive container
|
||||
/// environments that deny `--proc /proc`.
|
||||
#[arg(long = "no-proc", default_value_t = false)]
|
||||
pub no_proc: bool,
|
||||
|
||||
/// Full command args to run under the Linux sandbox helper.
|
||||
#[arg(trailing_var_arg = true)]
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
/// Entry point for the Linux sandbox helper.
|
||||
///
|
||||
/// The sequence is:
|
||||
/// 1. When needed, wrap the command with bubblewrap to construct the
|
||||
/// filesystem view.
|
||||
/// 2. Apply in-process restrictions (no_new_privs + seccomp).
|
||||
/// 3. `execvp` into the final command.
|
||||
pub fn run_main() -> ! {
|
||||
let LandlockCommand {
|
||||
sandbox_policy_cwd,
|
||||
sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
bwrap_path,
|
||||
use_vendored_bwrap,
|
||||
apply_seccomp_then_exec,
|
||||
no_proc,
|
||||
command,
|
||||
} = LandlockCommand::parse();
|
||||
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd) {
|
||||
panic!("error running landlock: {e:?}");
|
||||
}
|
||||
let use_bwrap_sandbox = use_bwrap_sandbox || bwrap_path.is_some() || use_vendored_bwrap;
|
||||
|
||||
if command.is_empty() {
|
||||
panic!("No command specified to execute.");
|
||||
}
|
||||
|
||||
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
|
||||
// established the filesystem view.
|
||||
if apply_seccomp_then_exec {
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd)
|
||||
{
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
let command = if sandbox_policy.has_full_disk_write_access() {
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd)
|
||||
{
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
command
|
||||
} else if use_bwrap_sandbox {
|
||||
// Outer stage: bubblewrap first, then re-enter this binary in the
|
||||
// sandboxed environment to apply seccomp.
|
||||
let inner = build_inner_seccomp_command(
|
||||
&sandbox_policy_cwd,
|
||||
&sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
bwrap_path.as_deref(),
|
||||
command,
|
||||
);
|
||||
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,
|
||||
&sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
options,
|
||||
bwrap_path.as_deref(),
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("error building bubblewrap command: {err:?}"))
|
||||
} else {
|
||||
// Legacy path: Landlock enforcement only.
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd)
|
||||
{
|
||||
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
command
|
||||
};
|
||||
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
/// Build the inner command that applies seccomp after bubblewrap.
|
||||
fn build_inner_seccomp_command(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &codex_core::protocol::SandboxPolicy,
|
||||
use_bwrap_sandbox: bool,
|
||||
bwrap_path: Option<&Path>,
|
||||
command: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let current_exe = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(err) => panic!("failed to resolve current executable path: {err}"),
|
||||
};
|
||||
let policy_json = match serde_json::to_string(sandbox_policy) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize sandbox policy: {err}"),
|
||||
};
|
||||
|
||||
let mut inner = vec![
|
||||
current_exe.to_string_lossy().to_string(),
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
sandbox_policy_cwd.to_string_lossy().to_string(),
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
];
|
||||
if use_bwrap_sandbox {
|
||||
inner.push("--use-bwrap-sandbox".to_string());
|
||||
inner.push("--apply-seccomp-then-exec".to_string());
|
||||
}
|
||||
if let Some(bwrap_path) = bwrap_path {
|
||||
inner.push("--bwrap-path".to_string());
|
||||
inner.push(bwrap_path.to_string_lossy().to_string());
|
||||
}
|
||||
inner.push("--".to_string());
|
||||
inner.extend(command);
|
||||
inner
|
||||
}
|
||||
|
||||
/// Exec the provided argv, panicking with context if it fails.
|
||||
fn exec_or_panic(command: Vec<String>) -> ! {
|
||||
#[expect(clippy::expect_used)]
|
||||
let c_command =
|
||||
CString::new(command[0].as_str()).expect("Failed to convert command to CString");
|
||||
@@ -54,3 +216,33 @@ pub fn run_main() -> ! {
|
||||
let err = std::io::Error::last_os_error();
|
||||
panic!("Failed to execvp {}: {err}", command[0].as_str());
|
||||
}
|
||||
|
||||
/// Ensure the `bwrap` binary is available when the sandbox needs it.
|
||||
fn ensure_bwrap_available(bwrap_path: Option<&Path>) {
|
||||
if let Some(path) = bwrap_path {
|
||||
if path.exists() {
|
||||
return;
|
||||
}
|
||||
panic!(
|
||||
"bubblewrap (bwrap) is required for Linux filesystem sandboxing but was not found at the configured path: {}\n\
|
||||
Install it and retry. Examples:\n\
|
||||
- Debian/Ubuntu: apt-get install bubblewrap\n\
|
||||
- Fedora/RHEL: dnf install bubblewrap\n\
|
||||
- Arch: pacman -S bubblewrap\n\
|
||||
If you are running the Codex Node package, ensure bwrap is installed on the host system.",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if which::which("bwrap").is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
panic!(
|
||||
"bubblewrap (bwrap) is required for Linux filesystem sandboxing but was not found on PATH.\n\
|
||||
Install it and retry. Examples:\n\
|
||||
- Debian/Ubuntu: apt-get install bubblewrap\n\
|
||||
- Fedora/RHEL: dnf install bubblewrap\n\
|
||||
- Arch: pacman -S bubblewrap\n\
|
||||
If you are running the Codex Node package, ensure bwrap is installed on the host system."
|
||||
);
|
||||
}
|
||||
|
||||
54
codex-rs/linux-sandbox/src/vendored_bwrap.rs
Normal file
54
codex-rs/linux-sandbox/src/vendored_bwrap.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! 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\
|
||||
Rebuild codex-linux-sandbox on Linux with CODEX_BWRAP_ENABLE_FFI=1.\n\
|
||||
Example:\n\
|
||||
- cd codex-rs && CODEX_BWRAP_ENABLE_FFI=1 cargo build -p codex-linux-sandbox\n\
|
||||
If this crate was already built without it, run:\n\
|
||||
- cargo clean -p codex-linux-sandbox\n\
|
||||
Notes:\n\
|
||||
- libcap headers must be available via pkg-config\n\
|
||||
- bubblewrap sources expected at codex-rs/vendor/bubblewrap (default)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use imp::exec_vendored_bwrap;
|
||||
Reference in New Issue
Block a user