diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs index 2f1ddc19b3..bc8bdb506f 100644 --- a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs +++ b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs @@ -22,6 +22,7 @@ use codex_windows_sandbox::log_note; use codex_windows_sandbox::log_writer; use codex_windows_sandbox::path_mask_allows; use codex_windows_sandbox::path_write_aces_need_refresh; +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; @@ -80,7 +81,6 @@ use sandbox_users::commit_setup_marker; use sandbox_users::prepare_setup_marker; use sandbox_users::provision_sandbox_users; use sandbox_users::resolve_sandbox_users_group_sid; -use sandbox_users::resolve_sid; use sandbox_users::sid_bytes_to_psid; #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win/sandbox_users.rs b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win/sandbox_users.rs index 0c7aa21988..da13115314 100644 --- a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win/sandbox_users.rs +++ b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win/sandbox_users.rs @@ -16,10 +16,8 @@ use windows_sys::Win32::Foundation::GENERIC_WRITE; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; use windows_sys::Win32::Foundation::LocalFree; -use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_INFO_1; use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_MEMBERS_INFO_3; use windows_sys::Win32::NetworkManagement::NetManagement::NERR_Success; -use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAdd; use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAddMembers; use windows_sys::Win32::NetworkManagement::NetManagement::NetUserAdd; use windows_sys::Win32::NetworkManagement::NetManagement::NetUserSetInfo; @@ -31,9 +29,6 @@ use windows_sys::Win32::NetworkManagement::NetManagement::USER_PRIV_USER; use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW; use windows_sys::Win32::Security::Authorization::SDDL_REVISION_1; -use windows_sys::Win32::Security::CopySid; -use windows_sys::Win32::Security::GetLengthSid; -use windows_sys::Win32::Security::LookupAccountNameW; use windows_sys::Win32::Security::LookupAccountSidW; use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR; use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; @@ -42,26 +37,19 @@ use windows_sys::Win32::Storage::FileSystem::CREATE_NEW; use windows_sys::Win32::Storage::FileSystem::CreateFileW; use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; +use codex_windows_sandbox::SANDBOX_USERS_GROUP; use codex_windows_sandbox::SETUP_VERSION; use codex_windows_sandbox::SetupErrorCode; use codex_windows_sandbox::SetupFailure; use codex_windows_sandbox::dpapi_protect; +use codex_windows_sandbox::ensure_sandbox_users_group; +use codex_windows_sandbox::resolve_sid; use codex_windows_sandbox::sandbox_dir; use codex_windows_sandbox::sandbox_secrets_dir; use codex_windows_sandbox::string_from_sid_bytes; use codex_windows_sandbox::to_wide; -pub const SANDBOX_USERS_GROUP: &str = "CodexSandboxUsers"; -const SANDBOX_USERS_GROUP_COMMENT: &str = "Codex sandbox internal group (managed)"; -const SID_ADMINISTRATORS: &str = "S-1-5-32-544"; const SID_USERS: &str = "S-1-5-32-545"; -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 ensure_sandbox_users_group(log: &mut dyn Write) -> Result<()> { - ensure_local_group(SANDBOX_USERS_GROUP, SANDBOX_USERS_GROUP_COMMENT, log) -} pub fn resolve_sandbox_users_group_sid() -> Result> { resolve_sid(SANDBOX_USERS_GROUP) @@ -73,7 +61,14 @@ pub fn provision_sandbox_users( online_username: &str, log: &mut dyn Write, ) -> Result<()> { - ensure_sandbox_users_group(log)?; + if let Err(err) = ensure_sandbox_users_group() { + let message = format!("failed to create local group {SANDBOX_USERS_GROUP}: {err}"); + super::log_line(log, &message)?; + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersGroupCreateFailed, + message, + ))); + } super::log_line( log, &format!("ensuring sandbox users offline={offline_username} online={online_username}"), @@ -162,38 +157,6 @@ pub fn ensure_local_user(name: &str, password: &str, log: &mut dyn Write) -> Res Ok(()) } -pub fn ensure_local_group(name: &str, comment: &str, log: &mut dyn Write) -> Result<()> { - const ERROR_ALIAS_EXISTS: u32 = 1379; - const NERR_GROUP_EXISTS: u32 = 2223; - - let name_w = to_wide(OsStr::new(name)); - let comment_w = to_wide(OsStr::new(comment)); - unsafe { - let info = LOCALGROUP_INFO_1 { - lgrpi1_name: name_w.as_ptr() as *mut u16, - lgrpi1_comment: comment_w.as_ptr() as *mut u16, - }; - let mut parm_err: u32 = 0; - let status = NetLocalGroupAdd( - std::ptr::null(), - 1, - &info as *const _ as *mut u8, - &mut parm_err as *mut _, - ); - if status != NERR_Success && status != ERROR_ALIAS_EXISTS && status != NERR_GROUP_EXISTS { - super::log_line( - log, - &format!("NetLocalGroupAdd failed for {name} code {status} parm_err={parm_err}"), - )?; - return Err(anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperUsersGroupCreateFailed, - format!("failed to create local group {name}, code {status}"), - ))); - } - } - Ok(()) -} - pub fn ensure_local_group_member(group_name: &str, member_name: &str) -> Result<()> { // If the member is already in the group, NetLocalGroupAddMembers may // return an error code. We don't care. @@ -214,82 +177,6 @@ pub fn ensure_local_group_member(group_name: &str, member_name: &str) -> Result< Ok(()) } -pub fn resolve_sid(name: &str) -> Result> { - if let Some(sid_str) = well_known_sid_str(name) { - return sid_bytes_from_string(sid_str); - } - let name_w = to_wide(OsStr::new(name)); - let mut sid_buffer = vec![0u8; 68]; - let mut sid_len: u32 = sid_buffer.len() as u32; - let mut domain: Vec = Vec::new(); - let mut domain_len: u32 = 0; - let mut use_type: SID_NAME_USE = 0; - loop { - let ok = unsafe { - LookupAccountNameW( - std::ptr::null(), - name_w.as_ptr(), - sid_buffer.as_mut_ptr() as *mut c_void, - &mut sid_len, - domain.as_mut_ptr(), - &mut domain_len, - &mut use_type, - ) - }; - if ok != 0 { - sid_buffer.truncate(sid_len as usize); - return Ok(sid_buffer); - } - let err = unsafe { GetLastError() }; - if err == ERROR_INSUFFICIENT_BUFFER { - sid_buffer.resize(sid_len as usize, 0); - domain.resize(domain_len as usize, 0); - continue; - } - return Err(anyhow::anyhow!( - "LookupAccountNameW failed for {name}: {err}" - )); - } -} - -fn well_known_sid_str(name: &str) -> Option<&'static str> { - match name { - "Administrators" => Some(SID_ADMINISTRATORS), - "Users" => Some(SID_USERS), - "Authenticated Users" => Some(SID_AUTHENTICATED_USERS), - "Everyone" => Some(SID_EVERYONE), - "SYSTEM" => Some(SID_SYSTEM), - _ => None, - } -} - -fn sid_bytes_from_string(sid_str: &str) -> Result> { - let sid_w = to_wide(OsStr::new(sid_str)); - let mut psid: *mut c_void = std::ptr::null_mut(); - if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 { - return Err(anyhow::anyhow!( - "ConvertStringSidToSidW failed for {sid_str}: {}", - unsafe { GetLastError() } - )); - } - let sid_len = unsafe { GetLengthSid(psid) }; - if sid_len == 0 { - unsafe { - LocalFree(psid as _); - } - return Err(anyhow::anyhow!("GetLengthSid failed for {sid_str}")); - } - let mut out = vec![0u8; sid_len as usize]; - let ok = unsafe { CopySid(sid_len, out.as_mut_ptr() as *mut c_void, psid) }; - unsafe { - LocalFree(psid as _); - } - if ok == 0 { - return Err(anyhow::anyhow!("CopySid failed for {sid_str}")); - } - Ok(out) -} - fn lookup_account_name_for_sid(sid_str: &str) -> Result { let sid_w = to_wide(OsStr::new(sid_str)); let mut psid: *mut c_void = std::ptr::null_mut(); diff --git a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs index 5363ae6686..1b1766ac79 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs @@ -19,12 +19,6 @@ use std::io::Read; use std::io::Write; use std::path::PathBuf; -/// Safety cap for a single framed message payload. -/// -/// This is not a protocol requirement; it simply bounds memory use and rejects -/// obviously invalid frames. -const MAX_FRAME_LEN: usize = 8 * 1024 * 1024; - /// Protocol version shared by the parent process and elevated command runner. pub const IPC_PROTOCOL_VERSION: u8 = 6; @@ -151,34 +145,13 @@ pub fn decode_bytes(data: &str) -> Result> { } /// Write a length-prefixed JSON frame. -pub fn write_frame(mut writer: W, msg: &FramedMessage) -> Result<()> { - let payload = serde_json::to_vec(msg)?; - if payload.len() > MAX_FRAME_LEN { - anyhow::bail!("frame too large: {}", payload.len()); - } - let len = payload.len() as u32; - writer.write_all(&len.to_le_bytes())?; - writer.write_all(&payload)?; - writer.flush()?; - Ok(()) +pub fn write_frame(writer: W, msg: &FramedMessage) -> Result<()> { + crate::framed_io::write_frame(writer, msg) } /// Read a length-prefixed JSON frame; returns `Ok(None)` on EOF. -pub fn read_frame(mut reader: R) -> Result> { - let mut len_buf = [0u8; 4]; - match reader.read_exact(&mut len_buf) { - Ok(()) => {} - Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), - Err(err) => return Err(err.into()), - } - let len = u32::from_le_bytes(len_buf) as usize; - if len > MAX_FRAME_LEN { - anyhow::bail!("frame too large: {len}"); - } - let mut payload = vec![0u8; len]; - reader.read_exact(&mut payload)?; - let msg: FramedMessage = serde_json::from_slice(&payload)?; - Ok(Some(msg)) +pub fn read_frame(reader: R) -> Result> { + crate::framed_io::read_frame(reader) } #[cfg(test)] diff --git a/codex-rs/windows-sandbox-rs/src/framed_io.rs b/codex-rs/windows-sandbox-rs/src/framed_io.rs new file mode 100644 index 0000000000..7ed5662ed5 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/framed_io.rs @@ -0,0 +1,39 @@ +//! Shared length-prefixed JSON framing for independent Windows sandbox IPC protocols. + +use anyhow::Result; +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::io::Read; +use std::io::Write; + +/// Bound the memory used by an individual untrusted IPC frame. +const MAX_FRAME_LEN: usize = 8 * 1024 * 1024; + +pub(crate) fn write_frame(mut writer: W, message: &T) -> Result<()> { + let payload = serde_json::to_vec(message)?; + if payload.len() > MAX_FRAME_LEN { + anyhow::bail!("frame too large: {}", payload.len()); + } + let len = payload.len() as u32; + writer.write_all(&len.to_le_bytes())?; + writer.write_all(&payload)?; + writer.flush()?; + Ok(()) +} + +pub(crate) fn read_frame(mut reader: R) -> Result> { + let mut len_buf = [0u8; 4]; + match reader.read_exact(&mut len_buf) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), + Err(err) => return Err(err.into()), + } + let len = u32::from_le_bytes(len_buf) as usize; + if len > MAX_FRAME_LEN { + anyhow::bail!("frame too large: {len}"); + } + let mut payload = vec![0u8; len]; + reader.read_exact(&mut payload)?; + let message = serde_json::from_slice(&payload)?; + Ok(Some(message)) +} diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs index 3fa21a83a0..6bcc285749 100644 --- a/codex-rs/windows-sandbox-rs/src/identity.rs +++ b/codex-rs/windows-sandbox-rs/src/identity.rs @@ -47,6 +47,24 @@ pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool { matches!(load_users(codex_home), Ok(Some(users)) if users.version_matches()) } +/// Returns true when setup artifacts and provisioned network settings match. +pub fn sandbox_setup_is_complete_with_settings( + codex_home: &Path, + settings: &crate::WindowsSandboxProvisioningSettings, +) -> bool { + let Ok(Some(mut marker)) = load_marker(codex_home) else { + return false; + }; + + marker.proxy_ports.sort_unstable(); + let mut proxy_ports = settings.proxy_ports.clone(); + proxy_ports.sort_unstable(); + marker.version_matches() + && marker.proxy_ports == proxy_ports + && marker.allow_local_binding == settings.allow_local_binding + && matches!(load_users(codex_home), Ok(Some(users)) if users.version_matches()) +} + fn load_marker(codex_home: &Path) -> Result> { let path = setup_marker_path(codex_home); let marker = match fs::read_to_string(&path) { diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 0ca4dcafe8..7ccea63198 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -65,6 +65,8 @@ mod dpapi; #[cfg(target_os = "windows")] mod env; #[cfg(target_os = "windows")] +mod framed_io; +#[cfg(target_os = "windows")] mod helper_materialization; #[cfg(target_os = "windows")] mod hide_users; @@ -77,6 +79,8 @@ mod path_normalization; #[cfg(target_os = "windows")] mod process; #[cfg(target_os = "windows")] +mod provisioning_protocol; +#[cfg(target_os = "windows")] mod resolved_permissions; #[cfg(target_os = "windows")] mod token; @@ -197,6 +201,8 @@ pub use identity::require_logon_sandbox_creds; #[cfg(target_os = "windows")] pub use identity::sandbox_setup_is_complete; #[cfg(target_os = "windows")] +pub use identity::sandbox_setup_is_complete_with_settings; +#[cfg(target_os = "windows")] pub use ipc_framed::ErrorPayload; #[cfg(target_os = "windows")] pub use ipc_framed::ErrorStage; @@ -253,6 +259,22 @@ pub use process::read_handle_loop; #[cfg(target_os = "windows")] pub use process::spawn_process_with_pipes; #[cfg(target_os = "windows")] +pub use provisioning_protocol::FramedProvisioningMessage; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::PROVISIONING_PROTOCOL_VERSION; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::ProvisioningMessage; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::SANDBOX_PROVISIONING_PIPE_NAME; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::SandboxProvisioningRequest; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::SandboxProvisioningResponse; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::read_provisioning_frame; +#[cfg(target_os = "windows")] +pub use provisioning_protocol::write_provisioning_frame; +#[cfg(target_os = "windows")] pub use resolved_permissions::ResolvedWindowsSandboxPermissions; #[cfg(target_os = "windows")] pub use resolved_permissions::WindowsSandboxTokenMode; @@ -338,8 +360,14 @@ pub use windows_impl::run_windows_sandbox_capture_with_filesystem_overrides; #[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_legacy_preflight; #[cfg(target_os = "windows")] +pub use winutil::SANDBOX_USERS_GROUP; +#[cfg(target_os = "windows")] +pub use winutil::ensure_sandbox_users_group; +#[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::string_from_sid_bytes; #[cfg(target_os = "windows")] pub use winutil::to_wide; diff --git a/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs b/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs new file mode 100644 index 0000000000..fb6dd5250a --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/provisioning_protocol.rs @@ -0,0 +1,64 @@ +//! Dedicated protocol exchanged with the Windows sandbox provisioning service. + +use anyhow::Result; +use serde::Deserialize; +use serde::Serialize; +use std::io::Read; +use std::io::Write; + +/// Protocol version shared by provisioning clients and the Windows service. +pub const PROVISIONING_PROTOCOL_VERSION: u8 = 1; + +/// Named pipe used by the machine-wide Windows sandbox provisioning service. +pub const SANDBOX_PROVISIONING_PIPE_NAME: &str = r"\\.\pipe\OpenAI.CodexSandbox"; + +/// Versioned provisioning-service message carried in a length-prefixed JSON frame. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FramedProvisioningMessage { + pub version: u8, + #[serde(flatten)] + pub message: ProvisioningMessage, +} + +/// Request and response messages exchanged with the provisioning service. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ProvisioningMessage { + ProvisionSandboxRequest { + payload: SandboxProvisioningRequest, + }, + ProvisionSandboxResponse { + payload: SandboxProvisioningResponse, + }, +} + +/// Sandbox setup parameters sent to the provisioning service. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct SandboxProvisioningRequest { + pub codex_home: String, + pub http_port: Option, + pub socks_port: Option, + pub allow_local_binding: bool, +} + +/// Result returned by the sandbox provisioning service. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum SandboxProvisioningResponse { + Ok, + Error { message: String }, +} + +/// Write a length-prefixed provisioning-service message. +pub fn write_provisioning_frame( + writer: W, + message: &FramedProvisioningMessage, +) -> Result<()> { + crate::framed_io::write_frame(writer, message) +} + +/// Read a length-prefixed provisioning-service message, returning `None` on EOF. +pub fn read_provisioning_frame(reader: R) -> Result> { + crate::framed_io::read_frame(reader) +} diff --git a/codex-rs/windows-sandbox-rs/src/winutil.rs b/codex-rs/windows-sandbox-rs/src/winutil.rs index aa281d9a6d..704cce5ec6 100644 --- a/codex-rs/windows-sandbox-rs/src/winutil.rs +++ b/codex-rs/windows-sandbox-rs/src/winutil.rs @@ -5,6 +5,9 @@ use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; use windows_sys::Win32::Foundation::GetLastError; 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::NetLocalGroupAdd; use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW; use windows_sys::Win32::Security::CopySid; @@ -16,6 +19,9 @@ use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM; use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS; use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW; +pub const SANDBOX_USERS_GROUP: &str = "CodexSandboxUsers"; +const SANDBOX_USERS_GROUP_COMMENT: &str = "Codex sandbox internal group (managed)"; + pub fn to_wide>(s: S) -> Vec { let mut v: Vec = s.as_ref().encode_wide().collect(); v.push(0); @@ -129,6 +135,34 @@ 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 ensure_sandbox_users_group() -> Result> { + const ERROR_ALIAS_EXISTS: u32 = 1379; + const NERR_GROUP_EXISTS: u32 = 2223; + + let name = to_wide(SANDBOX_USERS_GROUP); + let comment = to_wide(SANDBOX_USERS_GROUP_COMMENT); + let info = LOCALGROUP_INFO_1 { + lgrpi1_name: name.as_ptr() as *mut u16, + lgrpi1_comment: comment.as_ptr() as *mut u16, + }; + let mut parameter_error = 0; + let status = unsafe { + NetLocalGroupAdd( + std::ptr::null(), + 1, + (&raw const info).cast(), + &raw mut parameter_error, + ) + }; + if status != NERR_Success && status != ERROR_ALIAS_EXISTS && status != NERR_GROUP_EXISTS { + return Err(anyhow::anyhow!( + "NetLocalGroupAdd failed for {SANDBOX_USERS_GROUP} code {status} parm_err={parameter_error}" + )); + } + + resolve_sid(SANDBOX_USERS_GROUP) +} + pub fn resolve_sid(name: &str) -> Result> { if let Some(sid_str) = well_known_sid_str(name) { return sid_bytes_from_string(sid_str);