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(),
]
);
}
}

View File

@@ -42,18 +42,22 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
sandbox_policy: &SandboxPolicy,
cwd: &Path,
apply_landlock_fs: bool,
allow_network_for_proxy: bool,
) -> Result<()> {
let install_network_seccomp =
should_install_network_seccomp(sandbox_policy, allow_network_for_proxy);
// `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents
// setuid privilege elevation. Many `bwrap` deployments rely on setuid, so
// we avoid this unless we need seccomp or we are explicitly using the
// legacy Landlock filesystem pipeline.
if !sandbox_policy.has_full_network_access()
if install_network_seccomp
|| (apply_landlock_fs && !sandbox_policy.has_full_disk_write_access())
{
set_no_new_privs()?;
}
if !sandbox_policy.has_full_network_access() {
if install_network_seccomp {
install_network_seccomp_filter_on_current_thread()?;
}
@@ -72,6 +76,15 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
Ok(())
}
fn should_install_network_seccomp(
sandbox_policy: &SandboxPolicy,
allow_network_for_proxy: bool,
) -> bool {
// Managed-network sessions should remain fail-closed even for policies that
// would normally grant full network access (for example, DangerFullAccess).
!sandbox_policy.has_full_network_access() || allow_network_for_proxy
}
/// Enable `PR_SET_NO_NEW_PRIVS` so seccomp can be applied safely.
fn set_no_new_privs() -> Result<()> {
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
@@ -183,3 +196,38 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
Ok(())
}
#[cfg(test)]
mod tests {
use super::should_install_network_seccomp;
use codex_core::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn managed_network_enforces_seccomp_even_for_full_network_policy() {
assert_eq!(
should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, true),
true
);
}
#[test]
fn full_network_policy_without_managed_network_skips_seccomp() {
assert_eq!(
should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, false),
false
);
}
#[test]
fn restricted_network_policy_always_installs_seccomp() {
assert!(should_install_network_seccomp(
&SandboxPolicy::ReadOnly,
false
));
assert!(should_install_network_seccomp(
&SandboxPolicy::ReadOnly,
true
));
}
}

View File

@@ -6,6 +6,7 @@ use std::os::fd::FromRawFd;
use std::path::Path;
use std::path::PathBuf;
use crate::bwrap::BwrapNetworkMode;
use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::landlock::apply_sandbox_policy_to_current_thread;
@@ -40,6 +41,14 @@ pub struct LandlockCommand {
#[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)]
pub apply_seccomp_then_exec: bool,
/// Internal compatibility flag.
///
/// By default, restricted-network sandboxing uses isolated networking.
/// If set, sandbox setup switches to proxy-only network mode
/// (currently enforced the same as isolated networking).
#[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)]
pub allow_network_for_proxy: 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`.
@@ -64,6 +73,7 @@ pub fn run_main() -> ! {
sandbox_policy,
use_bwrap_sandbox,
apply_seccomp_then_exec,
allow_network_for_proxy,
no_proc,
command,
} = LandlockCommand::parse();
@@ -75,18 +85,24 @@ pub fn run_main() -> ! {
// 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, false)
{
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
) {
panic!("error applying Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
}
if sandbox_policy.has_full_disk_write_access() {
if let Err(e) =
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false)
{
if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
) {
panic!("error applying Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
@@ -100,15 +116,25 @@ pub fn run_main() -> ! {
&sandbox_policy_cwd,
&sandbox_policy,
use_bwrap_sandbox,
allow_network_for_proxy,
command,
);
run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc);
run_bwrap_with_proc_fallback(
&sandbox_policy_cwd,
&sandbox_policy,
inner,
!no_proc,
allow_network_for_proxy,
);
}
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
if let Err(e) =
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, true)
{
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
&sandbox_policy_cwd,
true,
allow_network_for_proxy,
) {
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
@@ -119,6 +145,7 @@ fn run_bwrap_with_proc_fallback(
sandbox_policy: &codex_core::protocol::SandboxPolicy,
inner: Vec<String>,
mount_proc: bool,
allow_network_for_proxy: bool,
) -> ! {
let mut mount_proc = mount_proc;
@@ -127,11 +154,28 @@ fn run_bwrap_with_proc_fallback(
mount_proc = false;
}
let options = BwrapOptions { mount_proc };
let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
let options = BwrapOptions {
mount_proc,
network_mode,
};
let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options);
exec_vendored_bwrap(argv);
}
fn bwrap_network_mode(
sandbox_policy: &codex_core::protocol::SandboxPolicy,
allow_network_for_proxy: bool,
) -> BwrapNetworkMode {
if allow_network_for_proxy {
BwrapNetworkMode::ProxyOnly
} else if sandbox_policy.has_full_network_access() {
BwrapNetworkMode::FullAccess
} else {
BwrapNetworkMode::Isolated
}
}
fn build_bwrap_argv(
inner: Vec<String>,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
@@ -164,7 +208,10 @@ fn preflight_proc_mount_support(
preflight_command,
sandbox_policy,
sandbox_policy_cwd,
BwrapOptions { mount_proc: true },
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
@@ -268,6 +315,7 @@ fn build_inner_seccomp_command(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_core::protocol::SandboxPolicy,
use_bwrap_sandbox: bool,
allow_network_for_proxy: bool,
command: Vec<String>,
) -> Vec<String> {
let current_exe = match std::env::current_exe() {
@@ -290,6 +338,9 @@ fn build_inner_seccomp_command(
inner.push("--use-bwrap-sandbox".to_string());
inner.push("--apply-seccomp-then-exec".to_string());
}
if allow_network_for_proxy {
inner.push("--allow-network-for-proxy".to_string());
}
inner.push("--".to_string());
inner.extend(command);
inner
@@ -342,7 +393,10 @@ mod tests {
vec!["/bin/true".to_string()],
&SandboxPolicy::ReadOnly,
Path::new("/"),
BwrapOptions { mount_proc: true },
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
);
assert_eq!(
argv,
@@ -366,4 +420,38 @@ mod tests {
]
);
}
#[test]
fn inserts_unshare_net_when_network_isolation_requested() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::ReadOnly,
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::Isolated,
},
);
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
}
#[test]
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::ReadOnly,
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::ProxyOnly,
},
);
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
}
#[test]
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
}
}