feat(sandbox): enforce proxy-aware network routing in sandbox (#11113)

## Summary
- expand proxy env injection to cover common tool env vars
(`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`/`NO_PROXY` families +
tool-specific variants)
- harden macOS Seatbelt network policy generation to route through
inferred loopback proxy endpoints and fail closed when proxy env is
malformed
- thread proxy-aware Linux sandbox flags and add minimal bwrap netns
isolation hook for restricted non-proxy runs
- add/refresh tests for proxy env wiring, Seatbelt policy generation,
and Linux sandbox argument wiring
This commit is contained in:
viyatb-oai
2026-02-09 23:44:21 -08:00
committed by GitHub
parent b61ea47e83
commit 3391e5ea86
24 changed files with 1046 additions and 122 deletions

View File

@@ -26,19 +26,47 @@ pub(crate) struct BwrapOptions {
/// This is the secure default, but some restrictive container environments
/// deny `--proc /proc` even when PID namespaces are available.
pub mount_proc: bool,
/// How networking should be configured inside the bubblewrap sandbox.
pub network_mode: BwrapNetworkMode,
}
impl Default for BwrapOptions {
fn default() -> Self {
Self { mount_proc: true }
Self {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
}
}
}
/// Network policy modes for bubblewrap.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum BwrapNetworkMode {
/// Keep access to the host network namespace.
#[default]
FullAccess,
/// Remove access to the host network namespace.
Isolated,
/// Intended proxy-only mode.
///
/// Bubblewrap does not currently enforce proxy-only egress, so this is
/// treated as isolated for fail-closed behavior.
ProxyOnly,
}
impl BwrapNetworkMode {
fn should_unshare_network(self) -> bool {
!matches!(self, Self::FullAccess)
}
}
/// 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.
/// When the policy grants full disk write access and full network access, this
/// returns `command` unchanged so we avoid unnecessary sandboxing overhead.
/// If network isolation is requested, we still wrap with bubblewrap so network
/// namespace restrictions apply while preserving full filesystem access.
pub(crate) fn create_bwrap_command_args(
command: Vec<String>,
sandbox_policy: &SandboxPolicy,
@@ -46,12 +74,37 @@ pub(crate) fn create_bwrap_command_args(
options: BwrapOptions,
) -> Result<Vec<String>> {
if sandbox_policy.has_full_disk_write_access() {
return Ok(command);
return if options.network_mode == BwrapNetworkMode::FullAccess {
Ok(command)
} else {
Ok(create_bwrap_flags_full_filesystem(command, options))
};
}
create_bwrap_flags(command, sandbox_policy, cwd, options)
}
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> Vec<String> {
let mut args = vec![
"--new-session".to_string(),
"--die-with-parent".to_string(),
"--bind".to_string(),
"/".to_string(),
"/".to_string(),
"--unshare-pid".to_string(),
];
if options.network_mode.should_unshare_network() {
args.push("--unshare-net".to_string());
}
if options.mount_proc {
args.push("--proc".to_string());
args.push("/proc".to_string());
}
args.push("--".to_string());
args.extend(command);
args
}
/// Build the bubblewrap flags (everything after `argv[0]`).
fn create_bwrap_flags(
command: Vec<String>,
@@ -65,6 +118,9 @@ fn create_bwrap_flags(
args.extend(create_filesystem_args(sandbox_policy, cwd)?);
// Isolate the PID namespace.
args.push("--unshare-pid".to_string());
if options.network_mode.should_unshare_network() {
args.push("--unshare-net".to_string());
}
// Mount a fresh /proc unless the caller explicitly disables it.
if options.mount_proc {
args.push("--proc".to_string());
@@ -250,3 +306,59 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use codex_core::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn full_disk_write_full_network_returns_unwrapped_command() {
let command = vec!["/bin/true".to_string()];
let args = create_bwrap_command_args(
command.clone(),
&SandboxPolicy::DangerFullAccess,
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
)
.expect("create bwrap args");
assert_eq!(args, command);
}
#[test]
fn full_disk_write_proxy_only_keeps_full_filesystem_but_unshares_network() {
let command = vec!["/bin/true".to_string()];
let args = create_bwrap_command_args(
command,
&SandboxPolicy::DangerFullAccess,
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::ProxyOnly,
},
)
.expect("create bwrap args");
assert_eq!(
args,
vec![
"--new-session".to_string(),
"--die-with-parent".to_string(),
"--bind".to_string(),
"/".to_string(),
"/".to_string(),
"--unshare-pid".to_string(),
"--unshare-net".to_string(),
"--proc".to_string(),
"/proc".to_string(),
"--".to_string(),
"/bin/true".to_string(),
]
);
}
}