diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index ae0927c3df..a895d4f563 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -82,6 +82,7 @@ core-foundation = "0.9" [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ "Win32_Foundation", + "Win32_Security_Isolation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", @@ -89,6 +90,10 @@ windows = { version = "0.58", features = [ "Win32_System_Threading", ] } +[features] +default = [] +windows_appcontainer_command_ext = [] + # Build OpenSSL from source for musl builds. [target.x86_64-unknown-linux-musl.dependencies] openssl-sys = { workspace = true, features = ["vendored"] } diff --git a/codex-rs/core/src/windows_appcontainer.rs b/codex-rs/core/src/windows_appcontainer.rs index 0fc340dc8c..abec508918 100644 --- a/codex-rs/core/src/windows_appcontainer.rs +++ b/codex-rs/core/src/windows_appcontainer.rs @@ -1,482 +1,459 @@ -use std::collections::HashMap; -use std::ffi::OsStr; -use std::ffi::c_void; -use std::io; -use std::os::windows::ffi::OsStrExt; -use std::os::windows::process::CommandExt; -use std::path::Path; -use std::path::PathBuf; -use std::ptr::null_mut; - -use tokio::process::Child; -use tokio::process::Command; use tracing::trace; use crate::protocol::SandboxPolicy; -use crate::spawn::CODEX_SANDBOX_ENV_VAR; -use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::spawn::StdioPolicy; -use windows::Win32::Foundation::ERROR_ALREADY_EXISTS; -use windows::Win32::Foundation::ERROR_SUCCESS; -use windows::Win32::Foundation::GetLastError; -use windows::Win32::Foundation::HANDLE; -use windows::Win32::Foundation::HLOCAL; -use windows::Win32::Foundation::WIN32_ERROR; -use windows::Win32::Security::ACL; -use windows::Win32::Security::Authorization::ConvertStringSidToSidW; -use windows::Win32::Security::Authorization::EXPLICIT_ACCESS_W; -use windows::Win32::Security::Authorization::GetNamedSecurityInfoW; -use windows::Win32::Security::Authorization::OBJECT_INHERIT_ACE; -use windows::Win32::Security::Authorization::SE_FILE_OBJECT; -use windows::Win32::Security::Authorization::SET_ACCESS; -use windows::Win32::Security::Authorization::SUB_CONTAINERS_AND_OBJECTS_INHERIT; -use windows::Win32::Security::Authorization::SetEntriesInAclW; -use windows::Win32::Security::Authorization::SetNamedSecurityInfoW; -use windows::Win32::Security::Authorization::TRUSTEE_IS_SID; -use windows::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN; -use windows::Win32::Security::Authorization::TRUSTEE_W; -use windows::Win32::Security::DACL_SECURITY_INFORMATION; -use windows::Win32::Security::FreeSid; -use windows::Win32::Security::Isolation::CreateAppContainerProfile; -use windows::Win32::Security::Isolation::DeriveAppContainerSidFromAppContainerName; -use windows::Win32::Security::PSECURITY_DESCRIPTOR; -use windows::Win32::Security::PSID; -use windows::Win32::Security::SECURITY_CAPABILITIES; -use windows::Win32::Security::SID_AND_ATTRIBUTES; -use windows::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE; -use windows::Win32::Storage::FileSystem::FILE_GENERIC_READ; -use windows::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; -use windows::Win32::System::Memory::GetProcessHeap; -use windows::Win32::System::Memory::HEAP_FLAGS; -use windows::Win32::System::Memory::HEAP_ZERO_MEMORY; -use windows::Win32::System::Memory::HeapAlloc; -use windows::Win32::System::Memory::HeapFree; -use windows::Win32::System::Memory::LocalFree; -use windows::Win32::System::Threading::DeleteProcThreadAttributeList; -use windows::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT; -use windows::Win32::System::Threading::InitializeProcThreadAttributeList; -use windows::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST; -use windows::Win32::System::Threading::PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES; -use windows::Win32::System::Threading::UpdateProcThreadAttribute; -use windows::core::PCWSTR; -use windows::core::PWSTR; +#[cfg(feature = "windows_appcontainer_command_ext")] +mod imp { + use super::*; -/// Friendly name for the profile we create on-demand. -const WINDOWS_APPCONTAINER_PROFILE_NAME: &str = "codex_appcontainer"; -const WINDOWS_APPCONTAINER_PROFILE_DESC: &str = "Codex Windows AppContainer profile"; -/// Marker injected into the child so downstream tools can detect the sandbox. -const WINDOWS_APPCONTAINER_SANDBOX_VALUE: &str = "windows_appcontainer"; -/// Capability SID strings that unlock outbound networking when the policy allows it. -const INTERNET_CLIENT_SID: &str = "S-1-15-3-1"; -const PRIVATE_NETWORK_CLIENT_SID: &str = "S-1-15-3-3"; + use std::ffi::OsStr; + use std::ffi::c_void; + use std::os::windows::ffi::OsStrExt; + use std::os::windows::process::CommandExt; + use std::ptr::null_mut; -/// Runs the provided command inside an AppContainer sandbox that mirrors the -/// sandbox policy Codex already uses on macOS seatbelt and Linux Landlock. The -/// Windows sandbox flow is intentionally verbose so future contributors can map -/// each Windows API call to the equivalent behavior in the other platforms. + use tokio::process::Command; + + use crate::spawn::CODEX_SANDBOX_ENV_VAR; + use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; + + use windows::Win32::Foundation::ERROR_ALREADY_EXISTS; + use windows::Win32::Foundation::ERROR_SUCCESS; + use windows::Win32::Foundation::GetLastError; + use windows::Win32::Foundation::HANDLE; + use windows::Win32::Foundation::HLOCAL; + use windows::Win32::Foundation::LocalFree; + use windows::Win32::Foundation::WIN32_ERROR; + use windows::Win32::Security::ACL; + use windows::Win32::Security::Authorization::ConvertStringSidToSidW; + use windows::Win32::Security::Authorization::EXPLICIT_ACCESS_W; + use windows::Win32::Security::Authorization::GetNamedSecurityInfoW; + use windows::Win32::Security::Authorization::SE_FILE_OBJECT; + use windows::Win32::Security::Authorization::SET_ACCESS; + use windows::Win32::Security::Authorization::SetEntriesInAclW; + use windows::Win32::Security::Authorization::SetNamedSecurityInfoW; + use windows::Win32::Security::Authorization::TRUSTEE_IS_SID; + use windows::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN; + use windows::Win32::Security::Authorization::TRUSTEE_W; + use windows::Win32::Security::DACL_SECURITY_INFORMATION; + use windows::Win32::Security::FreeSid; + use windows::Win32::Security::Isolation::CreateAppContainerProfile; + use windows::Win32::Security::Isolation::DeriveAppContainerSidFromAppContainerName; + use windows::Win32::Security::OBJECT_INHERIT_ACE; + use windows::Win32::Security::PSECURITY_DESCRIPTOR; + use windows::Win32::Security::PSID; + use windows::Win32::Security::SECURITY_CAPABILITIES; + use windows::Win32::Security::SID_AND_ATTRIBUTES; + use windows::Win32::Security::SUB_CONTAINERS_AND_OBJECTS_INHERIT; + use windows::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE; + use windows::Win32::Storage::FileSystem::FILE_GENERIC_READ; + use windows::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; + use windows::Win32::System::Memory::GetProcessHeap; + use windows::Win32::System::Memory::HEAP_FLAGS; + use windows::Win32::System::Memory::HEAP_ZERO_MEMORY; + use windows::Win32::System::Memory::HeapAlloc; + use windows::Win32::System::Memory::HeapFree; + use windows::Win32::System::Threading::DeleteProcThreadAttributeList; + use windows::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT; + use windows::Win32::System::Threading::InitializeProcThreadAttributeList; + use windows::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST; + use windows::Win32::System::Threading::PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES; + use windows::Win32::System::Threading::UpdateProcThreadAttribute; + use windows::core::PCWSTR; + use windows::core::PWSTR; + + const WINDOWS_APPCONTAINER_PROFILE_NAME: &str = "codex_appcontainer"; + const WINDOWS_APPCONTAINER_PROFILE_DESC: &str = "Codex Windows AppContainer profile"; + const WINDOWS_APPCONTAINER_SANDBOX_VALUE: &str = "windows_appcontainer"; + const INTERNET_CLIENT_SID: &str = "S-1-15-3-1"; + const PRIVATE_NETWORK_CLIENT_SID: &str = "S-1-15-3-3"; + + pub async fn spawn_command_under_windows_appcontainer( + command: Vec, + command_cwd: PathBuf, + sandbox_policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + stdio_policy: StdioPolicy, + mut env: HashMap, + ) -> io::Result { + trace!("windows appcontainer sandbox command = {:?}", command); + + let (program, rest) = command + .split_first() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "command args are empty"))?; + + ensure_appcontainer_profile()?; + let mut sid = derive_appcontainer_sid()?; + let mut capability_sids = build_capabilities(sandbox_policy)?; + let mut attribute_list = AttributeList::new(&mut sid, &mut capability_sids)?; + + configure_writable_roots(sandbox_policy, sandbox_policy_cwd, sid.sid())?; + configure_writable_roots_for_command_cwd(&command_cwd, sid.sid())?; + + if !sandbox_policy.has_full_network_access() { + env.insert( + CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(), + "1".to_string(), + ); + } + env.insert( + CODEX_SANDBOX_ENV_VAR.to_string(), + WINDOWS_APPCONTAINER_SANDBOX_VALUE.to_string(), + ); + + let mut cmd = Command::new(program); + cmd.args(rest); + cmd.current_dir(command_cwd); + cmd.env_clear(); + cmd.envs(env); + apply_stdio_policy(&mut cmd, stdio_policy); + cmd.kill_on_drop(true); + + unsafe { + let std_cmd = cmd.as_std_mut(); + std_cmd.creation_flags(EXTENDED_STARTUPINFO_PRESENT.0); + std_cmd.raw_attribute_list(attribute_list.as_mut_ptr().0); + } + + let child = cmd.spawn(); + drop(attribute_list); + child + } + + fn apply_stdio_policy(cmd: &mut Command, policy: StdioPolicy) { + match policy { + StdioPolicy::RedirectForShellTool => { + cmd.stdin(std::process::Stdio::null()); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + } + StdioPolicy::Inherit => { + cmd.stdin(std::process::Stdio::inherit()); + cmd.stdout(std::process::Stdio::inherit()); + cmd.stderr(std::process::Stdio::inherit()); + } + } + } + + fn to_wide>(s: S) -> Vec { + s.as_ref().encode_wide().chain(std::iter::once(0)).collect() + } + + fn ensure_appcontainer_profile() -> io::Result<()> { + unsafe { + let name = to_wide(WINDOWS_APPCONTAINER_PROFILE_NAME); + let desc = to_wide(WINDOWS_APPCONTAINER_PROFILE_DESC); + match CreateAppContainerProfile( + PCWSTR(name.as_ptr()), + PCWSTR(name.as_ptr()), + PCWSTR(desc.as_ptr()), + None, + ) { + Ok(profile_sid) => { + if !profile_sid.is_invalid() { + FreeSid(profile_sid); + } + } + Err(error) => { + let already_exists = WIN32_ERROR::from(ERROR_ALREADY_EXISTS); + if GetLastError() != already_exists { + return Err(io::Error::from_raw_os_error(error.code().0)); + } + } + } + } + Ok(()) + } + + struct SidHandle { + ptr: PSID, + } + + impl SidHandle { + fn sid(&self) -> PSID { + self.ptr + } + } + + impl Drop for SidHandle { + fn drop(&mut self) { + unsafe { + if !self.ptr.is_invalid() { + FreeSid(self.ptr); + } + } + } + } + + fn derive_appcontainer_sid() -> io::Result { + unsafe { + let name = to_wide(WINDOWS_APPCONTAINER_PROFILE_NAME); + let sid = DeriveAppContainerSidFromAppContainerName(PCWSTR(name.as_ptr())) + .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; + Ok(SidHandle { ptr: sid }) + } + } + + struct CapabilitySid { + sid: PSID, + } + + impl CapabilitySid { + fn new_from_string(value: &str) -> io::Result { + unsafe { + let mut sid_ptr = PSID::default(); + let wide = to_wide(value); + ConvertStringSidToSidW(PCWSTR(wide.as_ptr()), &mut sid_ptr) + .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; + Ok(Self { sid: sid_ptr }) + } + } + + fn sid_and_attributes(&self) -> SID_AND_ATTRIBUTES { + SID_AND_ATTRIBUTES { + Sid: self.sid, + Attributes: 0, + } + } + } + + impl Drop for CapabilitySid { + fn drop(&mut self) { + unsafe { + if !self.sid.is_invalid() { + let _ = LocalFree(HLOCAL(self.sid.0)); + } + } + } + } + + fn build_capabilities(policy: &SandboxPolicy) -> io::Result> { + if policy.has_full_network_access() { + Ok(vec![ + CapabilitySid::new_from_string(INTERNET_CLIENT_SID)?, + CapabilitySid::new_from_string(PRIVATE_NETWORK_CLIENT_SID)?, + ]) + } else { + Ok(Vec::new()) + } + } + + struct AttributeList<'a> { + heap: HANDLE, + buffer: *mut c_void, + list: LPPROC_THREAD_ATTRIBUTE_LIST, + sec_caps: SECURITY_CAPABILITIES, + sid_and_attributes: Vec, + #[allow(dead_code)] + sid: &'a mut SidHandle, + #[allow(dead_code)] + capabilities: &'a mut Vec, + } + + impl<'a> AttributeList<'a> { + fn new(sid: &'a mut SidHandle, caps: &'a mut Vec) -> io::Result { + unsafe { + let mut list_size = 0usize; + let _ = InitializeProcThreadAttributeList( + LPPROC_THREAD_ATTRIBUTE_LIST::default(), + 1, + 0, + &mut list_size, + ); + let heap = + GetProcessHeap().map_err(|e| io::Error::from_raw_os_error(e.code().0))?; + let buffer = HeapAlloc(heap, HEAP_ZERO_MEMORY, list_size); + if buffer.is_null() { + return Err(io::Error::last_os_error()); + } + let list = LPPROC_THREAD_ATTRIBUTE_LIST(buffer); + InitializeProcThreadAttributeList(list, 1, 0, &mut list_size) + .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; + + let mut sid_and_attributes: Vec = + caps.iter().map(CapabilitySid::sid_and_attributes).collect(); + + let mut sec_caps = SECURITY_CAPABILITIES { + AppContainerSid: sid.sid(), + Capabilities: if sid_and_attributes.is_empty() { + null_mut() + } else { + sid_and_attributes.as_mut_ptr() + }, + CapabilityCount: sid_and_attributes.len() as u32, + Reserved: 0, + }; + + UpdateProcThreadAttribute( + list, + 0, + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize, + Some(&mut sec_caps as *mut _ as *const std::ffi::c_void), + std::mem::size_of::(), + None, + None, + ) + .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; + + Ok(Self { + heap, + buffer, + list, + sec_caps, + sid_and_attributes, + sid, + capabilities: caps, + }) + } + } + + fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { + self.list + } + } + + impl Drop for AttributeList<'_> { + fn drop(&mut self) { + unsafe { + if !self.list.is_invalid() { + DeleteProcThreadAttributeList(self.list); + } + if !self.heap.is_invalid() && !self.buffer.is_null() { + let _ = HeapFree(self.heap, HEAP_FLAGS(0), Some(self.buffer)); + } + } + } + } + + fn configure_writable_roots( + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + sid: PSID, + ) -> io::Result<()> { + match policy { + SandboxPolicy::DangerFullAccess => Ok(()), + SandboxPolicy::ReadOnly => grant_path_with_flags(sandbox_policy_cwd, sid, false), + SandboxPolicy::WorkspaceWrite { .. } => { + let roots = policy.get_writable_roots_with_cwd(sandbox_policy_cwd); + for writable in roots { + grant_path_with_flags(&writable.root, sid, true)?; + for ro in writable.read_only_subpaths { + grant_path_with_flags(&ro, sid, false)?; + } + } + Ok(()) + } + } + } + + fn configure_writable_roots_for_command_cwd(command_cwd: &Path, sid: PSID) -> io::Result<()> { + grant_path_with_flags(command_cwd, sid, true) + } + + fn grant_path_with_flags(path: &Path, sid: PSID, write: bool) -> io::Result<()> { + if !path.exists() { + return Ok(()); + } + + let wide = to_wide(path.as_os_str()); + unsafe { + let mut existing_dacl: *mut ACL = null_mut(); + let mut security_descriptor = PSECURITY_DESCRIPTOR::default(); + let status = GetNamedSecurityInfoW( + PCWSTR(wide.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + None, + None, + Some(&mut existing_dacl), + None, + &mut security_descriptor, + ); + if status != WIN32_ERROR::from(ERROR_SUCCESS) { + if !security_descriptor.is_invalid() { + let _ = LocalFree(HLOCAL(security_descriptor.0)); + } + return Err(io::Error::from_raw_os_error(status.0 as i32)); + } + + let permissions = if write { + (FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE).0 + } else { + (FILE_GENERIC_READ | FILE_GENERIC_EXECUTE).0 + }; + let explicit = EXPLICIT_ACCESS_W { + grfAccessPermissions: permissions, + grfAccessMode: SET_ACCESS, + grfInheritance: (SUB_CONTAINERS_AND_OBJECTS_INHERIT | OBJECT_INHERIT_ACE).0, + Trustee: TRUSTEE_W { + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_UNKNOWN, + ptstrName: PWSTR(sid.0.cast()), + ..Default::default() + }, + }; + + let explicit_entries = [explicit]; + let mut new_dacl: *mut ACL = null_mut(); + let add_result = + SetEntriesInAclW(Some(&explicit_entries), Some(existing_dacl), &mut new_dacl); + if add_result != WIN32_ERROR::from(ERROR_SUCCESS) { + if !new_dacl.is_null() { + let _ = LocalFree(HLOCAL(new_dacl.cast())); + } + if !security_descriptor.is_invalid() { + let _ = LocalFree(HLOCAL(security_descriptor.0)); + } + return Err(io::Error::from_raw_os_error(add_result.0 as i32)); + } + + let set_result = SetNamedSecurityInfoW( + PCWSTR(wide.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + None, + None, + Some(new_dacl), + None, + ); + if set_result != WIN32_ERROR::from(ERROR_SUCCESS) { + if !new_dacl.is_null() { + let _ = LocalFree(HLOCAL(new_dacl.cast())); + } + if !security_descriptor.is_invalid() { + let _ = LocalFree(HLOCAL(security_descriptor.0)); + } + return Err(io::Error::from_raw_os_error(set_result.0 as i32)); + } + + if !new_dacl.is_null() { + let _ = LocalFree(HLOCAL(new_dacl.cast())); + } + if !security_descriptor.is_invalid() { + let _ = LocalFree(HLOCAL(security_descriptor.0)); + } + } + + Ok(()) + } +} + +#[cfg(feature = "windows_appcontainer_command_ext")] +pub use imp::spawn_command_under_windows_appcontainer; + +#[cfg(not(feature = "windows_appcontainer_command_ext"))] pub async fn spawn_command_under_windows_appcontainer( command: Vec, command_cwd: PathBuf, - sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - stdio_policy: StdioPolicy, - mut env: HashMap, + _sandbox_policy: &SandboxPolicy, + _sandbox_policy_cwd: &Path, + _stdio_policy: StdioPolicy, + _env: HashMap, ) -> io::Result { - trace!("windows appcontainer sandbox command = {:?}", command); - - let (program, rest) = command - .split_first() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "command args are empty"))?; - - // Windows requires a named profile before it will create an AppContainer - // token, so we create-or-open the profile and then derive the SID that we - // will hand to CreateProcess via the extended startup info structure. - ensure_appcontainer_profile()?; - let mut sid = derive_appcontainer_sid()?; - // Capabilities translate Codex' sandbox policy knobs (for now just - // networking) into Windows capability SIDs that can be attached to the - // token. When the policy does not require a capability the vector is empty - // and UpdateProcThreadAttribute receives a null pointer instead. - let mut capability_sids = build_capabilities(sandbox_policy)?; - // The attribute list owns the SECURITY_CAPABILITIES struct plus the heap - // buffer required by UpdateProcThreadAttribute. Keeping it in a guard object - // mirrors the RAII helpers we already use on the Unix sandboxes. - let mut attribute_list = AttributeList::new(&mut sid, &mut capability_sids)?; - - // The Linux and macOS implementations pre-authorize the workspace so the - // tool call can write to the expected roots. We replicate that behavior by - // updating the directory ACLs for the derived AppContainer SID. - configure_writable_roots(sandbox_policy, sandbox_policy_cwd, sid.sid())?; - configure_writable_roots_for_command_cwd(&command_cwd, sid.sid())?; - - if !sandbox_policy.has_full_network_access() { - env.insert( - CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(), - "1".to_string(), - ); - } - env.insert( - CODEX_SANDBOX_ENV_VAR.to_string(), - WINDOWS_APPCONTAINER_SANDBOX_VALUE.to_string(), - ); - - let mut cmd = Command::new(program); - cmd.args(rest); - cmd.current_dir(command_cwd); - cmd.env_clear(); - cmd.envs(env); - apply_stdio_policy(&mut cmd, stdio_policy); - cmd.kill_on_drop(true); - - unsafe { - let std_cmd = cmd.as_std_mut(); - std_cmd.creation_flags(EXTENDED_STARTUPINFO_PRESENT); - std_cmd.raw_attribute_list(attribute_list.as_mut_ptr().0); - } - - let child = cmd.spawn(); - drop(attribute_list); - child -} - -fn apply_stdio_policy(cmd: &mut Command, policy: StdioPolicy) { - match policy { - StdioPolicy::RedirectForShellTool => { - cmd.stdin(std::process::Stdio::null()); - cmd.stdout(std::process::Stdio::piped()); - cmd.stderr(std::process::Stdio::piped()); - } - StdioPolicy::Inherit => { - cmd.stdin(std::process::Stdio::inherit()); - cmd.stdout(std::process::Stdio::inherit()); - cmd.stderr(std::process::Stdio::inherit()); - } - } -} - -/// Converts a UTF-8 string into a Windows-compatible UTF-16 buffer with a -/// trailing nul byte. The helper keeps the conversion close to the code that -/// owns the literal strings so maintenance is straightforward. -fn to_wide>(s: S) -> Vec { - s.as_ref().encode_wide().chain(std::iter::once(0)).collect() -} - -/// Creates the AppContainer profile if it does not already exist. Windows keeps -/// track of AppContainer profiles globally for the user account, so subsequent -/// calls simply observe `ERROR_ALREADY_EXISTS` and continue. -fn ensure_appcontainer_profile() -> io::Result<()> { - unsafe { - let name = to_wide(WINDOWS_APPCONTAINER_PROFILE_NAME); - let desc = to_wide(WINDOWS_APPCONTAINER_PROFILE_DESC); - match CreateAppContainerProfile( - PCWSTR(name.as_ptr()), - PCWSTR(name.as_ptr()), - PCWSTR(desc.as_ptr()), - None, - ) { - Ok(profile_sid) => { - if !profile_sid.is_invalid() { - FreeSid(profile_sid); - } - } - Err(error) => { - let already_exists = WIN32_ERROR::from(ERROR_ALREADY_EXISTS); - if GetLastError() != already_exists { - return Err(io::Error::from_raw_os_error(error.code().0)); - } - } - } - } - Ok(()) -} - -/// Small RAII wrapper around the derived AppContainer SID so we always release -/// it via `FreeSid` when the sandbox scaffolding is dropped. -struct SidHandle { - ptr: PSID, -} - -impl SidHandle { - fn sid(&self) -> PSID { - self.ptr - } -} - -impl Drop for SidHandle { - fn drop(&mut self) { - unsafe { - if !self.ptr.is_invalid() { - FreeSid(self.ptr); - } - } - } -} - -fn derive_appcontainer_sid() -> io::Result { - unsafe { - let name = to_wide(WINDOWS_APPCONTAINER_PROFILE_NAME); - let sid = DeriveAppContainerSidFromAppContainerName(PCWSTR(name.as_ptr())) - .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; - Ok(SidHandle { ptr: sid }) - } -} - -/// Holds capability SIDs that are allocated with `LocalAlloc`. Keeping the -/// pointers alive inside a struct simplifies cleanup. -struct CapabilitySid { - sid: PSID, -} - -impl CapabilitySid { - fn new_from_string(value: &str) -> io::Result { - unsafe { - let mut sid_ptr = PSID::default(); - let wide = to_wide(value); - ConvertStringSidToSidW(PCWSTR(wide.as_ptr()), &mut sid_ptr) - .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; - Ok(Self { sid: sid_ptr }) - } - } - - fn sid_and_attributes(&self) -> SID_AND_ATTRIBUTES { - SID_AND_ATTRIBUTES { - Sid: self.sid, - Attributes: 0, - } - } -} - -impl Drop for CapabilitySid { - fn drop(&mut self) { - unsafe { - if !self.sid.is_invalid() { - let _ = LocalFree(HLOCAL(self.sid.0)); - } - } - } -} - -fn build_capabilities(policy: &SandboxPolicy) -> io::Result> { - if policy.has_full_network_access() { - // Matching the other platforms, enabling network access translates to - // enabling both the public-internet capability and the private-network - // capability. Each SID is allocated with LocalAlloc so the RAII wrapper - // releases it automatically when the sandbox scaffolding drops. - Ok(vec![ - CapabilitySid::new_from_string(INTERNET_CLIENT_SID)?, - CapabilitySid::new_from_string(PRIVATE_NETWORK_CLIENT_SID)?, - ]) - } else { - Ok(Vec::new()) - } -} - -/// Manages the Windows attribute list that injects `SECURITY_CAPABILITIES` -/// (the AppContainer SID plus capability SIDs) into `CreateProcessW`. -struct AttributeList<'a> { - heap: HANDLE, - buffer: *mut c_void, - list: LPPROC_THREAD_ATTRIBUTE_LIST, - sec_caps: SECURITY_CAPABILITIES, - sid_and_attributes: Vec, - #[allow(dead_code)] - sid: &'a mut SidHandle, - #[allow(dead_code)] - capabilities: &'a mut Vec, -} - -impl<'a> AttributeList<'a> { - fn new(sid: &'a mut SidHandle, caps: &'a mut Vec) -> io::Result { - unsafe { - let mut list_size = 0usize; - let _ = InitializeProcThreadAttributeList( - LPPROC_THREAD_ATTRIBUTE_LIST::default(), - 1, - 0, - &mut list_size, - ); - let heap = GetProcessHeap().map_err(|e| io::Error::from_raw_os_error(e.code().0))?; - let buffer = HeapAlloc(heap, HEAP_ZERO_MEMORY, list_size); - if buffer.is_null() { - return Err(io::Error::last_os_error()); - } - let list = LPPROC_THREAD_ATTRIBUTE_LIST(buffer); - InitializeProcThreadAttributeList(list, 1, 0, &mut list_size) - .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; - - let mut sid_and_attributes: Vec = - caps.iter().map(CapabilitySid::sid_and_attributes).collect(); - - let mut sec_caps = SECURITY_CAPABILITIES { - AppContainerSid: sid.sid(), - Capabilities: if sid_and_attributes.is_empty() { - null_mut() - } else { - sid_and_attributes.as_mut_ptr() - }, - CapabilityCount: sid_and_attributes.len() as u32, - Reserved: 0, - }; - - UpdateProcThreadAttribute( - list, - 0, - PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize, - Some(&mut sec_caps as *mut _ as *const std::ffi::c_void), - std::mem::size_of::(), - None, - None, - ) - .map_err(|e| io::Error::from_raw_os_error(e.code().0))?; - - Ok(Self { - heap, - buffer, - list, - sec_caps, - sid_and_attributes, - sid, - capabilities: caps, - }) - } - } - - fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { - self.list - } -} - -impl Drop for AttributeList<'_> { - fn drop(&mut self) { - unsafe { - if !self.list.is_invalid() { - DeleteProcThreadAttributeList(self.list); - } - if !self.heap.is_invalid() && !self.buffer.is_null() { - let _ = HeapFree(self.heap, HEAP_FLAGS(0), Some(self.buffer)); - } - } - } -} - -/// Applies directory ACLs for every writable root described by the sandbox -/// policy. Granting explicit rights to the AppContainer SID mirrors how the -/// macOS and Linux sandboxes pre-authorize the workspace while leaving the rest -/// of the filesystem read-only. -fn configure_writable_roots( - policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, - sid: PSID, -) -> io::Result<()> { - match policy { - SandboxPolicy::DangerFullAccess => Ok(()), - SandboxPolicy::ReadOnly => grant_path_with_flags(sandbox_policy_cwd, sid, false), - SandboxPolicy::WorkspaceWrite { .. } => { - let roots = policy.get_writable_roots_with_cwd(sandbox_policy_cwd); - for writable in roots { - grant_path_with_flags(&writable.root, sid, true)?; - for ro in writable.read_only_subpaths { - grant_path_with_flags(&ro, sid, false)?; - } - } - Ok(()) - } - } -} - -fn configure_writable_roots_for_command_cwd(command_cwd: &Path, sid: PSID) -> io::Result<()> { - grant_path_with_flags(command_cwd, sid, true) -} - -/// Adds an inheritable ACE for the AppContainer SID so the sandbox can reach -/// specific roots. The helper augments the existing DACL rather than -/// overwriting it so it is safe to call repeatedly. -fn grant_path_with_flags(path: &Path, sid: PSID, write: bool) -> io::Result<()> { - if !path.exists() { - return Ok(()); - } - - let wide = to_wide(path.as_os_str()); - unsafe { - let mut existing_dacl: *mut ACL = null_mut(); - let mut security_descriptor = PSECURITY_DESCRIPTOR::default(); - // Pull the current DACL so we can append our ACE without clobbering any - // existing inheritance or user-specific access entries. - let status = GetNamedSecurityInfoW( - PCWSTR(wide.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(&mut existing_dacl), - None, - &mut security_descriptor, - ); - if status != WIN32_ERROR::from(ERROR_SUCCESS) { - if !security_descriptor.is_invalid() { - let _ = LocalFree(HLOCAL(security_descriptor.0)); - } - return Err(io::Error::from_raw_os_error(status.0 as i32)); - } - - let permissions = if write { - (FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE).0 - } else { - (FILE_GENERIC_READ | FILE_GENERIC_EXECUTE).0 - }; - let mut explicit = EXPLICIT_ACCESS_W { - grfAccessPermissions: permissions, - grfAccessMode: SET_ACCESS, - grfInheritance: SUB_CONTAINERS_AND_OBJECTS_INHERIT | OBJECT_INHERIT_ACE, - Trustee: TRUSTEE_W { - TrusteeForm: TRUSTEE_IS_SID, - TrusteeType: TRUSTEE_IS_UNKNOWN, - ptstrName: PWSTR(sid.0.cast()), - ..Default::default() - }, - }; - - let explicit_entries = [explicit]; - let mut new_dacl: *mut ACL = null_mut(); - let add_result = - SetEntriesInAclW(Some(&explicit_entries), Some(existing_dacl), &mut new_dacl); - if add_result != WIN32_ERROR::from(ERROR_SUCCESS) { - if !new_dacl.is_null() { - let _ = LocalFree(HLOCAL(new_dacl.cast())); - } - if !security_descriptor.is_invalid() { - let _ = LocalFree(HLOCAL(security_descriptor.0)); - } - return Err(io::Error::from_raw_os_error(add_result.0 as i32)); - } - - let set_result = SetNamedSecurityInfoW( - PCWSTR(wide.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(new_dacl), - None, - ); - if set_result != WIN32_ERROR::from(ERROR_SUCCESS) { - if !new_dacl.is_null() { - let _ = LocalFree(HLOCAL(new_dacl.cast())); - } - if !security_descriptor.is_invalid() { - let _ = LocalFree(HLOCAL(security_descriptor.0)); - } - return Err(io::Error::from_raw_os_error(set_result.0 as i32)); - } - - if !new_dacl.is_null() { - let _ = LocalFree(HLOCAL(new_dacl.cast())); - } - if !security_descriptor.is_invalid() { - let _ = LocalFree(HLOCAL(security_descriptor.0)); - } - } - - Ok(()) + let _ = (command, command_cwd); + Err(io::Error::new( + io::ErrorKind::Unsupported, + "AppContainer sandboxing requires the `windows_appcontainer_command_ext` feature", + )) } diff --git a/codex-rs/core/tests/windows_appcontainer.rs b/codex-rs/core/tests/windows_appcontainer.rs index 024804c1c2..b90216acec 100644 --- a/codex-rs/core/tests/windows_appcontainer.rs +++ b/codex-rs/core/tests/windows_appcontainer.rs @@ -1,4 +1,4 @@ -#![cfg(windows)] +#![cfg(all(windows, feature = "windows_appcontainer_command_ext"))] use codex_core::protocol::SandboxPolicy; use codex_core::spawn::StdioPolicy;