Files
codex/codex-rs/windows-sandbox-rs/src/setup_error.rs
T
iceweasel-oaiandGitHub c9edb26755 windows-sandbox: fail elevated setup when firewall policy is ineffective (#22353)
## Why

Elevated Windows sandbox setup currently assumes that the firewall rules
it writes will take effect. On managed Windows hosts, local firewall
policy changes can be ignored or only partially apply across the active
profiles, which means setup can appear to succeed without providing the
expected network isolation.

## What changed

- Query `INetFwPolicy2::LocalPolicyModifyState` before configuring the
elevated sandbox firewall rules.
- Fail setup when Windows reports that local firewall policy edits are
ineffective or only apply to some current profiles.
- Surface that condition with a dedicated
`helper_firewall_policy_ineffective` setup error code so support and
IT-facing diagnostics can distinguish it from COM access failures.
- Add focused coverage for effective policy, group-policy override, and
partial-profile coverage cases.

## Testing

- `cargo test -p codex-windows-sandbox --bin
codex-windows-sandbox-setup`
2026-05-13 18:43:14 +00:00

284 lines
11 KiB
Rust

use anyhow::Context;
use anyhow::Result;
use codex_utils_string::sanitize_metric_tag_value;
use serde::Deserialize;
use serde::Serialize;
use std::fs;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
/// These represent the most common failures for the elevated sandbox setup.
///
/// Codes are used as metric tags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SetupErrorCode {
// Orchestrator (run in CLI) failures.
/// Failed to create `codex_home/.sandbox` in the orchestrator.
OrchestratorSandboxDirCreateFailed,
/// Failed to determine whether the current process is elevated.
OrchestratorElevationCheckFailed,
/// Failed to serialize the elevation payload before launching the helper.
OrchestratorPayloadSerializeFailed,
/// Failed to launch the setup helper process (spawn or ShellExecuteExW).
OrchestratorHelperLaunchFailed,
/// User canceled the UAC prompt while launching the helper.
OrchestratorHelperLaunchCanceled,
/// Helper exited non-zero and no structured report was available.
OrchestratorHelperExitNonzero,
/// Helper exited non-zero and reading `setup_error.json` failed.
OrchestratorHelperReportReadFailed,
// Helper (elevated process) failures.
/// Helper failed while validating or decoding the request payload.
HelperRequestArgsFailed,
/// Helper failed to create `codex_home/.sandbox`.
HelperSandboxDirCreateFailed,
/// Helper failed to open or write the setup log.
HelperLogFailed,
/// Helper failed in the provisioning phase (fallback bucket).
HelperUserProvisionFailed,
/// Helper failed to create the sandbox users local group.
HelperUsersGroupCreateFailed,
/// Helper failed to create or update a sandbox user account.
HelperUserCreateOrUpdateFailed,
/// Helper failed to protect user passwords with DPAPI.
HelperDpapiProtectFailed,
/// Helper failed to write the sandbox users secrets file.
HelperUsersFileWriteFailed,
/// Helper failed to write the setup marker file.
HelperSetupMarkerWriteFailed,
/// Helper failed to resolve a SID or convert it to a PSID.
HelperSidResolveFailed,
/// Helper failed to load or convert capability SIDs.
HelperCapabilitySidFailed,
/// Helper failed to initialize COM for firewall configuration.
HelperFirewallComInitFailed,
/// Helper failed to access firewall policy or rule collections.
HelperFirewallPolicyAccessFailed,
/// Helper detected that local firewall policy changes will not fully take effect.
HelperFirewallPolicyIneffective,
/// Helper failed to create, update, or add the firewall rule.
HelperFirewallRuleCreateOrAddFailed,
/// Helper failed to verify the configured firewall rule scope.
HelperFirewallRuleVerifyFailed,
/// Helper failed to spawn the read-ACL helper process.
HelperReadAclHelperSpawnFailed,
/// Helper failed to lock down sandbox directories via ACLs.
HelperSandboxLockFailed,
/// Helper failed for an unmapped or unexpected reason.
HelperUnknownError,
}
impl SetupErrorCode {
pub fn as_str(self) -> &'static str {
match self {
Self::OrchestratorSandboxDirCreateFailed => "orchestrator_sandbox_dir_create_failed",
Self::OrchestratorElevationCheckFailed => "orchestrator_elevation_check_failed",
Self::OrchestratorPayloadSerializeFailed => "orchestrator_payload_serialize_failed",
Self::OrchestratorHelperLaunchFailed => "orchestrator_helper_launch_failed",
Self::OrchestratorHelperLaunchCanceled => "orchestrator_helper_launch_canceled",
Self::OrchestratorHelperExitNonzero => "orchestrator_helper_exit_nonzero",
Self::OrchestratorHelperReportReadFailed => "orchestrator_helper_report_read_failed",
Self::HelperRequestArgsFailed => "helper_request_args_failed",
Self::HelperSandboxDirCreateFailed => "helper_sandbox_dir_create_failed",
Self::HelperLogFailed => "helper_log_failed",
Self::HelperUserProvisionFailed => "helper_user_provision_failed",
Self::HelperUsersGroupCreateFailed => "helper_users_group_create_failed",
Self::HelperUserCreateOrUpdateFailed => "helper_user_create_or_update_failed",
Self::HelperDpapiProtectFailed => "helper_dpapi_protect_failed",
Self::HelperUsersFileWriteFailed => "helper_users_file_write_failed",
Self::HelperSetupMarkerWriteFailed => "helper_setup_marker_write_failed",
Self::HelperSidResolveFailed => "helper_sid_resolve_failed",
Self::HelperCapabilitySidFailed => "helper_capability_sid_failed",
Self::HelperFirewallComInitFailed => "helper_firewall_com_init_failed",
Self::HelperFirewallPolicyAccessFailed => "helper_firewall_policy_access_failed",
Self::HelperFirewallPolicyIneffective => "helper_firewall_policy_ineffective",
Self::HelperFirewallRuleCreateOrAddFailed => {
"helper_firewall_rule_create_or_add_failed"
}
Self::HelperFirewallRuleVerifyFailed => "helper_firewall_rule_verify_failed",
Self::HelperReadAclHelperSpawnFailed => "helper_read_acl_helper_spawn_failed",
Self::HelperSandboxLockFailed => "helper_sandbox_lock_failed",
Self::HelperUnknownError => "helper_unknown_error",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupErrorReport {
pub code: SetupErrorCode,
pub message: String,
}
#[derive(Debug)]
pub struct SetupFailure {
pub code: SetupErrorCode,
pub message: String,
}
impl SetupFailure {
pub fn new(code: SetupErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn from_report(report: SetupErrorReport) -> Self {
Self::new(report.code, report.message)
}
pub fn metric_message(&self) -> String {
sanitize_setup_metric_tag_value(&self.message)
}
}
impl std::fmt::Display for SetupFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}
impl std::error::Error for SetupFailure {}
pub fn failure(code: SetupErrorCode, message: impl Into<String>) -> anyhow::Error {
anyhow::Error::new(SetupFailure::new(code, message))
}
pub fn extract_failure(err: &anyhow::Error) -> Option<&SetupFailure> {
err.downcast_ref::<SetupFailure>()
}
pub fn setup_error_path(codex_home: &Path) -> PathBuf {
codex_home.join(".sandbox").join("setup_error.json")
}
pub fn clear_setup_error_report(codex_home: &Path) -> Result<()> {
let path = setup_error_path(codex_home);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => Err(err).with_context(|| format!("remove {}", path.display())),
}
}
pub fn write_setup_error_report(codex_home: &Path, report: &SetupErrorReport) -> Result<()> {
let sandbox_dir = codex_home.join(".sandbox");
fs::create_dir_all(&sandbox_dir)
.with_context(|| format!("create sandbox dir {}", sandbox_dir.display()))?;
let path = setup_error_path(codex_home);
let json = serde_json::to_vec_pretty(report)?;
fs::write(&path, json).with_context(|| format!("write {}", path.display()))?;
Ok(())
}
pub fn read_setup_error_report(codex_home: &Path) -> Result<Option<SetupErrorReport>> {
let path = setup_error_path(codex_home);
let bytes = match fs::read(&path) {
Ok(bytes) => bytes,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err).with_context(|| format!("read {}", path.display())),
};
let report = serde_json::from_slice::<SetupErrorReport>(&bytes)
.with_context(|| format!("parse {}", path.display()))?;
Ok(Some(report))
}
/// Sanitize a setup error message for use as a metric tag.
pub fn sanitize_setup_metric_tag_value(value: &str) -> String {
sanitize_metric_tag_value(redact_home_paths(value).as_str())
}
fn redact_home_paths(value: &str) -> String {
let mut usernames: Vec<String> = Vec::new();
if let Ok(username) = std::env::var("USERNAME")
&& !username.trim().is_empty()
{
usernames.push(username);
}
if let Ok(user) = std::env::var("USER")
&& !user.trim().is_empty()
&& !usernames.iter().any(|v| v.eq_ignore_ascii_case(&user))
{
usernames.push(user);
}
redact_username_segments(value, &usernames)
}
fn redact_username_segments(value: &str, usernames: &[String]) -> String {
if usernames.is_empty() {
return value.to_string();
}
let mut segments: Vec<String> = Vec::new();
let mut separators: Vec<char> = Vec::new();
let mut current = String::new();
for ch in value.chars() {
if ch == '\\' || ch == '/' {
segments.push(std::mem::take(&mut current));
separators.push(ch);
} else {
current.push(ch);
}
}
segments.push(current);
for segment in &mut segments {
let matches = if cfg!(windows) {
usernames
.iter()
.any(|name| segment.eq_ignore_ascii_case(name))
} else {
usernames.iter().any(|name| segment == name)
};
if matches {
*segment = "<user>".to_string();
}
}
let mut out = String::new();
for (idx, segment) in segments.iter().enumerate() {
out.push_str(segment);
if let Some(sep) = separators.get(idx) {
out.push(*sep);
}
}
out
}
#[cfg(test)]
mod tests {
use super::redact_username_segments;
use pretty_assertions::assert_eq;
#[test]
fn sanitize_tag_value_redacts_username_segments() {
let usernames = vec!["Alice".to_string(), "Bob".to_string()];
let msg = "failed to write C:\\Users\\Alice\\file.txt; fallback D:\\Profiles\\Bob\\x";
let redacted = redact_username_segments(msg, &usernames);
assert_eq!(
redacted,
"failed to write C:\\Users\\<user>\\file.txt; fallback D:\\Profiles\\<user>\\x"
);
}
#[test]
fn sanitize_tag_value_leaves_unknown_segments() {
let usernames = vec!["Alice".to_string()];
let msg = "failed to write E:\\data\\file.txt";
let redacted = redact_username_segments(msg, &usernames);
assert_eq!(redacted, msg);
}
#[test]
fn sanitize_tag_value_redacts_multiple_occurrences() {
let usernames = vec!["Alice".to_string()];
let msg = "C:\\Users\\Alice\\a and C:\\Users\\Alice\\b";
let redacted = redact_username_segments(msg, &usernames);
assert_eq!(redacted, "C:\\Users\\<user>\\a and C:\\Users\\<user>\\b");
}
}