Add bounded environment transport for MXC launch requests (#44626)

## Why

Sandbox policies can exceed Windows command-line limits. Provide an environment transport for these larger MXC launch requests.

## What changed

- Make `MxcCommand` serializable and add JSON encoding and decoding through `CODEX_MXC_LAUNCH_*` environment variables.
- Split payloads into UTF-8-safe chunks of at most 4,096 bytes, with a 1,000,000-byte payload limit and at most 256 chunks.
- Reject malformed or duplicate transport entries and remove all transport variables during decoding, including on failure. Preserve unrelated environment variables.

## Testing

Add tests for large Unicode and escaped-argument round trips, stale variable removal, malformed transport rejection and cleanup, and oversized payload rejection without modifying the environment.

GitOrigin-RevId: 0e5ae2535ef313985a95eb8a5fd4884974dfb1eb
This commit is contained in:
iceweasel-oai
2026-09-10 19:29:54 +00:00
committed by copyberry
parent 9c9451131f
commit 4150a2c205
5 changed files with 208 additions and 0 deletions
+2
View File
@@ -4076,6 +4076,8 @@ dependencies = [
"codex-windows-sandbox",
"learning_mode_windows",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.18",
"tracelogging",
+2
View File
@@ -20,6 +20,8 @@ codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-windows-sandbox = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
wxc_common = { workspace = true }
+5
View File
@@ -7,6 +7,11 @@ use codex_protocol::models::PermissionProfile;
use std::path::PathBuf;
/// Typed inputs for the native policy adapter.
pub mod transport;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize)]
pub struct MxcCommand {
pub permissions: PermissionProfile,
pub sandbox_policy_cwd: PathBuf,
+118
View File
@@ -0,0 +1,118 @@
//! Bounded launcher-only environment transport for policies too large for
//! Windows command lines. Every transport variable is removed before spawn.
use std::collections::HashMap;
#[cfg(any(windows, test))]
use anyhow::Context;
use anyhow::Result;
use anyhow::ensure;
use crate::MxcCommand;
const PREFIX: &str = "CODEX_MXC_LAUNCH_";
const COUNT: &str = "CODEX_MXC_LAUNCH_COUNT";
const LENGTH: &str = "CODEX_MXC_LAUNCH_BYTES";
const CHUNK_BYTES: usize = 4096;
const MAX_CHUNKS: usize = 256;
const MAX_BYTES: usize = 1_000_000;
fn is_transport_key(key: &str) -> bool {
key.as_bytes()
.get(..PREFIX.len())
.is_some_and(|start| start.eq_ignore_ascii_case(PREFIX.as_bytes()))
}
pub fn encode(command: &MxcCommand, env: &mut HashMap<String, String>) -> Result<()> {
let payload = serde_json::to_string(command)?;
ensure!(
payload.len() <= MAX_BYTES,
"MXC launcher payload exceeds {MAX_BYTES} bytes"
);
let mut chunks = Vec::new();
let mut start = 0;
while start < payload.len() {
let mut end = (start + CHUNK_BYTES).min(payload.len());
while !payload.is_char_boundary(end) {
end -= 1;
}
chunks.push(payload[start..end].to_owned());
start = end;
}
ensure!(
chunks.len() <= MAX_CHUNKS,
"MXC launcher payload has too many chunks"
);
env.retain(|key, _| !is_transport_key(key));
env.insert(COUNT.to_owned(), chunks.len().to_string());
env.insert(LENGTH.to_owned(), payload.len().to_string());
for (index, chunk) in chunks.into_iter().enumerate() {
env.insert(format!("{PREFIX}{index}"), chunk);
}
Ok(())
}
#[cfg(any(windows, test))]
pub fn decode(env: &mut HashMap<String, String>) -> Result<MxcCommand> {
let mut encoded = HashMap::new();
let mut duplicate = false;
let mut oversized = false;
env.retain(|key, value| {
if !is_transport_key(key) {
return true;
}
if encoded.len() >= MAX_CHUNKS + 2
|| key.len() > PREFIX.len() + 32
|| value.len() > CHUNK_BYTES
{
oversized = true;
return false;
}
duplicate |= encoded
.insert(key.to_ascii_uppercase(), std::mem::take(value))
.is_some();
false
});
ensure!(!oversized, "oversized MXC launcher environment");
ensure!(!duplicate, "duplicate MXC launcher environment variables");
let count: usize = encoded
.get(COUNT)
.context("missing MXC launcher chunk count")?
.parse()?;
let length: usize = encoded
.get(LENGTH)
.context("missing MXC launcher payload length")?
.parse()?;
ensure!(
(1..=MAX_CHUNKS).contains(&count) && length <= MAX_BYTES,
"invalid MXC launcher payload size"
);
ensure!(
encoded.len() == count + 2,
"unexpected MXC launcher environment variables"
);
let mut payload = String::with_capacity(length);
for index in 0..count {
let chunk = encoded
.get(&format!("{PREFIX}{index}"))
.context("missing MXC launcher chunk")?;
ensure!(
!chunk.is_empty() && chunk.len() <= CHUNK_BYTES,
"invalid MXC launcher chunk size"
);
ensure!(
payload.len() + chunk.len() <= length,
"MXC launcher payload length mismatch"
);
payload.push_str(chunk);
}
ensure!(
payload.len() == length,
"MXC launcher payload length mismatch"
);
serde_json::from_str(&payload).context("invalid MXC launcher request")
}
#[cfg(test)]
#[path = "transport_tests.rs"]
mod tests;
@@ -0,0 +1,81 @@
//! Verify bounded transport, Unicode roundtrips, and child-environment cleanup.
use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use pretty_assertions::assert_eq;
use crate::MxcCommand;
use super::CHUNK_BYTES;
use super::COUNT;
use super::LENGTH;
use super::MAX_BYTES;
use super::PREFIX;
use super::decode;
use super::encode;
fn command(args: Vec<String>) -> MxcCommand {
MxcCommand {
permissions: PermissionProfile::read_only(),
sandbox_policy_cwd: PathBuf::from("workspace"),
command: args,
}
}
#[test]
fn large_unicode_launch_roundtrips_and_never_reaches_child_environment() -> Result<()> {
let args = vec![
"codex-windows-mxc".to_owned(),
"🧊".repeat(20_000),
"a\"\\b".to_owned(),
];
let expected_env = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
let mut env = expected_env.clone();
env.insert("codex_mxc_launch_999".to_owned(), "spoofed".to_owned());
let request = command(args);
encode(&request, &mut env)?;
assert!(!env.contains_key("codex_mxc_launch_999"));
assert!(
env.values()
.all(|value| value.encode_utf16().count() < 8192)
);
assert_eq!(
serde_json::to_value(decode(&mut env)?)?,
serde_json::to_value(request)?
);
assert_eq!(env, expected_env);
Ok(())
}
#[test]
fn malformed_transport_is_rejected_and_removed() -> Result<()> {
let expected_env = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
let mut valid = expected_env.clone();
encode(&command(vec!["program.exe".to_owned()]), &mut valid)?;
for (key, value) in [
(COUNT.to_owned(), "257".to_owned()),
(LENGTH.to_owned(), "0".to_owned()),
(format!("{PREFIX}0"), "x".repeat(CHUNK_BYTES + 1)),
(COUNT.to_ascii_lowercase(), "1".to_owned()),
] {
let mut env = valid.clone();
env.insert(key, value);
assert!(decode(&mut env).is_err());
assert_eq!(env, expected_env);
}
valid.remove(&format!("{PREFIX}0"));
assert!(decode(&mut valid).is_err());
assert_eq!(valid, expected_env);
Ok(())
}
#[test]
fn oversized_payload_fails_before_modifying_launcher_environment() {
let expected = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
let mut env = expected.clone();
assert!(encode(&command(vec!["x".repeat(MAX_BYTES)]), &mut env).is_err());
assert_eq!(env, expected);
}