Skip to content
Merged
38 changes: 14 additions & 24 deletions crates/cageforge-windows/src/filesystem/acl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,6 @@ struct AclOperation {
protect_dacl: bool,
}

struct SubtreePath {
path: PathBuf,
is_directory: bool,
}

enum AclOperationPath {
Pinned(ValidatedPath),
Discovered(PathBuf),
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -2656,7 +2649,7 @@ fn nearest_root_reuse(operation: &PreparedAclOperation, roots: &[(PathBuf, bool)
fn subtree_paths(
root: &Path,
excluded_roots: &[PathBuf],
) -> Result<Vec<SubtreePath>, FilesystemAclError> {
) -> Result<Vec<PathBuf>, FilesystemAclError> {
let metadata = fs::symlink_metadata(root).map_err(|source| FilesystemAclError::Metadata {
path: root.to_path_buf(),
source,
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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());
}

Expand Down
24 changes: 15 additions & 9 deletions crates/cageforge-windows/src/filesystem/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<File> {
self.handle.try_clone().map(File::from)
}
Expand Down Expand Up @@ -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)?;
Expand All @@ -218,6 +223,7 @@ impl ValidatedPath {
handle,
final_path,
identity,
is_directory,
})
}
}
Expand Down Expand Up @@ -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<bool, ValidatedPathError> {
let mut attributes = FILE_ATTRIBUTE_TAG_INFO::default();
if unsafe {
GetFileInformationByHandleEx(
Expand All @@ -319,7 +325,7 @@ fn reject_reparse_point(
path: path.to_path_buf(),
})
} else {
Ok(())
Ok(attributes.FileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0)
}
}

Expand Down
41 changes: 41 additions & 0 deletions crates/cageforge-windows/tests/support/windows_sandbox_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -100,6 +101,46 @@ fn main() -> ExitCode {
}
}

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")?;
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);
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:?}"
));
}
}
std::io::stdout()
.write_all(b"file-root-contract-ok")
.map_err(|error| format!("write file-root result: {error}"))
}

fn run() -> Result<(), String> {
let mode = environment(MODE)?.to_string_lossy().into_owned();
match mode.as_str() {
Expand Down
Loading
Loading