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:
iceweasel-oai
2026-09-11 17:09:23 +00:00
committed by copyberry
parent 68bc5369ba
commit 2c9e1a5775
7 changed files with 274 additions and 4 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -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",

View File

@@ -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 }

View 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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()?;

View File

@@ -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,
}
}