From 0248b2b7fa94a7034f03a0148ab0e4d7bbbe891c Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Wed, 4 Feb 2026 10:30:48 -0800 Subject: [PATCH] elevated setup smoke test. --- codex-rs/windows-sandbox-rs/src/firewall.rs | 12 +- codex-rs/windows-sandbox-rs/src/lib.rs | 4 + .../windows-sandbox-rs/src/setup_main_win.rs | 12 +- .../src/setup_orchestrator.rs | 43 +++- codex-rs/windows-sandbox-rs/tests/all.rs | 3 + .../tests/suite/elevated_setup_smoke.rs | 193 ++++++++++++++++++ .../windows-sandbox-rs/tests/suite/mod.rs | 1 + 7 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 codex-rs/windows-sandbox-rs/tests/all.rs create mode 100644 codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs create mode 100644 codex-rs/windows-sandbox-rs/tests/suite/mod.rs diff --git a/codex-rs/windows-sandbox-rs/src/firewall.rs b/codex-rs/windows-sandbox-rs/src/firewall.rs index b4939fd42b..1123d86c03 100644 --- a/codex-rs/windows-sandbox-rs/src/firewall.rs +++ b/codex-rs/windows-sandbox-rs/src/firewall.rs @@ -24,14 +24,14 @@ use windows::Win32::System::Com::COINIT_APARTMENTTHREADED; use codex_windows_sandbox::SetupErrorCode; use codex_windows_sandbox::SetupFailure; -// This is the stable identifier we use to find/update the rule idempotently. -// It intentionally does not change between installs. -const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound"; - // Friendly text shown in the firewall UI. const OFFLINE_BLOCK_RULE_FRIENDLY: &str = "Codex Sandbox Offline - Block Outbound"; -pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Result<()> { +pub fn ensure_offline_outbound_block( + offline_sid: &str, + offline_block_rule_name: &str, + log: &mut File, +) -> Result<()> { let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})"); let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; @@ -61,7 +61,7 @@ pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Resul // Block all outbound IP protocols for this user. ensure_block_rule( &rules, - OFFLINE_BLOCK_RULE_NAME, + offline_block_rule_name, OFFLINE_BLOCK_RULE_FRIENDLY, NET_FW_IP_PROTOCOL_ANY.0, &local_user_spec, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index b938a86e6b..5e756e5745 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -81,12 +81,16 @@ pub use process::create_process_as_user; #[cfg(target_os = "windows")] pub use setup::run_elevated_setup; #[cfg(target_os = "windows")] +pub use setup::run_elevated_setup_with_identity_overrides; +#[cfg(target_os = "windows")] pub use setup::run_setup_refresh; #[cfg(target_os = "windows")] pub use setup::sandbox_dir; #[cfg(target_os = "windows")] pub use setup::sandbox_secrets_dir; #[cfg(target_os = "windows")] +pub use setup::SetupIdentityOverrides; +#[cfg(target_os = "windows")] pub use setup::SETUP_VERSION; #[cfg(target_os = "windows")] pub use setup_error::extract_failure as extract_setup_failure; diff --git a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs index 28c94a769c..9750ba3589 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs @@ -78,6 +78,8 @@ struct Payload { version: u32, offline_username: String, online_username: String, + #[serde(default = "default_offline_block_rule_name")] + offline_block_rule_name: String, codex_home: PathBuf, command_cwd: PathBuf, read_roots: Vec, @@ -97,6 +99,10 @@ enum SetupMode { ReadAclsOnly, } +fn default_offline_block_rule_name() -> String { + "codex_sandbox_offline_block_outbound".to_string() +} + fn log_line(log: &mut File, msg: &str) -> Result<()> { let ts = chrono::Utc::now().to_rfc3339(); writeln!(log, "[{ts}] {msg}").map_err(|err| { @@ -572,7 +578,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( }; let mut refresh_errors: Vec = Vec::new(); if !refresh_only { - let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log); + let firewall_result = firewall::ensure_offline_outbound_block( + &offline_sid_str, + &payload.offline_block_rule_name, + log, + ); if let Err(err) = firewall_result { if extract_setup_failure(&err).is_some() { return Err(err); diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 0e43d1ba4b..7f73b554dc 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -35,10 +35,18 @@ use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY; pub const SETUP_VERSION: u32 = 5; pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline"; pub const ONLINE_USERNAME: &str = "CodexSandboxOnline"; +pub const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound"; const ERROR_CANCELLED: u32 = 1223; const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020; const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220; +#[derive(Debug, Clone, Default)] +pub struct SetupIdentityOverrides { + pub offline_username: Option, + pub online_username: Option, + pub offline_block_rule_name: Option, +} + pub fn sandbox_dir(codex_home: &Path) -> PathBuf { codex_home.join(".sandbox") } @@ -82,6 +90,7 @@ pub fn run_setup_refresh( version: SETUP_VERSION, offline_username: OFFLINE_USERNAME.to_string(), online_username: ONLINE_USERNAME.to_string(), + offline_block_rule_name: OFFLINE_BLOCK_RULE_NAME.to_string(), codex_home: codex_home.to_path_buf(), command_cwd: command_cwd.to_path_buf(), read_roots, @@ -260,6 +269,7 @@ struct ElevationPayload { version: u32, offline_username: String, online_username: String, + offline_block_rule_name: String, codex_home: PathBuf, command_cwd: PathBuf, read_roots: Vec, @@ -455,6 +465,28 @@ pub fn run_elevated_setup( codex_home: &Path, read_roots_override: Option>, write_roots_override: Option>, +) -> Result<()> { + run_elevated_setup_with_identity_overrides( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + read_roots_override, + write_roots_override, + None, + ) +} + +pub fn run_elevated_setup_with_identity_overrides( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_roots_override: Option>, + write_roots_override: Option>, + identity_overrides: Option<&SetupIdentityOverrides>, ) -> Result<()> { // Ensure the shared sandbox directory exists before we send it to the elevated helper. let sbx_dir = sandbox_dir(codex_home); @@ -475,8 +507,15 @@ pub fn run_elevated_setup( ); let payload = ElevationPayload { version: SETUP_VERSION, - offline_username: OFFLINE_USERNAME.to_string(), - online_username: ONLINE_USERNAME.to_string(), + offline_username: identity_overrides + .and_then(|o| o.offline_username.clone()) + .unwrap_or_else(|| OFFLINE_USERNAME.to_string()), + online_username: identity_overrides + .and_then(|o| o.online_username.clone()) + .unwrap_or_else(|| ONLINE_USERNAME.to_string()), + offline_block_rule_name: identity_overrides + .and_then(|o| o.offline_block_rule_name.clone()) + .unwrap_or_else(|| OFFLINE_BLOCK_RULE_NAME.to_string()), codex_home: codex_home.to_path_buf(), command_cwd: command_cwd.to_path_buf(), read_roots, diff --git a/codex-rs/windows-sandbox-rs/tests/all.rs b/codex-rs/windows-sandbox-rs/tests/all.rs new file mode 100644 index 0000000000..360e3968c9 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/all.rs @@ -0,0 +1,3 @@ +#![cfg(target_os = "windows")] + +mod suite; diff --git a/codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs b/codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs new file mode 100644 index 0000000000..9f62efc9cf --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs @@ -0,0 +1,193 @@ +#![cfg(target_os = "windows")] + +use anyhow::Context; +use anyhow::Result; +use codex_protocol::protocol::SandboxPolicy; +use codex_windows_sandbox::run_elevated_setup_with_identity_overrides; +use codex_windows_sandbox::sandbox_setup_is_complete; +use codex_windows_sandbox::SetupIdentityOverrides; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +struct CleanupGuard { + usernames: Vec, + firewall_rule_name: String, +} + +impl Drop for CleanupGuard { + fn drop(&mut self) { + cleanup_global_sandbox_artifacts(&self.usernames, &self.firewall_rule_name); + } +} + +#[derive(Debug, Deserialize)] +struct SandboxUsersFile { + offline: SandboxUserRecord, + online: SandboxUserRecord, +} + +#[derive(Debug, Deserialize)] +struct SandboxUserRecord { + username: String, +} + +#[test] +fn elevated_setup_creates_both_sandbox_users() -> Result<()> { + let suffix = unique_suffix(); + let offline_username = format!("CdxSbxOf{suffix}"); + let online_username = format!("CdxSbxOn{suffix}"); + let firewall_rule_name = format!("codex_sbx_test_{suffix}"); + + let pre_users = vec![offline_username.clone(), online_username.clone()]; + cleanup_global_sandbox_artifacts(&pre_users, &firewall_rule_name); + + let mut guard = CleanupGuard { + usernames: Vec::new(), + firewall_rule_name: firewall_rule_name.clone(), + }; + + ensure_setup_helper_on_path()?; + + let temp = tempfile::tempdir().context("create temp dir")?; + let codex_home = temp.path().join("codex-home"); + let workspace = temp.path().join("workspace"); + std::fs::create_dir_all(&codex_home).context("create codex_home")?; + std::fs::create_dir_all(&workspace).context("create workspace")?; + + let policy = SandboxPolicy::ReadOnly; + let env_map = HashMap::new(); + let overrides = SetupIdentityOverrides { + offline_username: Some(offline_username), + online_username: Some(online_username), + offline_block_rule_name: Some(firewall_rule_name), + }; + run_elevated_setup_with_identity_overrides( + &policy, + workspace.as_path(), + workspace.as_path(), + &env_map, + codex_home.as_path(), + None, + None, + Some(&overrides), + )?; + + assert!( + sandbox_setup_is_complete(codex_home.as_path()), + "sandbox setup should be complete after elevated setup runs" + ); + + let users = read_sandbox_users_file(codex_home.as_path())?; + guard.usernames = vec![users.offline.username.clone(), users.online.username.clone()]; + assert_local_user_exists(&users.offline.username)?; + assert_local_user_exists(&users.online.username)?; + + Ok(()) +} + +fn read_sandbox_users_file(codex_home: &Path) -> Result { + let path = codex_home + .join(".sandbox-secrets") + .join("sandbox_users.json"); + let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?; + let users: SandboxUsersFile = serde_json::from_slice(&bytes) + .with_context(|| format!("parse {}", path.display()))?; + Ok(users) +} + +fn assert_local_user_exists(username: &str) -> Result<()> { + let status = Command::new("net") + .args(["user", username]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .with_context(|| format!("query local user {username}"))?; + if status.success() { + return Ok(()); + } + anyhow::bail!("expected local user {username} to exist") +} + +fn ensure_setup_helper_on_path() -> Result<()> { + if let Some(helper) = helper_from_env() { + let helper_dir = Path::new(&helper) + .parent() + .context("setup helper path has no parent dir")?; + prepend_to_path(helper_dir)?; + return Ok(()); + } + + if let Some(helper_dir) = helper_dir_from_target_layout() { + prepend_to_path(&helper_dir)?; + return Ok(()); + } + + anyhow::bail!( + "setup helper path was not found via CARGO_BIN_EXE_* or target/debug layout" + ) +} + +fn helper_from_env() -> Option { + for key in [ + "CARGO_BIN_EXE_codex-windows-sandbox-setup", + "CARGO_BIN_EXE_codex_windows_sandbox_setup", + ] { + if let Ok(path) = std::env::var(key) { + return Some(path); + } + } + None +} + +fn helper_dir_from_target_layout() -> Option { + let current_exe = std::env::current_exe().ok()?; + let deps_dir = current_exe.parent()?; + let target_debug_dir = deps_dir.parent()?; + let helper = target_debug_dir.join("codex-windows-sandbox-setup.exe"); + if helper.is_file() { + return Some(target_debug_dir.to_path_buf()); + } + None +} + +fn prepend_to_path(path: &Path) -> Result<()> { + let existing = std::env::var("PATH").unwrap_or_default(); + let mut parts = vec![path.display().to_string()]; + if !existing.is_empty() { + parts.push(existing); + } + let joined = parts.join(";"); + std::env::set_var("PATH", joined); + Ok(()) +} + +fn cleanup_global_sandbox_artifacts(usernames: &[String], firewall_rule_name: &str) { + for user in usernames { + let _ = Command::new("net") + .args(["user", user, "/delete"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } + let cmd = format!( + "Remove-NetFirewallRule -Name '{firewall_rule_name}' -ErrorAction SilentlyContinue" + ); + let _ = Command::new("powershell") + .args(["-NoLogo", "-NoProfile", "-Command", &cmd]) + .status(); +} + +fn unique_suffix() -> String { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + let pid = std::process::id() as u128; + format!("{:08x}", ((nanos ^ pid) & 0xffff_ffff) as u32) +} diff --git a/codex-rs/windows-sandbox-rs/tests/suite/mod.rs b/codex-rs/windows-sandbox-rs/tests/suite/mod.rs new file mode 100644 index 0000000000..d5ccb6fa19 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/suite/mod.rs @@ -0,0 +1 @@ +mod elevated_setup_smoke;