1use serde::Serialize;
20use std::path::Path;
21
22pub const PROCESS_MODEL_DLL: &str = "processmodel.dll";
23pub const PROCESS_SANDBOX_EXPORT: &str = "Experimental_CreateProcessInSandbox";
24pub const EXPERIMENTAL_SPEC_VERSION: &str = "0.1.0";
25pub const DLL_SEARCH_SCOPE: &str = "system32-only";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct WindowsVersion {
32 pub major: u32,
33 pub minor: u32,
34 pub build: u32,
35}
36
37impl std::fmt::Display for WindowsVersion {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "{}.{}.{}", self.major, self.minor, self.build)
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
45#[serde(rename_all = "kebab-case")]
46pub enum ExperimentalApiStatus {
47 NotWindows,
48 DllUnavailable,
49 ExportUnavailable,
50 ExperimentalApiAvailable,
51}
52
53impl ExperimentalApiStatus {
54 pub fn as_str(self) -> &'static str {
55 match self {
56 Self::NotWindows => "not-windows",
57 Self::DllUnavailable => "dll-unavailable",
58 Self::ExportUnavailable => "export-unavailable",
59 Self::ExperimentalApiAvailable => "experimental-api-available",
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66#[serde(rename_all = "camelCase")]
67pub struct WindowsSandboxProbeReport {
68 pub host_os: &'static str,
69 pub windows_version: Option<WindowsVersion>,
70 pub dll: &'static str,
71 pub export: &'static str,
72 pub dll_search_scope: &'static str,
73 pub experimental_spec_version: &'static str,
74 pub api_status: ExperimentalApiStatus,
75 pub load_error_hresult: Option<i32>,
77 pub production_enabled: bool,
79 pub decision: &'static str,
80}
81
82impl WindowsSandboxProbeReport {
83 pub fn experimental_api_available(&self) -> bool {
84 self.api_status == ExperimentalApiStatus::ExperimentalApiAvailable
85 }
86
87 pub fn render_text(&self) -> String {
88 let version = self
89 .windows_version
90 .map(|version| version.to_string())
91 .unwrap_or_else(|| "unavailable".to_string());
92 let load_error = self
93 .load_error_hresult
94 .map(|code| format!("0x{:08x}", code as u32))
95 .unwrap_or_else(|| "none".to_string());
96 format!(
97 "Windows native containment probe\n\
98 host OS: {}\n\
99 Windows version: {version}\n\
100 DLL: {} ({})\n\
101 export: {}\n\
102 API status: {}\n\
103 load error HRESULT: {load_error}\n\
104 experimental spec: {}\n\
105 production enabled: {}\n\
106 decision: {}\n",
107 self.host_os,
108 self.dll,
109 self.dll_search_scope,
110 self.export,
111 self.api_status.as_str(),
112 self.experimental_spec_version,
113 self.production_enabled,
114 self.decision,
115 )
116 }
117}
118
119pub fn probe() -> WindowsSandboxProbeReport {
122 platform::probe()
123}
124
125#[cfg(windows)]
129pub fn prepare_appcontainer_host(root: &Path) -> std::result::Result<bool, String> {
130 crate::appcontainer_windows::prepare_appcontainer_host(root).map_err(|error| error.to_string())
131}
132
133#[cfg(not(windows))]
134pub fn prepare_appcontainer_host(_root: &Path) -> std::result::Result<bool, String> {
135 Err("AppContainer host preparation is available only on Windows".to_string())
136}
137
138#[cfg(windows)]
144pub fn prepare_appcontainer_profile_parent(
145) -> std::result::Result<(std::path::PathBuf, bool), String> {
146 crate::appcontainer_windows::prepare_appcontainer_profile_parent()
147 .map_err(|error| error.to_string())
148}
149
150#[cfg(not(windows))]
151pub fn prepare_appcontainer_profile_parent(
152) -> std::result::Result<(std::path::PathBuf, bool), String> {
153 Err("AppContainer profile-parent preparation is available only on Windows".to_string())
154}
155
156#[cfg(windows)]
160pub fn prepare_appcontainer_null_device() -> std::result::Result<(), String> {
161 crate::appcontainer_windows::prepare_appcontainer_null_device()
162 .map_err(|error| error.to_string())
163}
164
165#[cfg(not(windows))]
166pub fn prepare_appcontainer_null_device() -> std::result::Result<(), String> {
167 Err("AppContainer null-device preparation is available only on Windows".to_string())
168}
169
170#[cfg(windows)]
174pub fn internal_launcher_requested() -> bool {
175 crate::appcontainer_windows::internal_launcher_requested()
176}
177
178#[cfg(windows)]
179pub fn run_internal_launcher() -> std::result::Result<u32, String> {
180 crate::appcontainer_windows::run_internal_launcher()
181}
182
183#[cfg(windows)]
184pub fn internal_self_test_requested() -> bool {
185 crate::appcontainer_windows::internal_self_test_requested()
186}
187
188#[cfg(windows)]
189pub fn run_production_hostile_self_test() -> std::result::Result<String, String> {
190 crate::appcontainer_windows::run_production_hostile_self_test()
191}
192
193#[cfg(windows)]
194pub fn internal_gate_self_test_requested() -> bool {
195 crate::appcontainer_windows::internal_gate_self_test_requested()
196}
197
198#[cfg(windows)]
199pub fn run_production_gate_self_test() -> std::result::Result<String, String> {
200 crate::appcontainer_windows::run_production_gate_self_test()
201}
202
203#[cfg(windows)]
204pub fn internal_hostile_child_requested() -> bool {
205 crate::appcontainer_windows::internal_hostile_child_requested()
206}
207
208#[cfg(windows)]
209pub fn run_internal_hostile_child() -> std::result::Result<(), String> {
210 crate::appcontainer_windows::run_internal_hostile_child()
211}
212
213#[cfg(windows)]
214mod platform {
215 use super::*;
216 use windows::core::{s, w};
217 use windows::Win32::Foundation::{FreeLibrary, HMODULE};
218 use windows::Win32::System::LibraryLoader::{
219 GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32,
220 };
221 use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
222
223 #[link(name = "ntdll")]
224 extern "system" {
225 fn RtlGetVersion(version: *mut OSVERSIONINFOW) -> i32;
226 }
227
228 struct Library(HMODULE);
229
230 impl Drop for Library {
231 fn drop(&mut self) {
232 let _ = unsafe { FreeLibrary(self.0) };
235 }
236 }
237
238 fn windows_version() -> Option<WindowsVersion> {
239 let mut version = OSVERSIONINFOW {
240 dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
241 ..Default::default()
242 };
243 let status = unsafe { RtlGetVersion(&mut version) };
247 (status >= 0).then_some(WindowsVersion {
248 major: version.dwMajorVersion,
249 minor: version.dwMinorVersion,
250 build: version.dwBuildNumber,
251 })
252 }
253
254 pub(super) fn probe() -> WindowsSandboxProbeReport {
255 let version = windows_version();
256 let library = match unsafe {
259 LoadLibraryExW(w!("processmodel.dll"), None, LOAD_LIBRARY_SEARCH_SYSTEM32)
260 } {
261 Ok(module) => Library(module),
262 Err(error) => {
263 return WindowsSandboxProbeReport {
264 host_os: "windows",
265 windows_version: version,
266 dll: PROCESS_MODEL_DLL,
267 export: PROCESS_SANDBOX_EXPORT,
268 dll_search_scope: DLL_SEARCH_SCOPE,
269 experimental_spec_version: EXPERIMENTAL_SPEC_VERSION,
270 api_status: ExperimentalApiStatus::DllUnavailable,
271 load_error_hresult: Some(error.code().0),
272 production_enabled: true,
273 decision: "stable LPAC enforcement is enabled; the experimental System32 DLL is unavailable and unused",
274 };
275 }
276 };
277
278 let export =
282 unsafe { GetProcAddress(library.0, s!("Experimental_CreateProcessInSandbox")) };
283 let (api_status, decision) = if export.is_some() {
284 (
285 ExperimentalApiStatus::ExperimentalApiAvailable,
286 "stable LPAC enforcement is enabled; the experimental API is detected but unused",
287 )
288 } else {
289 (
290 ExperimentalApiStatus::ExportUnavailable,
291 "stable LPAC enforcement is enabled; processmodel.dll does not export the unused experimental API",
292 )
293 };
294 WindowsSandboxProbeReport {
295 host_os: "windows",
296 windows_version: version,
297 dll: PROCESS_MODEL_DLL,
298 export: PROCESS_SANDBOX_EXPORT,
299 dll_search_scope: DLL_SEARCH_SCOPE,
300 experimental_spec_version: EXPERIMENTAL_SPEC_VERSION,
301 api_status,
302 load_error_hresult: None,
303 production_enabled: true,
304 decision,
305 }
306 }
307}
308
309#[cfg(not(windows))]
310mod platform {
311 use super::*;
312
313 pub(super) fn probe() -> WindowsSandboxProbeReport {
314 WindowsSandboxProbeReport {
315 host_os: std::env::consts::OS,
316 windows_version: None,
317 dll: PROCESS_MODEL_DLL,
318 export: PROCESS_SANDBOX_EXPORT,
319 dll_search_scope: DLL_SEARCH_SCOPE,
320 experimental_spec_version: EXPERIMENTAL_SPEC_VERSION,
321 api_status: ExperimentalApiStatus::NotWindows,
322 load_error_hresult: None,
323 production_enabled: false,
324 decision:
325 "probe not run: native Windows containment can only be inspected on a Windows host",
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn probe_reports_the_platform_production_posture_independently_of_experimental_api() {
336 let report = probe();
337 assert_eq!(report.production_enabled, cfg!(windows));
338 assert_eq!(report.dll_search_scope, "system32-only");
339 assert_eq!(report.experimental_spec_version, "0.1.0");
340 assert!(report
341 .render_text()
342 .contains(&format!("production enabled: {}", cfg!(windows))));
343 }
344
345 #[cfg(windows)]
346 #[test]
347 fn windows_experimental_process_sandbox_probe_reports_capability() {
348 let report = probe();
349 println!(
350 "{}",
351 serde_json::to_string(&report).expect("probe report must serialize")
352 );
353 assert_eq!(report.host_os, "windows");
354 assert!(report.windows_version.is_some());
355 assert_ne!(report.api_status, ExperimentalApiStatus::NotWindows);
356 assert!(report.production_enabled);
357 if report.experimental_api_available() {
358 assert_eq!(
359 report.api_status,
360 ExperimentalApiStatus::ExperimentalApiAvailable
361 );
362 assert!(report.load_error_hresult.is_none());
363 }
364 }
365
366 #[cfg(windows)]
375 #[test]
376 fn windows_appcontainer_hostile_fixture_denies_out_of_root_write_and_network() {
377 appcontainer_fixture::run_parent().expect("AppContainer hostile fixture must pass");
378 }
379
380 #[cfg(windows)]
385 #[test]
386 fn windows_appcontainer_hostile_child() {
387 appcontainer_fixture::run_child_if_requested()
388 .expect("AppContainer hostile child must produce its receipt");
389 }
390
391 #[cfg(windows)]
392 mod appcontainer_fixture {
393 use serde::{Deserialize, Serialize};
394 use std::io;
395 use std::net::{SocketAddr, TcpListener, TcpStream};
396 use std::os::windows::ffi::OsStrExt;
397 use std::path::{Path, PathBuf};
398 use std::ptr::null_mut;
399 use std::time::Duration;
400 use windows::core::{PCWSTR, PWSTR};
401 use windows::Win32::Foundation::{
402 CloseHandle, LocalFree, HANDLE, HLOCAL, WAIT_OBJECT_0, WAIT_TIMEOUT,
403 };
404 use windows::Win32::Security::Authorization::{
405 GetNamedSecurityInfoW, SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W,
406 GRANT_ACCESS, SE_FILE_OBJECT, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W,
407 };
408 use windows::Win32::Security::Isolation::{
409 CreateAppContainerProfile, DeleteAppContainerProfile,
410 };
411 use windows::Win32::Security::{
412 FreeSid, GetTokenInformation, TokenIsAppContainer, ACL, CONTAINER_INHERIT_ACE,
413 DACL_SECURITY_INFORMATION, NO_INHERITANCE, OBJECT_INHERIT_ACE, PSECURITY_DESCRIPTOR,
414 PSID, SECURITY_CAPABILITIES, TOKEN_QUERY,
415 };
416 use windows::Win32::Storage::FileSystem::{
417 FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
418 };
419 use windows::Win32::System::Threading::{
420 CreateProcessW, DeleteProcThreadAttributeList, GetCurrentProcess, GetExitCodeProcess,
421 InitializeProcThreadAttributeList, OpenProcessToken, ResumeThread,
422 UpdateProcThreadAttribute, WaitForSingleObject, CREATE_SUSPENDED,
423 EXTENDED_STARTUPINFO_PRESENT, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
424 PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, STARTUPINFOEXW,
425 };
426
427 const MANIFEST_NAME: &str = "kranz-appcontainer-fixture.json";
428 const CHILD_TEST: &str = "sandbox_windows::tests::windows_appcontainer_hostile_child";
429 const CHILD_TIMEOUT_MS: u32 = 30_000;
430
431 #[derive(Debug, Serialize, Deserialize)]
432 #[serde(rename_all = "camelCase")]
433 struct FixtureManifest {
434 toolchain_marker: PathBuf,
435 toolchain_denied_write: PathBuf,
436 worktree_write: PathBuf,
437 scratch_write: PathBuf,
438 outside_write: PathBuf,
439 loopback_addr: SocketAddr,
440 receipt: PathBuf,
441 }
442
443 #[derive(Debug, Serialize, Deserialize)]
444 #[serde(rename_all = "camelCase")]
445 struct HostileReceipt {
446 token_is_appcontainer: bool,
447 toolchain_read: bool,
448 toolchain_write_denied: bool,
449 worktree_write: bool,
450 scratch_write: bool,
451 outside_write_denied: bool,
452 network_denied: bool,
453 }
454
455 struct Profile {
456 name: Vec<u16>,
457 sid: PSID,
458 deleted: bool,
459 }
460
461 impl Profile {
462 fn create() -> anyhow::Result<Self> {
463 let name = format!("kranz.phase3.{}", uuid::Uuid::new_v4());
464 let name = wide(&name);
465 let display_name = wide("Kranz phase 3 fixture");
466 let description = wide("Disposable native-containment proof");
467 let sid = unsafe {
470 CreateAppContainerProfile(
471 PCWSTR(name.as_ptr()),
472 PCWSTR(display_name.as_ptr()),
473 PCWSTR(description.as_ptr()),
474 None,
475 )?
476 };
477 Ok(Self {
478 name,
479 sid,
480 deleted: false,
481 })
482 }
483
484 fn remove(mut self) -> anyhow::Result<()> {
485 unsafe { DeleteAppContainerProfile(PCWSTR(self.name.as_ptr()))? };
488 self.deleted = true;
489 Ok(())
490 }
491 }
492
493 impl Drop for Profile {
494 fn drop(&mut self) {
495 if !self.deleted {
496 let _ = unsafe { DeleteAppContainerProfile(PCWSTR(self.name.as_ptr())) };
499 }
500 unsafe {
503 FreeSid(self.sid);
504 }
505 }
506 }
507
508 struct LocalAllocation(HLOCAL);
509
510 impl Drop for LocalAllocation {
511 fn drop(&mut self) {
512 unsafe {
515 LocalFree(Some(self.0));
516 }
517 }
518 }
519
520 struct AttributeList {
521 list: LPPROC_THREAD_ATTRIBUTE_LIST,
522 _storage: Vec<usize>,
523 }
524
525 impl AttributeList {
526 fn security_capabilities(value: &SECURITY_CAPABILITIES) -> anyhow::Result<Self> {
527 let mut bytes = 0usize;
528 let _ = unsafe { InitializeProcThreadAttributeList(None, 1, None, &mut bytes) };
531 anyhow::ensure!(bytes > 0, "attribute-list sizing returned zero bytes");
532 let words = bytes.div_ceil(std::mem::size_of::<usize>());
533 let mut storage = vec![0usize; words];
534 let list = LPPROC_THREAD_ATTRIBUTE_LIST(storage.as_mut_ptr().cast());
535 unsafe { InitializeProcThreadAttributeList(Some(list), 1, None, &mut bytes)? };
538 let result = Self {
539 list,
540 _storage: storage,
541 };
542 unsafe {
545 UpdateProcThreadAttribute(
546 result.list,
547 0,
548 PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES as usize,
549 Some((value as *const SECURITY_CAPABILITIES).cast()),
550 std::mem::size_of::<SECURITY_CAPABILITIES>(),
551 None,
552 None,
553 )?
554 };
555 Ok(result)
556 }
557 }
558
559 impl Drop for AttributeList {
560 fn drop(&mut self) {
561 unsafe { DeleteProcThreadAttributeList(self.list) };
564 }
565 }
566
567 struct ProcessHandles {
568 process: HANDLE,
569 thread: HANDLE,
570 }
571
572 impl Drop for ProcessHandles {
573 fn drop(&mut self) {
574 unsafe {
577 let _ = CloseHandle(self.thread);
578 let _ = CloseHandle(self.process);
579 }
580 }
581 }
582
583 struct OwnedHandle(HANDLE);
584
585 impl Drop for OwnedHandle {
586 fn drop(&mut self) {
587 let _ = unsafe { CloseHandle(self.0) };
589 }
590 }
591
592 fn wide(value: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
593 value.as_ref().encode_wide().chain(Some(0)).collect()
594 }
595
596 fn win32(status: windows::Win32::Foundation::WIN32_ERROR) -> anyhow::Result<()> {
597 status.ok().map_err(anyhow::Error::from)
598 }
599
600 fn grant_path(
601 path: &Path,
602 sid: PSID,
603 permissions: u32,
604 inherit: bool,
605 ) -> anyhow::Result<()> {
606 let path = wide(path.as_os_str());
607 let mut old_acl: *mut ACL = null_mut();
608 let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
609 win32(unsafe {
612 GetNamedSecurityInfoW(
613 PCWSTR(path.as_ptr()),
614 SE_FILE_OBJECT,
615 DACL_SECURITY_INFORMATION,
616 None,
617 None,
618 Some(&mut old_acl),
619 None,
620 &mut security_descriptor,
621 )
622 })?;
623 let _security_descriptor = LocalAllocation(HLOCAL(security_descriptor.0));
624
625 let entry = EXPLICIT_ACCESS_W {
626 grfAccessPermissions: permissions,
627 grfAccessMode: GRANT_ACCESS,
628 grfInheritance: if inherit {
629 OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE
630 } else {
631 NO_INHERITANCE
632 },
633 Trustee: TRUSTEE_W {
634 TrusteeForm: TRUSTEE_IS_SID,
635 TrusteeType: TRUSTEE_IS_UNKNOWN,
636 ptstrName: PWSTR(sid.0.cast()),
637 ..Default::default()
638 },
639 };
640 let mut new_acl: *mut ACL = null_mut();
641 win32(unsafe { SetEntriesInAclW(Some(&[entry]), Some(old_acl), &mut new_acl) })?;
644 let _new_acl = LocalAllocation(HLOCAL(new_acl.cast()));
645 win32(unsafe {
649 SetNamedSecurityInfoW(
650 PCWSTR(path.as_ptr()),
651 SE_FILE_OBJECT,
652 DACL_SECURITY_INFORMATION,
653 None,
654 None,
655 Some(new_acl),
656 None,
657 )
658 })
659 }
660
661 fn is_appcontainer_process() -> anyhow::Result<bool> {
662 let mut access_handle = HANDLE::default();
663 unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut access_handle)? };
666 let access_handle = OwnedHandle(access_handle);
667 let mut value = 0u32;
668 let mut returned = 0u32;
669 unsafe {
672 GetTokenInformation(
673 access_handle.0,
674 TokenIsAppContainer,
675 Some((&mut value as *mut u32).cast()),
676 std::mem::size_of::<u32>() as u32,
677 &mut returned,
678 )?
679 };
680 anyhow::ensure!(returned as usize == std::mem::size_of::<u32>());
681 Ok(value != 0)
682 }
683
684 fn quote_argument(value: &std::ffi::OsStr) -> String {
685 let value = value.to_string_lossy();
686 format!("\"{}\"", value.replace('"', "\\\""))
687 }
688
689 fn launch(executable: &Path, cwd: &Path, sid: PSID) -> anyhow::Result<u32> {
690 let security = SECURITY_CAPABILITIES {
691 AppContainerSid: sid,
692 Capabilities: null_mut(),
693 CapabilityCount: 0,
694 Reserved: 0,
695 };
696 let attributes = AttributeList::security_capabilities(&security)?;
697 let mut startup = STARTUPINFOEXW::default();
698 startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
699 startup.lpAttributeList = attributes.list;
700
701 let application = wide(executable.as_os_str());
702 let cwd = wide(cwd.as_os_str());
703 let command_line = format!(
704 "{} {} --exact --nocapture",
705 quote_argument(executable.as_os_str()),
706 quote_argument(std::ffi::OsStr::new(CHILD_TEST)),
707 );
708 let mut command_line = wide(command_line);
709 let mut process_info = PROCESS_INFORMATION::default();
710 unsafe {
713 CreateProcessW(
714 PCWSTR(application.as_ptr()),
715 Some(PWSTR(command_line.as_mut_ptr())),
716 None,
717 None,
718 false,
719 CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT,
720 None,
721 PCWSTR(cwd.as_ptr()),
722 &startup.StartupInfo,
723 &mut process_info,
724 )?
725 };
726 let handles = ProcessHandles {
727 process: process_info.hProcess,
728 thread: process_info.hThread,
729 };
730
731 let job =
735 crate::backend_claude::win_job::JobHandle::create_and_assign(handles.process.0)?;
736 anyhow::ensure!(unsafe { ResumeThread(handles.thread) } != u32::MAX);
738
739 let wait = unsafe { WaitForSingleObject(handles.process, CHILD_TIMEOUT_MS) };
741 if wait == WAIT_TIMEOUT {
742 job.kill();
743 anyhow::bail!("AppContainer fixture timed out after {CHILD_TIMEOUT_MS}ms");
744 }
745 anyhow::ensure!(
746 wait == WAIT_OBJECT_0,
747 "WaitForSingleObject returned {wait:?}"
748 );
749 let mut exit_code = 0u32;
750 unsafe { GetExitCodeProcess(handles.process, &mut exit_code)? };
753 Ok(exit_code)
754 }
755
756 pub(super) fn run_parent() -> anyhow::Result<()> {
757 let root = tempfile::tempdir()?;
758 let toolchain = root.path().join("toolchain");
759 let worktree = root.path().join("worktree");
760 let scratch = root.path().join("scratch");
761 let outside = root.path().join("outside");
762 for path in [&toolchain, &worktree, &scratch, &outside] {
763 std::fs::create_dir(path)?;
764 }
765
766 let profile = Profile::create()?;
767 let read_execute = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0;
768 let read_write_execute = read_execute | FILE_GENERIC_WRITE.0;
769 grant_path(root.path(), profile.sid, read_execute, false)?;
772 grant_path(&toolchain, profile.sid, read_execute, true)?;
773 grant_path(&worktree, profile.sid, read_write_execute, true)?;
774 grant_path(&scratch, profile.sid, read_write_execute, true)?;
775
776 let current_exe = std::env::current_exe()?;
777 let executable = toolchain.join(
778 current_exe
779 .file_name()
780 .ok_or_else(|| anyhow::anyhow!("test executable has no file name"))?,
781 );
782 std::fs::copy(¤t_exe, &executable)?;
783 let toolchain_marker = toolchain.join("read-only-marker.txt");
784 std::fs::write(&toolchain_marker, "kranz-appcontainer-phase3")?;
785
786 let listener = TcpListener::bind("127.0.0.1:0")?;
787 let manifest = FixtureManifest {
788 toolchain_marker,
789 toolchain_denied_write: toolchain.join("must-not-write.txt"),
790 worktree_write: worktree.join("allowed-worktree.txt"),
791 scratch_write: scratch.join("allowed-scratch.txt"),
792 outside_write: outside.join("must-not-write.txt"),
793 loopback_addr: listener.local_addr()?,
794 receipt: scratch.join("receipt.json"),
795 };
796 let manifest_path = worktree.join(MANIFEST_NAME);
797 std::fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)?;
798
799 let exit_code = launch(&executable, &worktree, profile.sid)?;
800 anyhow::ensure!(exit_code == 0, "AppContainer child exited {exit_code}");
801 let receipt: HostileReceipt =
802 serde_json::from_slice(&std::fs::read(&manifest.receipt)?)?;
803 println!("{}", serde_json::to_string(&receipt)?);
804 anyhow::ensure!(
805 receipt.token_is_appcontainer,
806 "child token was not AppContainer"
807 );
808 anyhow::ensure!(receipt.toolchain_read, "read-only toolchain was unreadable");
809 anyhow::ensure!(receipt.toolchain_write_denied, "toolchain write escaped");
810 anyhow::ensure!(receipt.worktree_write, "worktree write was denied");
811 anyhow::ensure!(receipt.scratch_write, "private scratch write was denied");
812 anyhow::ensure!(receipt.outside_write_denied, "out-of-root write escaped");
813 anyhow::ensure!(receipt.network_denied, "network access escaped");
814 anyhow::ensure!(!manifest.toolchain_denied_write.exists());
815 anyhow::ensure!(!manifest.outside_write.exists());
816 profile.remove()?;
817 Ok(())
818 }
819
820 pub(super) fn run_child_if_requested() -> anyhow::Result<()> {
821 let manifest_path = std::env::current_dir()?.join(MANIFEST_NAME);
822 if !manifest_path.is_file() {
823 return Ok(());
824 }
825 let manifest: FixtureManifest =
826 serde_json::from_slice(&std::fs::read(&manifest_path)?)?;
827 let toolchain_read = std::fs::read_to_string(&manifest.toolchain_marker)
828 .map(|value| value == "kranz-appcontainer-phase3")
829 .unwrap_or(false);
830 let toolchain_write_denied =
831 std::fs::write(&manifest.toolchain_denied_write, "escape").is_err();
832 let worktree_write = std::fs::write(&manifest.worktree_write, "allowed").is_ok();
833 let scratch_write = std::fs::write(&manifest.scratch_write, "allowed").is_ok();
834 let outside_write_denied = std::fs::write(&manifest.outside_write, "escape").is_err();
835 let network_denied =
836 TcpStream::connect_timeout(&manifest.loopback_addr, Duration::from_secs(2))
837 .is_err();
838 let receipt = HostileReceipt {
839 token_is_appcontainer: is_appcontainer_process()?,
840 toolchain_read,
841 toolchain_write_denied,
842 worktree_write,
843 scratch_write,
844 outside_write_denied,
845 network_denied,
846 };
847 std::fs::write(&manifest.receipt, serde_json::to_vec_pretty(&receipt)?)
848 .map_err(|error| io::Error::new(error.kind(), format!("write receipt: {error}")))?;
849 Ok(())
850 }
851 }
852}