mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Honor prepared Unix socket permissions in Seatbelt (#45548)
## Why Seatbelt ignored Unix socket permissions in `ManagedNetworkSandboxContext`, which could omit allowed sockets or inherit a live proxy's broader permissions. ## What changed Use the prepared context's `allow_unix_sockets` and `dangerously_allow_all_unix_sockets` settings when present, falling back to the live proxy only when no prepared context exists. Normalize allowlisted paths and preserve explicit extra socket allowances. ## Testing Add regression coverage for prepared-policy precedence, empty allowlists, invalid relative paths, and explicit extra allowances. Extend macOS exec-server tests to cover prepared and executor-local proxy socket permissions, including explicit allow-all behavior without unrestricted network access. GitOrigin-RevId: 3372e1f2f1b161e60bde585db8d70b4e3a0b1854
This commit is contained in:
@@ -9,6 +9,10 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkPolicyAuditObserver;
|
||||
use codex_network_proxy::NetworkPolicyDecider;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::NetworkUnixSocketPermission;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::NetworkUnixSocketPermissions;
|
||||
use codex_network_proxy::PROXY_ATTRIBUTION_TOKEN_ENV_KEY;
|
||||
use codex_network_proxy::RemoteNetworkProxyConfig;
|
||||
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
|
||||
@@ -210,8 +214,7 @@ async fn sandbox_request_routes_custom_arg0_to_inner_helper() {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
fn managed_network_sandbox_request() -> (ExecParams, ExecServerRuntimePaths) {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current directory")
|
||||
.try_into()
|
||||
@@ -237,13 +240,38 @@ async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
..Default::default()
|
||||
}),
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
};
|
||||
(params, runtime_paths)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn seatbelt_policy_arg(command: &[String]) -> &str {
|
||||
command
|
||||
.windows(2)
|
||||
.find_map(|args| (args[0] == "-p").then_some(args[1].as_str()))
|
||||
.expect("Seatbelt policy argument")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_preserves_prepared_managed_network_policy() {
|
||||
let (mut params, runtime_paths) = managed_network_sandbox_request();
|
||||
let socket_dir = tempfile::tempdir().expect("temporary socket directory");
|
||||
let allowed_socket = socket_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("canonical socket directory")
|
||||
.join("allowed.sock")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
params.managed_network = Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
allow_unix_sockets: vec![allowed_socket.clone()],
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
});
|
||||
|
||||
let prepared = prepare_exec_request(
|
||||
¶ms,
|
||||
@@ -254,13 +282,151 @@ async fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
)
|
||||
.await
|
||||
.expect("prepare managed-network sandbox request");
|
||||
let policy = prepared
|
||||
let policy = seatbelt_policy_arg(&prepared.command);
|
||||
let unix_socket_definitions = prepared
|
||||
.command
|
||||
.windows(2)
|
||||
.find_map(|args| (args[0] == "-p").then_some(args[1].as_str()))
|
||||
.expect("Seatbelt policy argument");
|
||||
.iter()
|
||||
.filter(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
|
||||
assert!(policy.contains("(allow system-socket (socket-domain AF_UNIX))"));
|
||||
assert!(policy.contains(
|
||||
"(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
|
||||
));
|
||||
assert_eq!(
|
||||
unix_socket_definitions,
|
||||
vec![format!("-DUNIX_SOCKET_PATH_0={allowed_socket}")]
|
||||
);
|
||||
assert!(!policy.contains("(allow network-outbound (remote unix-socket))"));
|
||||
assert!(!policy.contains("(allow network-outbound)\n"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_only_allows_all_unix_sockets_when_configured() {
|
||||
let (mut params, runtime_paths) = managed_network_sandbox_request();
|
||||
params.managed_network = Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
..ManagedNetworkSandboxContext::default()
|
||||
});
|
||||
|
||||
for allow_all in [false, true] {
|
||||
params
|
||||
.managed_network
|
||||
.as_mut()
|
||||
.expect("managed network context")
|
||||
.dangerously_allow_all_unix_sockets = allow_all;
|
||||
let prepared = prepare_exec_request(
|
||||
¶ms,
|
||||
HashMap::new(),
|
||||
Some(&runtime_paths),
|
||||
/*network_policy_decider*/ None,
|
||||
/*network_policy_audit_observer*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("prepare managed-network sandbox request");
|
||||
let policy = seatbelt_policy_arg(&prepared.command);
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
policy.contains("(allow system-socket (socket-domain AF_UNIX))"),
|
||||
policy.contains("(allow network-bind (local unix-socket))"),
|
||||
policy.contains("(allow network-outbound (remote unix-socket))"),
|
||||
),
|
||||
(allow_all, allow_all, allow_all)
|
||||
);
|
||||
assert!(!policy.contains("(allow network-outbound)\n"));
|
||||
assert!(
|
||||
!prepared
|
||||
.command
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn sandbox_request_preserves_executor_local_proxy_unix_socket_policy() {
|
||||
let (mut params, runtime_paths) = managed_network_sandbox_request();
|
||||
let socket_dir = tempfile::tempdir().expect("temporary socket directory");
|
||||
let socket_root = socket_dir
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("canonical socket directory");
|
||||
let allowed_socket = socket_root
|
||||
.join("allowed.sock")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let denied_socket = socket_root
|
||||
.join("denied.sock")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
enable_socks5: false,
|
||||
unix_sockets: Some(NetworkUnixSocketPermissions {
|
||||
entries: [
|
||||
(allowed_socket.clone(), NetworkUnixSocketPermission::Allow),
|
||||
(denied_socket, NetworkUnixSocketPermission::Deny),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
params.network_proxy = Some(RemoteNetworkProxyLaunchConfig::new(
|
||||
RemoteNetworkProxyConfig::from_effective_config(&config)
|
||||
.expect("supported remote proxy config"),
|
||||
));
|
||||
|
||||
let prepared = prepare_exec_request(
|
||||
¶ms,
|
||||
HashMap::new(),
|
||||
Some(&runtime_paths),
|
||||
/*network_policy_decider*/ None,
|
||||
/*network_policy_audit_observer*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("prepare sandbox request with executor-local proxy");
|
||||
let policy = seatbelt_policy_arg(&prepared.command);
|
||||
let proxy_addr: SocketAddr = prepared
|
||||
.env
|
||||
.get("HTTP_PROXY")
|
||||
.expect("HTTP proxy env")
|
||||
.strip_prefix("http://")
|
||||
.expect("HTTP proxy scheme")
|
||||
.parse()
|
||||
.expect("HTTP proxy address");
|
||||
let proxy_port = proxy_addr.port();
|
||||
let unix_socket_definitions = prepared
|
||||
.command
|
||||
.iter()
|
||||
.filter(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(policy.contains(&format!(
|
||||
"(allow network-outbound (remote ip \"localhost:{proxy_port}\"))"
|
||||
)));
|
||||
assert!(policy.contains(
|
||||
"(allow network-outbound (remote unix-socket (subpath (param \"UNIX_SOCKET_PATH_0\"))))"
|
||||
));
|
||||
assert_eq!(
|
||||
unix_socket_definitions,
|
||||
vec![format!("-DUNIX_SOCKET_PATH_0={allowed_socket}")]
|
||||
);
|
||||
assert!(!policy.contains("(allow network-outbound (remote unix-socket))"));
|
||||
assert!(!policy.contains("(allow network-outbound)\n"));
|
||||
|
||||
prepared
|
||||
.network_proxy_handle
|
||||
.expect("running executor proxy")
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("shut down executor proxy");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -130,6 +130,27 @@ impl Default for UnixDomainSocketPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl UnixDomainSocketPolicy {
|
||||
fn from_allowlist(paths: &[String], extra_allowed: Vec<AbsolutePathBuf>) -> Self {
|
||||
let mut allowed = paths
|
||||
.iter()
|
||||
.filter_map(|socket_path| {
|
||||
match normalize_path_for_sandbox(Path::new(socket_path)) {
|
||||
Some(path) => Some(path),
|
||||
None => {
|
||||
warn!(
|
||||
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
allowed.extend(extra_allowed);
|
||||
Self::Restricted { allowed }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UnixSocketPathParam {
|
||||
index: usize,
|
||||
@@ -147,30 +168,22 @@ fn proxy_policy_inputs(
|
||||
.filter_map(|socket_path| normalize_path_for_sandbox(socket_path.as_path()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let unix_domain_socket_policy = match network {
|
||||
Some(network) if network.dangerously_allow_all_unix_sockets() => {
|
||||
// Prefer this command's prepared Unix-socket policy.
|
||||
// Fall back to the live proxy only when no prepared context exists.
|
||||
let unix_domain_socket_policy = match (managed_network, network) {
|
||||
(Some(context), _) if context.dangerously_allow_all_unix_sockets => {
|
||||
UnixDomainSocketPolicy::AllowAll
|
||||
}
|
||||
Some(network) => {
|
||||
let mut allowed = network
|
||||
.allow_unix_sockets()
|
||||
.iter()
|
||||
.filter_map(|socket_path| {
|
||||
match normalize_path_for_sandbox(Path::new(socket_path)) {
|
||||
Some(path) => Some(path),
|
||||
None => {
|
||||
warn!(
|
||||
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
allowed.extend(extra_allowed);
|
||||
UnixDomainSocketPolicy::Restricted { allowed }
|
||||
(Some(context), _) => {
|
||||
UnixDomainSocketPolicy::from_allowlist(&context.allow_unix_sockets, extra_allowed)
|
||||
}
|
||||
None => UnixDomainSocketPolicy::Restricted {
|
||||
(None, Some(network)) if network.dangerously_allow_all_unix_sockets() => {
|
||||
UnixDomainSocketPolicy::AllowAll
|
||||
}
|
||||
(None, Some(network)) => {
|
||||
UnixDomainSocketPolicy::from_allowlist(&network.allow_unix_sockets(), extra_allowed)
|
||||
}
|
||||
(None, None) => UnixDomainSocketPolicy::Restricted {
|
||||
allowed: extra_allowed,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -798,6 +798,109 @@ fn prepared_managed_network_context_allows_only_its_proxy_ports() {
|
||||
assert!(!policy.contains("(allow network-outbound (remote ip \"localhost:9999\"))"));
|
||||
assert!(!policy.contains("(allow network-bind (local ip \"*:*\"))"));
|
||||
assert!(!policy.contains("(allow network-outbound)\n"));
|
||||
assert!(!policy.contains("(allow system-socket (socket-domain AF_UNIX))"));
|
||||
assert!(!policy.contains("(allow network-outbound (remote unix-socket))"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepared_managed_network_context_takes_precedence_over_live_proxy_socket_policy()
|
||||
-> anyhow::Result<()> {
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
cwd.path(),
|
||||
);
|
||||
let network_config = NetworkProxyConfig {
|
||||
enabled: true,
|
||||
mode: NetworkMode::Full,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..Default::default()
|
||||
};
|
||||
let state = build_config_state(network_config, NetworkProxyConstraints::default())?;
|
||||
let network_proxy = NetworkProxy::builder()
|
||||
.state(Arc::new(NetworkProxyState::with_reloader(
|
||||
state,
|
||||
Arc::new(TestConfigReloader),
|
||||
)))
|
||||
.managed_by_codex(/*managed_by_codex*/ false)
|
||||
.build()
|
||||
.await?;
|
||||
let prepared_socket = "/tmp/codex-prepared-use";
|
||||
let explicit_socket = "/tmp/codex-browser-use";
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_unix_sockets: vec![prepared_socket.to_string(), "relative.sock".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let extra_allow_unix_sockets = vec![absolute_path(explicit_socket)];
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/usr/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.path(),
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(&managed_network),
|
||||
environment_id: None,
|
||||
network: Some(&network_proxy),
|
||||
extra_allow_unix_sockets: &extra_allow_unix_sockets,
|
||||
})
|
||||
.expect("create seatbelt args");
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
|
||||
assert!(policy.contains("(allow system-socket (socket-domain AF_UNIX))"));
|
||||
assert!(!policy.contains("(allow network-bind (local unix-socket))"));
|
||||
assert!(!policy.contains("(allow network-outbound (remote unix-socket))"));
|
||||
let expected_explicit_socket = normalize_path_for_sandbox(Path::new(explicit_socket))
|
||||
.expect("explicit socket root should normalize");
|
||||
let expected_prepared_socket = normalize_path_for_sandbox(Path::new(prepared_socket))
|
||||
.expect("prepared socket root should normalize");
|
||||
assert_eq!(
|
||||
args.iter()
|
||||
.filter(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
format!(
|
||||
"-DUNIX_SOCKET_PATH_0={}",
|
||||
expected_explicit_socket.display()
|
||||
),
|
||||
format!(
|
||||
"-DUNIX_SOCKET_PATH_1={}",
|
||||
expected_prepared_socket.display()
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
// An empty prepared policy must not inherit the live proxy's allow-all grant.
|
||||
let managed_network = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
..Default::default()
|
||||
};
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: vec!["/usr/bin/true".to_string()],
|
||||
file_system_sandbox_policy: &file_system_policy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.path(),
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(&managed_network),
|
||||
environment_id: None,
|
||||
network: Some(&network_proxy),
|
||||
extra_allow_unix_sockets: &[],
|
||||
})
|
||||
.expect("create seatbelt args for empty prepared policy");
|
||||
|
||||
let policy = seatbelt_policy_arg(&args);
|
||||
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
|
||||
assert!(!policy.contains("(allow system-socket (socket-domain AF_UNIX))"));
|
||||
assert!(!policy.contains("(allow network-bind (local unix-socket))"));
|
||||
assert!(!policy.contains("(allow network-outbound (remote unix-socket))"));
|
||||
assert!(
|
||||
!args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("-DUNIX_SOCKET_PATH_"))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user