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
@@ -22,9 +22,18 @@ codex-cloud-config = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-windows-sandbox = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
toml = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
version = "0.58"
|
||||
features = [
|
||||
"ApplicationModel",
|
||||
"Win32_System_WinRT",
|
||||
]
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
version = "0.52"
|
||||
features = [
|
||||
@@ -40,6 +49,8 @@ features = [
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Services",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Persists the authenticated sandbox owner across service restarts and package updates.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::mem::size_of;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::validate_local_directory_path;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use windows_sys::Win32::Foundation as foundation;
|
||||
use windows_sys::Win32::System::Registry as registry;
|
||||
use windows_sys::Win32::UI::Shell::GetUserProfileDirectoryW;
|
||||
|
||||
// Package updates can replace the service key, so keep this record outside it.
|
||||
const INSTALLATION_KEY: &str = r"SOFTWARE\OpenAI\Codex\WindowsSandboxService";
|
||||
const INSTALLATION_VALUE: &str = "ProvisionedInstallation";
|
||||
const MAX_VALUE_UNITS: usize = 4096;
|
||||
const DESKTOP_INSTALLATION_MARKER: &str = ".desktop-created";
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct DesktopInstallation {
|
||||
pub(crate) created_codex_home: bool,
|
||||
pub(crate) cache_home: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct InstallationRecord {
|
||||
pub(crate) user_sid: String,
|
||||
pub(crate) codex_home: PathBuf,
|
||||
pub(crate) session_id: u32,
|
||||
#[serde(default)]
|
||||
pub(crate) desktop_installation: Option<DesktopInstallation>,
|
||||
}
|
||||
|
||||
// Read this app-owned marker while impersonating the authenticated user. The
|
||||
// desktop writes it only when creating the home. Cache cleanup does not need ownership.
|
||||
pub(crate) fn read_desktop_installation(
|
||||
home: &Path,
|
||||
user_token: foundation::HANDLE,
|
||||
) -> Result<DesktopInstallation> {
|
||||
let mut length = 0;
|
||||
unsafe { GetUserProfileDirectoryW(user_token, ptr::null_mut(), &mut length) };
|
||||
let mut profile = vec![0_u16; length as usize];
|
||||
if unsafe { GetUserProfileDirectoryW(user_token, profile.as_mut_ptr(), &mut length) } == 0 {
|
||||
return Err(io::Error::last_os_error()).context("read sandbox owner's profile directory");
|
||||
}
|
||||
let cache_home =
|
||||
PathBuf::from(OsString::from_wide(&profile[..length as usize - 1])).join(".cache");
|
||||
validate_local_directory_path(&cache_home)?;
|
||||
Ok(DesktopInstallation {
|
||||
created_codex_home: home.join(DESKTOP_INSTALLATION_MARKER).is_file(),
|
||||
cache_home,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn load() -> Result<Option<InstallationRecord>> {
|
||||
let mut value = [0_u16; MAX_VALUE_UNITS];
|
||||
let mut value_length = std::mem::size_of_val(&value) as u32;
|
||||
let status = unsafe {
|
||||
registry::RegGetValueW(
|
||||
registry::HKEY_LOCAL_MACHINE,
|
||||
to_wide(INSTALLATION_KEY).as_ptr(),
|
||||
to_wide(INSTALLATION_VALUE).as_ptr(),
|
||||
registry::RRF_RT_REG_SZ,
|
||||
ptr::null_mut(),
|
||||
value.as_mut_ptr().cast(),
|
||||
&mut value_length,
|
||||
)
|
||||
};
|
||||
match status {
|
||||
foundation::ERROR_FILE_NOT_FOUND | foundation::ERROR_PATH_NOT_FOUND => return Ok(None),
|
||||
foundation::ERROR_SUCCESS => {}
|
||||
status => {
|
||||
return Err(io::Error::from_raw_os_error(status as i32))
|
||||
.context("read protected sandbox installation record");
|
||||
}
|
||||
}
|
||||
ensure!(
|
||||
value_length as usize <= std::mem::size_of_val(&value)
|
||||
&& value_length.is_multiple_of(size_of::<u16>() as u32),
|
||||
"sandbox installation record has an invalid length"
|
||||
);
|
||||
let value = value[..value_length as usize / size_of::<u16>()]
|
||||
.strip_suffix(&[0])
|
||||
.context("sandbox installation record is not null-terminated")?;
|
||||
let value = String::from_utf16(value).context("decode sandbox installation record")?;
|
||||
let record = serde_json::from_str(&value).context("parse sandbox installation record")?;
|
||||
Ok(Some(record))
|
||||
}
|
||||
|
||||
pub(crate) fn save(record: &InstallationRecord) -> Result<()> {
|
||||
let value = to_wide(
|
||||
serde_json::to_string(record).context("serialize protected sandbox installation record")?,
|
||||
);
|
||||
ensure!(
|
||||
value.len() <= MAX_VALUE_UNITS,
|
||||
"sandbox installation record exceeds its size limit"
|
||||
);
|
||||
let status = unsafe {
|
||||
registry::RegSetKeyValueW(
|
||||
registry::HKEY_LOCAL_MACHINE,
|
||||
to_wide(INSTALLATION_KEY).as_ptr(),
|
||||
to_wide(INSTALLATION_VALUE).as_ptr(),
|
||||
registry::REG_SZ,
|
||||
value.as_ptr().cast(),
|
||||
(value.len() * size_of::<u16>()) as u32,
|
||||
)
|
||||
};
|
||||
if status == foundation::ERROR_SUCCESS {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::from_raw_os_error(status as i32))
|
||||
.context("persist protected sandbox installation record")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove() -> Result<()> {
|
||||
let status = unsafe {
|
||||
registry::RegDeleteKeyW(
|
||||
registry::HKEY_LOCAL_MACHINE,
|
||||
to_wide(INSTALLATION_KEY).as_ptr(),
|
||||
)
|
||||
};
|
||||
match status {
|
||||
foundation::ERROR_SUCCESS
|
||||
| foundation::ERROR_FILE_NOT_FOUND
|
||||
| foundation::ERROR_PATH_NOT_FOUND => Ok(()),
|
||||
status => Err(io::Error::from_raw_os_error(status as i32))
|
||||
.context("remove protected sandbox installation record"),
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,8 @@ use codex_windows_sandbox::sandbox_setup_is_complete_with_settings;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::write_provisioning_frame;
|
||||
use home::OwnedHandle;
|
||||
#[cfg(test)]
|
||||
use home::pin_existing_ancestors;
|
||||
pub(crate) use home::OwnedHandle;
|
||||
pub(crate) use home::pin_existing_ancestors;
|
||||
#[cfg(test)]
|
||||
use request::ProvisioningRequest;
|
||||
use request::validate_request;
|
||||
@@ -43,6 +42,8 @@ use windows_sys::Win32::Security::Authorization as authorization;
|
||||
use windows_sys::Win32::Storage::FileSystem as filesystem;
|
||||
use windows_sys::Win32::System::Pipes as pipes;
|
||||
|
||||
use crate::installation_record::InstallationRecord;
|
||||
|
||||
pub(crate) const PIPE_NAME: &str = codex_windows_sandbox::SANDBOX_PROVISIONING_PIPE_NAME;
|
||||
|
||||
const MAX_REQUEST_BYTES: usize = 4096;
|
||||
@@ -64,7 +65,12 @@ enum PipeConnection {
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub(crate) fn run(shutdown: Arc<AtomicBool>, on_ready: impl FnOnce() -> Result<()>) -> Result<()> {
|
||||
pub(crate) fn run(
|
||||
shutdown: Arc<AtomicBool>,
|
||||
on_ready: impl FnOnce() -> Result<()>,
|
||||
on_authenticated_user: impl Fn(&InstallationRecord, OwnedHandle) -> Result<()>,
|
||||
on_session_change: impl Fn() -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let sandbox_sid = ensure_sandbox_users_group()?;
|
||||
let sid_string = string_from_sid_bytes(&sandbox_sid).map_err(anyhow::Error::msg)?;
|
||||
let sddl = pipe_security_descriptor(&sid_string);
|
||||
@@ -109,12 +115,15 @@ pub(crate) fn run(shutdown: Arc<AtomicBool>, on_ready: impl FnOnce() -> Result<(
|
||||
on_ready().context("publish provisioning listener readiness")?;
|
||||
|
||||
while !shutdown.load(Ordering::Acquire) {
|
||||
if accept_pipe_connection(pipe.0)? == PipeConnection::Disconnected {
|
||||
continue;
|
||||
}
|
||||
let connection = accept_pipe_connection(pipe.0)?;
|
||||
if shutdown.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
// Session-change wakeups close the pipe immediately and can arrive disconnected.
|
||||
on_session_change().context("restore the signed-in user's uninstall listener")?;
|
||||
if connection == PipeConnection::Disconnected {
|
||||
continue;
|
||||
}
|
||||
|
||||
let authorized_process = match crate::package_identity::authorize_client_process(pipe.0) {
|
||||
Ok(process) => process,
|
||||
@@ -123,7 +132,13 @@ pub(crate) fn run(shutdown: Arc<AtomicBool>, on_ready: impl FnOnce() -> Result<(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let result = handle_request(pipe.0, &authorized_process, &sandbox_sid, &shutdown);
|
||||
let result = handle_request(
|
||||
pipe.0,
|
||||
&authorized_process,
|
||||
&sandbox_sid,
|
||||
&shutdown,
|
||||
&on_authenticated_user,
|
||||
);
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(error) if error.is::<home::UnsupportedHomeDrive>() => {
|
||||
@@ -241,6 +256,7 @@ fn handle_request(
|
||||
authorized_process: &crate::package_identity::AuthorizedClientProcess,
|
||||
sandbox_sid: &[u8],
|
||||
shutdown: &AtomicBool,
|
||||
on_authenticated_user: &dyn Fn(&InstallationRecord, OwnedHandle) -> Result<()>,
|
||||
) -> Result<SandboxProvisioningResponse> {
|
||||
let deadline = Instant::now() + REQUEST_IDLE_TIMEOUT;
|
||||
let mut request = [0_u8; MAX_REQUEST_BYTES];
|
||||
@@ -322,7 +338,22 @@ fn handle_request(
|
||||
return Err(error)
|
||||
.context("requested sandbox settings violate administrator-controlled machine policy");
|
||||
}
|
||||
// A policy-rejected request must not choose the uninstall owner. Use the
|
||||
// token already authenticated above instead of impersonating the pipe again.
|
||||
let previous = crate::installation_record::load()?.filter(|record| {
|
||||
record.user_sid == identity.user_sid && record.codex_home == identity.codex_home
|
||||
});
|
||||
let installation = InstallationRecord {
|
||||
codex_home: identity.codex_home.clone(),
|
||||
user_sid: identity.user_sid,
|
||||
session_id: identity.session_id,
|
||||
desktop_installation: previous
|
||||
.and_then(|record| record.desktop_installation)
|
||||
.or(identity.desktop_installation),
|
||||
};
|
||||
on_authenticated_user(&installation, identity.token)?;
|
||||
if sandbox_setup_is_complete_with_settings(&identity.codex_home, &request.settings) {
|
||||
crate::service::record_provisioned_user(&installation)?;
|
||||
return Ok(SandboxProvisioningResponse::Ok);
|
||||
}
|
||||
let helper = std::env::current_exe()
|
||||
@@ -352,6 +383,7 @@ fn handle_request(
|
||||
&retained_handles,
|
||||
) {
|
||||
Ok(()) => {
|
||||
crate::service::record_provisioned_user(&installation)?;
|
||||
crate::service::log_information(
|
||||
crate::service::EVENT_PROVISIONING_SUCCEEDED,
|
||||
"Codex sandbox provisioning completed successfully.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
use std::path::Path;
|
||||
@@ -20,6 +21,10 @@ use super::request::ProvisioningRequest;
|
||||
pub(super) struct ClientIdentity {
|
||||
pub(super) account: String,
|
||||
pub(super) codex_home: PathBuf,
|
||||
pub(super) user_sid: String,
|
||||
pub(super) session_id: u32,
|
||||
pub(super) token: OwnedHandle,
|
||||
pub(super) desktop_installation: Option<crate::installation_record::DesktopInstallation>,
|
||||
// Retained by both the service and helper throughout provisioning.
|
||||
pub(super) directory_handles: Vec<OwnedHandle>,
|
||||
}
|
||||
@@ -38,7 +43,7 @@ pub(super) fn authenticate_client(
|
||||
.context("impersonate provisioning client");
|
||||
}
|
||||
|
||||
let (identity, token) = authenticate_impersonated_client(
|
||||
let identity = authenticate_impersonated_client(
|
||||
authorized_process,
|
||||
sandbox_sid,
|
||||
&request.codex_home,
|
||||
@@ -47,7 +52,7 @@ pub(super) fn authenticate_client(
|
||||
&identity.codex_home,
|
||||
&request.settings,
|
||||
&request.listeners,
|
||||
token.0,
|
||||
identity.token.0,
|
||||
);
|
||||
Ok((identity, policy_result))
|
||||
})
|
||||
@@ -60,7 +65,7 @@ fn authenticate_impersonated_client(
|
||||
authorized_process: &crate::package_identity::AuthorizedClientProcess,
|
||||
sandbox_sid: &[u8],
|
||||
requested_home: &Path,
|
||||
) -> Result<(ClientIdentity, OwnedHandle)> {
|
||||
) -> Result<ClientIdentity> {
|
||||
let mut raw_token = 0;
|
||||
if unsafe {
|
||||
threading::OpenThreadToken(
|
||||
@@ -138,16 +143,34 @@ fn authenticate_impersonated_client(
|
||||
let sid = unsafe { ptr::read_unaligned(user.as_ptr().cast::<security::TOKEN_USER>()) }
|
||||
.User
|
||||
.Sid;
|
||||
let sid_length = unsafe { security::GetLengthSid(sid) };
|
||||
if sid_length == 0 {
|
||||
return Err(std::io::Error::last_os_error()).context("size provisioning client SID");
|
||||
}
|
||||
let user_sid = string_from_sid_bytes(unsafe {
|
||||
std::slice::from_raw_parts(sid.cast::<u8>(), sid_length as usize)
|
||||
})
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let account = account_name(sid)?;
|
||||
let (codex_home, handles) = prepare_codex_home(requested_home)?;
|
||||
Ok((
|
||||
ClientIdentity {
|
||||
account,
|
||||
codex_home,
|
||||
directory_handles: handles,
|
||||
},
|
||||
let desktop_installation =
|
||||
crate::installation_record::read_desktop_installation(&codex_home, token.0)
|
||||
.inspect_err(|_| {
|
||||
crate::service::log_error(
|
||||
crate::service::EVENT_SERVICE_FAILED,
|
||||
"unable to read desktop directory ownership; preserving desktop directories",
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
Ok(ClientIdentity {
|
||||
account,
|
||||
codex_home,
|
||||
user_sid,
|
||||
session_id: session,
|
||||
token,
|
||||
))
|
||||
desktop_installation,
|
||||
directory_handles: handles,
|
||||
})
|
||||
}
|
||||
|
||||
fn account_name(sid: *mut c_void) -> Result<String> {
|
||||
|
||||
@@ -33,7 +33,7 @@ impl std::fmt::Display for UnsupportedHomeDrive {
|
||||
|
||||
impl std::error::Error for UnsupportedHomeDrive {}
|
||||
|
||||
pub(super) struct OwnedHandle(pub(super) HANDLE);
|
||||
pub(crate) struct OwnedHandle(pub(crate) HANDLE);
|
||||
|
||||
impl Drop for OwnedHandle {
|
||||
fn drop(&mut self) {
|
||||
@@ -138,7 +138,7 @@ pub(super) fn prepare_codex_home(requested: &Path) -> Result<(PathBuf, Vec<Owned
|
||||
Ok((home, handles))
|
||||
}
|
||||
|
||||
pub(super) fn pin_existing_ancestors(path: &Path, handles: &mut Vec<OwnedHandle>) -> Result<()> {
|
||||
pub(crate) fn pin_existing_ancestors(path: &Path, handles: &mut Vec<OwnedHandle>) -> Result<()> {
|
||||
let mut current = PathBuf::new();
|
||||
for component in path.components() {
|
||||
current.push(component.as_os_str());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod installation_record;
|
||||
#[cfg(windows)]
|
||||
mod ipc;
|
||||
#[cfg(windows)]
|
||||
@@ -7,6 +9,8 @@ mod machine_policy;
|
||||
#[cfg(windows)]
|
||||
mod package_identity;
|
||||
#[cfg(windows)]
|
||||
mod package_lifecycle;
|
||||
#[cfg(windows)]
|
||||
mod service;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -165,7 +165,7 @@ fn package_family_name(
|
||||
))
|
||||
}
|
||||
|
||||
fn token_user(token: HANDLE) -> Result<Vec<u8>> {
|
||||
pub(crate) fn token_user(token: HANDLE) -> Result<Vec<u8>> {
|
||||
let mut length = 0;
|
||||
unsafe {
|
||||
security::GetTokenInformation(token, security::TokenUser, ptr::null_mut(), 0, &mut length)
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
//! Restores owner-scoped uninstall notifications and ties file cleanup to pinned directories.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_windows_sandbox::clean_up_packaged_windows_sandbox;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use windows::ApplicationModel::Package;
|
||||
use windows::ApplicationModel::PackageCatalog;
|
||||
use windows::ApplicationModel::PackageUninstallingEventArgs;
|
||||
use windows::Foundation::EventRegistrationToken;
|
||||
use windows::Foundation::TypedEventHandler;
|
||||
use windows::Win32::System::WinRT::RO_INIT_MULTITHREADED;
|
||||
use windows::Win32::System::WinRT::RoInitialize;
|
||||
use windows::Win32::System::WinRT::RoUninitialize;
|
||||
use windows::core::HSTRING;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Security as security;
|
||||
use windows_sys::Win32::System::RemoteDesktop::WTS_CURRENT_SERVER_HANDLE;
|
||||
use windows_sys::Win32::System::RemoteDesktop::WTSEnumerateSessionsW;
|
||||
use windows_sys::Win32::System::RemoteDesktop::WTSFreeMemory;
|
||||
use windows_sys::Win32::System::RemoteDesktop::WTSQueryUserToken;
|
||||
|
||||
use crate::installation_record::DesktopInstallation;
|
||||
use crate::installation_record::InstallationRecord;
|
||||
use crate::ipc::OwnedHandle;
|
||||
|
||||
struct UserInstallation {
|
||||
codex_home: Option<PathBuf>,
|
||||
directory_handles: Vec<OwnedHandle>,
|
||||
desktop_installation: Option<DesktopInstallation>,
|
||||
user_token: OwnedHandle,
|
||||
catalog: PackageCatalog,
|
||||
token: EventRegistrationToken,
|
||||
}
|
||||
|
||||
pub(crate) struct PackageLifecycle {
|
||||
package_name: HSTRING,
|
||||
uninstalling: Arc<AtomicBool>,
|
||||
installation: RefCell<Option<UserInstallation>>,
|
||||
}
|
||||
|
||||
impl PackageLifecycle {
|
||||
pub(crate) fn new(uninstalling: Arc<AtomicBool>) -> Result<Self> {
|
||||
unsafe { RoInitialize(RO_INIT_MULTITHREADED) }
|
||||
.context("initialize the sandbox service Windows Runtime apartment")?;
|
||||
Ok(Self {
|
||||
package_name: Package::Current()?.Id()?.FullName()?,
|
||||
uninstalling,
|
||||
installation: RefCell::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn watch_authenticated_user(
|
||||
&self,
|
||||
installation: &InstallationRecord,
|
||||
user_token: OwnedHandle,
|
||||
) -> Result<()> {
|
||||
if self.installation.borrow().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
with_owner_impersonation(user_token.0, || {
|
||||
let mut directory_handles = Vec::new();
|
||||
let codex_home = match crate::ipc::pin_existing_ancestors(
|
||||
&installation.codex_home,
|
||||
&mut directory_handles,
|
||||
) {
|
||||
Ok(()) => Some(installation.codex_home.clone()),
|
||||
Err(error) => {
|
||||
directory_handles.clear();
|
||||
crate::service::log_error(
|
||||
crate::service::EVENT_SERVICE_FAILED,
|
||||
&format!(
|
||||
"skipping sandbox file cleanup because the home could not be pinned: {error:#}"
|
||||
),
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
let catalog = PackageCatalog::OpenForCurrentUser()
|
||||
.context("open the authenticated user's package catalog")?;
|
||||
let package_name = self.package_name.clone();
|
||||
let uninstalling = Arc::clone(&self.uninstalling);
|
||||
let token = catalog
|
||||
.PackageUninstalling(&TypedEventHandler::<
|
||||
PackageCatalog,
|
||||
PackageUninstallingEventArgs,
|
||||
>::new(move |_, event| {
|
||||
if let Some(event) = event
|
||||
&& event.Package()?.Id()?.FullName()? == package_name
|
||||
{
|
||||
uninstalling.store(!event.IsComplete()?, Ordering::Release);
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
.context("subscribe to authenticated package uninstall notifications")?;
|
||||
self.installation.replace(Some(UserInstallation {
|
||||
codex_home,
|
||||
directory_handles,
|
||||
desktop_installation: installation.desktop_installation.clone(),
|
||||
user_token,
|
||||
catalog,
|
||||
token,
|
||||
}));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn restore_logged_in_owner(&self, recorded_session_id: u32) -> Result<()> {
|
||||
let Err(error) = self.restore_authenticated_user(recorded_session_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Session IDs can change while the service is stopped; the owner SID is durable.
|
||||
let mut sessions = std::ptr::null_mut();
|
||||
let mut count = 0;
|
||||
if unsafe {
|
||||
WTSEnumerateSessionsW(
|
||||
WTS_CURRENT_SERVER_HANDLE,
|
||||
/*reserved*/ 0,
|
||||
/*version*/ 1,
|
||||
&mut sessions,
|
||||
&mut count,
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(io::Error::last_os_error()).context("list current Windows sessions");
|
||||
}
|
||||
let restored = (0..count).any(|index| {
|
||||
let session_id = unsafe { (*sessions.add(index as usize)).SessionId };
|
||||
session_id != recorded_session_id && self.restore_authenticated_user(session_id).is_ok()
|
||||
});
|
||||
unsafe { WTSFreeMemory(sessions.cast()) };
|
||||
if restored { Ok(()) } else { Err(error) }
|
||||
}
|
||||
|
||||
pub(crate) fn restore_authenticated_user(&self, session_id: u32) -> Result<()> {
|
||||
if self.installation.borrow().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(record) = crate::installation_record::load()? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut raw_token = 0;
|
||||
if unsafe { WTSQueryUserToken(session_id, &mut raw_token) } == 0 {
|
||||
return Err(io::Error::last_os_error()).context("open the logged-in user's token");
|
||||
}
|
||||
let token = crate::ipc::OwnedHandle(raw_token);
|
||||
let user = crate::package_identity::token_user(token.0)?;
|
||||
let sid = unsafe { std::ptr::read_unaligned(user.as_ptr().cast::<security::TOKEN_USER>()) }
|
||||
.User
|
||||
.Sid;
|
||||
let sid_length = unsafe { security::GetLengthSid(sid) };
|
||||
if sid_length == 0 {
|
||||
return Err(io::Error::last_os_error()).context("read the logged-in user's SID");
|
||||
}
|
||||
let user_sid = string_from_sid_bytes(unsafe {
|
||||
std::slice::from_raw_parts(sid.cast::<u8>(), sid_length as usize)
|
||||
})
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
ensure!(
|
||||
user_sid == record.user_sid,
|
||||
"logged-in user does not match the recorded sandbox owner"
|
||||
);
|
||||
|
||||
self.watch_authenticated_user(&record, token)?;
|
||||
if session_id != record.session_id {
|
||||
crate::installation_record::save(&crate::installation_record::InstallationRecord {
|
||||
session_id,
|
||||
..record
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clean_up(&self) -> Result<()> {
|
||||
let mut installation = self.installation.borrow_mut();
|
||||
let installation = installation
|
||||
.as_mut()
|
||||
.context("the authenticated package installation was not recorded")?;
|
||||
crate::service::log_information(
|
||||
crate::service::EVENT_CLEANUP_STARTED,
|
||||
"sandbox uninstall cleanup started",
|
||||
);
|
||||
// A partial uninstall must not let a later install inherit stale directory ownership.
|
||||
crate::installation_record::remove()?;
|
||||
let codex_home = installation.codex_home.clone();
|
||||
clean_up_packaged_windows_sandbox(codex_home.as_deref(), || {
|
||||
let Some(desktop) = &installation.desktop_installation else {
|
||||
return Ok(());
|
||||
};
|
||||
// The marker is user-writable. It must never authorize deletion as LocalSystem.
|
||||
with_owner_impersonation(installation.user_token.0, || {
|
||||
let mut errors = Vec::new();
|
||||
let mut record_error = |result: io::Result<()>| {
|
||||
if let Err(error) = result
|
||||
&& error.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
errors.push(error.to_string());
|
||||
}
|
||||
};
|
||||
if let Some(home) = &codex_home
|
||||
&& desktop.created_codex_home
|
||||
{
|
||||
// Release the home itself so it can be deleted; keep its ancestors pinned.
|
||||
installation.directory_handles.pop();
|
||||
record_error(std::fs::remove_dir_all(home));
|
||||
}
|
||||
// The cache may have been created after provisioning. Pin it only for cleanup.
|
||||
let mut cache_directory_handles = Vec::new();
|
||||
if desktop.cache_home.is_dir() {
|
||||
match crate::ipc::pin_existing_ancestors(
|
||||
&desktop.cache_home,
|
||||
&mut cache_directory_handles,
|
||||
) {
|
||||
Ok(()) => record_error(std::fs::remove_dir_all(
|
||||
desktop.cache_home.join("codex-runtimes"),
|
||||
)),
|
||||
Err(error) => errors.push(error.to_string()),
|
||||
}
|
||||
}
|
||||
ensure!(
|
||||
errors.is_empty(),
|
||||
"remove desktop directories: {}",
|
||||
errors.join("; ")
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.context("remove packaged Windows sandbox resources")?;
|
||||
crate::service::log_information(
|
||||
crate::service::EVENT_CLEANUP_FINISHED,
|
||||
"sandbox uninstall cleanup finished",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn with_owner_impersonation(
|
||||
user_token: HANDLE,
|
||||
operation: impl FnOnce() -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if unsafe { security::ImpersonateLoggedOnUser(user_token) } == 0 {
|
||||
return Err(io::Error::last_os_error()).context("impersonate the sandbox owner");
|
||||
}
|
||||
let result = operation();
|
||||
if unsafe { security::RevertToSelf() } == 0 {
|
||||
crate::service::log_error(
|
||||
crate::service::EVENT_SERVICE_FAILED,
|
||||
&format!(
|
||||
"unable to revert sandbox-owner impersonation: {}",
|
||||
io::Error::last_os_error()
|
||||
),
|
||||
);
|
||||
// Continuing as the user would make later machine cleanup unsafe.
|
||||
std::process::abort();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
impl Drop for PackageLifecycle {
|
||||
fn drop(&mut self) {
|
||||
if let Some(installation) = self.installation.get_mut().take() {
|
||||
let _ = installation
|
||||
.catalog
|
||||
.RemovePackageUninstalling(installation.token);
|
||||
}
|
||||
unsafe { RoUninitialize() };
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::io;
|
||||
use std::mem::size_of;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
@@ -19,10 +20,13 @@ use windows_sys::Win32::System::EventLog::EVENTLOG_ERROR_TYPE;
|
||||
use windows_sys::Win32::System::EventLog::EVENTLOG_INFORMATION_TYPE;
|
||||
use windows_sys::Win32::System::EventLog::RegisterEventSourceW;
|
||||
use windows_sys::Win32::System::EventLog::ReportEventW;
|
||||
use windows_sys::Win32::System::RemoteDesktop::WTSSESSION_NOTIFICATION;
|
||||
use windows_sys::Win32::System::Services::RegisterServiceCtrlHandlerExW;
|
||||
use windows_sys::Win32::System::Services::SERVICE_ACCEPT_SESSIONCHANGE;
|
||||
use windows_sys::Win32::System::Services::SERVICE_ACCEPT_SHUTDOWN;
|
||||
use windows_sys::Win32::System::Services::SERVICE_ACCEPT_STOP;
|
||||
use windows_sys::Win32::System::Services::SERVICE_CONTROL_INTERROGATE;
|
||||
use windows_sys::Win32::System::Services::SERVICE_CONTROL_SESSIONCHANGE;
|
||||
use windows_sys::Win32::System::Services::SERVICE_CONTROL_SHUTDOWN;
|
||||
use windows_sys::Win32::System::Services::SERVICE_CONTROL_STOP;
|
||||
use windows_sys::Win32::System::Services::SERVICE_RUNNING;
|
||||
@@ -40,25 +44,33 @@ pub(crate) const SERVICE_NAME: &str = "CodexSandboxService";
|
||||
const EVENT_SERVICE_STARTED: u32 = 1000;
|
||||
const EVENT_SERVICE_STOP_REQUESTED: u32 = 1001;
|
||||
const EVENT_SERVICE_STOPPED: u32 = 1002;
|
||||
const EVENT_SERVICE_FAILED: u32 = 1003;
|
||||
pub(crate) const EVENT_SERVICE_FAILED: u32 = 1003;
|
||||
pub(crate) const EVENT_PROVISIONING_SUCCEEDED: u32 = 2000;
|
||||
pub(crate) const EVENT_PROVISIONING_FAILED: u32 = 2001;
|
||||
pub(crate) const EVENT_REQUEST_REJECTED: u32 = 2002;
|
||||
pub(crate) const EVENT_CLEANUP_STARTED: u32 = 3002;
|
||||
pub(crate) const EVENT_CLEANUP_FINISHED: u32 = 3003;
|
||||
const MAX_EVENT_MESSAGE_UNITS: usize = 1024;
|
||||
|
||||
static SERVICE_STATE: OnceLock<ServiceState> = OnceLock::new();
|
||||
|
||||
struct ServiceState {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
uninstalling: Arc<AtomicBool>,
|
||||
status_handle: OnceLock<SERVICE_STATUS_HANDLE>,
|
||||
current_status: AtomicU32,
|
||||
changed_session: AtomicU32,
|
||||
stop_requested: AtomicBool,
|
||||
}
|
||||
|
||||
pub(crate) fn run() -> Result<()> {
|
||||
let state = ServiceState {
|
||||
shutdown: Arc::new(AtomicBool::new(false)),
|
||||
uninstalling: Arc::new(AtomicBool::new(false)),
|
||||
status_handle: OnceLock::new(),
|
||||
current_status: AtomicU32::new(SERVICE_START_PENDING),
|
||||
changed_session: AtomicU32::new(u32::MAX),
|
||||
stop_requested: AtomicBool::new(false),
|
||||
};
|
||||
SERVICE_STATE
|
||||
.set(state)
|
||||
@@ -91,10 +103,15 @@ pub(crate) fn run() -> Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn run_foreground() -> Result<()> {
|
||||
crate::package_identity::enable_foreground_mode();
|
||||
crate::ipc::run(Arc::new(AtomicBool::new(false)), || {
|
||||
eprintln!("{SERVICE_NAME} listening on {}", crate::ipc::PIPE_NAME);
|
||||
Ok(())
|
||||
})
|
||||
crate::ipc::run(
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
|| {
|
||||
eprintln!("{SERVICE_NAME} listening on {}", crate::ipc::PIPE_NAME);
|
||||
Ok(())
|
||||
},
|
||||
|_, _| Ok(()),
|
||||
|| Ok(()),
|
||||
)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn service_main(_argument_count: u32, _arguments: *mut *mut u16) {
|
||||
@@ -137,16 +154,55 @@ fn service_main_inner(state: &ServiceState) -> Result<()> {
|
||||
.map_err(|_| anyhow::anyhow!("the service status handle was already registered"))?;
|
||||
|
||||
state.report_status(SERVICE_START_PENDING, NO_ERROR)?;
|
||||
crate::ipc::run(Arc::clone(&state.shutdown), || {
|
||||
state.report_status(SERVICE_RUNNING, NO_ERROR)?;
|
||||
log_information(
|
||||
EVENT_SERVICE_STARTED,
|
||||
"The Codex sandbox service is running.",
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
let package_lifecycle =
|
||||
crate::package_lifecycle::PackageLifecycle::new(Arc::clone(&state.uninstalling))?;
|
||||
crate::ipc::run(
|
||||
Arc::clone(&state.shutdown),
|
||||
|| {
|
||||
state.report_status(SERVICE_RUNNING, NO_ERROR)?;
|
||||
if let Some(record) = crate::installation_record::load()?
|
||||
&& let Err(error) = package_lifecycle.restore_logged_in_owner(record.session_id)
|
||||
{
|
||||
log_error(
|
||||
EVENT_SERVICE_FAILED,
|
||||
&format!("unable to restore package uninstall listener: {error:#}"),
|
||||
);
|
||||
}
|
||||
log_information(
|
||||
EVENT_SERVICE_STARTED,
|
||||
"The Codex sandbox service is running.",
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
|installation, user_token| {
|
||||
if let Err(error) = package_lifecycle.watch_authenticated_user(installation, user_token)
|
||||
{
|
||||
log_error(
|
||||
EVENT_SERVICE_FAILED,
|
||||
&format!("unable to observe package uninstall: {error:#}"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|| {
|
||||
let session = state.changed_session.swap(u32::MAX, Ordering::AcqRel);
|
||||
if session == u32::MAX {
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(error) = package_lifecycle.restore_authenticated_user(session) {
|
||||
log_error(
|
||||
EVENT_SERVICE_FAILED,
|
||||
&format!("unable to restore package uninstall listener: {error:#}"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.context("run the sandbox provisioning broker")?;
|
||||
|
||||
if state.stop_requested.load(Ordering::Acquire) && state.uninstalling.load(Ordering::Acquire) {
|
||||
package_lifecycle.clean_up()?;
|
||||
}
|
||||
log_information(
|
||||
EVENT_SERVICE_STOPPED,
|
||||
"The Codex sandbox service has stopped.",
|
||||
@@ -157,7 +213,7 @@ fn service_main_inner(state: &ServiceState) -> Result<()> {
|
||||
unsafe extern "system" fn service_control_handler(
|
||||
control: u32,
|
||||
_event_type: u32,
|
||||
_event_data: *mut c_void,
|
||||
event_data: *mut c_void,
|
||||
_context: *mut c_void,
|
||||
) -> u32 {
|
||||
let Some(state) = SERVICE_STATE.get() else {
|
||||
@@ -166,19 +222,22 @@ unsafe extern "system" fn service_control_handler(
|
||||
|
||||
match control {
|
||||
SERVICE_CONTROL_STOP | SERVICE_CONTROL_SHUTDOWN => {
|
||||
if control == SERVICE_CONTROL_STOP {
|
||||
state.stop_requested.store(true, Ordering::Release);
|
||||
}
|
||||
if !state.shutdown.swap(true, Ordering::AcqRel) {
|
||||
if let Err(error) = state.report_status(SERVICE_STOP_PENDING, NO_ERROR) {
|
||||
eprintln!("unable to report service shutdown: {error:#}");
|
||||
}
|
||||
log_information(
|
||||
EVENT_SERVICE_STOP_REQUESTED,
|
||||
"The Codex sandbox service was asked to stop.",
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
crate::ipc::wake(crate::ipc::PIPE_NAME, || {
|
||||
state.current_status.load(Ordering::Acquire) == SERVICE_STOPPED
|
||||
});
|
||||
});
|
||||
log_information(
|
||||
EVENT_SERVICE_STOP_REQUESTED,
|
||||
"The Codex sandbox service was asked to stop.",
|
||||
);
|
||||
}
|
||||
NO_ERROR
|
||||
}
|
||||
@@ -193,6 +252,22 @@ unsafe extern "system" fn service_control_handler(
|
||||
}
|
||||
NO_ERROR
|
||||
}
|
||||
SERVICE_CONTROL_SESSIONCHANGE => {
|
||||
if !event_data.is_null() {
|
||||
let event = unsafe { &*event_data.cast::<WTSSESSION_NOTIFICATION>() };
|
||||
if event.cbSize as usize >= size_of::<WTSSESSION_NOTIFICATION>() {
|
||||
state
|
||||
.changed_session
|
||||
.store(event.dwSessionId, Ordering::Release);
|
||||
std::thread::spawn(move || {
|
||||
crate::ipc::wake(crate::ipc::PIPE_NAME, || {
|
||||
state.current_status.load(Ordering::Acquire) == SERVICE_STOPPED
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
NO_ERROR
|
||||
}
|
||||
_ => ERROR_CALL_NOT_IMPLEMENTED,
|
||||
}
|
||||
}
|
||||
@@ -201,6 +276,15 @@ pub(crate) fn log_information(event_id: u32, message: &str) {
|
||||
log_event(EVENTLOG_INFORMATION_TYPE, event_id, message);
|
||||
}
|
||||
|
||||
pub(crate) fn record_provisioned_user(
|
||||
installation: &crate::installation_record::InstallationRecord,
|
||||
) -> Result<()> {
|
||||
if SERVICE_STATE.get().is_some() {
|
||||
crate::installation_record::save(installation)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn log_error(event_id: u32, message: &str) {
|
||||
log_event(EVENTLOG_ERROR_TYPE, event_id, message);
|
||||
}
|
||||
@@ -262,7 +346,7 @@ impl ServiceState {
|
||||
dwServiceType: SERVICE_WIN32_OWN_PROCESS,
|
||||
dwCurrentState: current_status,
|
||||
dwControlsAccepted: if current_status == SERVICE_RUNNING {
|
||||
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN
|
||||
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE
|
||||
} else {
|
||||
0
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user