Add MXC sandbox integrity dependency and policy preparation (#51831)

## What changed

Add Windows MXC backend helpers to inventory the Codex executable, sandbox
launcher, and available `rg` scanner when deny globs are configured. Prepare
filesystem policies for inspection by reusing MXC temporary-directory and volume
resolution and replacing deny globs with concrete path snapshots.

Allow Windows deny-glob resolution to run its scanner with an explicit command
environment and working directory, including an empty environment. The integrity
helpers are not yet invoked by an execution runner.

## Testing

Add a backend test verifying that deny-glob snapshots replace the original
patterns, including empty snapshots, and affect `FileContentsChecker` results.

GitOrigin-RevId: 409c527663d230d76d65be3bf0a37959f694d77e
This commit is contained in:
iceweasel-oai
2026-10-07 21:16:25 +00:00
committed by copyberry
parent 11da6b9edc
commit 3342ee8c07
14 changed files with 347 additions and 43 deletions
+2
View File
@@ -3214,6 +3214,7 @@ dependencies = [
"codex-file-system",
"codex-file-watcher",
"codex-http-client",
"codex-mxc-sandbox",
"codex-network-proxy",
"codex-otel",
"codex-protocol",
@@ -3260,6 +3261,7 @@ dependencies = [
"tracing-subscriber",
"url",
"uuid",
"which 8.0.0",
"windows-sys 0.61.2",
"wiremock",
]
+2
View File
@@ -80,6 +80,8 @@ rustix = { workspace = true, features = ["fs"] }
tempfile = { workspace = true }
[target.'cfg(windows)'.dependencies]
codex-mxc-sandbox = { workspace = true }
which = { workspace = true }
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
+6
View File
@@ -41,6 +41,12 @@ mod rpc;
mod rpc_server_requests;
mod rpc_timing;
mod runtime_options;
#[cfg(target_os = "windows")]
#[expect(
dead_code,
reason = "The integrity runner is added later in this stack."
)]
mod sandbox_integrity;
mod sandbox_selection;
mod sandboxed_file_open;
mod sandboxed_file_system;
@@ -0,0 +1,48 @@
# Sandbox integrity: scope and review contract
These checks currently emit telemetry only. Findings and preparation errors must
not change sandbox construction, permissions, approvals, or command execution.
Supported backends are Seatbelt, bubblewrap, and MXC; legacy Windows is out of scope.
## Responsibilities
- Backends supply complete dependency inventories and interpret the request's
original filesystem policy: resolve command temp directories and MXC volumes,
and reuse the backend's deny-glob expansion where enforcement expands globs.
- The runner prepares dependency facts and invokes each checker per dependency.
Checkers remain independent of inventories and telemetry.
Blocking work runs on the blocking pool with a five-second caller timeout.
Timeouts use the existing check metric (`dependency=preparation`, `outcome=error`,
`error_kind=timeout`). Late results are discarded; cancelling the blocking task
or its scanner subprocess is deferred.
- Telemetry derives bounded tags from the policy, dependency facts, and checker
results. Never emit paths, environment values, or error text.
A missing metrics client disables emission, never the checks themselves.
Use the process-global metrics client; separately injected clients in programs
embedding Codex are outside this phase's scope.
## Deliberate approximation
Honor the basic read/write/deny rules. Do not reconstruct native enforcement or
audit OS ACLs. Additional backend protections can produce false positives;
additional write grants (such as Seatbelt scratch access) can produce false
negatives. Backend glob/symlink nuances and writable hard-link aliases are not
fully modeled. The separate glob scan can differ from enforcement's snapshot.
Metrics describe observed checker outcomes, not accuracy or missed violations.
Windows scanner discovery currently uses `which`, while enforcement uses
`Command`. Their search-order and environment differences can select different
executables or cause only one scan to use the internal walker. This is an accepted
telemetry limitation for this phase; changing production scanner selection is deferred.
These are accepted limitations of this phase, not requests to duplicate backend
implementations. Review against this scope. Regressions in explicit policy
handling, executor context, or reporting remain actionable; this is not a blanket
exemption for false positives or false negatives.
## Open eligibility issue
Approved escalations that retain only deny-read restrictions should be excluded.
The current full-disk-write guard does not exclude `:root = write` plus denials,
and `SandboxExecRequest` lacks the approval context to distinguish that escalation
from a configured weak policy. Do not infer approval from policy shape.
@@ -0,0 +1,67 @@
//! Exercise backend policy preparation and dependency discovery.
use super::backend;
use codex_protocol::config_types::WindowsSandboxLevel;
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::NetworkSandboxPolicy;
use codex_protocol::sandbox::SandboxOverride;
use codex_sandboxing::FileContentsChecker;
use codex_sandboxing::SandboxExecRequest;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::fs;
use std::io;
#[test]
fn snapshot_denials_replace_patterns_including_an_empty_expansion() -> io::Result<()> {
let temp = tempfile::tempdir()?;
let root = AbsolutePathBuf::from_absolute_path(temp.path())?.canonicalize()?;
let target = root.join("helper");
fs::write(&target, "fixture")?;
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry::new(root.clone().into(), FileSystemAccessMode::Write),
deny_glob(format!("{}/*", root.display())),
]);
for (expanded, writable) in [(vec![], true), (vec![target.clone()], false)] {
let prepared = policy.clone().with_expanded_deny_globs(expanded);
let checker = FileContentsChecker::new(&prepared, &root)?;
assert_eq!(checker.check(&target)?.is_some(), writable);
}
Ok(())
}
pub(super) fn deny_glob(pattern: impl Into<String>) -> FileSystemSandboxEntry {
FileSystemSandboxEntry::new(
FileSystemPath::GlobPattern {
pattern: pattern.into(),
},
FileSystemAccessMode::Deny,
)
}
pub(super) fn sandbox_request(
cwd: &AbsolutePathBuf,
policy: &FileSystemSandboxPolicy,
) -> SandboxExecRequest {
SandboxExecRequest {
sandbox_override: SandboxOverride::NoOverride,
command: vec![cwd.join("not-executed").to_string_lossy().into_owned()],
cwd: PathUri::from_abs_path(cwd),
sandbox_policy_cwd: PathUri::from_abs_path(cwd),
env: Default::default(),
network: None,
network_environment_id: None,
sandbox: backend::SANDBOX_TYPE,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
permission_profile: PermissionProfile::from_runtime_permissions(
policy,
NetworkSandboxPolicy::Restricted,
),
arg0: None,
}
}
@@ -0,0 +1,21 @@
//! Shared containment dependency inventory entries.
use std::io;
use std::path::PathBuf;
pub(super) type Dependency = (&'static str, io::Result<PathBuf>);
pub(super) fn codex_executable() -> Dependency {
("codex", std::env::current_exe())
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub(super) fn codex_and_launcher_dependencies(
request: &codex_sandboxing::SandboxExecRequest,
) -> Vec<Dependency> {
let mut dependencies = vec![codex_executable()];
if let Some(exe) = request.command.first() {
dependencies.push(("sandbox_launcher", Ok(PathBuf::from(exe))));
}
dependencies
}
@@ -0,0 +1,7 @@
#[path = "mxc.rs"]
mod backend;
mod dependencies;
#[cfg(test)]
#[path = "backend_tests.rs"]
mod backend_tests;
@@ -0,0 +1,49 @@
//! Discover MXC dependencies separately from resolving configured policy rules.
use super::dependencies::Dependency;
use super::dependencies::codex_and_launcher_dependencies;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxType;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::io;
pub(super) const SANDBOX_TYPE: SandboxType = SandboxType::WindowsMxc;
pub(super) const NAME: &str = "windows_mxc";
pub(super) fn critical_dependencies(
request: &SandboxExecRequest,
policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<Vec<Dependency>> {
let command_cwd = request.cwd.to_abs_path()?;
let mut dependencies = codex_and_launcher_dependencies(request);
if !policy
.get_unreadable_globs_with_cwd(cwd.as_path())
.is_empty()
{
let path = request
.env
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("PATH"))
.map(|(_, value)| value);
if let Ok(scanner) = which::which_in(
codex_mxc_sandbox::GLOB_SCAN_PROGRAM,
path,
command_cwd.as_path(),
) {
dependencies.push(("glob_scanner", Ok(scanner)));
}
}
Ok(dependencies)
}
pub(super) fn prepare_policy(
request: &SandboxExecRequest,
policy: FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<FileSystemSandboxPolicy> {
let command_cwd = request.cwd.to_abs_path()?;
codex_mxc_sandbox::prepare_file_system_policy(policy, &request.env, cwd, command_cwd.as_path())
.map_err(io::Error::other)
}
+37
View File
@@ -99,6 +99,43 @@ pub fn is_available() -> bool {
}
}
#[cfg(windows)]
pub use codex_windows_sandbox::GLOB_SCAN_PROGRAM;
/// Resolve configured symbols and snapshot deny globs for policy inspection.
/// Reuses MXC's resolution without adding native grants or protections.
#[cfg(windows)]
pub fn prepare_file_system_policy(
policy: codex_protocol::permissions::FileSystemSandboxPolicy,
env: &HashMap<String, String>,
policy_cwd: &codex_utils_absolute_path::AbsolutePathBuf,
command_cwd: &Path,
) -> Result<codex_protocol::permissions::FileSystemSandboxPolicy> {
use codex_utils_path_uri::PathUri;
let env_entries = env
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>();
let mut policy = policy::materialize_temporary_paths(policy, &env_entries)?;
let denied = codex_windows_sandbox::resolve_windows_deny_read_paths_in_environment(
&policy,
policy_cwd,
env,
command_cwd,
)
.map_err(anyhow::Error::msg)?;
policy = policy.with_expanded_deny_globs(denied);
if policy.has_full_disk_write_access() {
return Ok(policy);
}
let volumes = windows::volume_roots(policy_cwd.as_path(), command_cwd)?
.into_iter()
.map(PathUri::from_host_native_path)
.collect::<std::io::Result<Vec<_>>>()?;
Ok(policy::materialize_volume_roots(policy, &volumes)?)
}
/// Entry point dispatched before ordinary Codex CLI parsing.
pub fn run_main() -> ! {
#[cfg(windows)]
+27 -19
View File
@@ -70,25 +70,11 @@ pub enum PolicyError {
CommandLine(#[from] CommandLineError),
}
pub(super) fn build_request(
command: &MxcCommand,
command_cwd: &Path,
env: Vec<String>,
volume_roots: &[PathBuf],
platform_read_roots: &[PathBuf],
) -> Result<ExecutionRequest, PolicyError> {
if command.command.is_empty() {
return Err(PolicyError::EmptyCommand);
}
let permissions = &command.permissions;
let cwd = &command.sandbox_policy_cwd;
if !cwd.is_absolute() {
return Err(PolicyError::RelativePolicyCwd);
}
if !command_cwd.is_absolute() {
return Err(PolicyError::RelativeCommandCwd);
}
let mut policy = permissions.file_system_sandbox_policy();
/// Bind MXC's temporary-directory symbols using the filtered command environment.
pub(super) fn materialize_temporary_paths(
mut policy: FileSystemSandboxPolicy,
env: &[String],
) -> std::io::Result<FileSystemSandboxPolicy> {
// Resolve Windows temporary directories from the filtered command
// environment, including case-insensitive names.
let mut temp_values = HashMap::new();
@@ -126,6 +112,28 @@ pub(super) fn build_request(
}
})
.collect();
Ok(policy)
}
pub(super) fn build_request(
command: &MxcCommand,
command_cwd: &Path,
env: Vec<String>,
volume_roots: &[PathBuf],
platform_read_roots: &[PathBuf],
) -> Result<ExecutionRequest, PolicyError> {
if command.command.is_empty() {
return Err(PolicyError::EmptyCommand);
}
let permissions = &command.permissions;
let cwd = &command.sandbox_policy_cwd;
if !cwd.is_absolute() {
return Err(PolicyError::RelativePolicyCwd);
}
if !command_cwd.is_absolute() {
return Err(PolicyError::RelativeCommandCwd);
}
let policy = materialize_temporary_paths(permissions.file_system_sandbox_policy(), &env)?;
let full_disk_write = policy.has_full_disk_write_access();
let volumes = volume_roots
.iter()
+24 -16
View File
@@ -48,23 +48,8 @@ pub(super) fn run() -> Result<i32> {
.map_err(|_| anyhow::anyhow!("invalid MXC launcher request"))?;
// The transport itself proves this is an explicit child environment; it
// may intentionally contain no variables once launcher state is removed.
let mask = unsafe { GetLogicalDrives() };
ensure!(
mask != 0,
"cannot enumerate Windows volumes: {}",
std::io::Error::last_os_error()
);
let mut volumes: Vec<PathBuf> = (0..26)
.filter(|index| mask & (1 << index) != 0)
.map(|index| PathBuf::from(format!("{}:\\", (b'A' + index as u8) as char)))
.collect();
let command_cwd = std::env::current_dir()?;
// An explicit UNC cwd has no drive letter and is absent from GetLogicalDrives.
for cwd in [&command.sandbox_policy_cwd, &command_cwd] {
if let Some(root) = cwd.ancestors().last() {
volumes.push(root.to_path_buf());
}
}
let volumes = volume_roots(&command.sandbox_policy_cwd, &command_cwd)?;
let request = crate::policy::build_request(
&command,
&command_cwd,
@@ -77,6 +62,29 @@ pub(super) fn run() -> Result<i32> {
crate::native::launch(&request)
}
pub(super) fn volume_roots(
policy_cwd: &std::path::Path,
command_cwd: &std::path::Path,
) -> Result<Vec<PathBuf>> {
let mask = unsafe { GetLogicalDrives() };
ensure!(
mask != 0,
"cannot enumerate Windows volumes: {}",
std::io::Error::last_os_error()
);
let mut volumes: Vec<PathBuf> = (0..26)
.filter(|index| mask & (1 << index) != 0)
.map(|index| PathBuf::from(format!("{}:\\", (b'A' + index as u8) as char)))
.collect();
// An explicit UNC cwd has no drive letter and is absent from GetLogicalDrives.
for cwd in [policy_cwd, command_cwd] {
if let Some(root) = cwd.ancestors().last() {
volumes.push(root.to_path_buf());
}
}
Ok(volumes)
}
fn platform_read_roots() -> Result<Vec<PathBuf>> {
use std::os::windows::ffi::OsStringExt;
+13
View File
@@ -1838,6 +1838,19 @@ impl FileSystemSandboxPolicy {
patterns
}
/// Replace deny globs with a backend's concrete snapshot, preserving other entries.
/// An empty snapshot still removes the original patterns.
pub fn with_expanded_deny_globs(mut self, paths: Vec<AbsolutePathBuf>) -> Self {
self.entries
.retain(|entry| !matches!(entry.path, FileSystemPath::GlobPattern { .. }));
self.entries.extend(
paths
.into_iter()
.map(|path| FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Deny)),
);
self
}
pub fn to_legacy_sandbox_policy(
&self,
network_policy: NetworkSandboxPolicy,
@@ -5,8 +5,11 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::ReadDenyMatcher;
use codex_protocol::permissions::windows_deny_read_glob_scan;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
#[path = "deny_read_walker.rs"]
mod walker;
@@ -14,6 +17,9 @@ mod walker;
use walker::DirectoryScanMode;
use walker::collect_existing_glob_directory_matches;
/// External scanner used by deny-glob expansion and its dependency inventory.
pub const GLOB_SCAN_PROGRAM: &str = "rg";
#[derive(Debug, Eq, PartialEq)]
struct GlobScanPlan {
root: PathBuf,
@@ -31,6 +37,27 @@ struct GlobScanPlan {
pub fn resolve_windows_deny_read_paths(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> Result<Vec<AbsolutePathBuf>, String> {
resolve_deny_read_paths(file_system_sandbox_policy, cwd, |_| {})
}
/// Resolve deny paths with the launcher's scanner in an explicit command environment.
/// The supplied environment replaces inheritance, including when it is empty.
pub fn resolve_windows_deny_read_paths_in_environment(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
env: &HashMap<String, String>,
command_cwd: &Path,
) -> Result<Vec<AbsolutePathBuf>, String> {
resolve_deny_read_paths(file_system_sandbox_policy, cwd, |command| {
command.env_clear().envs(env).current_dir(command_cwd);
})
}
fn resolve_deny_read_paths(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
configure: impl Fn(&mut Command),
) -> Result<Vec<AbsolutePathBuf>, String> {
let mut paths = Vec::new();
let mut seen = HashSet::new();
@@ -71,7 +98,7 @@ pub fn resolve_windows_deny_read_paths(
continue;
}
let directory_scan_mode = if let Some(file_paths) = ripgrep_files(&scan_plan)? {
let directory_scan_mode = if let Some(file_paths) = ripgrep_files(&scan_plan, &configure)? {
for path in file_paths {
if matcher.is_local_path_read_denied(&path) {
push_absolute_path(&mut paths, &mut seen, path)?;
@@ -98,8 +125,12 @@ pub fn resolve_windows_deny_read_paths(
Ok(paths)
}
fn ripgrep_files(scan_plan: &GlobScanPlan) -> Result<Option<Vec<PathBuf>>, String> {
let mut command = codex_utils_process::background_command("rg");
fn ripgrep_files(
scan_plan: &GlobScanPlan,
configure: &impl Fn(&mut Command),
) -> Result<Option<Vec<PathBuf>>, String> {
let mut command = codex_utils_process::background_command(GLOB_SCAN_PROGRAM);
configure(&mut command);
command
.arg("--files")
.arg("--hidden")
@@ -512,11 +543,14 @@ mod tests {
tmp.path().display()
))]);
if let Some(paths) = super::ripgrep_files(&GlobScanPlan {
root: tmp.path().to_path_buf(),
max_depth: None,
globs: vec!["**/*.env".to_string()],
})
if let Some(paths) = super::ripgrep_files(
&GlobScanPlan {
root: tmp.path().to_path_buf(),
max_depth: None,
globs: vec!["**/*.env".to_string()],
},
&|_| {},
)
.expect("case-insensitive ripgrep scan")
{
assert!(paths.contains(&uppercase_env));
+2
View File
@@ -263,7 +263,9 @@ pub use conpty::spawn_conpty_process_as_user;
pub use deny_read_acl::apply_deny_read_acls;
#[cfg(target_os = "windows")]
pub use deny_read_acl::plan_deny_read_acl_paths;
pub use deny_read_resolver::GLOB_SCAN_PROGRAM;
pub use deny_read_resolver::resolve_windows_deny_read_paths;
pub use deny_read_resolver::resolve_windows_deny_read_paths_in_environment;
#[cfg(target_os = "windows")]
pub use deny_read_state::sync_persistent_deny_read_acls;
#[cfg(target_os = "windows")]