mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
1 parent
a14ef02e1c
commit
665e5f45ab
25 files changed
+1349
-97
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user