From c5eb33aed12d4977dc38403ecf8b42d89939ea32 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 22 Jul 2026 00:26:03 +0000 Subject: [PATCH] Harden managed proxy setup for sandboxed executions (#34641) ## What changed - Make the generated Linux proxy socket directory readable inside restricted `bubblewrap` sandboxes so the proxy bridge can connect. - Route `WS_PROXY` and `WSS_PROXY` through the Linux managed proxy bridge. - Remove inherited proxy attribution tokens from unscoped executions while continuing to replace them with the current token for scoped executions. ## Testing - Exercise `WSS_PROXY` routing with a minimal filesystem policy. - Cover attribution-token removal and replacement during environment setup. GitOrigin-RevId: 2f14ddb26baab0147a786bc354d93eb5aad059f3 --- .../exec-server/src/process_sandbox_tests.rs | 6 +++ codex-rs/linux-sandbox/src/linux_run_main.rs | 21 +++++----- codex-rs/linux-sandbox/src/proxy_routing.rs | 11 +++++- .../tests/suite/managed_proxy.rs | 39 ++++++++++++++++++- codex-rs/network-proxy/src/proxy.rs | 16 ++++++++ 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index 0472625c38..1f930ff3ca 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -5,6 +5,7 @@ use std::time::Duration; #[cfg(target_os = "macos")] use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::PROXY_ATTRIBUTION_TOKEN_ENV_KEY; use codex_network_proxy::RemoteNetworkProxyConfig; use codex_network_proxy::RemoteNetworkProxyLaunchConfig; #[cfg(windows)] @@ -264,6 +265,10 @@ async fn native_request_handles_remote_proxy_config_for_platform() { let env = HashMap::from([ ("HTTP_PROXY".to_string(), stale_proxy.clone()), ("TEST_ENV".to_string(), "value".to_string()), + ( + PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(), + "foreign-token".to_string(), + ), ]); let prepared = prepare_exec_request(¶ms, env, /*runtime_paths*/ None) @@ -280,6 +285,7 @@ async fn native_request_handles_remote_proxy_config_for_platform() { let http_proxy = prepared.env.get("HTTP_PROXY").expect("HTTP proxy env"); assert_ne!(http_proxy, &stale_proxy); assert!(http_proxy.starts_with("http://127.0.0.1:")); + assert!(!prepared.env.contains_key(PROXY_ATTRIBUTION_TOKEN_ENV_KEY)); let proxy_addr: SocketAddr = http_proxy .strip_prefix("http://") .expect("HTTP proxy scheme") diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 75a734ee10..776db13ea1 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -167,7 +167,7 @@ pub fn run_main() -> ! { ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_legacy_landlock); let EffectivePermissions { permission_profile, - file_system_sandbox_policy, + mut file_system_sandbox_policy, network_sandbox_policy, } = resolve_permission_profile(permission_profile).unwrap_or_else(|err| panic!("{err}")); ensure_legacy_landlock_mode_supports_policy( @@ -218,14 +218,17 @@ pub fn run_main() -> ! { // Outer stage: bubblewrap first, then re-enter this binary in the // sandboxed environment to apply seccomp. This path never falls back // to legacy Landlock on failure. - let proxy_route_spec = - if allow_network_for_proxy { - Some(prepare_host_proxy_route_spec().unwrap_or_else(|err| { - panic!("failed to prepare host proxy routing bridge: {err}") - })) - } else { - None - }; + let proxy_route_spec = if allow_network_for_proxy { + let (proxy_route_spec, socket_dir) = prepare_host_proxy_route_spec() + .unwrap_or_else(|err| panic!("failed to prepare host proxy routing bridge: {err}")); + file_system_sandbox_policy = file_system_sandbox_policy.with_additional_readable_roots( + &sandbox_policy_cwd, + std::slice::from_ref(&socket_dir), + ); + Some(proxy_route_spec) + } else { + None + }; let inner = build_inner_seccomp_command(InnerSeccompCommandArgs { sandbox_policy_cwd: &sandbox_policy_cwd, command_cwd: command_cwd.as_deref(), diff --git a/codex-rs/linux-sandbox/src/proxy_routing.rs b/codex-rs/linux-sandbox/src/proxy_routing.rs index 707cbaafbe..57682aba81 100644 --- a/codex-rs/linux-sandbox/src/proxy_routing.rs +++ b/codex-rs/linux-sandbox/src/proxy_routing.rs @@ -1,5 +1,6 @@ use codex_network_proxy::PROXY_ATTRIBUTION_TOKEN_ENV_KEY; use codex_network_proxy::write_attribution_frame; +use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; @@ -29,6 +30,8 @@ use url::Url; const PROXY_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", + "WS_PROXY", + "WSS_PROXY", "ALL_PROXY", "FTP_PROXY", "YARN_HTTP_PROXY", @@ -72,7 +75,7 @@ struct ProxyRoutePlan { has_proxy_config: bool, } -pub(crate) fn prepare_host_proxy_route_spec() -> io::Result { +pub(crate) fn prepare_host_proxy_route_spec() -> io::Result<(String, AbsolutePathBuf)> { let (attribution_token, plan) = extract_attribution_token_and_plan(std::env::vars().collect()); // SAFETY: the sandbox helper is single-threaded here, before it forks bridge workers or // executes the user command. @@ -93,6 +96,7 @@ pub(crate) fn prepare_host_proxy_route_spec() -> io::Result { let _ = cleanup_stale_proxy_socket_dirs_in(socket_parent_dir.as_path()); let socket_dir = create_proxy_socket_dir()?; + let readable_socket_dir = AbsolutePathBuf::relative_to_current_dir(&socket_dir)?; let mut socket_by_endpoint: BTreeMap = BTreeMap::new(); let mut next_index = 0usize; for route in &plan.routes { @@ -128,7 +132,8 @@ pub(crate) fn prepare_host_proxy_route_spec() -> io::Result { }); } - serde_json::to_string(&ProxyRouteSpec { routes }).map_err(io::Error::other) + let spec = serde_json::to_string(&ProxyRouteSpec { routes }).map_err(io::Error::other)?; + Ok((spec, readable_socket_dir)) } fn extract_attribution_token_and_plan( @@ -732,6 +737,8 @@ mod tests { fn recognizes_proxy_env_keys_case_insensitively() { assert_eq!(is_proxy_env_key("HTTP_PROXY"), true); assert_eq!(is_proxy_env_key("http_proxy"), true); + assert_eq!(is_proxy_env_key("WS_PROXY"), true); + assert_eq!(is_proxy_env_key("wss_proxy"), true); assert_eq!(is_proxy_env_key("PATH"), false); } diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index 6c9a6a0c67..d009af4a79 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -4,6 +4,13 @@ use codex_core::exec_env::create_env; use codex_protocol::config_types::ShellEnvironmentPolicy; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::io::Read; @@ -28,6 +35,8 @@ const MANAGED_PROXY_PERMISSION_ERR_SNIPPETS: &[&str] = &[ const PROXY_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", + "WS_PROXY", + "WSS_PROXY", "ALL_PROXY", "FTP_PROXY", "YARN_HTTP_PROXY", @@ -209,14 +218,40 @@ async fn managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress() { "HTTP_PROXY".to_string(), format!("http://127.0.0.1:{proxy_port}"), ); + env.insert( + "WSS_PROXY".to_string(), + format!("http://127.0.0.1:{proxy_port}"), + ); + + let sandbox_helper_dir = std::path::Path::new(env!("CARGO_BIN_EXE_codex-linux-sandbox")) + .parent() + .expect("sandbox helper should have a parent"); + let file_system_sandbox_policy = + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Minimal, + }, + access: FileSystemAccessMode::Read, + missing_path_behavior: None, + }]) + .with_additional_readable_roots( + std::env::current_dir() + .expect("current directory should exist") + .as_path(), + &[AbsolutePathBuf::try_from(sandbox_helper_dir).expect("absolute helper dir")], + ); + let permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ); let routed_output = run_linux_sandbox_direct( &[ "bash", "-c", - "proxy=\"${HTTP_PROXY#*://}\"; host=\"${proxy%%:*}\"; port=\"${proxy##*:}\"; exec 3<>/dev/tcp/${host}/${port}; printf 'GET http://example.com/ HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n' >&3; IFS= read -r line <&3; printf '%s\\n' \"$line\"", + "proxy=\"${WSS_PROXY#*://}\"; host=\"${proxy%%:*}\"; port=\"${proxy##*:}\"; exec 3<>/dev/tcp/${host}/${port}; printf 'GET http://example.com/ HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n' >&3; IFS= read -r line <&3; printf '%s\\n' \"$line\"", ], - &PermissionProfile::Disabled, + &permission_profile, /*allow_network_for_proxy*/ true, env.clone(), NETWORK_TIMEOUT_MS, diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index a100311d17..610187bdd4 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -902,6 +902,8 @@ impl NetworkProxy { PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(), execution_scope.attribution_token.clone(), ); + } else { + env.remove(PROXY_ATTRIBUTION_TOKEN_ENV_KEY); } let expose_socks_port = self.socks_enabled; #[cfg(target_os = "windows")] @@ -1712,9 +1714,23 @@ mod tests { let scoped = proxy.for_execution("remote-env", "execution-1", "token-1".to_string())?; let launch = scoped.remote_launch_config().await?; + let prepared = scoped.prepare_for_optional_environment( + HashMap::from([( + PROXY_ATTRIBUTION_TOKEN_ENV_KEY.to_string(), + "foreign-token".to_string(), + )]), + /*environment_id*/ None, + )?; assert_eq!(launch.environment_id.as_deref(), Some("remote-env")); assert_eq!(launch.execution_id.as_deref(), Some("execution-1")); + assert_eq!( + prepared + .env + .get(PROXY_ATTRIBUTION_TOKEN_ENV_KEY) + .map(String::as_str), + Some("token-1") + ); Ok(()) }