Clean up Windows sandbox resources on app uninstall (#42375)

## What changed

- Record the authenticated sandbox owner and observe package uninstall events across service restarts and session changes.
- During uninstall, serialize setup and cleanup, disable sandbox accounts, stop their processes, and remove sandbox directories, firewall and WFP rules, hidden-user entries, accounts, and the sandbox group.
- Restrict desktop-owned directory cleanup to pinned paths and perform it while impersonating the authenticated owner.
- Detect missing or disabled sandbox accounts after interrupted cleanup and reprovision them before restoring network access.

GitOrigin-RevId: 7d63fff7ddcff3eb01d018653146df12044bd277
This commit is contained in:
chess authored and copyberry committed 2026-09-02 22:19:51 +00:00
1 parent a14ef02e1c
commit 665e5f45ab
25 files changed
+1349 -97

No files matched your search

+1
View File
@@ -74,6 +74,7 @@ features = [
"Win32_System_Memory",
"Win32_System_Kernel",
"Win32_System_Console",
"Win32_System_RemoteDesktop",
"Win32_Storage_FileSystem",
"Win32_System_Diagnostics_ToolHelp",
"Win32_NetworkManagement_NetManagement",
@@ -11,6 +11,7 @@ use codex_windows_sandbox::SETUP_VERSION;
use codex_windows_sandbox::SetupErrorCode;
use codex_windows_sandbox::SetupErrorReport;
use codex_windows_sandbox::SetupFailure;
use codex_windows_sandbox::acquire_sandbox_setup_lock;
use codex_windows_sandbox::add_deny_write_ace;
use codex_windows_sandbox::convert_string_sid_to_sid;
use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance;
@@ -18,6 +19,7 @@ use codex_windows_sandbox::ensure_allow_write_aces;
use codex_windows_sandbox::extract_setup_failure;
use codex_windows_sandbox::hide_newly_created_users;
use codex_windows_sandbox::install_wfp_filters;
use codex_windows_sandbox::local_user_flags;
use codex_windows_sandbox::log_note;
use codex_windows_sandbox::log_writer;
use codex_windows_sandbox::open_directory_no_reparse;
@@ -27,6 +29,7 @@ use codex_windows_sandbox::resolve_sid;
use codex_windows_sandbox::sandbox_bin_dir;
use codex_windows_sandbox::sandbox_dir;
use codex_windows_sandbox::sandbox_secrets_dir;
use codex_windows_sandbox::set_local_user_flags;
use codex_windows_sandbox::setup_error_path;
use codex_windows_sandbox::setup_log_writer;
use codex_windows_sandbox::string_from_sid_bytes;
@@ -52,6 +55,7 @@ use std::sync::mpsc;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::NetworkManagement::NetManagement::UF_ACCOUNTDISABLE;
use windows_sys::Win32::Security::ACL;
use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW;
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
@@ -75,6 +79,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
use windows_sys::Win32::Storage::FileSystem::WRITE_DAC;
use windows_sys::Win32::System::Threading::INFINITE;
const DENY_ACCESS: i32 = 3;
#[cfg(test)]
@@ -655,15 +660,26 @@ fn run_read_acl_only(payload: &Payload, log: &mut dyn Write) -> Result<()> {
Ok(())
}
fn provision_and_hide_sandbox_users(
payload: &Payload,
log: &mut dyn Write,
sbx_dir: &Path,
) -> Result<()> {
fn provision_sandbox(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> {
let _setup_lock = acquire_sandbox_setup_lock(INFINITE)?;
let mut repairing_disabled_accounts = false;
for username in [&payload.offline_username, &payload.online_username] {
if local_user_flags(username)?.is_some_and(|flags| flags & UF_ACCOUNTDISABLE != 0) {
repairing_disabled_accounts = true;
}
}
// Interrupted cleanup can leave one account missing and the other disabled. Keep any
// replacement disabled too until this repair has restored the network restrictions.
let new_user_flags = if repairing_disabled_accounts {
UF_ACCOUNTDISABLE
} else {
0
};
let provision_result = provision_sandbox_users(
&payload.codex_home,
&payload.offline_username,
&payload.online_username,
new_user_flags,
log,
payload.mode,
);
@@ -681,6 +697,36 @@ fn provision_and_hide_sandbox_users(
payload.online_username.clone(),
];
hide_newly_created_users(&users, sbx_dir);
let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| {
anyhow::Error::new(SetupFailure::new(
SetupErrorCode::HelperSidResolveFailed,
format!(
"resolve SID for offline user {} failed: {err}",
payload.offline_username
),
))
})?;
let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?;
configure_offline_sandbox_network(payload, &offline_sid_str, log)?;
let wfp_result = install_wfp_filters(
&payload.codex_home,
&payload.offline_username,
payload.otel.as_ref(),
|message| {
let _ = log_line(log, message);
},
);
if repairing_disabled_accounts {
// Ordinary setup keeps its best-effort WFP behavior. Recovery must not reopen logons
// after cleanup removed protections unless restoring those protections succeeded.
wfp_result?;
for username in [&payload.offline_username, &payload.online_username] {
let flags = local_user_flags(username)?.ok_or_else(|| {
anyhow::anyhow!("sandbox user {username} disappeared during repair")
})?;
set_local_user_flags(username, flags & !UF_ACCOUNTDISABLE)?;
}
}
Ok(())
}
@@ -714,14 +760,6 @@ fn configure_offline_sandbox_network(
format!("ensure offline outbound block failed: {err}"),
)));
}
install_wfp_filters(
&payload.codex_home,
&payload.offline_username,
payload.otel.as_ref(),
|message| {
let _ = log_line(log, message);
},
);
Ok(())
}
@@ -794,17 +832,7 @@ fn lock_sandbox_bin_dir(payload: &Payload, sandbox_group_sid: &[u8]) -> Result<(
}
fn run_provision_only(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> {
provision_and_hide_sandbox_users(payload, log, sbx_dir)?;
let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| {
anyhow::Error::new(SetupFailure::new(
SetupErrorCode::HelperSidResolveFailed,
format!(
"resolve SID for offline user {} failed: {err}",
payload.offline_username
),
))
})?;
let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?;
provision_sandbox(payload, log, sbx_dir)?;
let sandbox_group_sid = resolve_sandbox_users_group_sid().map_err(|err| {
anyhow::Error::new(SetupFailure::new(
@@ -813,8 +841,6 @@ fn run_provision_only(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) ->
))
})?;
configure_offline_sandbox_network(payload, &offline_sid_str, log)?;
lock_sandbox_bin_dir(payload, &sandbox_group_sid)?;
lock_persistent_sandbox_dirs(payload, &sandbox_group_sid)?;
log_note("setup provisioning binary completed", Some(sbx_dir));
@@ -824,18 +850,8 @@ fn run_provision_only(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) ->
fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> {
let refresh_only = payload.refresh_only;
if !refresh_only {
provision_and_hide_sandbox_users(payload, log, sbx_dir)?;
provision_sandbox(payload, log, sbx_dir)?;
}
let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| {
anyhow::Error::new(SetupFailure::new(
SetupErrorCode::HelperSidResolveFailed,
format!(
"resolve SID for offline user {} failed: {err}",
payload.offline_username
),
))
})?;
let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?;
let sandbox_group_sid = resolve_sandbox_users_group_sid().map_err(|err| {
anyhow::Error::new(SetupFailure::new(
@@ -853,9 +869,6 @@ fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Res
string_from_sid_bytes(&sandbox_group_sid).map_err(anyhow::Error::msg)?;
let mut refresh_errors: Vec<String> = Vec::new();
if !refresh_only {
configure_offline_sandbox_network(payload, &offline_sid_str, log)?;
}
// Deny-read ACEs must be present before the sandboxed command starts. Apply
// them synchronously here instead of delegating them to the background
@@ -63,6 +63,7 @@ pub(super) fn provision_sandbox_users(
codex_home: &Path,
offline_username: &str,
online_username: &str,
new_user_flags: u32,
log: &mut dyn Write,
mode: SetupMode,
) -> Result<()> {
@@ -80,8 +81,8 @@ pub(super) fn provision_sandbox_users(
)?;
let offline_password = random_password();
let online_password = random_password();
ensure_sandbox_user(offline_username, &offline_password, log)?;
ensure_sandbox_user(online_username, &online_password, log)?;
ensure_sandbox_user(offline_username, &offline_password, new_user_flags, log)?;
ensure_sandbox_user(online_username, &online_password, new_user_flags, log)?;
write_secrets(
codex_home,
offline_username,
@@ -93,13 +94,23 @@ pub(super) fn provision_sandbox_users(
Ok(())
}
pub fn ensure_sandbox_user(username: &str, password: &str, log: &mut dyn Write) -> Result<()> {
ensure_local_user(username, password, log)?;
pub fn ensure_sandbox_user(
username: &str,
password: &str,
new_user_flags: u32,
log: &mut dyn Write,
) -> Result<()> {
ensure_local_user(username, password, new_user_flags, log)?;
ensure_local_group_member(SANDBOX_USERS_GROUP, username)?;
Ok(())
}
pub fn ensure_local_user(name: &str, password: &str, log: &mut dyn Write) -> Result<()> {
pub fn ensure_local_user(
name: &str,
password: &str,
new_user_flags: u32,
log: &mut dyn Write,
) -> Result<()> {
let name_w = to_wide(OsStr::new(name));
let pwd_w = to_wide(OsStr::new(password));
unsafe {
@@ -110,7 +121,7 @@ pub fn ensure_local_user(name: &str, password: &str, log: &mut dyn Write) -> Res
usri1_priv: USER_PRIV_USER,
usri1_home_dir: std::ptr::null_mut(),
usri1_comment: std::ptr::null_mut(),
usri1_flags: UF_SCRIPT | UF_DONT_EXPIRE_PASSWD,
usri1_flags: UF_SCRIPT | UF_DONT_EXPIRE_PASSWD | new_user_flags,
usri1_script_path: std::ptr::null_mut(),
};
let status = NetUserAdd(
@@ -31,6 +31,7 @@ use std::time::Instant;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS;
use windows_sys::Win32::Foundation::DuplicateHandle;
use windows_sys::Win32::Foundation::ERROR_ACCOUNT_DISABLED;
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
@@ -97,7 +98,10 @@ pub(crate) struct RunnerTransport {
}
fn is_refreshable_windows_error(code: u32) -> bool {
matches!(code, ERROR_LOGON_FAILURE | ERROR_NO_SUCH_LOGON_SESSION)
matches!(
code,
ERROR_ACCOUNT_DISABLED | ERROR_LOGON_FAILURE | ERROR_NO_SUCH_LOGON_SESSION
)
}
fn command_targets_windows_apps(command: &[String]) -> bool {
@@ -455,6 +459,7 @@ mod tests {
use crate::ipc_framed::ErrorPayload;
use crate::ipc_framed::ErrorStage;
use pretty_assertions::assert_eq;
use windows_sys::Win32::Foundation::ERROR_ACCOUNT_DISABLED;
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
@@ -463,6 +468,7 @@ mod tests {
fn refreshable_sandbox_creds_error_recognizes_credential_and_child_start_failures() {
assert_eq!(
[
ERROR_ACCOUNT_DISABLED,
ERROR_LOGON_FAILURE,
ERROR_NO_SUCH_LOGON_SESSION,
ERROR_NOT_FOUND,
@@ -472,7 +478,7 @@ mod tests {
anyhow::Error::new(RunnerLogonError { code }).context("runner launch failed");
is_refreshable_sandbox_creds_error(&err, &[])
}),
[true, true, false]
[true, true, true, false]
);
assert_eq!(
@@ -5,6 +5,9 @@ use anyhow::anyhow;
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::Foundation::ERROR_PATH_NOT_FOUND;
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM;
@@ -13,11 +16,14 @@ use windows_sys::Win32::Storage::FileSystem::INVALID_FILE_ATTRIBUTES;
use windows_sys::Win32::Storage::FileSystem::SetFileAttributesW;
use windows_sys::Win32::System::Registry::HKEY;
use windows_sys::Win32::System::Registry::HKEY_LOCAL_MACHINE;
use windows_sys::Win32::System::Registry::KEY_SET_VALUE;
use windows_sys::Win32::System::Registry::KEY_WRITE;
use windows_sys::Win32::System::Registry::REG_DWORD;
use windows_sys::Win32::System::Registry::REG_OPTION_NON_VOLATILE;
use windows_sys::Win32::System::Registry::RegCloseKey;
use windows_sys::Win32::System::Registry::RegCreateKeyExW;
use windows_sys::Win32::System::Registry::RegDeleteValueW;
use windows_sys::Win32::System::Registry::RegOpenKeyExW;
use windows_sys::Win32::System::Registry::RegSetValueExW;
const USERLIST_KEY_PATH: &str =
@@ -35,6 +41,40 @@ pub fn hide_newly_created_users(usernames: &[String], log_base: &Path) {
}
}
pub(crate) fn unhide_sandbox_users(usernames: &[&str]) -> anyhow::Result<()> {
let mut key: HKEY = 0;
match unsafe {
RegOpenKeyExW(
HKEY_LOCAL_MACHINE,
to_wide(USERLIST_KEY_PATH).as_ptr(),
/*uloptions*/ 0,
KEY_SET_VALUE,
&mut key,
)
} {
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND => return Ok(()),
ERROR_SUCCESS => {}
error => return Err(anyhow!("open sandbox hidden-user registry key: {error}")),
}
let mut errors = Vec::new();
for username in usernames {
let status = unsafe { RegDeleteValueW(key, to_wide(username).as_ptr()) };
match status {
ERROR_SUCCESS | ERROR_FILE_NOT_FOUND => {}
error => errors.push(format!("remove hidden sandbox user {username}: {error}")),
}
}
unsafe {
RegCloseKey(key);
}
if errors.is_empty() {
Ok(())
} else {
Err(anyhow!(errors.join("; ")))
}
}
/// Best-effort: hides the current sandbox user's profile directory once it exists.
///
/// Windows only creates profile directories when that user first logs in.
@@ -1,6 +1,8 @@
use crate::dpapi;
use crate::logging::debug_log;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::OFFLINE_USERNAME;
use crate::setup::ONLINE_USERNAME;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
@@ -12,6 +14,7 @@ use crate::setup::run_elevated_setup_with_proxy_settings;
use crate::setup::run_setup_refresh_with_overrides_and_proxy_settings;
use crate::setup::sandbox_users_path;
use crate::setup::setup_marker_path;
use crate::winutil::local_user_flags;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
@@ -21,6 +24,7 @@ use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use windows_sys::Win32::NetworkManagement::NetManagement::UF_ACCOUNTDISABLE;
#[derive(Debug, Clone)]
struct SandboxIdentity {
@@ -217,6 +221,23 @@ pub fn require_logon_sandbox_creds(
}
};
if identity.is_some() {
// Cleanup may also have removed the group, so repair missing or disabled accounts before ACL
// refresh can fail, not only after a later logon reports ERROR_ACCOUNT_DISABLED.
for username in [OFFLINE_USERNAME, ONLINE_USERNAME] {
let needs_repair = match local_user_flags(username) {
Ok(Some(flags)) => flags & UF_ACCOUNTDISABLE != 0,
Ok(None) => true,
Err(_) => false,
};
if needs_repair {
setup_reason = Some("sandbox account is missing or disabled".to_string());
identity = None;
break;
}
}
}
if identity.is_none() {
if let Some(reason) = &setup_reason {
crate::logging::log_note(
+13
View File
@@ -104,6 +104,8 @@ mod winutil;
mod workspace_acl;
mod deny_read_resolver;
#[cfg(target_os = "windows")]
mod uninstall_windows;
#[cfg(target_os = "windows")]
mod conpty;
@@ -129,6 +131,9 @@ mod setup_error;
#[cfg(target_os = "windows")]
mod setup_launch;
#[cfg(target_os = "windows")]
mod setup_mutex;
#[cfg(target_os = "windows")]
mod spawn_prep;
@@ -354,6 +359,8 @@ pub use setup_error::setup_error_path;
#[cfg(target_os = "windows")]
pub use setup_error::write_setup_error_report;
#[cfg(target_os = "windows")]
pub use setup_mutex::acquire_sandbox_setup_lock;
#[cfg(target_os = "windows")]
pub use stdio_bridge::forward_sandbox_session_stdio;
#[cfg(target_os = "windows")]
#[doc(hidden)]
@@ -381,6 +388,8 @@ pub use unified_exec::spawn_windows_sandbox_session_for_level;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_legacy;
#[cfg(target_os = "windows")]
pub use uninstall_windows::clean_up_packaged_windows_sandbox;
#[cfg(target_os = "windows")]
pub use wfp::install_wfp_filters_for_account;
#[cfg(target_os = "windows")]
pub use wfp_setup::install_wfp_filters;
@@ -397,10 +406,14 @@ pub use winutil::SANDBOX_USERS_GROUP;
#[cfg(target_os = "windows")]
pub use winutil::ensure_sandbox_users_group;
#[cfg(target_os = "windows")]
pub use winutil::local_user_flags;
#[cfg(target_os = "windows")]
pub use winutil::quote_windows_arg;
#[cfg(target_os = "windows")]
pub use winutil::resolve_sid;
#[cfg(target_os = "windows")]
pub use winutil::set_local_user_flags;
#[cfg(target_os = "windows")]
pub use winutil::string_from_sid_bytes;
#[cfg(target_os = "windows")]
pub use winutil::to_wide;
@@ -0,0 +1,83 @@
//! Serializes sandbox account and network changes across setup and uninstall.
//! The acquiring thread must hold the lock until those changes finish.
use std::io;
use std::marker::PhantomData;
use std::os::windows::io::AsRawHandle;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::ptr::null_mut;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Foundation::WAIT_ABANDONED;
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
use windows_sys::Win32::Foundation::WAIT_TIMEOUT;
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::System::Threading::CreateMutexW;
use windows_sys::Win32::System::Threading::ReleaseMutex;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
use crate::winutil::to_wide;
/// Holds the machine-wide setup lock on the thread that acquired it.
#[must_use]
pub struct SandboxSetupLock {
handle: OwnedHandle,
_thread_bound: PhantomData<*mut ()>,
}
impl Drop for SandboxSetupLock {
fn drop(&mut self) {
unsafe {
ReleaseMutex(self.handle.as_raw_handle() as HANDLE);
}
}
}
/// Waits up to `timeout_ms` for exclusive access by LocalSystem or an elevated administrator.
pub fn acquire_sandbox_setup_lock(timeout_ms: u32) -> Result<SandboxSetupLock> {
let sddl = to_wide("D:P(A;;GA;;;SY)(A;;GA;;;BA)");
let mut descriptor = null_mut();
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
/*stringsdrevision*/ 1,
&mut descriptor,
null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error()).context("create sandbox setup mutex security");
}
let attributes = SECURITY_ATTRIBUTES {
nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor,
bInheritHandle: 0,
};
let name = to_wide(r"Global\CodexSandboxSetup");
let handle = unsafe {
CreateMutexW(&attributes, /*binitialowner*/ 0, name.as_ptr())
};
let result = if handle == 0 {
Err(io::Error::last_os_error())
} else {
Ok(unsafe { OwnedHandle::from_raw_handle(handle as _) })
};
unsafe { LocalFree(descriptor as HLOCAL) };
let handle = result.context("open sandbox setup mutex")?;
match unsafe { WaitForSingleObject(handle.as_raw_handle() as HANDLE, timeout_ms) } {
// A crashed owner releases the lock; setup still repairs any incomplete account changes.
WAIT_OBJECT_0 | WAIT_ABANDONED => Ok(SandboxSetupLock {
handle,
_thread_bound: PhantomData,
}),
WAIT_TIMEOUT => bail!("timed out waiting for sandbox setup mutex after {timeout_ms} ms"),
_ => Err(io::Error::last_os_error()).context("wait for sandbox setup mutex"),
}
}
@@ -0,0 +1,72 @@
//! Stops sandbox work before removing its protections, then continues independent cleanup.
use std::path::Path;
use anyhow::Result;
use anyhow::anyhow;
use crate::setup::OFFLINE_USERNAME;
use crate::setup::ONLINE_USERNAME;
mod firewall;
mod principals;
mod processes;
/// Removes sandbox resources created for one authenticated packaged installation.
/// Keep a supplied home and its ancestors pinned until `clean_up_desktop` starts.
/// That callback removes user-owned desktop files while the sandbox accounts remain disabled.
pub fn clean_up_packaged_windows_sandbox(
codex_home: Option<&Path>,
clean_up_desktop: impl FnOnce() -> Result<()>,
) -> Result<()> {
let _setup_lock = crate::setup_mutex::acquire_sandbox_setup_lock(/*timeout_ms*/ 5_000)?;
let mut errors = Vec::new();
let mut users = principals::DisabledSandboxUsers::default();
if let Err(error) = users.disable().and_then(|()| processes::stop(&users)) {
errors.push(format!("{error:#}"));
// No network protections have been removed, so failed preparation can restore these flags.
if let Err(error) = users.restore() {
errors.push(format!("{error:#}"));
}
return Err(anyhow!(errors.join("; ")));
}
if let Some(codex_home) = codex_home {
for directory in [
crate::setup::sandbox_dir(codex_home),
crate::setup::sandbox_secrets_dir(codex_home),
crate::setup::sandbox_bin_dir(codex_home),
] {
match std::fs::remove_dir_all(&directory) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
errors.push(format!("remove {}: {error}", directory.display()));
}
}
}
}
if let Err(error) = clean_up_desktop() {
errors.push(format!("{error:#}"));
}
for result in [
crate::wfp::remove_wfp_filters(),
firewall::cleanup_firewall_rules(),
principals::remove_sandbox_principal("CodexSandboxUsers"),
crate::hide_users::unhide_sandbox_users(&[OFFLINE_USERNAME, ONLINE_USERNAME]),
// Keep accounts disabled and setup locked until shared cleanup and account deletion finish.
principals::remove_sandbox_principal(OFFLINE_USERNAME),
principals::remove_sandbox_principal(ONLINE_USERNAME),
] {
if let Err(error) = result {
errors.push(format!("{error:#}"));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(anyhow!(errors.join("; ")))
}
}
@@ -0,0 +1,34 @@
//! Removes known sandbox firewall rules while preserving unrelated rules.
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use windows::Win32::NetworkManagement::WindowsFirewall::INetFwPolicy2;
use windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2;
use windows::Win32::System::Com::CLSCTX_INPROC_SERVER;
use windows::Win32::System::Com::CoCreateInstance;
use windows::core::BSTR;
pub(super) fn cleanup_firewall_rules() -> Result<()> {
let policy: INetFwPolicy2 = unsafe {
CoCreateInstance(&NetFwPolicy2, /*punkouter*/ None, CLSCTX_INPROC_SERVER)
.context("access firewall policy for sandbox uninstall")?
};
let rules = unsafe { policy.Rules() }.context("access sandbox firewall rules")?;
let mut errors = Vec::new();
for name in [
"codex_sandbox_offline_block_outbound",
"codex_sandbox_offline_block_loopback_tcp",
"codex_sandbox_offline_block_loopback_udp",
"codex_sandbox_offline_allow_loopback_proxy",
] {
if let Err(error) = unsafe { rules.Remove(&BSTR::from(name)) } {
errors.push(format!("remove sandbox firewall rule {name}: {error}"));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(anyhow!(errors.join("; ")))
}
}
@@ -0,0 +1,80 @@
//! Disables sandbox accounts for cleanup and removes sandbox principals.
//! Callers must restore original flags if preparation for cleanup fails.
use std::ptr::null;
use anyhow::Result;
use anyhow::bail;
use windows_sys::Win32::NetworkManagement::NetManagement as network;
use crate::setup::OFFLINE_USERNAME;
use crate::setup::ONLINE_USERNAME;
use crate::winutil::local_user_flags;
use crate::winutil::resolve_sid;
use crate::winutil::set_local_user_flags;
use crate::winutil::to_wide;
#[derive(Default)]
pub(super) struct DisabledSandboxUsers {
users: Vec<SandboxUser>,
}
struct SandboxUser {
name: &'static str,
original_flags: u32,
sid: Vec<u8>,
}
impl DisabledSandboxUsers {
pub(super) fn disable(&mut self) -> Result<()> {
for name in [OFFLINE_USERNAME, ONLINE_USERNAME] {
let Some(original_flags) = local_user_flags(name)? else {
continue;
};
let sid = resolve_sid(name)?;
self.users.push(SandboxUser {
name,
original_flags,
sid,
});
set_local_user_flags(name, original_flags | network::UF_ACCOUNTDISABLE)?;
}
Ok(())
}
pub(super) fn sids(&self) -> impl Iterator<Item = &[u8]> {
self.users.iter().map(|user| user.sid.as_slice())
}
pub(super) fn restore(&self) -> Result<()> {
let mut errors = Vec::new();
for user in &self.users {
if let Err(error) = set_local_user_flags(user.name, user.original_flags)
&& error
.downcast_ref::<std::io::Error>()
.and_then(std::io::Error::raw_os_error)
!= Some(network::NERR_UserNotFound as i32)
{
errors.push(format!("{error:#}"));
}
}
if !errors.is_empty() {
bail!("{}", errors.join("; "));
}
Ok(())
}
}
pub(super) fn remove_sandbox_principal(name: &str) -> Result<()> {
let name_wide = to_wide(name);
let status = if name == "CodexSandboxUsers" {
unsafe { network::NetLocalGroupDel(null(), name_wide.as_ptr()) }
} else {
unsafe { network::NetUserDel(null(), name_wide.as_ptr()) }
};
match status {
network::NERR_Success | network::NERR_GroupNotFound | network::NERR_UserNotFound => Ok(()),
status => bail!("remove local sandbox principal {name}: {status}"),
}
}
@@ -0,0 +1,220 @@
//! Stops sandbox-account processes and waits for their logon tokens to be released.
use std::ffi::c_void;
use std::io;
use std::os::windows::io::AsRawHandle;
use std::os::windows::io::FromRawHandle;
use std::os::windows::io::OwnedHandle;
use std::ptr::null_mut;
use std::time::Duration;
use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER;
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::LUID;
use windows_sys::Win32::Foundation::STATUS_SUCCESS;
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
use windows_sys::Win32::Foundation::WAIT_TIMEOUT;
use windows_sys::Win32::Security::Authentication::Identity::LsaEnumerateLogonSessions;
use windows_sys::Win32::Security::Authentication::Identity::LsaFreeReturnBuffer;
use windows_sys::Win32::Security::Authentication::Identity::LsaGetLogonSessionData;
use windows_sys::Win32::Security::Authentication::Identity::LsaNtStatusToWinError;
use windows_sys::Win32::Security::Authentication::Identity::SECURITY_LOGON_SESSION_DATA;
use windows_sys::Win32::Security::EqualSid;
use windows_sys::Win32::Security::TOKEN_QUERY;
use windows_sys::Win32::System::RemoteDesktop::WTS_CURRENT_SERVER_HANDLE;
use windows_sys::Win32::System::RemoteDesktop::WTS_PROCESS_INFOW;
use windows_sys::Win32::System::RemoteDesktop::WTSEnumerateProcessesW;
use windows_sys::Win32::System::RemoteDesktop::WTSFreeMemory;
use windows_sys::Win32::System::Threading::OpenProcess;
use windows_sys::Win32::System::Threading::OpenProcessToken;
use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
use windows_sys::Win32::System::Threading::PROCESS_SYNCHRONIZE;
use windows_sys::Win32::System::Threading::PROCESS_TERMINATE;
use windows_sys::Win32::System::Threading::TerminateProcess;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
use super::principals::DisabledSandboxUsers;
use crate::token::get_user_sid_bytes;
pub(super) fn stop(users: &DisabledSandboxUsers) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let processes = sandbox_processes(users)?;
for process in &processes {
let handle = process.as_raw_handle() as HANDLE;
if unsafe {
TerminateProcess(handle, /*uexitcode*/ 1)
} == 0
{
let error = io::Error::last_os_error();
if unsafe {
WaitForSingleObject(handle, /*dwmilliseconds*/ 0)
} != WAIT_OBJECT_0
{
return Err(error).context("stop sandbox process before uninstall");
}
}
}
for process in &processes {
let remaining = deadline.saturating_duration_since(Instant::now());
// The five-second deadline keeps milliseconds within a DWORD.
match unsafe {
WaitForSingleObject(
process.as_raw_handle() as HANDLE,
remaining.as_millis() as u32,
)
} {
WAIT_OBJECT_0 => {}
WAIT_TIMEOUT => bail!("sandbox process did not exit before the uninstall deadline"),
_ => {
return Err(io::Error::last_os_error())
.context("wait for sandbox process exit");
}
}
}
drop(processes);
// A token can outlive its process or exist before a runner starts. Account disable does
// not revoke it, so release our handles and check for tokens before removing protections.
if !has_sandbox_logon_session(users)? {
return Ok(());
}
if Instant::now() >= deadline {
bail!("sandbox processes or logon tokens remain after the uninstall deadline");
}
// Repeat to catch descendants created during the previous snapshot.
std::thread::sleep(
Duration::from_millis(50).min(deadline.saturating_duration_since(Instant::now())),
);
}
}
fn sandbox_processes(users: &DisabledSandboxUsers) -> Result<Vec<OwnedHandle>> {
let mut process_info = null_mut();
let mut count = 0;
if unsafe {
WTSEnumerateProcessesW(
WTS_CURRENT_SERVER_HANDLE,
/*reserved*/ 0,
/*version*/ 1,
&mut process_info,
&mut count,
)
} == 0
{
return Err(io::Error::last_os_error())
.context("enumerate sandbox processes for uninstall");
}
let process_info = ProcessList(process_info);
let mut handles = Vec::new();
if count == 0 {
return Ok(handles);
}
for process in unsafe { std::slice::from_raw_parts(process_info.0, count as usize) } {
if !is_sandbox_user(users, process.pUserSid) {
continue;
}
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | PROCESS_SYNCHRONIZE,
/*binherithandle*/ 0,
process.ProcessId,
)
};
if handle == 0 {
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
continue;
}
return Err(error).context("open sandbox process for uninstall");
}
let handle = unsafe { OwnedHandle::from_raw_handle(handle as *mut c_void) };
let mut token = 0;
if unsafe { OpenProcessToken(handle.as_raw_handle() as HANDLE, TOKEN_QUERY, &mut token) }
== 0
{
let error = io::Error::last_os_error();
if unsafe {
WaitForSingleObject(handle.as_raw_handle() as HANDLE, /*dwmilliseconds*/ 0)
} == WAIT_OBJECT_0
{
continue;
}
return Err(error).context("identify sandbox process before uninstall");
}
let token = unsafe { OwnedHandle::from_raw_handle(token as *mut c_void) };
let sid = unsafe { get_user_sid_bytes(token.as_raw_handle() as HANDLE) }?;
// A PID can be reused after enumeration. Check the opened process before terminating it.
if users.sids().any(|user_sid| user_sid == sid.as_slice()) {
handles.push(handle);
}
}
Ok(handles)
}
fn has_sandbox_logon_session(users: &DisabledSandboxUsers) -> Result<bool> {
let mut count = 0;
let mut logons = null_mut();
let status = unsafe { LsaEnumerateLogonSessions(&mut count, &mut logons) };
if status != STATUS_SUCCESS {
bail!("enumerate sandbox logon sessions for uninstall: {status:#x}");
}
let logons = LsaBuffer(logons.cast());
if count == 0 {
return Ok(false);
}
for logon in unsafe { std::slice::from_raw_parts(logons.0.cast::<LUID>(), count as usize) } {
// LocalSystem uses the reserved logon ID 0:0x3e7 and has no normal logon data.
if logon.LowPart == 0x3e7 && logon.HighPart == 0 {
continue;
}
let mut data = null_mut();
let status = unsafe { LsaGetLogonSessionData(logon, &mut data) };
if status != STATUS_SUCCESS {
if unsafe { LsaNtStatusToWinError(status) } == ERROR_NO_SUCH_LOGON_SESSION {
continue;
}
bail!("read sandbox logon session for uninstall: {status:#x}");
}
// Keep protections when LSA returns no session data.
if data.is_null() {
return Ok(true);
}
let data = LsaBuffer(data.cast());
let data = unsafe { &*data.0.cast::<SECURITY_LOGON_SESSION_DATA>() };
// LSA also lists sessions without a user SID, even before sandbox accounts exist.
// Only sessions identified as sandbox users are evidence of remaining sandbox tokens.
if is_sandbox_user(users, data.Sid) {
return Ok(true);
}
}
Ok(false)
}
fn is_sandbox_user(users: &DisabledSandboxUsers, sid: *mut c_void) -> bool {
!sid.is_null()
&& users
.sids()
.any(|user_sid| unsafe { EqualSid(sid, user_sid.as_ptr() as *mut c_void) } != 0)
}
struct ProcessList(*mut WTS_PROCESS_INFOW);
impl Drop for ProcessList {
fn drop(&mut self) {
unsafe { WTSFreeMemory(self.0.cast()) };
}
}
struct LsaBuffer(*mut c_void);
impl Drop for LsaBuffer {
fn drop(&mut self) {
unsafe { LsaFreeReturnBuffer(self.0) };
}
}
+33 -4
View File
@@ -9,6 +9,8 @@ use std::ptr::null_mut;
use windows_sys::Win32::Foundation::FWP_E_ALREADY_EXISTS;
use windows_sys::Win32::Foundation::FWP_E_FILTER_NOT_FOUND;
use windows_sys::Win32::Foundation::FWP_E_NOT_FOUND;
use windows_sys::Win32::Foundation::FWP_E_PROVIDER_NOT_FOUND;
use windows_sys::Win32::Foundation::FWP_E_SUBLAYER_NOT_FOUND;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
@@ -43,7 +45,9 @@ use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmEngineO
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmFilterAdd0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmFilterDeleteByKey0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmProviderAdd0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmProviderDeleteByKey0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmSubLayerAdd0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmSubLayerDeleteByKey0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionAbort0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionBegin0;
use windows_sys::Win32::NetworkManagement::WindowsFilteringPlatform::FwpmTransactionCommit0;
@@ -75,9 +79,10 @@ const SUBLAYER_KEY: GUID = GUID::from_u128(0xe65054fd_4d32_4c7c_95ef_621f0cf6431
/// Installs the persistent Codex WFP filters for `account`.
///
/// This is intended to run from the already-elevated setup helper. Callers
/// should treat any returned error as non-fatal to the rest of setup.
/// may continue ordinary setup after an error, but must restore these filters
/// before re-enabling accounts left disabled by interrupted cleanup.
pub fn install_wfp_filters_for_account(account: &str) -> Result<usize> {
let engine = Engine::open()?;
let engine = Engine::open(INFINITE)?;
let mut transaction = engine.begin_transaction()?;
ensure_provider(engine.handle)?;
ensure_sublayer(engine.handle)?;
@@ -94,20 +99,44 @@ pub fn install_wfp_filters_for_account(account: &str) -> Result<usize> {
Ok(installed_filter_count)
}
pub(crate) fn remove_wfp_filters() -> Result<()> {
// Leave time for other cleanup if a WFP policy writer holds the transaction lock.
let engine = Engine::open(/*transaction_wait_timeout_ms*/ 1_000)?;
let mut transaction = engine.begin_transaction()?;
for spec in FILTER_SPECS {
delete_filter_if_present(engine.handle, &spec.key)?;
}
for (result, operation, missing) in [
(
unsafe { FwpmSubLayerDeleteByKey0(engine.handle, &SUBLAYER_KEY) },
"FwpmSubLayerDeleteByKey0",
FWP_E_SUBLAYER_NOT_FOUND as u32,
),
(
unsafe { FwpmProviderDeleteByKey0(engine.handle, &PROVIDER_KEY) },
"FwpmProviderDeleteByKey0",
FWP_E_PROVIDER_NOT_FOUND as u32,
),
] {
ensure_success_or(result, operation, &[missing, FWP_E_NOT_FOUND as u32])?;
}
transaction.commit()
}
/// Owns an open WFP engine handle and closes it on drop.
struct Engine {
handle: HANDLE,
}
impl Engine {
fn open() -> Result<Self> {
fn open(transaction_wait_timeout_ms: u32) -> Result<Self> {
let session_name = to_wide(OsStr::new(SESSION_NAME));
let mut session: FWPM_SESSION0 = unsafe { zeroed() };
session.displayData = FWPM_DISPLAY_DATA0 {
name: session_name.as_ptr() as *mut _,
description: null_mut(),
};
session.txnWaitTimeoutInMSec = INFINITE;
session.txnWaitTimeoutInMSec = transaction_wait_timeout_ms;
let mut handle = HANDLE::default();
let result = unsafe {
+8 -5
View File
@@ -128,7 +128,8 @@ pub fn install_wfp_filters<F>(
offline_username: &str,
otel: Option<&StatsigMetricsSettings>,
mut log: F,
) where
) -> Result<()>
where
F: FnMut(&str),
{
let metric = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
@@ -147,9 +148,7 @@ pub fn install_wfp_filters<F>(
}
Ok(Err(err)) => {
let error = err.to_string();
log(&format!(
"WFP setup failed for {offline_username}: {error}; continuing elevated setup"
));
log(&format!("WFP setup failed for {offline_username}: {error}"));
WfpSetupMetric {
outcome: WfpSetupMetricOutcome::Failure,
target_account: offline_username.to_string(),
@@ -160,7 +159,7 @@ pub fn install_wfp_filters<F>(
Err(panic_payload) => {
let error = panic_payload_to_string(panic_payload);
log(&format!(
"WFP setup panicked for {offline_username}: {error}; continuing elevated setup"
"WFP setup panicked for {offline_username}: {error}"
));
WfpSetupMetric {
outcome: WfpSetupMetricOutcome::Failure,
@@ -172,4 +171,8 @@ pub fn install_wfp_filters<F>(
};
emit_wfp_setup_metric_safely(codex_home, otel, offline_username, &metric, &mut log);
match metric.error {
Some(error) => Err(anyhow::anyhow!(error)),
None => Ok(()),
}
}
@@ -1,3 +1,4 @@
use anyhow::Context;
use anyhow::Result;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
@@ -7,7 +8,13 @@ use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_INFO_1;
use windows_sys::Win32::NetworkManagement::NetManagement::NERR_Success;
use windows_sys::Win32::NetworkManagement::NetManagement::NERR_UserNotFound;
use windows_sys::Win32::NetworkManagement::NetManagement::NetApiBufferFree;
use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAdd;
use windows_sys::Win32::NetworkManagement::NetManagement::NetUserGetInfo;
use windows_sys::Win32::NetworkManagement::NetManagement::NetUserSetInfo;
use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1;
use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1008;
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW;
use windows_sys::Win32::Security::CopySid;
@@ -135,6 +142,50 @@ const SID_AUTHENTICATED_USERS: &str = "S-1-5-11";
const SID_EVERYONE: &str = "S-1-1-0";
const SID_SYSTEM: &str = "S-1-5-18";
pub fn local_user_flags(name: &str) -> Result<Option<u32>> {
let name_wide = to_wide(name);
let mut buffer = std::ptr::null_mut();
let status = unsafe {
NetUserGetInfo(
std::ptr::null(),
name_wide.as_ptr(),
/*level*/ 1,
&mut buffer,
)
};
if status == NERR_UserNotFound {
return Ok(None);
}
if status != NERR_Success {
return Err(std::io::Error::from_raw_os_error(status as i32))
.with_context(|| format!("read local sandbox user {name}"));
}
let flags = unsafe { (*buffer.cast::<USER_INFO_1>()).usri1_flags };
unsafe { NetApiBufferFree(buffer.cast()) };
Ok(Some(flags))
}
pub fn set_local_user_flags(name: &str, flags: u32) -> Result<()> {
let name_wide = to_wide(name);
let info = USER_INFO_1008 {
usri1008_flags: flags,
};
let status = unsafe {
NetUserSetInfo(
std::ptr::null(),
name_wide.as_ptr(),
/*level*/ 1008,
(&raw const info).cast(),
std::ptr::null_mut(),
)
};
if status != NERR_Success {
return Err(std::io::Error::from_raw_os_error(status as i32))
.with_context(|| format!("set local sandbox user {name} flags"));
}
Ok(())
}
pub fn ensure_sandbox_users_group() -> Result<Vec<u8>> {
const ERROR_ALIAS_EXISTS: u32 = 1379;
const NERR_GROUP_EXISTS: u32 = 2223;