mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Enable authenticated Windows sandbox provisioning (#42351)
## Why The Windows sandbox service had provisioning policy and client authentication in place, but its IPC listener was still disabled. ## What changed - Serve framed provisioning requests over a local named pipe, authenticate packaged clients, validate requests and machine policy, and report provisioning outcomes through bounded responses and Windows event logging. - Return `unavailable` for configuration parse failures so clients can fall back to the elevated setup helper. - Keep validated directory handles alive in the setup helper so path protections survive an unexpected service exit. - Make connection recovery and shutdown wakeups tolerate clients that disconnect before the listener accepts them. ## Testing Added Windows tests for response framing, configuration-error classification, pipe security and reconnect behavior, shutdown wakeups, and retained helper handles. GitOrigin-RevId: 68e9d546dcada88ff162ea4f98c0f4d748706b34
This commit is contained in:
1 parent
4fdf4c1113
commit
7e45bdb5fd
12 files changed
+857
-59
No files matched your search
@@ -23,8 +23,6 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-windows-sandbox = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
toml = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
|
||||
@@ -1,36 +1,378 @@
|
||||
//! Disabled provisioning IPC until authenticated request handling is installed.
|
||||
//! Authenticated local IPC for the Windows sandbox provisioning service.
|
||||
//! Configuration parse failures defer provisioning to the client's elevated helper.
|
||||
//! Shutdown wakeups are retried until the listener connects or stops.
|
||||
|
||||
// Keep these private until the authenticated transport is connected.
|
||||
#[allow(dead_code)]
|
||||
mod authentication;
|
||||
#[allow(dead_code)]
|
||||
mod home;
|
||||
#[allow(dead_code)]
|
||||
mod request;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
#[cfg(test)]
|
||||
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;
|
||||
#[cfg(test)]
|
||||
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;
|
||||
|
||||
pub(crate) const PIPE_NAME: &str = r"\\.\pipe\OpenAI.CodexSandbox";
|
||||
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";
|
||||
|
||||
pub(crate) fn run(
|
||||
_shutdown: Arc<AtomicBool>,
|
||||
_on_ready: impl FnOnce() -> Result<()>,
|
||||
) -> Result<()> {
|
||||
anyhow::bail!("sandbox provisioning IPC is disabled until authenticated handling is installed")
|
||||
struct SecurityDescriptor(security::PSECURITY_DESCRIPTOR);
|
||||
|
||||
impl Drop for SecurityDescriptor {
|
||||
fn drop(&mut self) {
|
||||
unsafe { foundation::LocalFree(self.0 as foundation::HLOCAL) };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wake() {}
|
||||
#[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) => {
|
||||
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"]
|
||||
|
||||
@@ -20,8 +20,8 @@ use super::request::ProvisioningRequest;
|
||||
pub(super) struct ClientIdentity {
|
||||
pub(super) account: String,
|
||||
pub(super) codex_home: PathBuf,
|
||||
// Denying FILE_SHARE_DELETE pins every checked directory through provisioning.
|
||||
_directory_handles: Vec<OwnedHandle>,
|
||||
// Retained by both the service and helper throughout provisioning.
|
||||
pub(super) directory_handles: Vec<OwnedHandle>,
|
||||
}
|
||||
|
||||
pub(super) fn authenticate_client(
|
||||
@@ -144,7 +144,7 @@ fn authenticate_impersonated_client(
|
||||
ClientIdentity {
|
||||
account,
|
||||
codex_home,
|
||||
_directory_handles: handles,
|
||||
directory_handles: handles,
|
||||
},
|
||||
token,
|
||||
))
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::OwnedHandle;
|
||||
use super::PipeConnection;
|
||||
use super::ProvisioningRequest;
|
||||
use super::accept_pipe_connection;
|
||||
use super::is_config_parse_error;
|
||||
use super::pin_existing_ancestors;
|
||||
use super::pipe_security_descriptor;
|
||||
use super::validate_request;
|
||||
use super::wake;
|
||||
use codex_windows_sandbox::FramedProvisioningMessage;
|
||||
use codex_windows_sandbox::PROVISIONING_PROTOCOL_VERSION;
|
||||
use codex_windows_sandbox::ProvisioningMessage;
|
||||
@@ -9,12 +14,17 @@ use codex_windows_sandbox::SandboxProvisioningRequest;
|
||||
use codex_windows_sandbox::SandboxProvisioningResponse;
|
||||
use codex_windows_sandbox::WindowsSandboxProvisioningSettings;
|
||||
use codex_windows_sandbox::WindowsSandboxProxyListeners;
|
||||
use codex_windows_sandbox::read_provisioning_frame;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::write_provisioning_frame;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows_sys::Win32::Foundation as foundation;
|
||||
use windows_sys::Win32::Storage::FileSystem as filesystem;
|
||||
use windows_sys::Win32::Storage::Packaging::Appx;
|
||||
@@ -33,6 +43,67 @@ fn framed_request(request: SandboxProvisioningRequest) -> Vec<u8> {
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_response_round_trips_through_provisioning_frame() {
|
||||
let mut frame = Vec::new();
|
||||
write_provisioning_frame(
|
||||
&mut frame,
|
||||
&FramedProvisioningMessage {
|
||||
version: PROVISIONING_PROTOCOL_VERSION,
|
||||
message: ProvisioningMessage::ProvisionSandboxResponse {
|
||||
payload: SandboxProvisioningResponse::Unavailable,
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
read_provisioning_frame(frame.as_slice()).unwrap(),
|
||||
Some(FramedProvisioningMessage {
|
||||
version: PROVISIONING_PROTOCOL_VERSION,
|
||||
message: ProvisioningMessage::ProvisionSandboxResponse {
|
||||
payload: SandboxProvisioningResponse::Unavailable,
|
||||
},
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_parse_errors_are_distinguished_from_policy_and_io_failures() {
|
||||
let schema_error = codex_core::config::deserialize_config_toml_with_base(
|
||||
toml::from_str(r#"model_verbosity = "future-verbosity""#).unwrap(),
|
||||
Path::new(r"C:\CodexTest"),
|
||||
)
|
||||
.unwrap_err();
|
||||
let contents = "[";
|
||||
let parse_error = toml::from_str::<toml::Value>(contents).unwrap_err();
|
||||
let syntax_error = codex_config::io_error_from_config_error(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
codex_config::config_error_from_toml("config.toml", contents, parse_error.clone()),
|
||||
Some(parse_error),
|
||||
);
|
||||
for error in [schema_error, syntax_error] {
|
||||
assert!(is_config_parse_error(
|
||||
&anyhow::Error::new(error).context("load managed configuration"),
|
||||
));
|
||||
}
|
||||
|
||||
for error in [
|
||||
anyhow::anyhow!("managed policy does not permit the elevated Windows sandbox"),
|
||||
anyhow::Error::new(std::io::Error::from_raw_os_error(
|
||||
foundation::ERROR_ACCESS_DENIED as i32,
|
||||
)),
|
||||
anyhow::Error::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"invalid credentials",
|
||||
)),
|
||||
] {
|
||||
assert!(!is_config_parse_error(
|
||||
&error.context("load managed configuration"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provisioning_request_preserves_home_spaces_and_unicode() {
|
||||
let request = framed_request(SandboxProvisioningRequest {
|
||||
@@ -214,25 +285,6 @@ fn provisioning_request_rejects_empty_control_characters_and_invalid_utf8() {
|
||||
assert!(validate_request(&invalid_utf8).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pin_existing_ancestors_accepts_drive_and_verbatim_drive_roots() {
|
||||
let executable = std::env::current_exe().unwrap().canonicalize().unwrap();
|
||||
let verbatim_root = executable.ancestors().last().unwrap();
|
||||
let ordinary_root = PathBuf::from(
|
||||
verbatim_root
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.strip_prefix(r"\\?\")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for root in [ordinary_root.as_path(), verbatim_root] {
|
||||
let mut handles = Vec::new();
|
||||
pin_existing_ancestors(root, &mut handles).unwrap();
|
||||
assert_eq!(handles.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpackaged_pipe_clients_are_rejected_before_sending_a_request() {
|
||||
let mut family_length = 0;
|
||||
@@ -297,3 +349,150 @@ fn unpackaged_pipe_clients_are_rejected_before_sending_a_request() {
|
||||
"unexpected package authorization failure: {error:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnected_pipe_clients_do_not_prevent_subsequent_connections() {
|
||||
let pipe_name = format!(
|
||||
r"\\.\pipe\OpenAI.CodexSandbox.DisconnectTests.{}",
|
||||
std::process::id()
|
||||
);
|
||||
let name = to_wide(&pipe_name);
|
||||
let server = unsafe {
|
||||
pipes::CreateNamedPipeW(
|
||||
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,
|
||||
1024,
|
||||
0,
|
||||
ptr::null(),
|
||||
)
|
||||
};
|
||||
assert_ne!(server, foundation::INVALID_HANDLE_VALUE);
|
||||
let server = OwnedHandle(server);
|
||||
|
||||
let first_client = unsafe {
|
||||
filesystem::CreateFileW(
|
||||
name.as_ptr(),
|
||||
foundation::GENERIC_READ | foundation::GENERIC_WRITE,
|
||||
0,
|
||||
ptr::null(),
|
||||
filesystem::OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
assert_ne!(first_client, foundation::INVALID_HANDLE_VALUE);
|
||||
drop(OwnedHandle(first_client));
|
||||
|
||||
assert_eq!(
|
||||
accept_pipe_connection(server.0).unwrap(),
|
||||
PipeConnection::Disconnected
|
||||
);
|
||||
|
||||
let server_handle = server.0;
|
||||
let listener = std::thread::spawn(move || accept_pipe_connection(server_handle));
|
||||
let available = unsafe { pipes::WaitNamedPipeW(name.as_ptr(), 5_000) };
|
||||
assert_ne!(
|
||||
available,
|
||||
0,
|
||||
"wait for replacement named-pipe listener: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
let next_client = unsafe {
|
||||
filesystem::CreateFileW(
|
||||
name.as_ptr(),
|
||||
foundation::GENERIC_READ | foundation::GENERIC_WRITE,
|
||||
0,
|
||||
ptr::null(),
|
||||
filesystem::OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
assert_ne!(
|
||||
next_client,
|
||||
foundation::INVALID_HANDLE_VALUE,
|
||||
"connect replacement named-pipe client: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
let next_client = OwnedHandle(next_client);
|
||||
assert_eq!(
|
||||
listener.join().expect("join named-pipe listener").unwrap(),
|
||||
PipeConnection::Connected
|
||||
);
|
||||
|
||||
drop(next_client);
|
||||
assert_ne!(unsafe { pipes::DisconnectNamedPipe(server.0) }, 0);
|
||||
|
||||
let timeout = Duration::from_secs(5);
|
||||
let deadline = Instant::now() + timeout;
|
||||
let (checks_tx, checks_rx) = mpsc::channel();
|
||||
let waker = std::thread::spawn(move || {
|
||||
wake(&pipe_name, || {
|
||||
let _ = checks_tx.send(());
|
||||
Instant::now() >= deadline
|
||||
});
|
||||
});
|
||||
checks_rx.recv_timeout(timeout).unwrap();
|
||||
// The second check proves a wake failed before the listener started reconnecting.
|
||||
checks_rx.recv_timeout(timeout).unwrap();
|
||||
|
||||
let (accepted_tx, accepted_rx) = mpsc::channel();
|
||||
let listener = std::thread::spawn(move || {
|
||||
let _ = accepted_tx.send(accept_pipe_connection(server.0));
|
||||
drop(server);
|
||||
});
|
||||
let accepted = accepted_rx.recv_timeout(timeout);
|
||||
waker.join().expect("join shutdown waker");
|
||||
// The wake handle may close before ConnectNamedPipe observes the connection.
|
||||
assert!(
|
||||
accepted
|
||||
.expect("shutdown wake should unblock the listener")
|
||||
.is_ok()
|
||||
);
|
||||
listener.join().expect("join shutdown listener");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_pipe_connection_errors_remain_fatal() {
|
||||
let error = accept_pipe_connection(foundation::INVALID_HANDLE_VALUE).unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
error
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.and_then(std::io::Error::raw_os_error),
|
||||
Some(foundation::ERROR_INVALID_HANDLE as i32)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pin_existing_ancestors_accepts_drive_and_verbatim_drive_roots() {
|
||||
let executable = std::env::current_exe().unwrap().canonicalize().unwrap();
|
||||
let verbatim_root = executable.ancestors().last().unwrap();
|
||||
let ordinary_root = PathBuf::from(
|
||||
verbatim_root
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.strip_prefix(r"\\?\")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for root in [ordinary_root.as_path(), verbatim_root] {
|
||||
let mut handles = Vec::new();
|
||||
pin_existing_ancestors(root, &mut handles).unwrap();
|
||||
assert_eq!(handles.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_descriptor_denies_sandbox_group_before_interactive_users() {
|
||||
let descriptor = pipe_security_descriptor("S-1-5-21-11-12-13-14");
|
||||
let deny = descriptor.find("(D;;GA;;;S-1-5-21-11-12-13-14)").unwrap();
|
||||
let interactive = descriptor.find("(A;;0x0012019b;;;IU)").unwrap();
|
||||
assert!(deny < interactive);
|
||||
}
|
||||
@@ -2,12 +2,9 @@ use anyhow::Result;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod ipc;
|
||||
// Provisioning remains disabled until authenticated transport connects this policy.
|
||||
#[cfg(windows)]
|
||||
#[allow(dead_code)]
|
||||
mod machine_policy;
|
||||
#[cfg(windows)]
|
||||
#[allow(dead_code)]
|
||||
mod package_identity;
|
||||
#[cfg(windows)]
|
||||
mod service;
|
||||
|
||||
@@ -41,6 +41,9 @@ 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_PROVISIONING_SUCCEEDED: u32 = 2000;
|
||||
pub(crate) const EVENT_PROVISIONING_FAILED: u32 = 2001;
|
||||
pub(crate) const EVENT_REQUEST_REJECTED: u32 = 2002;
|
||||
const MAX_EVENT_MESSAGE_UNITS: usize = 1024;
|
||||
|
||||
static SERVICE_STATE: OnceLock<ServiceState> = OnceLock::new();
|
||||
@@ -167,7 +170,11 @@ unsafe extern "system" fn service_control_handler(
|
||||
if let Err(error) = state.report_status(SERVICE_STOP_PENDING, NO_ERROR) {
|
||||
eprintln!("unable to report service shutdown: {error:#}");
|
||||
}
|
||||
crate::ipc::wake();
|
||||
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.",
|
||||
|
||||
Reference in new issue
Block a user