mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
## What changed - Add the default-off `windows_sandbox_service` feature and expose it through app-server experimental feature enablement. - When enabled, attempt service provisioning during elevated Windows sandbox setup, using the effective network proxy ports and listener roles. Continue through the existing setup path when the service is unavailable or the configuration is unsupported. - Report unsupported Codex home drives as service unavailability so the client can fall back to the elevated helper. ## Testing - Add a Windows-only unit test covering HTTP and SOCKS listener derivation from the effective proxy configuration. GitOrigin-RevId: 40b119cf51f84e5d0e94d6a0a8d5b506d1123526
384 lines
14 KiB
Rust
384 lines
14 KiB
Rust
//! Authenticated local IPC for the Windows sandbox provisioning service.
|
|
//! Configuration parse failures and unsupported home drives defer provisioning to
|
|
//! the client's elevated helper.
|
|
//! Shutdown wakeups are retried until the listener connects or stops.
|
|
|
|
mod authentication;
|
|
mod home;
|
|
mod request;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::bail;
|
|
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::ensure_sandbox_users_group;
|
|
use codex_windows_sandbox::run_elevated_provisioning_setup_with_retained_handles;
|
|
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;
|
|
#[cfg(test)]
|
|
use request::ProvisioningRequest;
|
|
use request::validate_request;
|
|
use std::mem::size_of;
|
|
use std::os::windows::fs::MetadataExt;
|
|
use std::os::windows::io::BorrowedHandle;
|
|
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::Security as security;
|
|
use windows_sys::Win32::Security::Authorization as authorization;
|
|
use windows_sys::Win32::Storage::FileSystem as filesystem;
|
|
use windows_sys::Win32::System::Pipes as pipes;
|
|
|
|
pub(crate) const PIPE_NAME: &str = codex_windows_sandbox::SANDBOX_PROVISIONING_PIPE_NAME;
|
|
|
|
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";
|
|
|
|
struct SecurityDescriptor(security::PSECURITY_DESCRIPTOR);
|
|
|
|
impl Drop for SecurityDescriptor {
|
|
fn drop(&mut self) {
|
|
unsafe { foundation::LocalFree(self.0 as foundation::HLOCAL) };
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
enum PipeConnection {
|
|
Connected,
|
|
Disconnected,
|
|
}
|
|
|
|
pub(crate) fn run(shutdown: Arc<AtomicBool>, on_ready: impl FnOnce() -> 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);
|
|
let mut descriptor: security::PSECURITY_DESCRIPTOR = ptr::null_mut();
|
|
if unsafe {
|
|
authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
to_wide(sddl).as_ptr(),
|
|
authorization::SDDL_REVISION_1,
|
|
&mut descriptor,
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
return Err(std::io::Error::last_os_error()).context("create provisioning pipe DACL");
|
|
}
|
|
let descriptor = SecurityDescriptor(descriptor);
|
|
let attributes = security::SECURITY_ATTRIBUTES {
|
|
nLength: size_of::<security::SECURITY_ATTRIBUTES>() as u32,
|
|
lpSecurityDescriptor: descriptor.0,
|
|
bInheritHandle: 0,
|
|
};
|
|
|
|
let pipe = unsafe {
|
|
pipes::CreateNamedPipeW(
|
|
to_wide(PIPE_NAME).as_ptr(),
|
|
filesystem::PIPE_ACCESS_DUPLEX | filesystem::FILE_FLAG_FIRST_PIPE_INSTANCE,
|
|
pipes::PIPE_TYPE_BYTE
|
|
| pipes::PIPE_READMODE_BYTE
|
|
| pipes::PIPE_WAIT
|
|
| pipes::PIPE_REJECT_REMOTE_CLIENTS,
|
|
1,
|
|
1024,
|
|
MAX_REQUEST_BYTES as u32,
|
|
0,
|
|
&attributes,
|
|
)
|
|
};
|
|
if pipe == foundation::INVALID_HANDLE_VALUE {
|
|
return Err(std::io::Error::last_os_error()).context("create provisioning pipe");
|
|
}
|
|
let pipe = OwnedHandle(pipe);
|
|
on_ready().context("publish provisioning listener readiness")?;
|
|
|
|
while !shutdown.load(Ordering::Acquire) {
|
|
if accept_pipe_connection(pipe.0)? == PipeConnection::Disconnected {
|
|
continue;
|
|
}
|
|
if shutdown.load(Ordering::Acquire) {
|
|
break;
|
|
}
|
|
|
|
let authorized_process = match crate::package_identity::authorize_client_process(pipe.0) {
|
|
Ok(process) => process,
|
|
Err(_) => {
|
|
unsafe { pipes::DisconnectNamedPipe(pipe.0) };
|
|
continue;
|
|
}
|
|
};
|
|
let result = handle_request(pipe.0, &authorized_process, &sandbox_sid, &shutdown);
|
|
let response = match result {
|
|
Ok(response) => response,
|
|
Err(error) if error.is::<home::UnsupportedHomeDrive>() => {
|
|
SandboxProvisioningResponse::Unavailable
|
|
}
|
|
Err(error) => {
|
|
eprintln!("sandbox provisioning request failed: {error}");
|
|
let mut message = String::new();
|
|
for character in error.to_string().chars() {
|
|
let character = if character.is_control() {
|
|
' '
|
|
} else {
|
|
character
|
|
};
|
|
if message.len() + character.len_utf8() > MAX_RESPONSE_MESSAGE_BYTES {
|
|
break;
|
|
}
|
|
message.push(character);
|
|
}
|
|
SandboxProvisioningResponse::Error { message }
|
|
}
|
|
};
|
|
let response = FramedProvisioningMessage {
|
|
version: PROVISIONING_PROTOCOL_VERSION,
|
|
message: ProvisioningMessage::ProvisionSandboxResponse { payload: response },
|
|
};
|
|
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.0,
|
|
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.0,
|
|
ptr::null_mut(),
|
|
0,
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
ptr::null_mut(),
|
|
)
|
|
} == 0
|
|
{
|
|
break;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
}
|
|
unsafe { pipes::DisconnectNamedPipe(pipe.0) };
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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,
|
|
/*htemplatefile*/ 0,
|
|
)
|
|
};
|
|
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: HANDLE,
|
|
authorized_process: &crate::package_identity::AuthorizedClientProcess,
|
|
sandbox_sid: &[u8],
|
|
shutdown: &AtomicBool,
|
|
) -> 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,
|
|
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,
|
|
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,
|
|
&format!("Codex sandbox provisioning was rejected by administrator policy: {error}"),
|
|
);
|
|
return Err(error)
|
|
.context("requested sandbox settings violate administrator-controlled machine policy");
|
|
}
|
|
if sandbox_setup_is_complete_with_settings(&identity.codex_home, &request.settings) {
|
|
return Ok(SandboxProvisioningResponse::Ok);
|
|
}
|
|
let helper = std::env::current_exe()
|
|
.context("locate the provisioning service executable")?
|
|
.with_file_name("codex-windows-sandbox-setup.exe");
|
|
let helper_metadata = helper
|
|
.symlink_metadata()
|
|
.with_context(|| format!("inspect packaged setup helper {}", helper.display()))?;
|
|
if !helper_metadata.is_file()
|
|
|| helper_metadata.file_attributes() & filesystem::FILE_ATTRIBUTE_REPARSE_POINT != 0
|
|
{
|
|
bail!(
|
|
"refusing invalid packaged setup helper {}",
|
|
helper.display()
|
|
);
|
|
}
|
|
let retained_handles = identity
|
|
.directory_handles
|
|
.iter()
|
|
// The identity owns these handles through the synchronous helper launch and wait.
|
|
.map(|handle| unsafe { BorrowedHandle::borrow_raw(handle.0 as _) })
|
|
.collect::<Vec<_>>();
|
|
match run_elevated_provisioning_setup_with_retained_handles(
|
|
&identity.codex_home,
|
|
&identity.account,
|
|
request.settings,
|
|
&retained_handles,
|
|
) {
|
|
Ok(()) => {
|
|
crate::service::log_information(
|
|
crate::service::EVENT_PROVISIONING_SUCCEEDED,
|
|
"Codex sandbox provisioning completed successfully.",
|
|
);
|
|
Ok(SandboxProvisioningResponse::Ok)
|
|
}
|
|
Err(error) => {
|
|
crate::service::log_error(
|
|
crate::service::EVENT_PROVISIONING_FAILED,
|
|
&format!("Codex sandbox provisioning failed: {error}"),
|
|
);
|
|
Err(error).context("sandbox provisioning failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|