//! Per-instance AppContainer profile ownership. No existing profile is adopted. use crate::workspace::{HostError, Result}; use windows_sys::Win32::Security::{ FreeSid, IsValidSid, Isolation::{CreateAppContainerProfile, DeleteAppContainerProfile}, PSID, }; pub struct Profile { name: Vec, sid: PSID, exists: bool, } impl Profile { pub fn create() -> Result { Self::create_named(format!( "OpenNexus.sandbox.{}", uuid::Uuid::new_v4().simple() )) } fn create_named(name: String) -> Result { let name: Vec = name.encode_utf16().chain(Some(0)).collect(); let mut sid = std::ptr::null_mut(); let status = unsafe { CreateAppContainerProfile( name.as_ptr(), name.as_ptr(), name.as_ptr(), std::ptr::null(), 0, &mut sid, ) }; if status < 0 { // In particular, ERROR_ALREADY_EXISTS must not transfer ownership. if !sid.is_null() { unsafe { FreeSid(sid); } } return Err(HostError::new("EXTENSION_CONTAINER_CREATE_FAILED")); } let profile = Self { name, sid, exists: true, }; if sid.is_null() || unsafe { IsValidSid(sid) } == 0 { return Err(HostError::new("EXTENSION_CONTAINER_SID_INVALID")); } Ok(profile) } /// Borrowed SID for SECURITY_CAPABILITIES. Valid only while this owner lives. pub fn sid(&self) -> PSID { self.sid } /// Grant this instance read/execute access to one Host-owned package object. /// The caller must open it without following reparse points and retain the /// verified package handles for the entire launch. No recursive inheritance /// is used: every directory and file must be checked and granted separately. /// This adds an ACE; it does not sanitize pre-existing permissions. pub fn grant_package_read_execute(&self, object: &std::fs::File) -> Result<()> { use std::os::windows::{fs::MetadataExt, io::AsRawHandle}; use windows_sys::Win32::{ Foundation::LocalFree, Security::{Authorization::*, DACL_SECURITY_INFORMATION}, Storage::FileSystem::{ GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_REPARSE_POINT, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, }, }; struct LocalAllocation(*mut core::ffi::c_void); impl Drop for LocalAllocation { fn drop(&mut self) { if !self.0.is_null() { unsafe { LocalFree(self.0); } } } } let metadata = object .metadata() .map_err(|_| HostError::new("EXTENSION_CONTAINER_ACL_FAILED"))?; if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 || !(metadata.is_file() || metadata.is_dir()) { return Err(HostError::new("EXTENSION_CONTAINER_ACL_OBJECT_INVALID")); } if metadata.is_file() { let mut info = BY_HANDLE_FILE_INFORMATION::default(); if unsafe { GetFileInformationByHandle(object.as_raw_handle(), &mut info) } == 0 || info.nNumberOfLinks != 1 { return Err(HostError::new("EXTENSION_CONTAINER_ACL_OBJECT_INVALID")); } } let mut old_acl = std::ptr::null_mut(); let mut descriptor = std::ptr::null_mut(); let status = unsafe { GetSecurityInfo( object.as_raw_handle(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, std::ptr::null_mut(), std::ptr::null_mut(), &mut old_acl, std::ptr::null_mut(), &mut descriptor, ) }; let _descriptor = LocalAllocation(descriptor); // A null DACL grants everyone full access, so fail closed rather than // silently treating it as a suitably isolated package object. if status != 0 || old_acl.is_null() { return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED")); } let entry = EXPLICIT_ACCESS_W { grfAccessPermissions: FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, grfAccessMode: GRANT_ACCESS, grfInheritance: 0, Trustee: TRUSTEE_W { TrusteeForm: TRUSTEE_IS_SID, TrusteeType: TRUSTEE_IS_UNKNOWN, ptstrName: self.sid.cast(), ..Default::default() }, }; let mut acl = std::ptr::null_mut(); let status = unsafe { SetEntriesInAclW(1, &entry, old_acl, &mut acl) }; let _acl = LocalAllocation(acl.cast()); if status != 0 || acl.is_null() { return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED")); } let status = unsafe { SetSecurityInfo( object.as_raw_handle(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, std::ptr::null_mut(), std::ptr::null_mut(), acl, std::ptr::null(), ) }; if status != 0 { return Err(HostError::new("EXTENSION_CONTAINER_ACL_FAILED")); } Ok(()) } pub fn folder(&self) -> Result { use std::os::windows::ffi::OsStringExt; use windows_sys::Win32::{ Foundation::LocalFree, Security::{ Authorization::ConvertSidToStringSidW, Isolation::GetAppContainerFolderPath, }, System::Com::CoTaskMemFree, }; let mut string = std::ptr::null_mut(); if unsafe { ConvertSidToStringSidW(self.sid, &mut string) } == 0 { return Err(HostError::new("EXTENSION_CONTAINER_SID_INVALID")); } let mut folder = std::ptr::null_mut(); let status = unsafe { GetAppContainerFolderPath(string, &mut folder) }; unsafe { LocalFree(string.cast()); } if status < 0 || folder.is_null() { if !folder.is_null() { unsafe { CoTaskMemFree(folder.cast()); } } return Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED")); } let mut length = 0; while length < 32768 && unsafe { *folder.add(length) } != 0 { length += 1; } let result = if length == 32768 { Err(HostError::new("EXTENSION_CONTAINER_FOLDER_FAILED")) } else { Ok(std::path::PathBuf::from(std::ffi::OsString::from_wide( unsafe { std::slice::from_raw_parts(folder, length) }, ))) }; unsafe { CoTaskMemFree(folder.cast()); } result } /// Stop all container processes and close their handles before removal. pub fn remove(mut self) -> Result<()> { self.remove_inner() } fn remove_inner(&mut self) -> Result<()> { if self.exists { if unsafe { DeleteAppContainerProfile(self.name.as_ptr()) } < 0 { return Err(HostError::new("EXTENSION_CONTAINER_CLEANUP_FAILED")); } self.exists = false; } Ok(()) } } impl Drop for Profile { fn drop(&mut self) { // Explicit remove reports failures; drop is a final best-effort retry. let _ = self.remove_inner(); if !self.sid.is_null() { unsafe { FreeSid(self.sid); } self.sid = std::ptr::null_mut(); } } } #[cfg(test)] mod tests { use super::*; use std::{ mem::size_of, os::windows::{ ffi::OsStrExt, io::{AsHandle, AsRawHandle, FromRawHandle, OwnedHandle}, }, }; use windows_sys::Win32::{ Security::{ EqualSid, GetTokenInformation, TokenIsAppContainer, SECURITY_CAPABILITIES, TOKEN_QUERY, }, System::Threading::*, }; #[test] fn distinct_profiles_and_collision_do_not_take_ownership_of_existing_data() { let one = Profile::create().unwrap(); let two = Profile::create().unwrap(); assert_eq!(unsafe { EqualSid(one.sid(), two.sid()) }, 0); let name = String::from_utf16(&one.name[..one.name.len() - 1]).unwrap(); assert!(Profile::create_named(name.clone()).is_err()); // Repeated collision must still fail: the failing owner did not delete it. assert!(Profile::create_named(name.clone()).is_err()); one.remove().unwrap(); Profile::create_named(name).unwrap().remove().unwrap(); two.remove().unwrap(); } struct Attributes { buffer: Vec, } impl Attributes { fn new() -> Self { let mut bytes = 0; unsafe { InitializeProcThreadAttributeList(std::ptr::null_mut(), 1, 0, &mut bytes); } assert!(bytes > 0); let mut result = Self { buffer: vec![0; bytes.div_ceil(size_of::())], }; assert_ne!( unsafe { InitializeProcThreadAttributeList( result.buffer.as_mut_ptr().cast(), 1, 0, &mut bytes, ) }, 0 ); result } } impl Drop for Attributes { fn drop(&mut self) { unsafe { DeleteProcThreadAttributeList(self.buffer.as_mut_ptr().cast()); } } } struct Process { process: OwnedHandle, _thread: OwnedHandle, } impl Drop for Process { fn drop(&mut self) { unsafe { TerminateProcess(self.process.as_raw_handle(), 1); WaitForSingleObject(self.process.as_raw_handle(), 5000); } } } #[test] fn real_suspended_process_has_appcontainer_token_before_job_resume() { let profile = Profile::create().unwrap(); checked_process(&profile, None); profile.remove().unwrap(); } // Only tests use cmd.exe, with fixed commands and controlled temporary paths. // A production extension launcher must use a verified entry, never a shell. fn checked_process(profile: &Profile, command: Option<&str>) -> Option { let executable = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap()) .join("System32/cmd.exe"); checked_executable( profile, &executable, command.map(|c| format!("cmd.exe /d /c {c}")), ) } fn checked_executable( profile: &Profile, executable: &std::path::Path, command: Option, ) -> Option { checked_executable_data(profile, executable, command, None) } fn checked_executable_data( profile: &Profile, executable: &std::path::Path, command: Option, mut data: Option, ) -> Option { if let Some(data) = data.take() { let suspended = crate::extension_process::Suspended::create(profile, executable, data).unwrap(); // Controlled test fixture only; no user extension is authorized here. let running = unsafe { suspended.resume().unwrap() }; let result = running.wait(std::time::Duration::from_secs(10)).unwrap(); assert!(result.is_some()); running.terminate().unwrap(); return result; } let resume = command.is_some() || data.is_some(); let mut attributes = Attributes::new(); let caps = SECURITY_CAPABILITIES { AppContainerSid: profile.sid(), Capabilities: std::ptr::null_mut(), CapabilityCount: 0, Reserved: 0, }; assert_ne!( unsafe { UpdateProcThreadAttribute( attributes.buffer.as_mut_ptr().cast(), 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize, (&caps as *const SECURITY_CAPABILITIES).cast(), size_of::(), std::ptr::null_mut(), std::ptr::null(), ) }, 0 ); let mut startup = STARTUPINFOEXW::default(); startup.StartupInfo.cb = size_of::() as u32; startup.lpAttributeList = attributes.buffer.as_mut_ptr().cast(); let executable: Vec = executable .as_os_str() .encode_wide() .chain(Some(0)) .collect(); let folder = profile.folder().unwrap(); let environment: Vec = format!( "LOCALAPPDATA={}\0SystemRoot={}\0TEMP={}\0TMP={}\0\0", folder.display(), std::env::var("SystemRoot").unwrap(), folder.join("Temp").display(), folder.join("Temp").display() ) .encode_utf16() .collect(); let mut command_line: Vec = command .as_ref() .map(|command| command.encode_utf16().chain(Some(0)).collect()) .unwrap_or_default(); let environment_ptr = data .as_ref() .map_or(environment.as_ptr(), |d| d.environment().as_ptr()); let command_ptr = data.as_mut().map_or_else( || { if command_line.is_empty() { std::ptr::null_mut() } else { command_line.as_mut_ptr() } }, |d| d.command_mut().as_mut_ptr(), ); let mut info = PROCESS_INFORMATION::default(); assert_ne!( unsafe { CreateProcessW( executable.as_ptr(), command_ptr, std::ptr::null(), std::ptr::null(), 0, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, environment_ptr.cast(), std::ptr::null(), &startup.StartupInfo, &mut info, ) }, 0, "AppContainer process creation failed: {}", std::io::Error::last_os_error() ); let process = Process { process: unsafe { OwnedHandle::from_raw_handle(info.hProcess) }, _thread: unsafe { OwnedHandle::from_raw_handle(info.hThread) }, }; let job = crate::extension_job::Job::new().unwrap(); unsafe { job.assign_suspended(process.process.as_handle()).unwrap(); } let mut token = std::ptr::null_mut(); assert_ne!( unsafe { OpenProcessToken(process.process.as_raw_handle(), TOKEN_QUERY, &mut token) }, 0 ); let token = unsafe { OwnedHandle::from_raw_handle(token) }; let mut contained = 0u32; let mut length = 0; assert_ne!( unsafe { GetTokenInformation( token.as_raw_handle(), TokenIsAppContainer, (&mut contained as *mut u32).cast(), size_of::() as u32, &mut length, ) }, 0 ); assert_eq!(contained, 1); use windows_sys::Win32::Security::{ TokenAppContainerSid, TokenCapabilities, TOKEN_APPCONTAINER_INFORMATION, }; let mut required = 0; unsafe { GetTokenInformation( token.as_raw_handle(), TokenAppContainerSid, std::ptr::null_mut(), 0, &mut required, ); } assert!(required > 0 && required < 4096); let mut data = vec![0usize; (required as usize).div_ceil(size_of::())]; assert_ne!( unsafe { GetTokenInformation( token.as_raw_handle(), TokenAppContainerSid, data.as_mut_ptr().cast(), required, &mut required, ) }, 0 ); let identity = unsafe { &*data.as_ptr().cast::() }; assert_ne!( unsafe { EqualSid(identity.TokenAppContainer, profile.sid()) }, 0 ); let mut required = 0; unsafe { GetTokenInformation( token.as_raw_handle(), TokenCapabilities, std::ptr::null_mut(), 0, &mut required, ); } assert!((4..4096).contains(&required)); let mut capabilities = vec![0usize; (required as usize).div_ceil(size_of::())]; assert_ne!( unsafe { GetTokenInformation( token.as_raw_handle(), TokenCapabilities, capabilities.as_mut_ptr().cast(), required, &mut required, ) }, 0 ); assert_eq!(unsafe { *capabilities.as_ptr().cast::() }, 0); let exit = resume.then(|| { assert_ne!( unsafe { ResumeThread(process._thread.as_raw_handle()) }, u32::MAX ); assert_eq!( unsafe { WaitForSingleObject(process.process.as_raw_handle(), 10_000) }, 0 ); let mut exit = 0; assert_ne!( unsafe { GetExitCodeProcess(process.process.as_raw_handle(), &mut exit) }, 0 ); exit }); job.terminate().unwrap(); drop(token); drop(process); drop(job); drop(attributes); exit } #[test] fn real_container_can_read_only_explicitly_granted_package_objects() { use std::os::windows::fs::OpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::*; let profile = Profile::create().unwrap(); let directory = tempfile::tempdir().unwrap(); let payload = directory.path().join("payload.txt"); let hidden = directory.path().join("ungranted.txt"); std::fs::write(&payload, b"verified package content").unwrap(); std::fs::write(&hidden, b"private sibling").unwrap(); let open = |path: &std::path::Path| { std::fs::OpenOptions::new() .access_mode(READ_CONTROL | WRITE_DAC) .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(path) .unwrap() }; let root_handle = open(directory.path()); let file_handle = open(&payload); let read = format!("set /p value=<\"{}\"", payload.display()); assert_ne!(checked_process(&profile, Some(&read)), Some(0)); profile.grant_package_read_execute(&root_handle).unwrap(); // The directory ACE does not propagate to existing children. assert_ne!(checked_process(&profile, Some(&read)), Some(0)); profile.grant_package_read_execute(&file_handle).unwrap(); assert_eq!(checked_process(&profile, Some(&read)), Some(0)); let other = Profile::create().unwrap(); assert_ne!(checked_process(&other, Some(&read)), Some(0)); other.remove().unwrap(); let created = directory.path().join("new.txt"); assert_ne!( checked_process( &profile, Some(&format!("echo changed>\"{}\"", created.display())) ), Some(0) ); assert!(!created.exists()); assert_ne!( checked_process( &profile, Some(&format!("set /p value=<\"{}\"", hidden.display())) ), Some(0) ); assert_ne!( checked_process( &profile, Some(&format!("echo changed>\"{}\"", payload.display())) ), Some(0) ); assert_eq!( std::fs::read(&payload).unwrap(), b"verified package content" ); drop(file_handle); drop(root_handle); profile.remove().unwrap(); } #[test] fn package_grant_rejects_hardlinks_and_handles_without_acl_write_access() { use std::os::windows::fs::OpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::*; let profile = Profile::create().unwrap(); let directory = tempfile::tempdir().unwrap(); let payload = directory.path().join("payload.txt"); let alias = directory.path().join("alias.txt"); std::fs::write(&payload, b"unchanged").unwrap(); let read_only = std::fs::File::open(&payload).unwrap(); assert!(profile.grant_package_read_execute(&read_only).is_err()); drop(read_only); std::fs::hard_link(&payload, &alias).unwrap(); let handle = std::fs::OpenOptions::new() .access_mode(READ_CONTROL | WRITE_DAC) .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) .open(&payload) .unwrap(); assert_eq!( profile .grant_package_read_execute(&handle) .unwrap_err() .code, "EXTENSION_CONTAINER_ACL_OBJECT_INVALID" ); assert_eq!(std::fs::read(&alias).unwrap(), b"unchanged"); drop(handle); profile.remove().unwrap(); } #[test] fn real_container_cannot_reach_ipv4_or_ipv6_loopback_listeners() { use std::{ net::{TcpListener, UdpSocket}, os::windows::fs::OpenOptionsExt, }; use windows_sys::Win32::Storage::FileSystem::*; let profile = Profile::create().unwrap(); let package = tempfile::tempdir().unwrap(); let executable = package.path().join("network-probe.exe"); let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sandbox_network_probe.rs"); let compile = std::process::Command::new("rustc") .arg("--edition=2021") .arg(&fixture) .arg("-o") .arg(&executable) .output() .unwrap(); assert!( compile.status.success(), "{}", String::from_utf8_lossy(&compile.stderr) ); let open = |path: &std::path::Path| { std::fs::OpenOptions::new() .access_mode(READ_CONTROL | WRITE_DAC) .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(path) .unwrap() }; let root = open(package.path()); let entry = open(&executable); #[cfg(feature = "desktop")] let _pinned = { use sha2::{Digest, Sha256}; let bytes = std::fs::read(&executable).unwrap(); let inventory = crate::extension_package::Inventory { files: [( "network-probe.exe".to_owned(), format!("{:x}", Sha256::digest(&bytes)), )] .into_iter() .collect(), expanded_size: bytes.len() as u64, manifest: "network-probe.exe".to_owned(), }; let dir = cap_std::fs::Dir::open_ambient_dir(package.path(), cap_std::ambient_authority()) .unwrap(); let hash = crate::extension_unpack::verify_tree(&dir, &inventory).unwrap(); let pinned = crate::extension_pinned::PinnedPackage::open(&dir, &inventory, &hash).unwrap(); pinned.grant_read_execute(&profile).unwrap(); pinned }; #[cfg(not(feature = "desktop"))] { profile.grant_package_read_execute(&root).unwrap(); profile.grant_package_read_execute(&entry).unwrap(); } // Exercise the actual builder, not a test-side quote decoder. The child // compares argv and its entire environment without logging values. let args = [ "launch", "", "space value", "引号🦀", "trailing\\", "a\"b", "slash\\\"quote", "&|%PATH%", "line\nbreak", ] .into_iter() .map(str::to_owned) .collect::>(); let folder = profile.folder().unwrap(); let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap()); let data = crate::extension_launch_data::LaunchData::new( &executable, &args, &system, &folder, &folder.join("Temp"), &[("CUSTOM".to_owned(), "declared=值".to_owned())] .into_iter() .collect(), ) .unwrap(); assert_eq!( checked_executable_data(&profile, &executable, None, Some(data)), Some(0) ); let data = crate::extension_launch_data::LaunchData::new( &executable, &["wait".to_owned()], &system, &folder, &folder.join("Temp"), &std::collections::BTreeMap::new(), ) .unwrap(); let suspended = crate::extension_process::Suspended::create(&profile, &executable, data).unwrap(); let running = unsafe { suspended.resume().unwrap() }; assert_eq!(running.wait(std::time::Duration::ZERO).unwrap(), None); assert_eq!( running .wait(std::time::Duration::from_millis(60001)) .unwrap_err() .code, "EXTENSION_PROCESS_WAIT_INVALID" ); let completed = running.start_tool_call().unwrap(); completed.finish().unwrap(); assert_eq!(running.wait(std::time::Duration::ZERO).unwrap(), None); let deadline = running .start_test_tool_call(std::time::Duration::from_millis(100)) .unwrap(); // No check() or finish() drives expiration; wait only on the OS process. assert!(running .wait(std::time::Duration::from_secs(5)) .unwrap() .is_some()); assert_eq!( deadline.finish().unwrap_err().code, "EXTENSION_TOOL_DEADLINE_EXCEEDED" ); assert!(running .wait(std::time::Duration::from_secs(5)) .unwrap() .is_some()); drop(running); for explicit_cancel in [false, true] { let data = crate::extension_launch_data::LaunchData::new( &executable, &["wait".to_owned()], &system, &folder, &folder.join("Temp"), &std::collections::BTreeMap::new(), ) .unwrap(); let suspended = crate::extension_process::Suspended::create(&profile, &executable, data).unwrap(); let running = unsafe { suspended.resume().unwrap() }; let deadline = running.start_tool_call().unwrap(); assert_eq!(running.wait(std::time::Duration::ZERO).unwrap(), None); if explicit_cancel { deadline.cancel().unwrap(); } else { drop(deadline); } assert!(running .wait(std::time::Duration::from_secs(5)) .unwrap() .is_some()); } for ip in ["127.0.0.1:0", "[::1]:0"] { let tcp = TcpListener::bind(ip).unwrap(); let udp = UdpSocket::bind(ip).unwrap(); for (mode, address) in [ ("tcp", tcp.local_addr().unwrap()), ("udp", udp.local_addr().unwrap()), ] { let address = address.to_string(); // The exact executable and target work outside containment. assert!(std::process::Command::new(&executable) .args([mode, &address]) .status() .unwrap() .success()); if mode == "tcp" { tcp.set_nonblocking(true).unwrap(); tcp.accept().unwrap(); } else { udp.set_read_timeout(Some(std::time::Duration::from_secs(2))) .unwrap(); let mut bytes = [0u8; 8]; assert_eq!(udp.recv(&mut bytes).unwrap(), 5); assert_eq!(&bytes[..5], b"probe"); udp.set_nonblocking(true).unwrap(); } let data = crate::extension_launch_data::LaunchData::new( &executable, &[mode.to_owned(), address.clone()], &system, &folder, &folder.join("Temp"), &std::collections::BTreeMap::new(), ) .unwrap(); // Loopback isolation can silently drop packets. TCP must // explicitly report denial or timeout; UDP send may succeed, // but no datagram may reach the controlled listener below. let exit = checked_executable_data(&profile, &executable, None, Some(data)); eprintln!("container network probe {mode} {address}: {exit:?}"); if mode == "tcp" { assert!(matches!(exit, Some(77 | 80)), "{mode} {address}: {exit:?}"); } else { assert!(matches!(exit, Some(0 | 77)), "{mode} {address}: {exit:?}"); std::thread::sleep(std::time::Duration::from_millis(200)); } if mode == "tcp" { assert_eq!( tcp.accept().unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); } else { assert_eq!( udp.recv(&mut [0u8; 8]).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); } assert!(std::process::Command::new(&executable) .args([mode, &address]) .status() .unwrap() .success()); if mode == "tcp" { tcp.accept().unwrap(); } else { udp.set_nonblocking(false).unwrap(); assert_eq!(udp.recv(&mut [0u8; 8]).unwrap(), 5); } } } drop(entry); drop(root); profile.remove().unwrap(); } #[test] #[ignore = "production 60-second wall-clock and process-tree acceptance; run explicitly"] fn real_sixty_second_tool_deadline_kills_tree_and_host_can_save() { use std::{ os::windows::fs::OpenOptionsExt, time::{Duration, Instant}, }; use windows_sys::Win32::Storage::FileSystem::*; let profile = Profile::create().unwrap(); let package = tempfile::tempdir().unwrap(); let executable = package.path().join("network-probe.exe"); let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/sandbox_network_probe.rs"); let compile = std::process::Command::new("rustc") .arg("--edition=2021") .arg(&fixture) .arg("-o") .arg(&executable) .output() .unwrap(); assert!( compile.status.success(), "{}", String::from_utf8_lossy(&compile.stderr) ); let open = |path: &std::path::Path| { std::fs::OpenOptions::new() .access_mode(READ_CONTROL | WRITE_DAC) .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(path) .unwrap() }; let root = open(package.path()); let entry = open(&executable); #[cfg(feature = "desktop")] let _pinned = { use sha2::{Digest, Sha256}; let bytes = std::fs::read(&executable).unwrap(); let inventory = crate::extension_package::Inventory { files: [( "network-probe.exe".to_owned(), format!("{:x}", Sha256::digest(&bytes)), )] .into_iter() .collect(), expanded_size: bytes.len() as u64, manifest: "network-probe.exe".to_owned(), }; let dir = cap_std::fs::Dir::open_ambient_dir(package.path(), cap_std::ambient_authority()) .unwrap(); let hash = crate::extension_unpack::verify_tree(&dir, &inventory).unwrap(); let pinned = crate::extension_pinned::PinnedPackage::open(&dir, &inventory, &hash).unwrap(); pinned.grant_read_execute(&profile).unwrap(); pinned }; #[cfg(not(feature = "desktop"))] { profile.grant_package_read_execute(&root).unwrap(); profile.grant_package_read_execute(&entry).unwrap(); } let folder = profile.folder().unwrap(); let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap()); let data = crate::extension_launch_data::LaunchData::new( &executable, &["wait_tree".to_owned()], &system, &folder, &folder.join("Temp"), &std::collections::BTreeMap::new(), ) .unwrap(); let suspended = crate::extension_process::Suspended::create(&profile, &executable, data).unwrap(); let running = unsafe { suspended.resume().unwrap() }; let start = Instant::now(); let deadline = running.start_tool_call().unwrap(); while running.active_test_processes().unwrap() != 2 && start.elapsed() < Duration::from_secs(5) { std::thread::sleep(Duration::from_millis(10)); } assert_eq!( running.active_test_processes().unwrap(), 2, "container child did not start" ); let vault = tempfile::tempdir().unwrap(); let mut workspace = crate::workspace::Workspace::open(vault.path()).unwrap(); let before = workspace .write("deadline.md", "", b"during tool call", "local") .unwrap(); eprintln!("production deadline: two managed processes; Host save succeeded; waiting 60-second budget"); let exit = running .wait(Duration::from_secs(60)) .unwrap() .or_else(|| running.wait(Duration::from_secs(5)).unwrap()) .unwrap(); assert_ne!( exit, 84, "probe ended naturally before the watchdog killed it" ); while running.active_test_processes().unwrap() != 0 && start.elapsed() < Duration::from_secs(70) { std::thread::sleep(Duration::from_millis(10)); } assert_eq!(running.active_test_processes().unwrap(), 0); let elapsed = start.elapsed(); assert!( elapsed >= Duration::from_secs(60) && elapsed < Duration::from_secs(70), "{elapsed:?}" ); assert_eq!( deadline.finish().unwrap_err().code, "EXTENSION_TOOL_DEADLINE_EXCEEDED" ); workspace .write( "deadline.md", &before.hash, b"after process-tree cleanup", "local", ) .unwrap(); drop(workspace); let mut reopened = crate::workspace::Workspace::open(vault.path()).unwrap(); assert_eq!( reopened.read("deadline.md").unwrap().content, "after process-tree cleanup" ); eprintln!( "production deadline: tree empty after {elapsed:?}; Host save and reopen succeeded" ); drop(reopened); drop(running); drop(entry); drop(root); profile.remove().unwrap(); } }