Compare commits

...

1 Commits

Author SHA1 Message Date
iceweasel-oai
0248b2b7fa elevated setup smoke test. 2026-02-04 10:30:48 -08:00
7 changed files with 259 additions and 9 deletions

View File

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

View File

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

View File

@@ -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<PathBuf>,
@@ -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<String> = 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);

View File

@@ -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<String>,
pub online_username: Option<String>,
pub offline_block_rule_name: Option<String>,
}
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<PathBuf>,
@@ -455,6 +465,28 @@ pub fn run_elevated_setup(
codex_home: &Path,
read_roots_override: Option<Vec<PathBuf>>,
write_roots_override: Option<Vec<PathBuf>>,
) -> 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<String, String>,
codex_home: &Path,
read_roots_override: Option<Vec<PathBuf>>,
write_roots_override: Option<Vec<PathBuf>>,
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,

View File

@@ -0,0 +1,3 @@
#![cfg(target_os = "windows")]
mod suite;

View File

@@ -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<String>,
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<SandboxUsersFile> {
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<String> {
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<PathBuf> {
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)
}

View File

@@ -0,0 +1 @@
mod elevated_setup_smoke;