mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
Add managed network policy support to the Windows MXC sandbox (#44872)
## What changed Add `create_command_args()` to encode argv, permissions, policy cwd, and optional managed network context through the existing bounded environment transport. For managed networking, generate a policy that allows IPv4 and IPv6 loopback while denying direct non-loopback egress and general inbound access. Require nonempty, nonzero proxy ports and reject `allow_local_binding=false` at both launcher and policy boundaries because native host-loopback access is bidirectional. Document the MXC launch contract, platform requirements, and limitations. ## Testing Add portable tests covering managed network transport and policy translation, plus rejection of missing proxy ports, zero ports, and unsupported local-binding restrictions at both boundaries. GitOrigin-RevId: 8d31b98f94a2ba3769369aa50de8e53dc143bac4
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -4070,6 +4070,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"appcontainer_common",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path-uri",
|
||||
|
||||
@@ -16,6 +16,7 @@ ignored = ["tracelogging"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
|
||||
75
codex-rs/mxc-sandbox/README.md
Normal file
75
codex-rs/mxc-sandbox/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Native Windows MXC sandbox
|
||||
|
||||
This crate routes a command through the current Codex executable and directly
|
||||
into Microsoft's MXC `BaseContainerRunner`. It requires a working Windows
|
||||
process security environment (PSEC). It never invokes MXC's AppContainer
|
||||
dispatcher, edits host ACLs, creates sandbox users, runs setup, or requests
|
||||
elevation. The existing Codex Windows sandboxes remain separate backends.
|
||||
|
||||
Windows executors record `codex.windows_mxc.available` once per process with an
|
||||
`available=true|false` tag. This measures capability independently of selection.
|
||||
The exec-server handshake also exposes `windows_mxc_available`; older or
|
||||
unsupported Windows executors reject MXC requests before execution.
|
||||
|
||||
`is_available()` uses MXC's cached create/close probe, rather than an OS build
|
||||
number or the SDK's broad `platform_support()` result. The latter also reports
|
||||
older AppContainer backends as supported. A requested deny path additionally
|
||||
requires the native `PSE_SUPPORT_FS_DENY` capability; otherwise the command
|
||||
fails before launch.
|
||||
|
||||
The wrapper inherits the command's pipes or ConPTY console. MXC creates its
|
||||
child suspended, assigns a kill-on-close job before resuming it, and retains
|
||||
the native policy through workload completion. Filesystem permissions come
|
||||
from the canonical Codex permission profile, including protected metadata
|
||||
carveouts. Supported managed network access allows IPv4 and IPv6 loopback
|
||||
clients and servers, including the dedicated proxy listeners, while denying
|
||||
direct non-loopback egress and general inbound network access.
|
||||
|
||||
## Launch contract
|
||||
|
||||
`create_command_args()` wraps the command like the Seatbelt backend, using the
|
||||
executor's Codex executable as the native SDK helper. It carries one typed
|
||||
request: the canonical `PermissionProfile`, policy cwd, proxy context, and exact
|
||||
argv. The helper inherits command cwd, environment, and stdio from the shared
|
||||
process path; policy cwd can differ from command cwd.
|
||||
|
||||
The request uses launcher-only environment chunks to leave Windows' command-line
|
||||
budget to the workload. Payloads are limited to 1,000,000 UTF-8 bytes and chunks
|
||||
to 4096 bytes; the helper removes them before native process creation. Use
|
||||
`codex sandbox windows` to debug through the same preparation path as execution.
|
||||
|
||||
## Limits and Windows validation
|
||||
|
||||
- Deny globs use the existing Windows sandbox resolver to expand matching files
|
||||
and directories into concrete paths before launch. This has the same snapshot
|
||||
semantics and scan limits as the existing Windows sandbox.
|
||||
- Native deny paths depend on the host's capability probe. An installed Windows
|
||||
update alone is not treated as evidence that every policy feature is enabled.
|
||||
- This adapter rejects managed networking with `allow_local_binding=false`:
|
||||
its host-loopback permission is bidirectional. MXC's proxy-peer identity mode
|
||||
is not integrated here. With local binding enabled, direct DNS remains denied,
|
||||
matching the existing Windows sandbox.
|
||||
- Windows volume-root grants do not recurse. The adapter grants the root and
|
||||
its immediate children; directories added or newly mounted during a running
|
||||
command are not implicitly granted.
|
||||
- The native API represents paths and environment values as Unicode strings.
|
||||
Non-Unicode values fail instead of undergoing lossy conversion.
|
||||
- An explicitly empty child environment is rejected: the SDK replaces an empty
|
||||
environment list with profile defaults and has no explicit-empty option.
|
||||
- The upstream runner terminates remaining descendants when the foreground
|
||||
process exits, as well as on cancellation. Both existing Windows backends
|
||||
preserve descendants after normal exit, so detached servers currently lose
|
||||
that behavior under MXC. Retaining descendants safely requires a longer-lived
|
||||
owner for the native policy and job.
|
||||
- The command itself remains subject to Windows' command-line length limit.
|
||||
- Portable tests validate policy translation and wrapper arguments. Actual
|
||||
enforcement, nested access overrides, alternate path encodings, junctions
|
||||
and hardlinks, protected metadata, ConPTY behavior, process-tree cancellation,
|
||||
proxy isolation, and
|
||||
comparison with the existing Windows and Unix sandboxes require the smoke
|
||||
suite on supported Windows and the corresponding platform hosts.
|
||||
Test the normal `powershell.exe` and `pwsh.exe` command paths, not only `cmd.exe`.
|
||||
|
||||
The MXC git revision is pinned with the workspace dependencies. Native launch
|
||||
errors are returned without dumping the SDK's diagnostic buffer, which may
|
||||
contain command or environment data.
|
||||
@@ -3,7 +3,13 @@
|
||||
#[cfg(windows)]
|
||||
pub mod native;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Typed inputs for the native policy adapter.
|
||||
@@ -11,15 +17,74 @@ pub mod transport;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const CODEX_WINDOWS_MXC_ARG1: &str = "--__codex-windows-mxc";
|
||||
|
||||
const CLIENT_ONLY_LOOPBACK_UNSUPPORTED: &str = "MXC cannot enforce managed networking with allow_local_binding=false: native host-loopback access is bidirectional";
|
||||
|
||||
fn validate_managed_network(network: &ManagedNetworkSandboxContext) -> Result<()> {
|
||||
ensure!(
|
||||
!network.loopback_ports.is_empty(),
|
||||
"MXC managed networking requires dedicated proxy ports"
|
||||
);
|
||||
ensure!(
|
||||
!network.loopback_ports.contains(&0),
|
||||
"MXC proxy ports must be nonzero"
|
||||
);
|
||||
ensure!(
|
||||
network.allow_local_binding,
|
||||
CLIENT_ONLY_LOOPBACK_UNSUPPORTED
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MxcCommand {
|
||||
pub permissions: PermissionProfile,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
pub mod policy;
|
||||
|
||||
/// Inputs used to build an MXC helper invocation.
|
||||
#[derive(Debug)]
|
||||
pub struct CreateMxcCommandArgsParams<'a> {
|
||||
pub command: Vec<String>,
|
||||
pub permission_profile: &'a PermissionProfile,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
pub managed_network: Option<&'a ManagedNetworkSandboxContext>,
|
||||
pub env: &'a mut HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Wrap exact argv and add bounded launcher-only environment variables. The
|
||||
/// helper removes these variables before starting the sandboxed command.
|
||||
pub fn create_command_args(args: CreateMxcCommandArgsParams<'_>) -> Result<Vec<String>> {
|
||||
let CreateMxcCommandArgsParams {
|
||||
command,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
managed_network,
|
||||
env,
|
||||
} = args;
|
||||
sandbox_policy_cwd
|
||||
.to_str()
|
||||
.context("MXC requires a Unicode policy working directory")?;
|
||||
if let Some(network) = managed_network {
|
||||
validate_managed_network(network)?;
|
||||
}
|
||||
transport::encode(
|
||||
&MxcCommand {
|
||||
permissions: permission_profile.clone(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_owned(),
|
||||
managed_network: managed_network.cloned(),
|
||||
command,
|
||||
},
|
||||
env,
|
||||
)?;
|
||||
Ok(vec![CODEX_WINDOWS_MXC_ARG1.to_owned()])
|
||||
}
|
||||
|
||||
/// Whether the executor can create a native MXC process security environment.
|
||||
/// This deliberately excludes MXC's older AppContainer fallback backends.
|
||||
pub fn is_available() -> bool {
|
||||
|
||||
@@ -25,9 +25,12 @@ use wxc_common::models::ContainerPolicy;
|
||||
use wxc_common::models::ExecutionRequest;
|
||||
use wxc_common::models::FallbackPolicy;
|
||||
use wxc_common::models::NetworkAction;
|
||||
use wxc_common::models::NetworkCidr;
|
||||
use wxc_common::models::NetworkEgressPolicy;
|
||||
use wxc_common::models::NetworkIngressPolicy;
|
||||
use wxc_common::models::NetworkPeer;
|
||||
use wxc_common::models::NetworkPolicy;
|
||||
use wxc_common::models::NetworkRule;
|
||||
|
||||
use crate::MxcCommand;
|
||||
|
||||
@@ -209,20 +212,49 @@ pub fn build_request(
|
||||
// API receives one effective access mode for each path identity.
|
||||
read.retain(|key, _| !write.contains_key(key) && !deny.contains_key(key));
|
||||
let network_enabled = permissions.network_sandbox_policy().is_enabled();
|
||||
let egress_default = if network_enabled {
|
||||
let proxied = command.managed_network.is_some();
|
||||
if let Some(network) = &command.managed_network {
|
||||
crate::validate_managed_network(network)
|
||||
.map_err(|error| PolicyError::PolicyResolution(error.to_string()))?;
|
||||
}
|
||||
let egress_default = if network_enabled && !proxied {
|
||||
NetworkAction::Allow
|
||||
} else {
|
||||
NetworkAction::Deny
|
||||
};
|
||||
let ingress_default = if network_enabled {
|
||||
let ingress_default = if network_enabled && !proxied {
|
||||
NetworkAction::Allow
|
||||
} else {
|
||||
NetworkAction::Deny
|
||||
};
|
||||
let egress = NetworkEgressPolicy {
|
||||
let mut egress = NetworkEgressPolicy {
|
||||
default: egress_default,
|
||||
..Default::default()
|
||||
};
|
||||
if proxied {
|
||||
// PSEC host loopback is bidirectional. This shape is supported only
|
||||
// when the caller already allows local clients and servers. Keep
|
||||
// private-network ingress denied and allow no direct DNS bypass.
|
||||
egress.allow.push(NetworkRule {
|
||||
to: vec![
|
||||
NetworkPeer {
|
||||
cidr: NetworkCidr {
|
||||
address: std::net::Ipv4Addr::new(127, 0, 0, 0).into(),
|
||||
prefix_length: 8,
|
||||
},
|
||||
except: Vec::new(),
|
||||
},
|
||||
NetworkPeer {
|
||||
cidr: NetworkCidr {
|
||||
address: std::net::Ipv6Addr::LOCALHOST.into(),
|
||||
prefix_length: 128,
|
||||
},
|
||||
except: Vec::new(),
|
||||
},
|
||||
],
|
||||
ports: Vec::new(),
|
||||
});
|
||||
}
|
||||
let mut request = ExecutionRequest {
|
||||
script_code: cmdline_from_argv_for_context(
|
||||
&command.command,
|
||||
@@ -250,7 +282,7 @@ pub fn build_request(
|
||||
network_egress: Some(egress),
|
||||
network_ingress: Some(NetworkIngressPolicy {
|
||||
default: ingress_default,
|
||||
host_loopback: if network_enabled {
|
||||
host_loopback: if network_enabled || proxied {
|
||||
NetworkAction::Allow
|
||||
} else {
|
||||
NetworkAction::Deny
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Portable policy and launcher regressions, with Windows identity coverage.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::protocol::FileSystemAccessMode;
|
||||
@@ -15,8 +17,16 @@ use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use wxc_common::models::NetworkAction;
|
||||
use wxc_common::models::NetworkCidr;
|
||||
use wxc_common::models::NetworkEgressPolicy;
|
||||
use wxc_common::models::NetworkIngressPolicy;
|
||||
use wxc_common::models::NetworkPeer;
|
||||
use wxc_common::models::NetworkRule;
|
||||
|
||||
use crate::CreateMxcCommandArgsParams;
|
||||
use crate::MxcCommand;
|
||||
use crate::create_command_args;
|
||||
use crate::policy::PolicyError;
|
||||
use crate::policy::build_request;
|
||||
use crate::policy::materialize_volume_roots;
|
||||
@@ -91,6 +101,7 @@ fn command(permissions: &PermissionProfile, cwd: &Path) -> MxcCommand {
|
||||
MxcCommand {
|
||||
permissions: permissions.clone(),
|
||||
sandbox_policy_cwd: cwd.to_owned(),
|
||||
managed_network: None,
|
||||
command: vec!["program.exe".to_owned(), "--arg".to_owned()],
|
||||
}
|
||||
}
|
||||
@@ -346,6 +357,90 @@ fn full_access_enumerates_children_of_every_volume() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_network_allows_authorized_loopback_without_lan_or_dns_access() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let profile = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(Vec::new()),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
let proxy = ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 43124],
|
||||
allow_local_binding: true,
|
||||
};
|
||||
let mut env = HashMap::new();
|
||||
create_command_args(CreateMxcCommandArgsParams {
|
||||
command: vec!["program.exe".to_owned()],
|
||||
permission_profile: &profile,
|
||||
sandbox_policy_cwd: root.path(),
|
||||
managed_network: Some(&proxy),
|
||||
env: &mut env,
|
||||
})?;
|
||||
let parsed = crate::transport::decode(&mut env)?;
|
||||
let request = build_request(&parsed, root.path(), Vec::new(), &[], &[])?;
|
||||
assert_eq!(
|
||||
request.policy.network_egress,
|
||||
Some(NetworkEgressPolicy {
|
||||
default: NetworkAction::Deny,
|
||||
allow: vec![NetworkRule {
|
||||
to: vec![
|
||||
NetworkPeer {
|
||||
cidr: "127.0.0.0/8".parse()?,
|
||||
except: Vec::new()
|
||||
},
|
||||
NetworkPeer {
|
||||
cidr: NetworkCidr {
|
||||
address: std::net::Ipv6Addr::LOCALHOST.into(),
|
||||
prefix_length: 128
|
||||
},
|
||||
except: Vec::new()
|
||||
},
|
||||
],
|
||||
ports: Vec::new(),
|
||||
}],
|
||||
deny: Vec::new(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
request.policy.network_ingress,
|
||||
Some(NetworkIngressPolicy {
|
||||
default: NetworkAction::Deny,
|
||||
host_loopback: NetworkAction::Allow,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_managed_network_is_rejected_at_both_boundaries() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let cwd = root.path();
|
||||
let profile = PermissionProfile::read_only();
|
||||
for (loopback_ports, allow_local_binding) in
|
||||
[(Vec::new(), true), (vec![0], true), (vec![43123], false)]
|
||||
{
|
||||
let proxy = ManagedNetworkSandboxContext {
|
||||
loopback_ports,
|
||||
allow_local_binding,
|
||||
};
|
||||
let mut env = HashMap::new();
|
||||
assert!(
|
||||
create_command_args(CreateMxcCommandArgsParams {
|
||||
command: Vec::new(),
|
||||
permission_profile: &profile,
|
||||
sandbox_policy_cwd: cwd,
|
||||
managed_network: Some(&proxy),
|
||||
env: &mut env,
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let mut parsed = command(&profile, cwd);
|
||||
parsed.managed_network = Some(proxy);
|
||||
assert!(build_request(&parsed, cwd, Vec::new(), &[], &[]).is_err());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deny_globs_expand_files_and_directories_before_launch() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
|
||||
@@ -21,6 +21,7 @@ fn command(args: Vec<String>) -> MxcCommand {
|
||||
MxcCommand {
|
||||
permissions: PermissionProfile::read_only(),
|
||||
sandbox_policy_cwd: PathBuf::from("workspace"),
|
||||
managed_network: None,
|
||||
command: args,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user