Avoid UAC bootstrap for unelevated Windows sandbox

This commit is contained in:
David Wiesen
2026-05-20 09:53:58 -07:00
parent 0035d7bd18
commit 7a735a85c8
5 changed files with 47 additions and 10 deletions
+11
View File
@@ -121,6 +121,14 @@ fn windows_sandbox_uses_elevated_backend(
proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated)
}
fn windows_sandbox_allows_setup_bootstrap(
sandbox_level: WindowsSandboxLevel,
proxy_enforced: bool,
) -> bool {
matches!(sandbox_level, WindowsSandboxLevel::Elevated)
|| !windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExecCapturePolicy {
/// Shell-like execs keep the historical output cap and timeout behavior.
@@ -604,6 +612,8 @@ async fn exec_windows_sandbox(
let sandbox_level = windows_sandbox_level;
let proxy_enforced = network.is_some();
let use_elevated = windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced);
let allow_setup_bootstrap =
windows_sandbox_allows_setup_bootstrap(sandbox_level, proxy_enforced);
let additional_deny_write_paths = windows_sandbox_filesystem_overrides
.map(|overrides| {
overrides
@@ -641,6 +651,7 @@ async fn exec_windows_sandbox(
timeout_ms,
use_private_desktop: windows_sandbox_private_desktop,
proxy_enforced,
allow_setup_bootstrap,
read_roots_override: elevated_read_roots_override.as_deref(),
read_roots_include_platform_defaults:
elevated_read_roots_include_platform_defaults,
+16
View File
@@ -425,6 +425,22 @@ fn windows_proxy_enforcement_uses_elevated_backend() {
));
}
#[test]
fn windows_proxy_enforcement_only_bootstraps_setup_for_explicit_elevated_mode() {
assert!(windows_sandbox_allows_setup_bootstrap(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ false,
));
assert!(!windows_sandbox_allows_setup_bootstrap(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ true,
));
assert!(windows_sandbox_allows_setup_bootstrap(
WindowsSandboxLevel::Elevated,
/*proxy_enforced*/ false,
));
}
#[test]
fn windows_restricted_token_rejects_network_only_restrictions() {
let policy = SandboxPolicy::ExternalSandbox {
@@ -12,6 +12,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> {
pub timeout_ms: Option<u64>,
pub use_private_desktop: bool,
pub proxy_enforced: bool,
pub allow_setup_bootstrap: bool,
pub read_roots_override: Option<&'a [PathBuf]>,
pub read_roots_include_platform_defaults: bool,
pub write_roots_override: Option<&'a [PathBuf]>,
@@ -121,6 +122,7 @@ mod windows_impl {
timeout_ms,
use_private_desktop,
proxy_enforced,
allow_setup_bootstrap,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
@@ -148,6 +150,7 @@ mod windows_impl {
write_roots_override,
deny_write_paths_override,
proxy_enforced,
allow_setup_bootstrap,
)?;
// Build capability SID for ACL grants.
if matches!(
+16 -10
View File
@@ -1,6 +1,10 @@
use crate::dpapi;
use crate::logging::debug_log;
use crate::policy::SandboxPolicy;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
use crate::setup::SetupMarker;
use crate::setup::gather_read_roots;
use crate::setup::gather_write_roots;
use crate::setup::offline_proxy_settings_from_env;
@@ -8,15 +12,11 @@ use crate::setup::run_elevated_setup;
use crate::setup::run_setup_refresh_with_overrides;
use crate::setup::sandbox_users_path;
use crate::setup::setup_marker_path;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
use crate::setup::SetupMarker;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use anyhow::anyhow;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
@@ -141,6 +141,7 @@ pub fn require_logon_sandbox_creds(
write_roots_override: Option<&[PathBuf]>,
deny_write_paths_override: &[PathBuf],
proxy_enforced: bool,
allow_setup_bootstrap: bool,
) -> Result<SandboxCreds> {
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
let needed_read = read_roots_override
@@ -150,8 +151,7 @@ pub fn require_logon_sandbox_creds(
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
let desired_offline_proxy_settings =
offline_proxy_settings_from_env(env_map, network_identity);
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
// granting the sandbox group access to this directory without granting the capability SID.
@@ -167,8 +167,9 @@ pub fn require_logon_sandbox_creds(
} else {
let selected = select_identity(network_identity, codex_home)?;
if selected.is_none() {
setup_reason =
Some("sandbox users missing or incompatible with marker version".to_string());
setup_reason = Some(
"sandbox users missing or incompatible with marker version".to_string(),
);
}
selected
}
@@ -188,6 +189,11 @@ pub fn require_logon_sandbox_creds(
} else {
crate::logging::log_note("sandbox setup required", Some(&sandbox_dir));
}
if !allow_setup_bootstrap {
return Err(anyhow!(
"windows unelevated sandbox cannot auto-run elevated setup to enforce restricted network access; rerun Windows sandbox setup in elevated mode or enable network access"
));
}
run_elevated_setup(
crate::setup::SandboxSetupRequest {
policy,
@@ -299,6 +299,7 @@ pub(crate) fn prepare_elevated_spawn_context(
write_roots_override,
&deny_write_paths,
/*proxy_enforced*/ false,
/*allow_setup_bootstrap*/ true,
)?;
let caps = load_or_create_cap_sids(codex_home)?;
let (psid_to_use, cap_sids) = match &common.policy {