From edebeb072fc4a6a5b19b9784243846dfe805b142 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 12:49:14 +0500 Subject: [PATCH 01/10] test(windows): reproduce explicit file-root ACL launch Co-authored-by: codex --- .../tests/windows_backend.rs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/crates/cageforge-windows/tests/windows_backend.rs b/crates/cageforge-windows/tests/windows_backend.rs index 4ed45b63..b58b6205 100644 --- a/crates/cageforge-windows/tests/windows_backend.rs +++ b/crates/cageforge-windows/tests/windows_backend.rs @@ -1514,6 +1514,129 @@ fn restricted_command_can_start_a_native_runtime_program() { drop(cleanup); } +#[test] +fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { + let _setup_test_guard = setup_test_lock(); + let temporary = tempfile::tempdir().expect("temporary state"); + let config = WindowsSetupConfig::new() + .with_state_directory(temporary.path().join("state")) + .expect("absolute state directory") + .with_setup_helper_path(PathBuf::from(env!("CARGO_BIN_EXE_cageforge-windows-setup"))) + .expect("setup helper") + .with_command_runner_path(PathBuf::from(env!( + "CARGO_BIN_EXE_cageforge-windows-command-runner" + ))) + .expect("command runner"); + let setup = WindowsSetup::new(config); + let mut cleanup = SetupCleanup { + setup: &setup, + armed: true, + }; + setup.install().expect("install file-root setup"); + let backend = WindowsBackend::new( + WindowsBackendConfig::new() + .with_setup(setup.config().clone()) + .with_default_timeout(END_TO_END_PROBE_TIMEOUT) + .expect("bounded probe"), + ) + .expect("file-root backend"); + let workspace = tempfile::tempdir().expect("workspace"); + // Keep the file outside the writable workspace and grant only this leaf. + let resources = tempfile::tempdir().expect("external resources"); + let file = resources.path().join("single resource.jar"); + let sibling = resources.path().join("unrelated.txt"); + fs::write(&file, b"cageforge-file-root\r\n").expect("readable file"); + fs::write(&sibling, b"unrelated").expect("neighboring file"); + let paths = [file.as_path(), sibling.as_path(), resources.path()]; + let before = paths.map(raw_dacl_fingerprint); + for descriptor in &before { + assert!( + descriptor.starts_with("bytes="), + "DACL capture failed: {descriptor}" + ); + } + let system_root = PathBuf::from(std::env::var_os("SystemRoot").expect("SystemRoot")); + for (script, expected_success) in [ + (r#"type "%CAGEFORGE_READ_FILE%""#, true), + (r#"type "%CAGEFORGE_READ_FILE%""#, true), + (r#"echo forbidden>> "%CAGEFORGE_READ_FILE%""#, false), + ] { + let command = CommandSpec::new(system_root.join("System32/cmd.exe")) + .expect("system cmd.exe") + .with_args(["/d", "/c", script]) + .expect("cmd arguments"); + let environment = EnvironmentSpec::inherit_core() + .with_var("CAGEFORGE_READ_FILE", file.as_os_str()) + .expect("file path environment"); + let filesystem = FilesystemPolicy::restricted([ + FilesystemRule::new(PathSelector::minimal(), AccessMode::Read), + FilesystemRule::new(PathSelector::workspace_root(), AccessMode::Write), + FilesystemRule::new( + PathSelector::absolute(&file).expect("absolute file"), + AccessMode::Read, + ), + ]); + let (request, effective, context) = request_with_filesystem_environment( + workspace.path(), + filesystem, + NetworkPolicy::disabled(), + command, + environment, + ); + let prepared = backend + .prepare(BackendRequest::new(&request, &effective), &context) + .expect("prepare explicit file-root policy"); + let mut child = backend + .spawn(prepared) + .expect("spawn with explicit read file root"); + let status = child.wait().expect("wait file-root command"); + let mut stdout = String::new(); + child + .stdout() + .expect("stdout") + .read_to_string(&mut stdout) + .expect("read stdout"); + let mut stderr = String::new(); + child + .stderr() + .expect("stderr") + .read_to_string(&mut stderr) + .expect("read stderr"); + assert_eq!( + status.success(), + expected_success, + "status={status:?}; stdout={stdout:?}; stderr={stderr:?}" + ); + if expected_success { + assert_eq!(stdout, "cageforge-file-root\r\n"); + } + assert_eq!( + fs::read(&file).expect("read original file"), + b"cageforge-file-root\r\n" + ); + } + assert_eq!( + raw_dacl_fingerprint(&sibling), + before[1], + "sibling DACL changed" + ); + assert_eq!( + raw_dacl_fingerprint(resources.path()), + before[2], + "parent DACL changed" + ); + drop(backend); + setup + .uninstall() + .expect("restore file-root ACL and uninstall"); + cleanup.armed = false; + assert_eq!( + paths.map(raw_dacl_fingerprint), + before, + "original DACLs were not restored" + ); +} + #[test] fn runnable_windows_profile_launches_through_the_native_backend_api() { let _setup_test_guard = setup_test_lock(); From bfda5199b8fc555f1de37faa6836a94ab59dd823 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 12:53:13 +0500 Subject: [PATCH 02/10] fix(windows): normalize ACL entries for pinned file roots Co-authored-by: codex --- .../cageforge-windows/src/filesystem/acl.rs | 38 +++++++------------ .../cageforge-windows/src/filesystem/path.rs | 24 +++++++----- specs/0016-windows-backend-implementation.md | 17 ++++++++- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/crates/cageforge-windows/src/filesystem/acl.rs b/crates/cageforge-windows/src/filesystem/acl.rs index 0255f1b6..fe18eed6 100644 --- a/crates/cageforge-windows/src/filesystem/acl.rs +++ b/crates/cageforge-windows/src/filesystem/acl.rs @@ -111,11 +111,6 @@ struct AclOperation { protect_dacl: bool, } -struct SubtreePath { - path: PathBuf, - is_directory: bool, -} - enum AclOperationPath { Pinned(ValidatedPath), Discovered(PathBuf), @@ -706,8 +701,8 @@ impl<'plan> AclPlanBuilder<'plan> { for descendant in subtree_paths(&root, &exclusions)? { merge_pending( &mut self.continuation, - &descendant.path, - entries_for_existing_path(&entries, descendant.is_directory), + &descendant, + entries.clone(), true, Vec::new(), Vec::new(), @@ -733,8 +728,8 @@ impl<'plan> AclPlanBuilder<'plan> { for descendant in subtree_paths(&root, &exclusions)? { merge_pending( &mut self.continuation, - &descendant.path, - entries_for_existing_path(&entries, descendant.is_directory), + &descendant, + entries.clone(), true, Vec::new(), Vec::new(), @@ -813,10 +808,8 @@ impl AclOperation { }, }; let original = SecurityDescriptor::read(&path)?; - let entries = self - .entries - .iter() - .cloned() + let entries = entries_for_existing_path(&self.entries, path.is_directory()) + .into_iter() .map(|declaration| { let sid = LocalSid::parse("ACL entry", &declaration.sid)?; Ok(PreparedAclEntry { declaration, sid }) @@ -2656,7 +2649,7 @@ fn nearest_root_reuse(operation: &PreparedAclOperation, roots: &[(PathBuf, bool) fn subtree_paths( root: &Path, excluded_roots: &[PathBuf], -) -> Result, FilesystemAclError> { +) -> Result, FilesystemAclError> { let metadata = fs::symlink_metadata(root).map_err(|source| FilesystemAclError::Metadata { path: root.to_path_buf(), source, @@ -2715,13 +2708,10 @@ fn subtree_paths( if is_directory { stack.push(child.clone()); } - paths.push(SubtreePath { - path: child, - is_directory, - }); + paths.push(child); } } - paths.sort_by_key(|entry| NativePathKey::new(&entry.path)); + paths.sort_by_key(|entry| NativePathKey::new(entry)); Ok(paths) } @@ -3344,9 +3334,9 @@ mod tests { let paths = subtree_paths(&denied, std::slice::from_ref(&writable)) .expect("enumerate denied subtree"); - assert!(paths.iter().any(|entry| entry.path == sibling)); - assert!(!paths.iter().any(|entry| entry.path == writable)); - assert!(!paths.iter().any(|entry| entry.path == writable_child)); + assert!(paths.contains(&sibling)); + assert!(!paths.contains(&writable)); + assert!(!paths.contains(&writable_child)); } #[test] @@ -3387,8 +3377,8 @@ mod tests { let paths = subtree_paths(&root, &[]).expect("enumerate without following reparse point"); - assert!(!paths.iter().any(|entry| entry.path == junction)); - assert!(!paths.iter().any(|entry| entry.path == outside_child)); + assert!(!paths.contains(&junction)); + assert!(!paths.contains(&outside_child)); assert!(subtree_paths(&junction, &[]).is_err()); } diff --git a/crates/cageforge-windows/src/filesystem/path.rs b/crates/cageforge-windows/src/filesystem/path.rs index 35862e63..c669a4b0 100644 --- a/crates/cageforge-windows/src/filesystem/path.rs +++ b/crates/cageforge-windows/src/filesystem/path.rs @@ -15,17 +15,18 @@ use windows_sys::Win32::Foundation::{ ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND, GetLastError, INVALID_HANDLE_VALUE, }; use windows_sys::Win32::Storage::FileSystem::{ - CreateFileW, DELETE, FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO, - FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ, FILE_SHARE_READ, - FILE_SHARE_WRITE, FileAttributeTagInfo, GetFileInformationByHandleEx, - GetFinalPathNameByHandleW, GetLongPathNameW, OPEN_EXISTING, READ_CONTROL, VOLUME_NAME_DOS, - WRITE_DAC, + CreateFileW, DELETE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, + FILE_ATTRIBUTE_TAG_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, + FILE_GENERIC_READ, FILE_SHARE_READ, FILE_SHARE_WRITE, FileAttributeTagInfo, + GetFileInformationByHandleEx, GetFinalPathNameByHandleW, GetLongPathNameW, OPEN_EXISTING, + READ_CONTROL, VOLUME_NAME_DOS, WRITE_DAC, }; pub(crate) struct ValidatedPath { handle: OwnedHandle, final_path: PathBuf, identity: FilesystemObjectIdentity, + is_directory: bool, } #[derive(Debug, PartialEq, Eq)] @@ -174,6 +175,10 @@ impl ValidatedPath { &self.identity } + pub(crate) const fn is_directory(&self) -> bool { + self.is_directory + } + pub(crate) fn try_clone_file(&self) -> std::io::Result { self.handle.try_clone().map(File::from) } @@ -204,7 +209,7 @@ impl ValidatedPath { }); } let handle = unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) }; - reject_reparse_point(path, handle.as_raw_handle() as _)?; + let is_directory = validate_object_kind(path, handle.as_raw_handle() as _)?; let identity = object_identity(path, handle.as_raw_handle() as _)?; let final_path = final_path(path, handle.as_raw_handle() as _)?; let expanded_path = long_path(path)?; @@ -218,6 +223,7 @@ impl ValidatedPath { handle, final_path, identity, + is_directory, }) } } @@ -295,10 +301,10 @@ fn validate_lexical_path(path: &Path) -> Result<(), ValidatedPathError> { } #[allow(unsafe_code)] -fn reject_reparse_point( +fn validate_object_kind( path: &Path, handle: windows_sys::Win32::Foundation::HANDLE, -) -> Result<(), ValidatedPathError> { +) -> Result { let mut attributes = FILE_ATTRIBUTE_TAG_INFO::default(); if unsafe { GetFileInformationByHandleEx( @@ -319,7 +325,7 @@ fn reject_reparse_point( path: path.to_path_buf(), }) } else { - Ok(()) + Ok(attributes.FileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0) } } diff --git a/specs/0016-windows-backend-implementation.md b/specs/0016-windows-backend-implementation.md index b4e3f57e..cdfb291b 100644 --- a/specs/0016-windows-backend-implementation.md +++ b/specs/0016-windows-backend-implementation.md @@ -672,8 +672,10 @@ disagreement before its no-write read-back is typed descriptor drift and blocks launch. Thus an idempotent launch cannot cause duplicate inherited ACEs in existing descendants. New descendants inherit from the protected root while new siblings outside it continue to inherit the deny. -The scan retains each object's directory-or-file kind: direct ACEs on existing -files are exact, while ACEs on existing directories retain their required +ACL preparation obtains each object's directory-or-file kind from the same +non-reparse handle used for descriptor inspection and mutation. This applies +to explicitly selected file roots as well as enumerated descendants. Direct +ACEs on files are exact, while ACEs on directories retain their required inheritance flags for future children. Windows correctly strips inheritance flags from a file ACE, so Cageforge never treats that canonicalization as either evidence of enforcement or a reason to weaken a directory ACE check. Unknown or @@ -1171,6 +1173,17 @@ rights even if it writes visually equivalent ACEs. ### 12.3 Filesystem ACL review record +File-root inheritance was additionally compared with +`windows-sandbox-rs/src/acl.rs::ensure_allow_mask_aces_with_inheritance` at the +frozen baseline and the local upstream revision +`50d77959bf927293c4b5ddcca81d05331ae582ea`. Those implementations pass the +caller's inheritance flags to `SetEntriesInAclW` and apply the resulting ACL +through `SetNamedSecurityInfoW`. Cageforge determines the native object kind +through its retained handle before constructing the expected descriptor, so +file ACEs have exact scope in both the durable journal and native read-back. +Directory inheritance, masks, SID matching, descriptor equality, and original +DACL restoration retain their existing contracts. + Filesystem lowering and ACL reconciliation were reviewed line by line against the frozen versions of `src/acl.rs`, `src/allow.rs`, `src/workspace_acl.rs`, `src/deny_read_acl.rs`, `src/deny_read_state.rs`, `src/deny_read_resolver.rs`, From 7796209f1b2818da935c1b54d1826d49c28c84d6 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:00:44 +0500 Subject: [PATCH 03/10] test(windows): make file-root command deterministic Co-authored-by: codex --- crates/cageforge-windows/tests/windows_backend.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/cageforge-windows/tests/windows_backend.rs b/crates/cageforge-windows/tests/windows_backend.rs index b58b6205..5a329f7a 100644 --- a/crates/cageforge-windows/tests/windows_backend.rs +++ b/crates/cageforge-windows/tests/windows_backend.rs @@ -1557,17 +1557,14 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { } let system_root = PathBuf::from(std::env::var_os("SystemRoot").expect("SystemRoot")); for (script, expected_success) in [ - (r#"type "%CAGEFORGE_READ_FILE%""#, true), - (r#"type "%CAGEFORGE_READ_FILE%""#, true), - (r#"echo forbidden>> "%CAGEFORGE_READ_FILE%""#, false), + (format!(r#"type "{}""#, file.display()), true), + (format!(r#"echo forbidden>> "{}""#, file.display()), false), ] { let command = CommandSpec::new(system_root.join("System32/cmd.exe")) .expect("system cmd.exe") - .with_args(["/d", "/c", script]) + .with_args(["/d", "/c", script.as_str()]) .expect("cmd arguments"); - let environment = EnvironmentSpec::inherit_core() - .with_var("CAGEFORGE_READ_FILE", file.as_os_str()) - .expect("file path environment"); + let environment = EnvironmentSpec::inherit_core(); let filesystem = FilesystemPolicy::restricted([ FilesystemRule::new(PathSelector::minimal(), AccessMode::Read), FilesystemRule::new(PathSelector::workspace_root(), AccessMode::Write), From 8b065c912a8b66468d18dd3e27c9bab7c9bf3289 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:07:35 +0500 Subject: [PATCH 04/10] test(windows): avoid cmd path quoting in ACL regression Co-authored-by: codex --- crates/cageforge-windows/tests/windows_backend.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/cageforge-windows/tests/windows_backend.rs b/crates/cageforge-windows/tests/windows_backend.rs index 5a329f7a..661028bb 100644 --- a/crates/cageforge-windows/tests/windows_backend.rs +++ b/crates/cageforge-windows/tests/windows_backend.rs @@ -1543,7 +1543,7 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { let workspace = tempfile::tempdir().expect("workspace"); // Keep the file outside the writable workspace and grant only this leaf. let resources = tempfile::tempdir().expect("external resources"); - let file = resources.path().join("single resource.jar"); + let file = resources.path().join("single-resource.jar"); let sibling = resources.path().join("unrelated.txt"); fs::write(&file, b"cageforge-file-root\r\n").expect("readable file"); fs::write(&sibling, b"unrelated").expect("neighboring file"); @@ -1557,8 +1557,8 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { } let system_root = PathBuf::from(std::env::var_os("SystemRoot").expect("SystemRoot")); for (script, expected_success) in [ - (format!(r#"type "{}""#, file.display()), true), - (format!(r#"echo forbidden>> "{}""#, file.display()), false), + (format!("type {}", file.display()), true), + (format!("echo forbidden>> {}", file.display()), false), ] { let command = CommandSpec::new(system_root.join("System32/cmd.exe")) .expect("system cmd.exe") From 165a1729126de9e91d763fe355240f6a32d913b9 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:18:21 +0500 Subject: [PATCH 05/10] fix(windows): grant traversal for explicit file roots Co-authored-by: codex --- .../cageforge-windows/src/filesystem/acl.rs | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/crates/cageforge-windows/src/filesystem/acl.rs b/crates/cageforge-windows/src/filesystem/acl.rs index fe18eed6..e7404278 100644 --- a/crates/cageforge-windows/src/filesystem/acl.rs +++ b/crates/cageforge-windows/src/filesystem/acl.rs @@ -32,9 +32,9 @@ use windows_sys::Win32::Security::{ use windows_sys::Win32::Storage::FileSystem::{ CREATE_NEW, CreateDirectoryW, CreateFileW, DELETE, FILE_ALL_ACCESS, FILE_APPEND_DATA, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_REPARSE_POINT, FILE_DELETE_CHILD, FILE_DISPOSITION_INFO, - FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, - FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, FileDispositionInfo, - SetFileInformationByHandle, WRITE_DAC, WRITE_OWNER, + FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, + FILE_WRITE_EA, FileDispositionInfo, SetFileInformationByHandle, WRITE_DAC, WRITE_OWNER, }; use crate::capability::state::{ @@ -57,6 +57,7 @@ const ACCESS_DENIED_ACE_TYPE: u8 = 1; const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE; const READ_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE; +const PATH_TRAVERSAL_ALLOW_MASK: u32 = FILE_READ_ATTRIBUTES | FILE_TRAVERSE; const WRITE_DENY_MASK: u32 = FILE_GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA @@ -219,6 +220,11 @@ pub(crate) enum FilesystemAclError { CapabilityTransition(#[from] CapabilityStateTransitionError), #[error(transparent)] InvalidPath(#[from] ValidatedPathError), + #[error("Windows filesystem ACL expected directory path {path:?}, but found a file")] + PathKindMismatch { + path: PathBuf, + expected_directory: bool, + }, #[error("capability state returned {actual} authorities for {expected} filesystem roles")] AuthorityCount { expected: usize, actual: usize }, #[error("no write-root capability SID exists for {path:?}")] @@ -563,6 +569,9 @@ impl<'plan> AclPlanBuilder<'plan> { AclEntry::allow(&self.authorities.read_base_sid, READ_ALLOW_MASK), ]; self.insert_foundation(path, entries, true, self.inherited_write_sids(path)); + if !target.path().is_directory() { + self.insert_file_path_ancestors(path)?; + } } FilesystemPlanAccess::WriteRoot => { let write_sid = self.authorities.write_sid(path).ok_or_else(|| { @@ -616,6 +625,43 @@ impl<'plan> AclPlanBuilder<'plan> { self.expand_existing_descendants() } + fn insert_file_path_ancestors(&mut self, file: &Path) -> Result<(), FilesystemAclError> { + let mut ancestor = file.parent().map(Path::to_path_buf); + while let Some(path) = ancestor { + let Some(parent) = path.parent() else { + break; + }; + if paths_equal(&path, parent) { + break; + } + let validated = ValidatedPath::open_for_acl(&path)?; + if !validated.is_directory() { + return Err(FilesystemAclError::PathKindMismatch { + path: path.clone(), + expected_directory: true, + }); + } + let key = NativePathKey::new(validated.final_path()); + if self.foundation.contains_key(&key) { + ancestor = Some(parent.to_path_buf()); + continue; + } + merge_pending( + &mut self.foundation, + validated.final_path(), + vec![AclEntry::allow_exact( + self.group_sid, + PATH_TRAVERSAL_ALLOW_MASK, + )], + false, + Vec::new(), + Vec::new(), + ); + ancestor = Some(parent.to_path_buf()); + } + Ok(()) + } + fn collect_materialized_foundations( &mut self, directories: &[PathBuf], @@ -1301,6 +1347,15 @@ impl AclEntry { inheritance: AclInheritance::Subtree, } } + + fn allow_exact(sid: &str, mask: u32) -> Self { + Self { + sid: sid.to_string(), + mode: AclAccessMode::Allow, + mask, + inheritance: AclInheritance::Exact, + } + } } fn entries_for_existing_path(entries: &[AclEntry], is_directory: bool) -> Vec { From a9fd77bb86d977a58b3546c3c790d2f2adc92560 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:19:03 +0500 Subject: [PATCH 06/10] docs(windows): document explicit file traversal Co-authored-by: codex --- specs/0016-windows-backend-implementation.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/specs/0016-windows-backend-implementation.md b/specs/0016-windows-backend-implementation.md index cdfb291b..118b2fe0 100644 --- a/specs/0016-windows-backend-implementation.md +++ b/specs/0016-windows-backend-implementation.md @@ -686,6 +686,16 @@ descendant disappears before its ACL handle can be opened, only and skipped. Reparse substitution, final-path drift, access denial, malformed ACLs, and every other open failure remain typed fail-closed errors. +An explicit file root is still accessed by the child through its pathname. The +planner therefore adds a temporary, exact (non-inheriting) ancestor ACE with +only `FILE_TRAVERSE | FILE_READ_ATTRIBUTES` on each existing non-volume parent +that the restricted token must cross. These ACEs do not grant directory listing, +file data, child inheritance, or access to sibling files. Each parent is opened +and validated without following reparse points, journaled under the same ACL +transaction, read back, and restored with the exact original descriptor during +release. A failure to validate or restore any parent blocks launch or cleanup; +the implementation must not replace this with a broad parent read root. + All capability-state and ACL reconciliation uses the same protected cross-process lock. A later profile may preserve another profile's capability ACEs but must never revoke or rewrite them as its own. Persistent state records From bc8fc7337b800ac2ce780a4e8611a4559b1f15a5 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:29:22 +0500 Subject: [PATCH 07/10] fix(windows): apply file traversal to both capability SIDs Co-authored-by: codex --- crates/cageforge-windows/src/filesystem/acl.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/cageforge-windows/src/filesystem/acl.rs b/crates/cageforge-windows/src/filesystem/acl.rs index e7404278..f132825b 100644 --- a/crates/cageforge-windows/src/filesystem/acl.rs +++ b/crates/cageforge-windows/src/filesystem/acl.rs @@ -649,10 +649,13 @@ impl<'plan> AclPlanBuilder<'plan> { merge_pending( &mut self.foundation, validated.final_path(), - vec![AclEntry::allow_exact( - self.group_sid, - PATH_TRAVERSAL_ALLOW_MASK, - )], + vec![ + AclEntry::allow_exact(self.group_sid, PATH_TRAVERSAL_ALLOW_MASK), + AclEntry::allow_exact( + &self.authorities.read_base_sid, + PATH_TRAVERSAL_ALLOW_MASK, + ), + ], false, Vec::new(), Vec::new(), From f16df9c99ef3da4c6593eb5e369cb52e76350f19 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Tue, 22 Sep 2026 13:40:55 +0500 Subject: [PATCH 08/10] fix(windows): keep file ancestor grants non-recursive Co-authored-by: codex --- crates/cageforge-windows/src/filesystem/acl.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/cageforge-windows/src/filesystem/acl.rs b/crates/cageforge-windows/src/filesystem/acl.rs index f132825b..cb800e5c 100644 --- a/crates/cageforge-windows/src/filesystem/acl.rs +++ b/crates/cageforge-windows/src/filesystem/acl.rs @@ -94,6 +94,7 @@ struct AclPlanBuilder<'plan> { foundation: BTreeMap, continuation: BTreeMap, denies: BTreeMap, + file_path_ancestors: BTreeSet, write_roots: Vec, } @@ -554,6 +555,7 @@ impl<'plan> AclPlanBuilder<'plan> { foundation: BTreeMap::new(), continuation: BTreeMap::new(), denies: BTreeMap::new(), + file_path_ancestors: BTreeSet::new(), write_roots, } } @@ -646,6 +648,7 @@ impl<'plan> AclPlanBuilder<'plan> { ancestor = Some(parent.to_path_buf()); continue; } + self.file_path_ancestors.insert(key); merge_pending( &mut self.foundation, validated.final_path(), @@ -737,7 +740,9 @@ impl<'plan> AclPlanBuilder<'plan> { fn expand_existing_descendants(&mut self) -> Result<(), FilesystemAclError> { let allow_roots = self .foundation - .values() + .iter() + .filter(|(key, _)| !self.file_path_ancestors.contains(*key)) + .map(|(_, operation)| operation) .map(|operation| { ( operation.path.clone(), From e6cd66a3e0e314479add11c25958a6a67942000b Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Wed, 23 Sep 2026 10:06:52 +0500 Subject: [PATCH 09/10] test(windows): isolate explicit file access operations Co-authored-by: codex --- .../tests/support/windows_sandbox_fixture.rs | 46 +++++++++++++++++++ .../tests/windows_backend.rs | 42 +++++++++++++---- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs b/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs index 3490e8bf..eb0e73fa 100644 --- a/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs +++ b/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs @@ -87,6 +87,7 @@ const UNRELATED_NAMED_OBJECT: &str = "CAGEFORGE_WINDOWS_SANDBOX_FIXTURE_UNRELATE fn main() -> ExitCode { let result = match std::env::args_os().nth(1).as_deref() { + Some(argument) if argument == "--file-root-probe" => file_root_probe(), Some(argument) if argument == "--process-broker-child" => process_broker_child(), Some(argument) if argument == "--shell-activation-child" => shell_activation_child(), _ => run(), @@ -100,6 +101,51 @@ fn main() -> ExitCode { } } +#[cfg(target_os = "windows")] +fn file_root_probe() -> Result<(), String> { + let file = PathBuf::from(std::env::args_os().nth(2).ok_or("missing file path")?); + let sibling = PathBuf::from(std::env::args_os().nth(3).ok_or("missing sibling path")?); + let parent = file.parent().ok_or("missing parent")?; + let read = std::fs::read(&file); + let write = std::fs::OpenOptions::new().write(true).open(&file); + let sibling_read = std::fs::read(&sibling); + let listing = std::fs::read_dir(parent); + println!("file read: {read:?}"); + println!("file write-open: {write:?}"); + println!("sibling read: {sibling_read:?}"); + println!("parent listing: {listing:?}"); + println!("file metadata: {:?}", std::fs::metadata(&file)); + println!("parent metadata: {:?}", std::fs::metadata(parent)); + let cmd = PathBuf::from(environment("SystemRoot")?).join("System32/cmd.exe"); + for script in [ + format!("type {}", file.display()), + format!("type < {}", file.display()), + ] { + let output = Command::new(&cmd).args(["/d", "/c", &script]).output(); + println!("command {script:?}: {output:?}"); + } + if read.as_deref().map_err(|error| error.kind()) != Ok(b"cageforge-file-root\r\n") { + return Err("explicit file read failed".to_string()); + } + for (operation, error) in [ + ("file write", write.err()), + ("sibling read", sibling_read.err()), + ("parent listing", listing.err()), + ] { + if error.as_ref().map(std::io::Error::kind) != Some(std::io::ErrorKind::PermissionDenied) { + return Err(format!( + "{operation}: expected access denied, got {error:?}" + )); + } + } + Ok(()) +} + +#[cfg(not(target_os = "windows"))] +fn file_root_probe() -> Result<(), String> { + Err("file-root probe requires Windows".to_string()) +} + fn run() -> Result<(), String> { let mode = environment(MODE)?.to_string_lossy().into_owned(); match mode.as_str() { diff --git a/crates/cageforge-windows/tests/windows_backend.rs b/crates/cageforge-windows/tests/windows_backend.rs index 661028bb..a279bc32 100644 --- a/crates/cageforge-windows/tests/windows_backend.rs +++ b/crates/cageforge-windows/tests/windows_backend.rs @@ -1556,14 +1556,39 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { ); } let system_root = PathBuf::from(std::env::var_os("SystemRoot").expect("SystemRoot")); - for (script, expected_success) in [ - (format!("type {}", file.display()), true), - (format!("echo forbidden>> {}", file.display()), false), - ] { - let command = CommandSpec::new(system_root.join("System32/cmd.exe")) + let fixture = workspace.path().join("file-root-probe.exe"); + fs::copy( + env!("CARGO_BIN_EXE_cageforge-windows-test-fixture"), + &fixture, + ) + .expect("copy file-root fixture"); + let direct_probe = CommandSpec::new(&fixture) + .expect("fixture command") + .with_args([ + std::ffi::OsStr::new("--file-root-probe"), + file.as_os_str(), + sibling.as_os_str(), + ]) + .expect("fixture arguments"); + let shell = |script: String| { + CommandSpec::new(system_root.join("System32/cmd.exe")) .expect("system cmd.exe") .with_args(["/d", "/c", script.as_str()]) - .expect("cmd arguments"); + .expect("cmd arguments") + }; + for (command, expected_success, expected_stdout) in [ + (direct_probe, true, None), + ( + shell(format!("type {}", file.display())), + true, + Some("cageforge-file-root\r\n"), + ), + ( + shell(format!("echo forbidden>> {}", file.display())), + false, + None, + ), + ] { let environment = EnvironmentSpec::inherit_core(); let filesystem = FilesystemPolicy::restricted([ FilesystemRule::new(PathSelector::minimal(), AccessMode::Read), @@ -1599,13 +1624,14 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { .expect("stderr") .read_to_string(&mut stderr) .expect("read stderr"); + eprintln!("file-root probe: status={status:?}; stdout={stdout:?}; stderr={stderr:?}"); assert_eq!( status.success(), expected_success, "status={status:?}; stdout={stdout:?}; stderr={stderr:?}" ); - if expected_success { - assert_eq!(stdout, "cageforge-file-root\r\n"); + if let Some(expected_stdout) = expected_stdout { + assert_eq!(stdout, expected_stdout); } assert_eq!( fs::read(&file).expect("read original file"), From d815ded3c049b9e1d71cbbb315b584d7cf8fb353 Mon Sep 17 00:00:00 2001 From: Mansur Azatbek Date: Wed, 23 Sep 2026 10:14:39 +0500 Subject: [PATCH 10/10] fix(windows): keep explicit file grants scoped to the file Co-authored-by: codex --- .../cageforge-windows/src/filesystem/acl.rs | 71 +-------------- .../tests/support/windows_sandbox_fixture.rs | 45 +++++----- .../tests/windows_backend.rs | 88 +++++++++++++------ specs/0016-windows-backend-implementation.md | 16 ++-- 4 files changed, 91 insertions(+), 129 deletions(-) diff --git a/crates/cageforge-windows/src/filesystem/acl.rs b/crates/cageforge-windows/src/filesystem/acl.rs index cb800e5c..fe18eed6 100644 --- a/crates/cageforge-windows/src/filesystem/acl.rs +++ b/crates/cageforge-windows/src/filesystem/acl.rs @@ -32,9 +32,9 @@ use windows_sys::Win32::Security::{ use windows_sys::Win32::Storage::FileSystem::{ CREATE_NEW, CreateDirectoryW, CreateFileW, DELETE, FILE_ALL_ACCESS, FILE_APPEND_DATA, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_REPARSE_POINT, FILE_DELETE_CHILD, FILE_DISPOSITION_INFO, - FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, - FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, - FILE_WRITE_EA, FileDispositionInfo, SetFileInformationByHandle, WRITE_DAC, WRITE_OWNER, + FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, + FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, FileDispositionInfo, + SetFileInformationByHandle, WRITE_DAC, WRITE_OWNER, }; use crate::capability::state::{ @@ -57,7 +57,6 @@ const ACCESS_DENIED_ACE_TYPE: u8 = 1; const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE; const READ_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE; -const PATH_TRAVERSAL_ALLOW_MASK: u32 = FILE_READ_ATTRIBUTES | FILE_TRAVERSE; const WRITE_DENY_MASK: u32 = FILE_GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA @@ -94,7 +93,6 @@ struct AclPlanBuilder<'plan> { foundation: BTreeMap, continuation: BTreeMap, denies: BTreeMap, - file_path_ancestors: BTreeSet, write_roots: Vec, } @@ -221,11 +219,6 @@ pub(crate) enum FilesystemAclError { CapabilityTransition(#[from] CapabilityStateTransitionError), #[error(transparent)] InvalidPath(#[from] ValidatedPathError), - #[error("Windows filesystem ACL expected directory path {path:?}, but found a file")] - PathKindMismatch { - path: PathBuf, - expected_directory: bool, - }, #[error("capability state returned {actual} authorities for {expected} filesystem roles")] AuthorityCount { expected: usize, actual: usize }, #[error("no write-root capability SID exists for {path:?}")] @@ -555,7 +548,6 @@ impl<'plan> AclPlanBuilder<'plan> { foundation: BTreeMap::new(), continuation: BTreeMap::new(), denies: BTreeMap::new(), - file_path_ancestors: BTreeSet::new(), write_roots, } } @@ -571,9 +563,6 @@ impl<'plan> AclPlanBuilder<'plan> { AclEntry::allow(&self.authorities.read_base_sid, READ_ALLOW_MASK), ]; self.insert_foundation(path, entries, true, self.inherited_write_sids(path)); - if !target.path().is_directory() { - self.insert_file_path_ancestors(path)?; - } } FilesystemPlanAccess::WriteRoot => { let write_sid = self.authorities.write_sid(path).ok_or_else(|| { @@ -627,47 +616,6 @@ impl<'plan> AclPlanBuilder<'plan> { self.expand_existing_descendants() } - fn insert_file_path_ancestors(&mut self, file: &Path) -> Result<(), FilesystemAclError> { - let mut ancestor = file.parent().map(Path::to_path_buf); - while let Some(path) = ancestor { - let Some(parent) = path.parent() else { - break; - }; - if paths_equal(&path, parent) { - break; - } - let validated = ValidatedPath::open_for_acl(&path)?; - if !validated.is_directory() { - return Err(FilesystemAclError::PathKindMismatch { - path: path.clone(), - expected_directory: true, - }); - } - let key = NativePathKey::new(validated.final_path()); - if self.foundation.contains_key(&key) { - ancestor = Some(parent.to_path_buf()); - continue; - } - self.file_path_ancestors.insert(key); - merge_pending( - &mut self.foundation, - validated.final_path(), - vec![ - AclEntry::allow_exact(self.group_sid, PATH_TRAVERSAL_ALLOW_MASK), - AclEntry::allow_exact( - &self.authorities.read_base_sid, - PATH_TRAVERSAL_ALLOW_MASK, - ), - ], - false, - Vec::new(), - Vec::new(), - ); - ancestor = Some(parent.to_path_buf()); - } - Ok(()) - } - fn collect_materialized_foundations( &mut self, directories: &[PathBuf], @@ -740,9 +688,7 @@ impl<'plan> AclPlanBuilder<'plan> { fn expand_existing_descendants(&mut self) -> Result<(), FilesystemAclError> { let allow_roots = self .foundation - .iter() - .filter(|(key, _)| !self.file_path_ancestors.contains(*key)) - .map(|(_, operation)| operation) + .values() .map(|operation| { ( operation.path.clone(), @@ -1355,15 +1301,6 @@ impl AclEntry { inheritance: AclInheritance::Subtree, } } - - fn allow_exact(sid: &str, mask: u32) -> Self { - Self { - sid: sid.to_string(), - mode: AclAccessMode::Allow, - mask, - inheritance: AclInheritance::Exact, - } - } } fn entries_for_existing_path(entries: &[AclEntry], is_directory: bool) -> Vec { diff --git a/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs b/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs index eb0e73fa..5fd4de4f 100644 --- a/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs +++ b/crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs @@ -101,32 +101,30 @@ fn main() -> ExitCode { } } -#[cfg(target_os = "windows")] fn file_root_probe() -> Result<(), String> { let file = PathBuf::from(std::env::args_os().nth(2).ok_or("missing file path")?); let sibling = PathBuf::from(std::env::args_os().nth(3).ok_or("missing sibling path")?); + let spaced_file = PathBuf::from( + std::env::args_os() + .nth(4) + .ok_or("missing spaced file path")?, + ); let parent = file.parent().ok_or("missing parent")?; - let read = std::fs::read(&file); + for path in [&file, &spaced_file] { + let data = + std::fs::read(path).map_err(|error| format!("read approved file {path:?}: {error}"))?; + if data != b"cageforge-file-root\r\n" { + return Err(format!("unexpected approved file contents: {path:?}")); + } + let metadata = std::fs::metadata(path) + .map_err(|error| format!("read approved file metadata {path:?}: {error}"))?; + if !metadata.is_file() || metadata.len() != data.len() as u64 { + return Err(format!("unexpected approved file metadata: {path:?}")); + } + } let write = std::fs::OpenOptions::new().write(true).open(&file); let sibling_read = std::fs::read(&sibling); let listing = std::fs::read_dir(parent); - println!("file read: {read:?}"); - println!("file write-open: {write:?}"); - println!("sibling read: {sibling_read:?}"); - println!("parent listing: {listing:?}"); - println!("file metadata: {:?}", std::fs::metadata(&file)); - println!("parent metadata: {:?}", std::fs::metadata(parent)); - let cmd = PathBuf::from(environment("SystemRoot")?).join("System32/cmd.exe"); - for script in [ - format!("type {}", file.display()), - format!("type < {}", file.display()), - ] { - let output = Command::new(&cmd).args(["/d", "/c", &script]).output(); - println!("command {script:?}: {output:?}"); - } - if read.as_deref().map_err(|error| error.kind()) != Ok(b"cageforge-file-root\r\n") { - return Err("explicit file read failed".to_string()); - } for (operation, error) in [ ("file write", write.err()), ("sibling read", sibling_read.err()), @@ -138,12 +136,9 @@ fn file_root_probe() -> Result<(), String> { )); } } - Ok(()) -} - -#[cfg(not(target_os = "windows"))] -fn file_root_probe() -> Result<(), String> { - Err("file-root probe requires Windows".to_string()) + std::io::stdout() + .write_all(b"file-root-contract-ok") + .map_err(|error| format!("write file-root result: {error}")) } fn run() -> Result<(), String> { diff --git a/crates/cageforge-windows/tests/windows_backend.rs b/crates/cageforge-windows/tests/windows_backend.rs index a279bc32..4d8a9950 100644 --- a/crates/cageforge-windows/tests/windows_backend.rs +++ b/crates/cageforge-windows/tests/windows_backend.rs @@ -1544,10 +1544,17 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { // Keep the file outside the writable workspace and grant only this leaf. let resources = tempfile::tempdir().expect("external resources"); let file = resources.path().join("single-resource.jar"); + let spaced_file = resources.path().join("single resource.jar"); let sibling = resources.path().join("unrelated.txt"); fs::write(&file, b"cageforge-file-root\r\n").expect("readable file"); + fs::write(&spaced_file, b"cageforge-file-root\r\n").expect("readable spaced file"); fs::write(&sibling, b"unrelated").expect("neighboring file"); - let paths = [file.as_path(), sibling.as_path(), resources.path()]; + let paths = [ + file.as_path(), + sibling.as_path(), + resources.path(), + spaced_file.as_path(), + ]; let before = paths.map(raw_dacl_fingerprint); for descriptor in &before { assert!( @@ -1562,42 +1569,78 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { &fixture, ) .expect("copy file-root fixture"); - let direct_probe = CommandSpec::new(&fixture) - .expect("fixture command") - .with_args([ - std::ffi::OsStr::new("--file-root-probe"), - file.as_os_str(), - sibling.as_os_str(), - ]) - .expect("fixture arguments"); + let direct_probe = || { + CommandSpec::new(&fixture) + .expect("fixture command") + .with_args([ + std::ffi::OsStr::new("--file-root-probe"), + file.as_os_str(), + sibling.as_os_str(), + spaced_file.as_os_str(), + ]) + .expect("fixture arguments") + }; let shell = |script: String| { CommandSpec::new(system_root.join("System32/cmd.exe")) .expect("system cmd.exe") .with_args(["/d", "/c", script.as_str()]) .expect("cmd arguments") }; - for (command, expected_success, expected_stdout) in [ - (direct_probe, true, None), + for (command, allow_parent, expected_success, expected_stdout) in [ + (direct_probe(), false, true, Some("file-root-contract-ok")), + (direct_probe(), false, true, Some("file-root-contract-ok")), + // TYPE enumerates the directory, beyond opening the approved file. ( shell(format!("type {}", file.display())), - true, - Some("cageforge-file-root\r\n"), + false, + false, + Some(""), ), ( shell(format!("echo forbidden>> {}", file.display())), false, - None, + false, + Some(""), + ), + // Keep the identical shell command as a positive control with an + // explicit directory grant; never infer that grant from a file rule. + ( + shell(format!("type {}", file.display())), + true, + true, + Some("cageforge-file-root\r\n"), ), ] { + assert_eq!( + raw_dacl_fingerprint(&sibling), + before[1], + "sibling DACL changed by file grant" + ); + assert_eq!( + raw_dacl_fingerprint(resources.path()), + before[2], + "parent DACL changed by file grant" + ); let environment = EnvironmentSpec::inherit_core(); - let filesystem = FilesystemPolicy::restricted([ + let mut rules = vec![ FilesystemRule::new(PathSelector::minimal(), AccessMode::Read), FilesystemRule::new(PathSelector::workspace_root(), AccessMode::Write), FilesystemRule::new( PathSelector::absolute(&file).expect("absolute file"), AccessMode::Read, ), - ]); + FilesystemRule::new( + PathSelector::absolute(&spaced_file).expect("absolute spaced file"), + AccessMode::Read, + ), + ]; + if allow_parent { + rules.push(FilesystemRule::new( + PathSelector::absolute(resources.path()).expect("absolute resource directory"), + AccessMode::Read, + )); + } + let filesystem = FilesystemPolicy::restricted(rules); let (request, effective, context) = request_with_filesystem_environment( workspace.path(), filesystem, @@ -1624,11 +1667,10 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { .expect("stderr") .read_to_string(&mut stderr) .expect("read stderr"); - eprintln!("file-root probe: status={status:?}; stdout={stdout:?}; stderr={stderr:?}"); assert_eq!( status.success(), expected_success, - "status={status:?}; stdout={stdout:?}; stderr={stderr:?}" + "allow_parent={allow_parent}; status={status:?}; stdout={stdout:?}; stderr={stderr:?}" ); if let Some(expected_stdout) = expected_stdout { assert_eq!(stdout, expected_stdout); @@ -1638,16 +1680,6 @@ fn explicit_read_file_root_supports_launch_and_exact_acl_cleanup() { b"cageforge-file-root\r\n" ); } - assert_eq!( - raw_dacl_fingerprint(&sibling), - before[1], - "sibling DACL changed" - ); - assert_eq!( - raw_dacl_fingerprint(resources.path()), - before[2], - "parent DACL changed" - ); drop(backend); setup .uninstall() diff --git a/specs/0016-windows-backend-implementation.md b/specs/0016-windows-backend-implementation.md index 118b2fe0..64cf44f6 100644 --- a/specs/0016-windows-backend-implementation.md +++ b/specs/0016-windows-backend-implementation.md @@ -686,15 +686,13 @@ descendant disappears before its ACL handle can be opened, only and skipped. Reparse substitution, final-path drift, access denial, malformed ACLs, and every other open failure remain typed fail-closed errors. -An explicit file root is still accessed by the child through its pathname. The -planner therefore adds a temporary, exact (non-inheriting) ancestor ACE with -only `FILE_TRAVERSE | FILE_READ_ATTRIBUTES` on each existing non-volume parent -that the restricted token must cross. These ACEs do not grant directory listing, -file data, child inheritance, or access to sibling files. Each parent is opened -and validated without following reparse points, journaled under the same ACL -transaction, read back, and restored with the exact original descriptor during -release. A failure to validate or restore any parent blocks launch or cleanup; -the implementation must not replace this with a broad parent read root. +An explicit file read root grants access to that file without changing its +parent or siblings. The verified `SeChangeNotifyPrivilege` permits traversal +to an authorized file; it does not grant directory listing. Applications that +enumerate the containing directory need a separate directory read rule. +The backend must not infer that broader grant from an individual file rule. +File and directory grants use the same exact journal and uninstall restoration +contract. All capability-state and ACL reconciliation uses the same protected cross-process lock. A later profile may preserve another profile's capability