mirror of
https://github.com/openai/codex.git
synced 2026-11-11 21:10:39 +00:00
## What changed - Centralize `windows-sys` at version `0.61.2` in the workspace and migrate dependent crates to it. - Adapt Windows API calls to the updated handle, boolean, and type definitions. Replace custom handle owners with `OwnedHandle` and use `BorrowedHandle` where ownership stays with the caller. - Add explicit wrappers for non-owning Windows handles passed between threads. ## Testing Enable the MCP refresh cleanup test on Windows. Retain process handles to verify that a superseded MCP server stays alive during an in-flight call, exits after cancellation, and leaves the replacement running until shutdown. GitOrigin-RevId: 86f53d9b300d13edff7154f4a7c32e34dacd2919
406 lines
14 KiB
Rust
406 lines
14 KiB
Rust
//! Authenticated local IPC for the Windows sandbox provisioning service.
|
|
//! Expected service limitations defer provisioning to the client's elevated helper;
|
|
//! authentication and policy rejections remain errors.
|
|
//! Shutdown wakeups are retried until the listener connects or stops.
|
|
|
|
mod authentication;
|
|
mod home;
|
|
mod listener;
|
|
mod request;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::bail;
|
|
pub(crate) use authentication::ClientIdentity;
|
|
use authentication::authenticate_client;
|
|
use codex_windows_sandbox::FramedProvisioningMessage;
|
|
use codex_windows_sandbox::PROVISIONING_PROTOCOL_VERSION;
|
|
use codex_windows_sandbox::ProvisioningMessage;
|
|
use codex_windows_sandbox::SandboxProvisioningResponse;
|
|
use codex_windows_sandbox::to_wide;
|
|
use codex_windows_sandbox::write_provisioning_frame;
|
|
pub(crate) use home::pin_directory;
|
|
pub(crate) use home::pin_existing_ancestors;
|
|
pub(crate) use request::ProvisioningRequest;
|
|
pub(crate) use request::ServiceRequest;
|
|
use request::validate_request;
|
|
use std::mem::size_of;
|
|
use std::os::windows::io::AsHandle;
|
|
use std::os::windows::io::AsRawHandle;
|
|
use std::os::windows::io::BorrowedHandle;
|
|
use std::os::windows::io::OwnedHandle;
|
|
use std::ptr;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
use windows_sys::Win32::Foundation as foundation;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Storage::FileSystem as filesystem;
|
|
use windows_sys::Win32::System::Pipes as pipes;
|
|
|
|
use crate::installation_record::InstallationRecord;
|
|
|
|
const MAX_REQUEST_BYTES: usize = 4096;
|
|
const MAX_RESPONSE_MESSAGE_BYTES: usize = 512;
|
|
const REQUEST_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
|
|
const PIPE_USER_ACCESS: &str = "0x0012019b";
|
|
|
|
/// The service cannot complete this request; the interactive setup helper may still work.
|
|
#[derive(Debug)]
|
|
pub(crate) struct ServiceUnavailable(pub(crate) &'static str);
|
|
|
|
impl std::fmt::Display for ServiceUnavailable {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
formatter.write_str(self.0)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ServiceUnavailable {}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
enum PipeConnection {
|
|
Connected,
|
|
Disconnected,
|
|
}
|
|
|
|
pub(crate) fn run(
|
|
shutdown: Arc<AtomicBool>,
|
|
on_ready: impl FnOnce() -> Result<()>,
|
|
on_authenticated_user: impl Fn(
|
|
InstallationRecord,
|
|
OwnedHandle,
|
|
codex_windows_sandbox::SetupRuntime,
|
|
) -> Result<InstallationRecord>,
|
|
on_session_change: impl Fn() -> Result<()>,
|
|
) -> Result<()> {
|
|
if shutdown.load(Ordering::Acquire) {
|
|
return Ok(());
|
|
}
|
|
let mut listener = listener::ProvisioningListener::open()?;
|
|
on_ready().context("publish provisioning listener readiness")?;
|
|
|
|
// Refresh before blocking even if a direct setup claim's zero-byte wake was lost.
|
|
while refresh_session(&shutdown, &on_session_change)
|
|
.context("refresh the recorded sandbox owner")?
|
|
{
|
|
let connection = accept_pipe_connection(listener.pipe.as_raw_handle())?;
|
|
// A session refresh can finish cleanup. Never dispatch an already accepted
|
|
// connection after it stops the listener, including a disconnected wakeup.
|
|
if !refresh_session(&shutdown, &on_session_change)
|
|
.context("restore the signed-in user's uninstall listener")?
|
|
{
|
|
break;
|
|
}
|
|
if connection == PipeConnection::Disconnected {
|
|
continue;
|
|
}
|
|
|
|
let authorized_process = match crate::package_identity::authorize_client_process(
|
|
listener.pipe.as_raw_handle(),
|
|
) {
|
|
Ok(process) => process,
|
|
Err(_) => {
|
|
unsafe { pipes::DisconnectNamedPipe(listener.pipe.as_raw_handle()) };
|
|
continue;
|
|
}
|
|
};
|
|
let result = handle_request(
|
|
listener.pipe.as_handle(),
|
|
&authorized_process,
|
|
&listener.sandbox_sid,
|
|
&shutdown,
|
|
&on_authenticated_user,
|
|
);
|
|
let response = match result {
|
|
Ok(response) => response,
|
|
Err(error) if error.is::<crate::registered_runtime::RegistrationInterrupted>() => {
|
|
return Err(error);
|
|
}
|
|
Err(error) if error.is::<ServiceUnavailable>() => {
|
|
SandboxProvisioningResponse::Unavailable
|
|
}
|
|
Err(error) => {
|
|
eprintln!("sandbox provisioning request failed: {error}");
|
|
SandboxProvisioningResponse::Error {
|
|
message: response_error_message(&error),
|
|
}
|
|
}
|
|
};
|
|
let response = FramedProvisioningMessage {
|
|
version: PROVISIONING_PROTOCOL_VERSION,
|
|
message: ProvisioningMessage::ProvisionSandboxResponse { payload: response },
|
|
};
|
|
write_response(&listener.pipe, &response, &shutdown)?;
|
|
if shutdown.load(Ordering::Acquire) {
|
|
break;
|
|
}
|
|
listener = listener.refresh()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
fn refresh_session(
|
|
shutdown: &AtomicBool,
|
|
on_session_change: impl FnOnce() -> Result<()>,
|
|
) -> Result<bool> {
|
|
if shutdown.load(Ordering::Acquire) {
|
|
return Ok(false);
|
|
}
|
|
on_session_change()?;
|
|
Ok(!shutdown.load(Ordering::Acquire))
|
|
}
|
|
|
|
/// Sends one frame, then waits briefly for the client to close before disconnecting.
|
|
fn write_response(
|
|
pipe: &OwnedHandle,
|
|
response: &FramedProvisioningMessage,
|
|
shutdown: &AtomicBool,
|
|
) -> Result<()> {
|
|
let mut frame = Vec::new();
|
|
write_provisioning_frame(&mut frame, response)
|
|
.context("serialize sandbox provisioning response")?;
|
|
let mut written = 0;
|
|
let sent = unsafe {
|
|
filesystem::WriteFile(
|
|
pipe.as_raw_handle(),
|
|
frame.as_ptr(),
|
|
frame.len() as u32,
|
|
&mut written,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if sent != 0 {
|
|
let deadline = Instant::now() + Duration::from_secs(1);
|
|
while !shutdown.load(Ordering::Acquire) && Instant::now() < deadline {
|
|
if unsafe {
|
|
pipes::PeekNamedPipe(
|
|
pipe.as_raw_handle(),
|
|
ptr::null_mut(),
|
|
0,
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
break;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn response_error_message(error: &anyhow::Error) -> String {
|
|
let mut message = String::new();
|
|
for character in format!("{error:#}").chars() {
|
|
let character = if character.is_control() {
|
|
' '
|
|
} else {
|
|
character
|
|
};
|
|
if message.len() + character.len_utf8() > MAX_RESPONSE_MESSAGE_BYTES {
|
|
break;
|
|
}
|
|
message.push(character);
|
|
}
|
|
message
|
|
}
|
|
|
|
fn accept_pipe_connection(pipe: HANDLE) -> Result<PipeConnection> {
|
|
if unsafe { pipes::ConnectNamedPipe(pipe, ptr::null_mut()) } != 0 {
|
|
return Ok(PipeConnection::Connected);
|
|
}
|
|
|
|
let error = unsafe { foundation::GetLastError() };
|
|
match error {
|
|
foundation::ERROR_PIPE_CONNECTED => Ok(PipeConnection::Connected),
|
|
foundation::ERROR_NO_DATA | foundation::ERROR_BROKEN_PIPE => {
|
|
if unsafe { pipes::DisconnectNamedPipe(pipe) } == 0 {
|
|
let reset_error = std::io::Error::last_os_error();
|
|
if reset_error.raw_os_error() != Some(foundation::ERROR_PIPE_NOT_CONNECTED as i32) {
|
|
return Err(reset_error).context("reset disconnected provisioning client");
|
|
}
|
|
}
|
|
Ok(PipeConnection::Disconnected)
|
|
}
|
|
_ => Err(std::io::Error::from_raw_os_error(error as i32))
|
|
.context("accept provisioning client"),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn wake(pipe_name: &str, is_stopped: impl Fn() -> bool) {
|
|
let pipe_name = to_wide(pipe_name);
|
|
while !is_stopped() {
|
|
let handle = unsafe {
|
|
filesystem::CreateFileW(
|
|
pipe_name.as_ptr(),
|
|
foundation::GENERIC_WRITE,
|
|
/*dwsharemode*/ 0,
|
|
ptr::null(),
|
|
filesystem::OPEN_EXISTING,
|
|
/*dwflagsandattributes*/ 0,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if handle != foundation::INVALID_HANDLE_VALUE {
|
|
unsafe { foundation::CloseHandle(handle) };
|
|
return;
|
|
}
|
|
// A disconnected instance cannot accept the wakeup until ConnectNamedPipe runs.
|
|
std::thread::sleep(Duration::from_millis(25));
|
|
}
|
|
}
|
|
|
|
fn pipe_security_descriptor(sandbox_sid: &str) -> String {
|
|
format!("D:P(D;;GA;;;{sandbox_sid})(A;;GA;;;SY)(A;;GA;;;BA)(A;;{PIPE_USER_ACCESS};;;IU)")
|
|
}
|
|
|
|
fn handle_request(
|
|
pipe: BorrowedHandle<'_>,
|
|
authorized_process: &crate::package_identity::AuthorizedClientProcess,
|
|
sandbox_sid: &[u8],
|
|
shutdown: &AtomicBool,
|
|
on_authenticated_user: &dyn Fn(
|
|
InstallationRecord,
|
|
OwnedHandle,
|
|
codex_windows_sandbox::SetupRuntime,
|
|
) -> Result<InstallationRecord>,
|
|
) -> Result<SandboxProvisioningResponse> {
|
|
let deadline = Instant::now() + REQUEST_IDLE_TIMEOUT;
|
|
let mut request = [0_u8; MAX_REQUEST_BYTES];
|
|
let mut request_length = 0;
|
|
loop {
|
|
if shutdown.load(Ordering::Acquire) {
|
|
bail!("service is stopping");
|
|
}
|
|
if Instant::now() >= deadline {
|
|
bail!("provisioning request timed out");
|
|
}
|
|
let mut available = 0;
|
|
if unsafe {
|
|
pipes::PeekNamedPipe(
|
|
pipe.as_raw_handle(),
|
|
ptr::null_mut(),
|
|
0,
|
|
ptr::null_mut(),
|
|
&mut available,
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(std::io::Error::last_os_error()).context("inspect provisioning request");
|
|
}
|
|
if available as usize > MAX_REQUEST_BYTES - request_length {
|
|
bail!("provisioning request exceeds size limit");
|
|
}
|
|
if available != 0 {
|
|
let mut read = 0;
|
|
if unsafe {
|
|
filesystem::ReadFile(
|
|
pipe.as_raw_handle(),
|
|
request[request_length..].as_mut_ptr(),
|
|
available,
|
|
&mut read,
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(std::io::Error::last_os_error()).context("read provisioning request");
|
|
}
|
|
if read == 0 {
|
|
bail!("provisioning client sent an empty request");
|
|
}
|
|
request_length += read as usize;
|
|
let received = &request[..request_length];
|
|
if received.len() >= size_of::<u32>() {
|
|
let payload_length =
|
|
u32::from_le_bytes([received[0], received[1], received[2], received[3]])
|
|
as usize;
|
|
if payload_length > MAX_REQUEST_BYTES - size_of::<u32>() {
|
|
bail!("provisioning request exceeds size limit");
|
|
}
|
|
let frame_length = size_of::<u32>() + payload_length;
|
|
if request_length > frame_length {
|
|
bail!("provisioning requests must contain exactly one IPC frame");
|
|
}
|
|
if request_length == frame_length {
|
|
break;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(25));
|
|
}
|
|
|
|
let request = validate_request(&request[..request_length])?;
|
|
let (identity, policy_result) =
|
|
authenticate_client(pipe, authorized_process, sandbox_sid, &request)?;
|
|
if let Err(error) = policy_result {
|
|
if is_config_parse_error(&error) {
|
|
return Ok(SandboxProvisioningResponse::Unavailable);
|
|
}
|
|
crate::service::log_error(
|
|
crate::service::EVENT_REQUEST_REJECTED,
|
|
&policy_rejection_diagnostic(&error),
|
|
);
|
|
return Err(error)
|
|
.context("requested sandbox settings violate administrator-controlled machine policy");
|
|
}
|
|
crate::provisioning::run(
|
|
identity,
|
|
request,
|
|
sandbox_sid,
|
|
shutdown,
|
|
on_authenticated_user,
|
|
)
|
|
}
|
|
|
|
fn policy_rejection_diagnostic(error: &anyhow::Error) -> String {
|
|
// Only service-owned labels and typed codes belong in persistent events.
|
|
// Parser wrappers can stringify entire config lines, including credentials.
|
|
let stage = match error.to_string().as_str() {
|
|
"start managed configuration runtime" => "runtime",
|
|
"load bootstrap configuration" => "bootstrap",
|
|
"resolve cloud configuration authentication" => "resolve_auth",
|
|
"initialize cloud configuration authentication" => "initialize_auth",
|
|
"load managed configuration" => "managed",
|
|
"enforce managed provisioning requirements" => "enforce",
|
|
_ => "unknown",
|
|
};
|
|
let detail = error.chain().find_map(|cause| {
|
|
let io = cause.downcast_ref::<std::io::Error>();
|
|
let inner = io
|
|
.and_then(std::io::Error::get_ref)
|
|
.map(|inner| inner as &(dyn std::error::Error + 'static))
|
|
.unwrap_or(cause);
|
|
if let Some(cloud) = inner.downcast_ref::<codex_config::CloudConfigBundleLoadError>() {
|
|
return Some(format!(
|
|
"cloud={:?} http={:?}",
|
|
cloud.code(),
|
|
cloud.status_code()
|
|
));
|
|
}
|
|
io.map(|io| format!("io={:?} win32={:?}", io.kind(), io.raw_os_error()))
|
|
});
|
|
let detail = detail.unwrap_or_else(|| "cause=unavailable".to_string());
|
|
format!(
|
|
"Codex sandbox provisioning was rejected by administrator policy: stage={stage} {detail}"
|
|
)
|
|
}
|
|
|
|
fn is_config_parse_error(error: &anyhow::Error) -> bool {
|
|
error.chain().any(|cause| {
|
|
cause.is::<toml::de::Error>()
|
|
|| cause
|
|
.downcast_ref::<std::io::Error>()
|
|
.and_then(std::io::Error::get_ref)
|
|
.is_some_and(<dyn std::error::Error + Send + Sync>::is::<toml::de::Error>)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "ipc_tests.rs"]
|
|
mod tests;
|