Skip to main content

car_secrets/
secure_path.rs

1//! Descriptor-safe owner-private filesystem primitives.
2//!
3//! CAR-owned state must be private from its first byte, must not follow a
4//! symlink supplied at any component, and must fail closed when the opened
5//! object is not the current user's single-link regular file. These helpers
6//! centralize that contract instead of asking every store to reproduce it.
7
8use std::fs::File;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex};
12
13/// Deterministic failure point for private-path durability tests.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum PrivatePathDurabilityFailurePoint {
16    /// Fail immediately before flushing a retained parent directory after a
17    /// new child directory or file entry has been created.
18    ParentDirectorySync,
19}
20
21/// Cloneable, one-shot fault injector for first-use private-path durability.
22///
23/// This is deliberately explicit and passed only by tests/configured stores;
24/// production callers use the ordinary helpers and cannot be affected by
25/// process-global state.
26#[derive(Clone, Debug, Default)]
27pub struct PrivatePathDurabilityFailureInjector {
28    failures: Arc<Mutex<Vec<PrivatePathDurabilityFailurePoint>>>,
29}
30
31impl PrivatePathDurabilityFailureInjector {
32    /// Fail the next matching durability boundary exactly once.
33    pub fn fail_next(&self, point: PrivatePathDurabilityFailurePoint) {
34        self.failures
35            .lock()
36            .expect("private-path failure injector lock poisoned")
37            .push(point);
38    }
39
40    fn check(&self, point: PrivatePathDurabilityFailurePoint) -> io::Result<()> {
41        let mut failures = self
42            .failures
43            .lock()
44            .map_err(|_| io::Error::other("private-path failure injector lock poisoned"))?;
45        if failures.first() == Some(&point) {
46            failures.remove(0);
47            return Err(io::Error::other(
48                "injected private-path parent directory sync failure",
49            ));
50        }
51        Ok(())
52    }
53}
54
55/// Exact relative roots to harden below a retained private-tree descriptor.
56#[derive(Clone, Debug, Default, PartialEq, Eq)]
57pub struct PrivateTreePolicy {
58    selected: Vec<PathBuf>,
59}
60
61impl PrivateTreePolicy {
62    /// Revalidate and harden only the tree root.
63    pub fn root_only() -> Self {
64        Self::default()
65    }
66
67    /// Select exact relative files or directories. A selected directory is
68    /// traversed recursively; siblings outside the selection are untouched.
69    pub fn selected<I, P>(paths: I) -> Self
70    where
71        I: IntoIterator<Item = P>,
72        P: AsRef<Path>,
73    {
74        Self {
75            selected: paths
76                .into_iter()
77                .map(|path| path.as_ref().to_path_buf())
78                .collect(),
79        }
80    }
81}
82
83/// Aggregate receipt for one selected private-tree hardening pass.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
85pub struct PrivateTreeReport {
86    pub directories_hardened: usize,
87    pub files_hardened: usize,
88}
89
90/// A verified tree root whose descriptor remains open across migration.
91#[derive(Debug)]
92pub struct PrivateTree {
93    root: PathBuf,
94    root_descriptor: File,
95    operation_lock: std::sync::Mutex<()>,
96}
97
98impl PrivateTree {
99    /// Open and harden an existing root while retaining its descriptor.
100    pub fn open(root: &Path) -> io::Result<Self> {
101        #[cfg(unix)]
102        let root_descriptor = {
103            let descriptor = unix_walk_directory(root, false, true)?;
104            unix_validate_private_dir_exact(&descriptor)?;
105            unix_revalidate_directory_path(root, &descriptor)?;
106            descriptor
107        };
108        #[cfg(target_os = "windows")]
109        let root_descriptor = {
110            let descriptor = windows_open_directory_for_hardening(root)?;
111            windows_validate_directory(&descriptor)?;
112            windows_validate_hardenable_file_owner(&descriptor)?;
113            windows_harden_file_acl(&descriptor)?;
114            windows_revalidate_directory_path(root, &descriptor)?;
115            descriptor
116        };
117        #[cfg(not(any(unix, target_os = "windows")))]
118        let root_descriptor = File::open(root)?;
119
120        let tree = Self {
121            root: root.to_path_buf(),
122            root_descriptor,
123            operation_lock: std::sync::Mutex::new(()),
124        };
125        tree.revalidate_root()?;
126        Ok(tree)
127    }
128
129    /// Harden the exact selected roots and recursively harden selected
130    /// directories. Errors have no receipt; completed hardening remains safe
131    /// and retrying the same policy is idempotent.
132    pub fn harden_selected(&self, policy: &PrivateTreePolicy) -> io::Result<PrivateTreeReport> {
133        let _operation = self
134            .operation_lock
135            .lock()
136            .map_err(|_| io::Error::other("private-tree operation lock is poisoned"))?;
137        let selected = normalized_private_tree_selection(policy)?;
138        self.revalidate_root()?;
139        #[cfg(unix)]
140        let report = unix_harden_private_tree(self, &selected)?;
141        #[cfg(target_os = "windows")]
142        let report = windows_harden_private_tree(self, &selected)?;
143        #[cfg(not(any(unix, target_os = "windows")))]
144        let report = generic_harden_private_tree(self, &selected)?;
145        self.revalidate_root()?;
146        Ok(report)
147    }
148
149    /// Verify that the root path still names the retained descriptor.
150    pub fn revalidate_root(&self) -> io::Result<()> {
151        #[cfg(unix)]
152        {
153            unix_validate_private_dir_exact(&self.root_descriptor)?;
154            unix_revalidate_directory_path(&self.root, &self.root_descriptor)
155        }
156        #[cfg(target_os = "windows")]
157        {
158            windows_validate_directory(&self.root_descriptor)?;
159            windows_validate_file_owner(&self.root_descriptor)?;
160            windows_revalidate_directory_path(&self.root, &self.root_descriptor)
161        }
162        #[cfg(not(any(unix, target_os = "windows")))]
163        {
164            if self.root_descriptor.metadata()?.is_dir() {
165                Ok(())
166            } else {
167                Err(permission_denied("private tree root is not a directory"))
168            }
169        }
170    }
171}
172
173/// Open, harden, and revalidate an existing selected private tree.
174pub fn harden_private_tree(
175    root: &Path,
176    policy: &PrivateTreePolicy,
177) -> io::Result<PrivateTreeReport> {
178    PrivateTree::open(root)?.harden_selected(policy)
179}
180
181fn normalized_private_tree_selection(policy: &PrivateTreePolicy) -> io::Result<Vec<PathBuf>> {
182    let mut selected = policy.selected.clone();
183    for path in &selected {
184        if path.as_os_str().is_empty() || path.is_absolute() {
185            return Err(io::Error::new(
186                io::ErrorKind::InvalidInput,
187                "private-tree selections must be non-empty relative paths",
188            ));
189        }
190        for component in path.components() {
191            if !matches!(component, std::path::Component::Normal(_)) {
192                return Err(io::Error::new(
193                    io::ErrorKind::InvalidInput,
194                    "private-tree selections must contain only normal components",
195                ));
196            }
197        }
198    }
199    selected.sort();
200    selected.dedup();
201    Ok(selected)
202}
203
204#[derive(Clone, Copy, PartialEq, Eq)]
205enum PrivateOpenMode {
206    Read,
207    Append,
208    Truncate,
209    CreateNew,
210}
211
212/// Tighten `path` to owner-only access, preserving the historical best-effort
213/// API. New privacy-bearing writes should use the fallible helpers below.
214pub fn harden_owner_only(path: &Path) {
215    #[cfg(target_os = "windows")]
216    if let Err(error) = harden_windows_acl(path) {
217        tracing::warn!(?error, ?path, "owner-only ACL hardening failed");
218    }
219    #[cfg(not(target_os = "windows"))]
220    let _ = path;
221}
222
223/// Tighten an existing regular file or directory and propagate any failure.
224pub fn harden_owner_only_fallible(path: &Path) -> io::Result<()> {
225    #[cfg(unix)]
226    {
227        let metadata = std::fs::symlink_metadata(path)?;
228        if metadata.file_type().is_symlink() {
229            return Err(permission_denied("owner-private path cannot be a symlink"));
230        }
231        if metadata.is_dir() {
232            return ensure_private_dir(path);
233        }
234        let _file = open_private_read(path)?;
235        Ok(())
236    }
237    #[cfg(target_os = "windows")]
238    {
239        let metadata = std::fs::symlink_metadata(path)?;
240        reject_windows_reparse_metadata(&metadata)?;
241        if metadata.is_dir() {
242            return windows_ensure_private_dir(path);
243        }
244        let _file = windows_open_private(path, PrivateOpenMode::Read, None)?;
245        Ok(())
246    }
247    #[cfg(not(any(unix, target_os = "windows")))]
248    {
249        let _ = path;
250        Ok(())
251    }
252}
253
254/// Create missing directory components with mode `0700` and harden the final
255/// directory. Symlink components and final directories not owned by the
256/// effective user are rejected.
257pub fn ensure_private_dir(path: &Path) -> io::Result<()> {
258    ensure_private_dir_inner(path, None)
259}
260
261/// [`ensure_private_dir`] with a deterministic first-use durability fault seam.
262pub fn ensure_private_dir_with_failure_injector(
263    path: &Path,
264    failures: &PrivatePathDurabilityFailureInjector,
265) -> io::Result<()> {
266    ensure_private_dir_inner(path, Some(failures))
267}
268
269fn ensure_private_dir_inner(
270    path: &Path,
271    failures: Option<&PrivatePathDurabilityFailureInjector>,
272) -> io::Result<()> {
273    #[cfg(unix)]
274    {
275        let _ = unix_walk_directory_with_failure_injector(path, true, true, failures)?;
276        Ok(())
277    }
278    #[cfg(target_os = "windows")]
279    {
280        windows_ensure_private_dir_with_failure_injector(path, failures)
281    }
282    #[cfg(not(any(unix, target_os = "windows")))]
283    {
284        std::fs::create_dir_all(path)
285    }
286}
287
288/// Exclusively create a new owner-private regular file (`0600` on Unix).
289pub fn create_private_file(path: &Path) -> io::Result<File> {
290    open_private(path, PrivateOpenMode::CreateNew, None)
291}
292
293/// [`create_private_file`] with a deterministic entry-durability fault seam.
294pub fn create_private_file_with_failure_injector(
295    path: &Path,
296    failures: &PrivatePathDurabilityFailureInjector,
297) -> io::Result<File> {
298    open_private(path, PrivateOpenMode::CreateNew, Some(failures))
299}
300
301/// Open an existing CAR-owned file for reading, hardening and validating it.
302/// Historical external replay paths should deliberately use [`File::open`]
303/// instead so read-only compatibility does not mutate their modes.
304pub fn open_private_read(path: &Path) -> io::Result<File> {
305    open_private(path, PrivateOpenMode::Read, None)
306}
307
308/// Create or open an owner-private regular file for append.
309pub fn open_private_append(path: &Path) -> io::Result<File> {
310    open_private(path, PrivateOpenMode::Append, None)
311}
312
313/// [`open_private_append`] with a deterministic entry-durability fault seam.
314pub fn open_private_append_with_failure_injector(
315    path: &Path,
316    failures: &PrivatePathDurabilityFailureInjector,
317) -> io::Result<File> {
318    open_private(path, PrivateOpenMode::Append, Some(failures))
319}
320
321/// Open an existing owner-private regular file, validate its descriptor and
322/// path identity, and only then truncate its contents.
323pub fn open_private_truncate(path: &Path) -> io::Result<File> {
324    open_private(path, PrivateOpenMode::Truncate, None)
325}
326
327/// Revalidate an already-opened CAR-owned file descriptor, hardening it to
328/// `0600` on Unix. The descriptor must name a current-user, single-link regular
329/// file and must remain the same inode across hardening.
330pub fn revalidate_private_file(file: &File) -> io::Result<()> {
331    #[cfg(unix)]
332    {
333        unix_revalidate_private_file(file)
334    }
335    #[cfg(target_os = "windows")]
336    {
337        windows_validate_file(file)
338    }
339    #[cfg(not(any(unix, target_os = "windows")))]
340    {
341        if file.metadata()?.is_file() {
342            Ok(())
343        } else {
344            Err(permission_denied("private file is not a regular file"))
345        }
346    }
347}
348
349/// Revalidate both a private descriptor and its no-follow path identity.
350/// Fails when the path was unlinked, renamed, or substituted after open.
351pub fn revalidate_private_path(path: &Path, file: &File) -> io::Result<()> {
352    revalidate_private_file(file)?;
353    #[cfg(unix)]
354    {
355        use std::ffi::CString;
356        use std::os::fd::AsRawFd;
357        use std::os::unix::ffi::OsStrExt;
358
359        let name = path.file_name().ok_or_else(|| {
360            io::Error::new(
361                io::ErrorKind::InvalidInput,
362                "private file path has no file name",
363            )
364        })?;
365        let name = CString::new(name.as_bytes())
366            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "file name contains NUL"))?;
367        let parent = unix_walk_directory(normalized_parent(path), false, false)?;
368        unix_validate_path_matches_file(parent.as_raw_fd(), &name, file)
369    }
370    #[cfg(target_os = "windows")]
371    {
372        windows_revalidate_private_path(path, file)
373    }
374    #[cfg(not(any(unix, target_os = "windows")))]
375    {
376        let _ = path;
377        Err(io::Error::new(
378            io::ErrorKind::Unsupported,
379            "path identity revalidation is unsupported on this platform",
380        ))
381    }
382}
383
384/// Atomically replace `destination` with a private temporary file in the same
385/// directory, then revalidate the published file and sync its directory.
386pub fn atomic_replace_private_file(temp: &Path, destination: &Path) -> io::Result<()> {
387    if normalized_parent(temp) != normalized_parent(destination) {
388        return Err(io::Error::new(
389            io::ErrorKind::InvalidInput,
390            "private atomic replacement requires one lexical parent directory",
391        ));
392    }
393    #[cfg(unix)]
394    {
395        unix_atomic_replace_private_file(temp, destination)
396    }
397    #[cfg(target_os = "windows")]
398    {
399        windows_atomic_replace_private_file(temp, destination)
400    }
401    #[cfg(not(any(unix, target_os = "windows")))]
402    {
403        std::fs::rename(temp, destination)?;
404        let file = open_private_read(destination)?;
405        revalidate_private_file(&file)
406    }
407}
408
409fn open_private(
410    path: &Path,
411    mode: PrivateOpenMode,
412    failures: Option<&PrivatePathDurabilityFailureInjector>,
413) -> io::Result<File> {
414    #[cfg(unix)]
415    {
416        unix_open_private(path, mode, failures)
417    }
418    #[cfg(target_os = "windows")]
419    {
420        windows_open_private(path, mode, failures)
421    }
422    #[cfg(not(any(unix, target_os = "windows")))]
423    {
424        let _ = failures;
425        generic_open_private(path, mode)
426    }
427}
428
429fn normalized_parent(path: &Path) -> &Path {
430    path.parent()
431        .filter(|parent| !parent.as_os_str().is_empty())
432        .unwrap_or_else(|| Path::new("."))
433}
434
435fn permission_denied(message: &'static str) -> io::Error {
436    io::Error::new(io::ErrorKind::PermissionDenied, message)
437}
438
439#[cfg(unix)]
440fn unix_walk_directory(path: &Path, create: bool, harden_final: bool) -> io::Result<File> {
441    unix_walk_directory_with_failure_injector(path, create, harden_final, None)
442}
443
444#[cfg(unix)]
445fn unix_walk_directory_with_failure_injector(
446    path: &Path,
447    create: bool,
448    harden_final: bool,
449    failures: Option<&PrivatePathDurabilityFailureInjector>,
450) -> io::Result<File> {
451    unix_walk_directory_with_hook(path, create, harden_final, failures, |_| Ok(()))
452}
453
454#[cfg(unix)]
455fn unix_walk_directory_with_hook<F>(
456    path: &Path,
457    create: bool,
458    harden_final: bool,
459    failures: Option<&PrivatePathDurabilityFailureInjector>,
460    mut after_child_barrier: F,
461) -> io::Result<File>
462where
463    F: FnMut(&std::ffi::OsStr) -> io::Result<()>,
464{
465    use std::ffi::{CString, OsStr};
466    use std::os::fd::{AsRawFd, FromRawFd};
467    use std::os::unix::ffi::OsStrExt;
468
469    let descriptor_path = unix_descriptor_path(path);
470    let mut components = Vec::new();
471    let mut absolute = false;
472    for component in descriptor_path.components() {
473        match component {
474            std::path::Component::RootDir => absolute = true,
475            std::path::Component::CurDir => {}
476            std::path::Component::Normal(value) => components.push(value.to_os_string()),
477            std::path::Component::ParentDir => {
478                return Err(io::Error::new(
479                    io::ErrorKind::InvalidInput,
480                    "owner-private paths cannot contain parent traversal",
481                ));
482            }
483            std::path::Component::Prefix(_) => {
484                return Err(io::Error::new(
485                    io::ErrorKind::InvalidInput,
486                    "unsupported Unix path prefix",
487                ));
488            }
489        }
490    }
491
492    let base = CString::new(if absolute { "/" } else { "." }).expect("static path");
493    // SAFETY: `base` is NUL-terminated and flags do not require a mode.
494    let raw = unsafe {
495        libc::open(
496            base.as_ptr(),
497            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
498        )
499    };
500    if raw < 0 {
501        return Err(io::Error::last_os_error());
502    }
503    // SAFETY: `raw` is a newly-owned descriptor returned by `open`.
504    let directory = unsafe { File::from_raw_fd(raw) };
505    // Retain every ancestor descriptor until the final component has been
506    // opened, flushed, and revalidated. Retention does not prevent rename on
507    // Unix; it lets each later full-chain check detect that rename before the
508    // walk advances or returns success.
509    let mut hierarchy = vec![directory];
510
511    for (index, component) in components.iter().enumerate() {
512        let name = CString::new(OsStr::new(component).as_bytes()).map_err(|_| {
513            io::Error::new(io::ErrorKind::InvalidInput, "path component contains NUL")
514        })?;
515        let flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
516        // SAFETY: the parent descriptor and component C string are valid.
517        let parent = hierarchy
518            .last()
519            .expect("private directory hierarchy always retains its base");
520        let mut child_raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags) };
521        let mut created = false;
522        if child_raw < 0 && io::Error::last_os_error().kind() == io::ErrorKind::NotFound && create {
523            // SAFETY: the parent descriptor and component C string are valid.
524            let mkdir_result = unsafe { libc::mkdirat(parent.as_raw_fd(), name.as_ptr(), 0o700) };
525            if mkdir_result < 0 {
526                let error = io::Error::last_os_error();
527                if error.kind() != io::ErrorKind::AlreadyExists {
528                    return Err(error);
529                }
530            } else {
531                created = true;
532            }
533            // SAFETY: same validated descriptor-relative path as above.
534            child_raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags) };
535        }
536        if child_raw < 0 {
537            return Err(io::Error::last_os_error());
538        }
539        // SAFETY: `child_raw` is a newly-owned descriptor returned by `openat`.
540        let child = unsafe { File::from_raw_fd(child_raw) };
541        let is_final = index + 1 == components.len();
542        unix_validate_private_dir(&child, created || (is_final && harden_final))?;
543        let already_private = unix_private_dir_is_exact(&child)?;
544        if created {
545            if let Some(failures) = failures {
546                failures.check(PrivatePathDurabilityFailurePoint::ParentDirectorySync)?;
547            }
548        }
549        // A retry after a transient parent-sync failure observes an existing
550        // owner-private component. Syncing those parents again makes retry a
551        // real durability barrier instead of a false acknowledgement.
552        if created || (create && already_private) {
553            parent.sync_all()?;
554            unix_validate_path_matches_directory(parent.as_raw_fd(), &name, &child)?;
555        }
556        hierarchy.push(child);
557        after_child_barrier(component.as_os_str())?;
558        unix_revalidate_retained_directory_chain(&hierarchy, &components[..=index])?;
559    }
560
561    if components.is_empty() && harden_final {
562        return Err(io::Error::new(
563            io::ErrorKind::InvalidInput,
564            "refusing to harden a filesystem root or current directory",
565        ));
566    }
567    // Repeat the complete root-to-leaf proof immediately before handing the
568    // final descriptor to the caller. An open Unix directory descriptor does
569    // not prevent rename, so validating only the last parent/child pair can
570    // otherwise acknowledge a tree that has moved out from under its path.
571    unix_revalidate_retained_directory_chain(&hierarchy, &components)?;
572    hierarchy
573        .pop()
574        .ok_or_else(|| io::Error::other("private directory hierarchy is empty"))
575}
576
577#[cfg(unix)]
578fn unix_revalidate_retained_directory_chain(
579    hierarchy: &[File],
580    components: &[std::ffi::OsString],
581) -> io::Result<()> {
582    use std::ffi::{CString, OsStr};
583    use std::os::fd::AsRawFd;
584    use std::os::unix::ffi::OsStrExt;
585
586    if hierarchy.len() != components.len() + 1 {
587        return Err(io::Error::other(
588            "private directory hierarchy does not match its component chain",
589        ));
590    }
591    for (index, component) in components.iter().enumerate() {
592        let name = CString::new(OsStr::new(component).as_bytes()).map_err(|_| {
593            io::Error::new(io::ErrorKind::InvalidInput, "path component contains NUL")
594        })?;
595        unix_validate_path_matches_directory(
596            hierarchy[index].as_raw_fd(),
597            &name,
598            &hierarchy[index + 1],
599        )?;
600    }
601    Ok(())
602}
603
604#[cfg(unix)]
605fn unix_private_dir_is_exact(directory: &File) -> io::Result<bool> {
606    use std::os::unix::fs::{MetadataExt, PermissionsExt};
607
608    let metadata = directory.metadata()?;
609    Ok(metadata.is_dir()
610        && metadata.uid() == unsafe { libc::geteuid() }
611        && metadata.permissions().mode() & 0o777 == 0o700)
612}
613
614#[cfg(unix)]
615fn unix_descriptor_path(path: &Path) -> std::path::PathBuf {
616    #[cfg(target_os = "macos")]
617    {
618        // macOS exposes three root-owned compatibility symlinks for mutable
619        // system trees. Resolve only these fixed aliases lexically; arbitrary
620        // user-controlled symlinks remain rejected by the descriptor walk.
621        for (alias, real) in [
622            (Path::new("/var"), Path::new("/private/var")),
623            (Path::new("/tmp"), Path::new("/private/tmp")),
624            (Path::new("/etc"), Path::new("/private/etc")),
625        ] {
626            if let Ok(suffix) = path.strip_prefix(alias) {
627                return real.join(suffix);
628            }
629        }
630    }
631    path.to_path_buf()
632}
633
634#[cfg(unix)]
635fn unix_validate_private_dir(directory: &File, harden: bool) -> io::Result<()> {
636    use std::os::unix::fs::{MetadataExt, PermissionsExt};
637
638    let before = directory.metadata()?;
639    if !before.is_dir() {
640        return Err(permission_denied(
641            "private path component is not a directory",
642        ));
643    }
644    if harden {
645        if before.uid() != unsafe { libc::geteuid() } {
646            return Err(permission_denied(
647                "private directory is not owned by the effective user",
648            ));
649        }
650        directory.set_permissions(std::fs::Permissions::from_mode(0o700))?;
651        let after = directory.metadata()?;
652        if after.dev() != before.dev()
653            || after.ino() != before.ino()
654            || after.uid() != before.uid()
655            || !after.is_dir()
656            || after.permissions().mode() & 0o777 != 0o700
657        {
658            return Err(permission_denied(
659                "private directory changed while it was hardened",
660            ));
661        }
662    }
663    Ok(())
664}
665
666#[cfg(unix)]
667fn unix_validate_private_dir_exact(directory: &File) -> io::Result<()> {
668    use std::os::unix::fs::{MetadataExt, PermissionsExt};
669
670    let metadata = directory.metadata()?;
671    if !metadata.is_dir()
672        || metadata.uid() != unsafe { libc::geteuid() }
673        || metadata.permissions().mode() & 0o777 != 0o700
674    {
675        return Err(permission_denied(
676            "private directory must be current-user owned with mode 0700",
677        ));
678    }
679    Ok(())
680}
681
682#[cfg(unix)]
683fn unix_revalidate_directory_path(path: &Path, directory: &File) -> io::Result<()> {
684    use std::ffi::CString;
685    use std::os::fd::AsRawFd;
686    use std::os::unix::ffi::OsStrExt;
687
688    unix_validate_private_dir_exact(directory)?;
689    let name = path.file_name().ok_or_else(|| {
690        io::Error::new(
691            io::ErrorKind::InvalidInput,
692            "private directory path has no file name",
693        )
694    })?;
695    let name = CString::new(name.as_bytes())
696        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "directory name contains NUL"))?;
697    let parent = unix_walk_directory(normalized_parent(path), false, false)?;
698    unix_validate_path_matches_directory(parent.as_raw_fd(), &name, directory)
699}
700
701#[cfg(unix)]
702fn unix_validate_path_matches_directory(
703    parent_fd: std::os::fd::RawFd,
704    name: &std::ffi::CStr,
705    directory: &File,
706) -> io::Result<()> {
707    use std::mem::MaybeUninit;
708    use std::os::unix::fs::MetadataExt;
709
710    let mut stat = MaybeUninit::<libc::stat>::uninit();
711    // SAFETY: `stat` is writable and the parent/name pair is valid.
712    let result = unsafe {
713        libc::fstatat(
714            parent_fd,
715            name.as_ptr(),
716            stat.as_mut_ptr(),
717            libc::AT_SYMLINK_NOFOLLOW,
718        )
719    };
720    if result < 0 {
721        return Err(io::Error::last_os_error());
722    }
723    // SAFETY: successful `fstatat` initialized `stat`.
724    let stat = unsafe { stat.assume_init() };
725    let metadata = directory.metadata()?;
726    if i128::from(stat.st_dev) != i128::from(metadata.dev())
727        || stat.st_ino != metadata.ino()
728        || (u64::from(stat.st_mode) & libc::S_IFMT as u64) != libc::S_IFDIR as u64
729    {
730        return Err(permission_denied(
731            "private directory path changed during descriptor validation",
732        ));
733    }
734    Ok(())
735}
736
737#[cfg(unix)]
738fn unix_harden_private_tree(
739    tree: &PrivateTree,
740    selected: &[PathBuf],
741) -> io::Result<PrivateTreeReport> {
742    use std::collections::BTreeSet;
743
744    let mut report = PrivateTreeReport::default();
745    if selected.is_empty() {
746        unix_validate_private_dir(&tree.root_descriptor, true)?;
747        unix_validate_private_dir_exact(&tree.root_descriptor)?;
748        report.directories_hardened = 1;
749        return Ok(report);
750    }
751
752    let mut visited = BTreeSet::new();
753    visited.insert(unix_file_identity(&tree.root_descriptor)?);
754    for relative in selected {
755        let components = relative
756            .components()
757            .map(|component| component.as_os_str().to_os_string())
758            .collect::<Vec<_>>();
759        unix_harden_selected_path(
760            &tree.root_descriptor,
761            &components,
762            &mut visited,
763            &mut report,
764        )?;
765    }
766    Ok(report)
767}
768
769#[cfg(unix)]
770fn unix_harden_selected_path(
771    parent: &File,
772    components: &[std::ffi::OsString],
773    visited: &mut std::collections::BTreeSet<(u64, u64)>,
774    report: &mut PrivateTreeReport,
775) -> io::Result<()> {
776    use std::ffi::{CString, OsStr};
777    use std::os::fd::AsRawFd;
778    use std::os::unix::ffi::OsStrExt;
779
780    let (component, remaining) = components.split_first().ok_or_else(|| {
781        io::Error::new(
782            io::ErrorKind::InvalidInput,
783            "private-tree selection has no components",
784        )
785    })?;
786    let name = CString::new(OsStr::new(component).as_bytes())
787        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "tree component contains NUL"))?;
788
789    if !remaining.is_empty() {
790        let directory = unix_open_directory_at(parent, &name)?;
791        let identity = unix_file_identity(&directory)?;
792        if visited.insert(identity) {
793            report.directories_hardened += 1;
794        }
795        unix_harden_selected_path(&directory, remaining, visited, report)?;
796        unix_validate_path_matches_directory(parent.as_raw_fd(), &name, &directory)?;
797        return Ok(());
798    }
799
800    match unix_entry_kind(parent.as_raw_fd(), &name)? {
801        UnixEntryKind::Directory => {
802            let directory = unix_open_directory_at(parent, &name)?;
803            let identity = unix_file_identity(&directory)?;
804            if visited.insert(identity) {
805                report.directories_hardened += 1;
806                unix_harden_directory_contents(&directory, visited, report)?;
807            }
808            unix_validate_path_matches_directory(parent.as_raw_fd(), &name, &directory)
809        }
810        UnixEntryKind::RegularFile => {
811            let file = unix_open_file_at(parent, &name, PrivateOpenMode::Read)?;
812            unix_validate_path_matches_file(parent.as_raw_fd(), &name, &file)?;
813            if visited.insert(unix_file_identity(&file)?) {
814                report.files_hardened += 1;
815            }
816            unix_validate_path_matches_file(parent.as_raw_fd(), &name, &file)
817        }
818        UnixEntryKind::Rejected => Err(permission_denied(
819            "private tree contains a symlink or special file",
820        )),
821    }
822}
823
824#[cfg(unix)]
825fn unix_harden_directory_contents(
826    directory: &File,
827    visited: &mut std::collections::BTreeSet<(u64, u64)>,
828    report: &mut PrivateTreeReport,
829) -> io::Result<()> {
830    use std::ffi::{CStr, OsStr};
831    use std::os::fd::AsRawFd;
832    use std::os::unix::ffi::{OsStrExt, OsStringExt};
833
834    // `fdopendir` assumes ownership. Open `.` relative to the retained
835    // descriptor so each pass gets an independent directory offset.
836    let dot = c".";
837    let enumeration_fd = unsafe {
838        libc::openat(
839            directory.as_raw_fd(),
840            dot.as_ptr(),
841            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
842        )
843    };
844    if enumeration_fd < 0 {
845        return Err(io::Error::last_os_error());
846    }
847    let stream = unsafe { libc::fdopendir(enumeration_fd) };
848    if stream.is_null() {
849        let error = io::Error::last_os_error();
850        unsafe {
851            libc::close(enumeration_fd);
852        }
853        return Err(error);
854    }
855
856    let mut names = Vec::new();
857    let enumeration_result = loop {
858        unix_set_errno(0);
859        let entry = unsafe { libc::readdir(stream) };
860        if entry.is_null() {
861            let errno = unix_errno();
862            break if errno == 0 {
863                Ok(())
864            } else {
865                Err(io::Error::from_raw_os_error(errno))
866            };
867        }
868        let bytes = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
869        if bytes != b"." && bytes != b".." {
870            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
871        }
872    };
873    let close_result = unsafe { libc::closedir(stream) };
874    enumeration_result?;
875    if close_result < 0 {
876        return Err(io::Error::last_os_error());
877    }
878    names.sort();
879
880    for child in names {
881        let name = std::ffi::CString::new(OsStr::new(&child).as_bytes())
882            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "tree entry contains NUL"))?;
883        match unix_entry_kind(directory.as_raw_fd(), &name)? {
884            UnixEntryKind::Directory => {
885                let child_directory = unix_open_directory_at(directory, &name)?;
886                let identity = unix_file_identity(&child_directory)?;
887                if visited.insert(identity) {
888                    report.directories_hardened += 1;
889                    unix_harden_directory_contents(&child_directory, visited, report)?;
890                }
891                unix_validate_path_matches_directory(
892                    directory.as_raw_fd(),
893                    &name,
894                    &child_directory,
895                )?;
896            }
897            UnixEntryKind::RegularFile => {
898                let file = unix_open_file_at(directory, &name, PrivateOpenMode::Read)?;
899                unix_validate_path_matches_file(directory.as_raw_fd(), &name, &file)?;
900                if visited.insert(unix_file_identity(&file)?) {
901                    report.files_hardened += 1;
902                }
903                unix_validate_path_matches_file(directory.as_raw_fd(), &name, &file)?;
904            }
905            UnixEntryKind::Rejected => {
906                return Err(permission_denied(
907                    "private tree contains a symlink or special file",
908                ));
909            }
910        }
911    }
912    Ok(())
913}
914
915#[cfg(unix)]
916fn unix_open_directory_at(parent: &File, name: &std::ffi::CStr) -> io::Result<File> {
917    use std::os::fd::{AsRawFd, FromRawFd};
918
919    let raw = unsafe {
920        libc::openat(
921            parent.as_raw_fd(),
922            name.as_ptr(),
923            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
924        )
925    };
926    if raw < 0 {
927        return Err(io::Error::last_os_error());
928    }
929    let directory = unsafe { File::from_raw_fd(raw) };
930    unix_validate_private_dir(&directory, true)?;
931    unix_validate_private_dir_exact(&directory)?;
932    unix_validate_path_matches_directory(parent.as_raw_fd(), name, &directory)?;
933    Ok(directory)
934}
935
936#[cfg(unix)]
937#[derive(Clone, Copy, Debug, PartialEq, Eq)]
938enum UnixEntryKind {
939    Directory,
940    RegularFile,
941    Rejected,
942}
943
944#[cfg(unix)]
945fn unix_entry_kind(
946    parent_fd: std::os::fd::RawFd,
947    name: &std::ffi::CStr,
948) -> io::Result<UnixEntryKind> {
949    use std::mem::MaybeUninit;
950
951    let mut stat = MaybeUninit::<libc::stat>::uninit();
952    let result = unsafe {
953        libc::fstatat(
954            parent_fd,
955            name.as_ptr(),
956            stat.as_mut_ptr(),
957            libc::AT_SYMLINK_NOFOLLOW,
958        )
959    };
960    if result < 0 {
961        return Err(io::Error::last_os_error());
962    }
963    let mode = u64::from(unsafe { stat.assume_init() }.st_mode) & libc::S_IFMT as u64;
964    Ok(if mode == libc::S_IFDIR as u64 {
965        UnixEntryKind::Directory
966    } else if mode == libc::S_IFREG as u64 {
967        UnixEntryKind::RegularFile
968    } else {
969        UnixEntryKind::Rejected
970    })
971}
972
973#[cfg(all(unix, target_os = "linux"))]
974fn unix_errno() -> i32 {
975    unsafe { *libc::__errno_location() }
976}
977
978#[cfg(all(unix, target_os = "linux"))]
979fn unix_set_errno(value: i32) {
980    unsafe {
981        *libc::__errno_location() = value;
982    }
983}
984
985#[cfg(target_os = "android")]
986fn unix_errno() -> i32 {
987    unsafe { *libc::__errno() }
988}
989
990#[cfg(target_os = "android")]
991fn unix_set_errno(value: i32) {
992    unsafe {
993        *libc::__errno() = value;
994    }
995}
996
997#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
998fn unix_errno() -> i32 {
999    unsafe { *libc::__error() }
1000}
1001
1002#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
1003fn unix_set_errno(value: i32) {
1004    unsafe {
1005        *libc::__error() = value;
1006    }
1007}
1008
1009#[cfg(unix)]
1010fn unix_open_private(
1011    path: &Path,
1012    mode: PrivateOpenMode,
1013    failures: Option<&PrivatePathDurabilityFailureInjector>,
1014) -> io::Result<File> {
1015    use std::ffi::CString;
1016    use std::os::fd::AsRawFd;
1017    use std::os::unix::ffi::OsStrExt;
1018
1019    let name = path.file_name().ok_or_else(|| {
1020        io::Error::new(
1021            io::ErrorKind::InvalidInput,
1022            "private file path has no file name",
1023        )
1024    })?;
1025    let name = CString::new(name.as_bytes())
1026        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "file name contains NUL"))?;
1027    let parent_path = normalized_parent(path);
1028    let harden_parent = parent_path != Path::new(".");
1029    let create_parent = matches!(mode, PrivateOpenMode::Append | PrivateOpenMode::CreateNew);
1030    let parent = unix_walk_directory_with_failure_injector(
1031        parent_path,
1032        create_parent,
1033        harden_parent,
1034        failures,
1035    )?;
1036    if harden_parent {
1037        unix_revalidate_directory_path(parent_path, &parent)?;
1038    }
1039    let (file, created) = unix_open_file_at_with_created(&parent, &name, mode)?;
1040    unix_validate_path_matches_file(parent.as_raw_fd(), &name, &file)?;
1041    if harden_parent {
1042        unix_revalidate_directory_path(parent_path, &parent)?;
1043    }
1044    if mode == PrivateOpenMode::Truncate {
1045        file.set_len(0)?;
1046        unix_revalidate_private_file(&file)?;
1047        unix_validate_path_matches_file(parent.as_raw_fd(), &name, &file)?;
1048        if harden_parent {
1049            unix_revalidate_directory_path(parent_path, &parent)?;
1050        }
1051    }
1052    if matches!(mode, PrivateOpenMode::Append | PrivateOpenMode::CreateNew) {
1053        if created {
1054            if let Some(failures) = failures {
1055                failures.check(PrivatePathDurabilityFailurePoint::ParentDirectorySync)?;
1056            }
1057        }
1058        // Sync on every append/create open, not only the creating call. This
1059        // makes retry after a transient creation-sync failure establish the
1060        // missing name durability before it can return success.
1061        parent.sync_all()?;
1062        unix_validate_path_matches_file(parent.as_raw_fd(), &name, &file)?;
1063        if harden_parent {
1064            unix_revalidate_directory_path(parent_path, &parent)?;
1065        }
1066    }
1067    Ok(file)
1068}
1069
1070#[cfg(unix)]
1071fn unix_open_file_at(
1072    parent: &File,
1073    name: &std::ffi::CStr,
1074    mode: PrivateOpenMode,
1075) -> io::Result<File> {
1076    unix_open_file_at_with_created(parent, name, mode).map(|(file, _created)| file)
1077}
1078
1079#[cfg(unix)]
1080fn unix_open_file_at_with_created(
1081    parent: &File,
1082    name: &std::ffi::CStr,
1083    mode: PrivateOpenMode,
1084) -> io::Result<(File, bool)> {
1085    use std::os::fd::{AsRawFd, FromRawFd};
1086
1087    let base_flags = libc::O_CLOEXEC | libc::O_NOFOLLOW;
1088    let (mut flags, mut created) = match mode {
1089        PrivateOpenMode::Read => (base_flags | libc::O_RDONLY, false),
1090        PrivateOpenMode::Append => (base_flags | libc::O_RDWR | libc::O_APPEND, false),
1091        PrivateOpenMode::Truncate => (base_flags | libc::O_RDWR, false),
1092        PrivateOpenMode::CreateNew => (
1093            base_flags | libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL,
1094            true,
1095        ),
1096    };
1097    // SAFETY: the parent descriptor and component C string are valid; a mode is
1098    // supplied because some variants include O_CREAT.
1099    let mut raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags, 0o600) };
1100    if raw < 0
1101        && mode == PrivateOpenMode::Append
1102        && io::Error::last_os_error().kind() == io::ErrorKind::NotFound
1103    {
1104        flags |= libc::O_CREAT | libc::O_EXCL;
1105        raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags, 0o600) };
1106        if raw < 0 && io::Error::last_os_error().kind() == io::ErrorKind::AlreadyExists {
1107            flags &= !(libc::O_CREAT | libc::O_EXCL);
1108            raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags, 0o600) };
1109        } else if raw >= 0 {
1110            created = true;
1111        }
1112    }
1113    if raw < 0 {
1114        return Err(io::Error::last_os_error());
1115    }
1116    // SAFETY: `raw` is a newly-owned descriptor returned by `openat`.
1117    let file = unsafe { File::from_raw_fd(raw) };
1118    unix_revalidate_private_file(&file)?;
1119    Ok((file, created))
1120}
1121
1122#[cfg(unix)]
1123fn unix_revalidate_private_file(file: &File) -> io::Result<()> {
1124    use std::os::unix::fs::{MetadataExt, PermissionsExt};
1125
1126    let before = file.metadata()?;
1127    let euid = unsafe { libc::geteuid() };
1128    if !before.is_file() || before.uid() != euid || before.nlink() != 1 {
1129        return Err(permission_denied(
1130            "private file must be a current-user, single-link regular file",
1131        ));
1132    }
1133    file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
1134    let after = file.metadata()?;
1135    if !after.is_file()
1136        || after.uid() != euid
1137        || after.nlink() != 1
1138        || after.dev() != before.dev()
1139        || after.ino() != before.ino()
1140        || after.permissions().mode() & 0o777 != 0o600
1141    {
1142        return Err(permission_denied(
1143            "private file changed while it was hardened",
1144        ));
1145    }
1146    Ok(())
1147}
1148
1149#[cfg(unix)]
1150fn unix_validate_path_matches_file(
1151    parent_fd: std::os::fd::RawFd,
1152    name: &std::ffi::CStr,
1153    file: &File,
1154) -> io::Result<()> {
1155    use std::mem::MaybeUninit;
1156    use std::os::unix::fs::MetadataExt;
1157
1158    let mut stat = MaybeUninit::<libc::stat>::uninit();
1159    // SAFETY: `stat` is writable and the parent/name pair is valid.
1160    let result = unsafe {
1161        libc::fstatat(
1162            parent_fd,
1163            name.as_ptr(),
1164            stat.as_mut_ptr(),
1165            libc::AT_SYMLINK_NOFOLLOW,
1166        )
1167    };
1168    if result < 0 {
1169        return Err(io::Error::last_os_error());
1170    }
1171    // SAFETY: successful `fstatat` initialized `stat`.
1172    let stat = unsafe { stat.assume_init() };
1173    let metadata = file.metadata()?;
1174    if i128::from(stat.st_dev) != i128::from(metadata.dev())
1175        || stat.st_ino != metadata.ino()
1176        || (u64::from(stat.st_mode) & libc::S_IFMT as u64) != libc::S_IFREG as u64
1177    {
1178        return Err(permission_denied(
1179            "private file path changed during descriptor validation",
1180        ));
1181    }
1182    Ok(())
1183}
1184
1185#[cfg(unix)]
1186fn unix_atomic_replace_private_file(temp: &Path, destination: &Path) -> io::Result<()> {
1187    unix_atomic_replace_private_file_with_hook(temp, destination, || Ok(()))
1188}
1189
1190#[cfg(unix)]
1191fn unix_atomic_replace_private_file_with_hook<F>(
1192    temp: &Path,
1193    destination: &Path,
1194    before_rename: F,
1195) -> io::Result<()>
1196where
1197    F: FnOnce() -> io::Result<()>,
1198{
1199    use std::ffi::CString;
1200    use std::os::fd::AsRawFd;
1201    use std::os::unix::ffi::OsStrExt;
1202
1203    let parent_path = normalized_parent(destination);
1204    let parent = unix_walk_directory(parent_path, false, parent_path != Path::new("."))?;
1205    let revalidate_parent = parent_path != Path::new(".");
1206    if revalidate_parent {
1207        unix_revalidate_directory_path(parent_path, &parent)?;
1208    }
1209    let temp_name = CString::new(
1210        temp.file_name()
1211            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "temp has no name"))?
1212            .as_bytes(),
1213    )
1214    .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "temp name contains NUL"))?;
1215    let destination_name = CString::new(
1216        destination
1217            .file_name()
1218            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "destination has no name"))?
1219            .as_bytes(),
1220    )
1221    .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "destination name contains NUL"))?;
1222
1223    let temp_file = unix_open_file_at(&parent, &temp_name, PrivateOpenMode::Read)?;
1224    unix_validate_path_matches_file(parent.as_raw_fd(), &temp_name, &temp_file)?;
1225    let temp_identity = unix_file_identity(&temp_file)?;
1226    temp_file.sync_all()?;
1227    before_rename()?;
1228    unix_revalidate_private_file(&temp_file)?;
1229    unix_validate_path_matches_file(parent.as_raw_fd(), &temp_name, &temp_file)?;
1230    if revalidate_parent {
1231        unix_revalidate_directory_path(parent_path, &parent)?;
1232    }
1233
1234    // SAFETY: both names are descriptor-relative C strings in the same opened
1235    // private directory; `renameat` atomically replaces the destination.
1236    let result = unsafe {
1237        libc::renameat(
1238            parent.as_raw_fd(),
1239            temp_name.as_ptr(),
1240            parent.as_raw_fd(),
1241            destination_name.as_ptr(),
1242        )
1243    };
1244    if result < 0 {
1245        return Err(io::Error::last_os_error());
1246    }
1247    let published = unix_open_file_at(&parent, &destination_name, PrivateOpenMode::Read)?;
1248    unix_validate_path_matches_file(parent.as_raw_fd(), &destination_name, &published)?;
1249    if unix_file_identity(&published)? != temp_identity {
1250        return Err(permission_denied(
1251            "published private file does not match the validated temp descriptor",
1252        ));
1253    }
1254    unix_revalidate_private_file(&temp_file)?;
1255    parent.sync_all()?;
1256    unix_revalidate_private_file(&published)?;
1257    unix_validate_path_matches_file(parent.as_raw_fd(), &destination_name, &published)?;
1258    if revalidate_parent {
1259        unix_revalidate_directory_path(parent_path, &parent)?;
1260    }
1261    Ok(())
1262}
1263
1264#[cfg(unix)]
1265fn unix_file_identity(file: &File) -> io::Result<(u64, u64)> {
1266    use std::os::unix::fs::MetadataExt;
1267
1268    let metadata = file.metadata()?;
1269    Ok((metadata.dev(), metadata.ino()))
1270}
1271
1272#[cfg(target_os = "windows")]
1273const FILE_ATTRIBUTE_REPARSE_POINT_VALUE: u32 = 0x0000_0400;
1274
1275#[cfg(target_os = "windows")]
1276struct WindowsPrivateSecurityDescriptor {
1277    descriptor: windows::Win32::Security::PSECURITY_DESCRIPTOR,
1278}
1279
1280#[cfg(target_os = "windows")]
1281impl WindowsPrivateSecurityDescriptor {
1282    fn new() -> io::Result<Self> {
1283        use std::os::windows::ffi::OsStrExt;
1284        use windows::core::PCWSTR;
1285        use windows::Win32::Security::Authorization::{
1286            ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
1287        };
1288        use windows::Win32::Security::PSECURITY_DESCRIPTOR;
1289
1290        let owner = current_process_default_owner_sid_string()?;
1291        let user = current_process_sid_string()?;
1292        // Windows defines TokenOwner as the ownership identity for newly
1293        // created securable objects. Elevated tokens commonly use the
1294        // Administrators SID there while TokenUser remains the exact account.
1295        // Ownership therefore follows TokenOwner; the protected one-ACE DACL
1296        // grants access only to TokenUser.
1297        let sddl = format!("O:{owner}D:P(A;;FA;;;{user})");
1298        let mut wide = std::ffi::OsStr::new(&sddl)
1299            .encode_wide()
1300            .collect::<Vec<_>>();
1301        wide.push(0);
1302        let mut descriptor = PSECURITY_DESCRIPTOR::default();
1303        unsafe {
1304            ConvertStringSecurityDescriptorToSecurityDescriptorW(
1305                PCWSTR(wide.as_ptr()),
1306                SDDL_REVISION_1,
1307                &mut descriptor,
1308                None,
1309            )
1310        }
1311        .map_err(windows_io_error)?;
1312        if descriptor.0.is_null() {
1313            return Err(io::Error::other(
1314                "Windows returned a null private security descriptor",
1315            ));
1316        }
1317        Ok(Self { descriptor })
1318    }
1319
1320    fn security_attributes(&self) -> windows::Win32::Security::SECURITY_ATTRIBUTES {
1321        use windows::Win32::Foundation::BOOL;
1322        use windows::Win32::Security::SECURITY_ATTRIBUTES;
1323
1324        SECURITY_ATTRIBUTES {
1325            nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
1326            lpSecurityDescriptor: self.descriptor.0,
1327            bInheritHandle: BOOL(0),
1328        }
1329    }
1330
1331    fn dacl(&self) -> io::Result<*const windows::Win32::Security::ACL> {
1332        use windows::Win32::Foundation::BOOL;
1333        use windows::Win32::Security::{GetSecurityDescriptorDacl, ACL};
1334
1335        let mut present = BOOL(0);
1336        let mut defaulted = BOOL(0);
1337        let mut dacl = std::ptr::null_mut::<ACL>();
1338        unsafe {
1339            GetSecurityDescriptorDacl(self.descriptor, &mut present, &mut dacl, &mut defaulted)
1340        }
1341        .map_err(windows_io_error)?;
1342        if !present.as_bool() || dacl.is_null() {
1343            return Err(io::Error::other(
1344                "private Windows security descriptor has no DACL",
1345            ));
1346        }
1347        Ok(dacl)
1348    }
1349}
1350
1351#[cfg(target_os = "windows")]
1352impl Drop for WindowsPrivateSecurityDescriptor {
1353    fn drop(&mut self) {
1354        use windows::Win32::Foundation::{LocalFree, HLOCAL};
1355
1356        unsafe {
1357            let _ = LocalFree(HLOCAL(self.descriptor.0));
1358        }
1359    }
1360}
1361
1362#[cfg(target_os = "windows")]
1363fn windows_io_error(error: windows::core::Error) -> io::Error {
1364    use windows::Win32::Foundation::WIN32_ERROR;
1365
1366    WIN32_ERROR::from_error(&error)
1367        .map(|code| io::Error::from_raw_os_error(code.0 as i32))
1368        .unwrap_or_else(|| io::Error::other(error.to_string()))
1369}
1370
1371#[cfg(target_os = "windows")]
1372fn windows_error_stage<T>(stage: &'static str, result: io::Result<T>) -> io::Result<T> {
1373    result.map_err(|error| io::Error::new(error.kind(), format!("{stage}: {error}")))
1374}
1375
1376#[cfg(target_os = "windows")]
1377fn windows_path_wide(path: &Path) -> io::Result<Vec<u16>> {
1378    use std::os::windows::ffi::OsStrExt;
1379
1380    let mut wide = path.as_os_str().encode_wide().collect::<Vec<_>>();
1381    if wide.contains(&0) {
1382        return Err(io::Error::new(
1383            io::ErrorKind::InvalidInput,
1384            "Windows private path contains NUL",
1385        ));
1386    }
1387    wide.push(0);
1388    Ok(wide)
1389}
1390
1391#[cfg(target_os = "windows")]
1392fn windows_harden_file_acl(file: &File) -> io::Result<()> {
1393    use std::os::windows::io::AsRawHandle;
1394    use windows::Win32::Foundation::{CloseHandle, ERROR_SUCCESS, HANDLE, PSID};
1395    use windows::Win32::Security::Authorization::{SetSecurityInfo, SE_FILE_OBJECT};
1396    use windows::Win32::Security::{
1397        GetTokenInformation, TokenOwner, DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
1398        PROTECTED_DACL_SECURITY_INFORMATION, TOKEN_OWNER, TOKEN_QUERY,
1399    };
1400    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
1401
1402    windows_validate_hardenable_file_owner(file)?;
1403    let descriptor = WindowsPrivateSecurityDescriptor::new()?;
1404
1405    // Feed SetSecurityInfo the same TokenOwner identity that post-hardening
1406    // validation compares. Keep the token buffer alive through the call so
1407    // its SID pointer remains valid. The protected DACL below remains bound
1408    // to the exact TokenUser account, not the broader elevated owner group.
1409    let windows_error = |error: windows::core::Error| io::Error::other(error.to_string());
1410    let mut token = HANDLE::default();
1411    unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }
1412        .map_err(windows_error)?;
1413    let result = (|| {
1414        let mut needed = 0u32;
1415        let _ = unsafe { GetTokenInformation(token, TokenOwner, None, 0, &mut needed) };
1416        if needed == 0 {
1417            return Err(io::Error::last_os_error());
1418        }
1419        let words = (needed as usize).div_ceil(std::mem::size_of::<usize>());
1420        let mut buffer = vec![0usize; words];
1421        unsafe {
1422            GetTokenInformation(
1423                token,
1424                TokenOwner,
1425                Some(buffer.as_mut_ptr().cast()),
1426                needed,
1427                &mut needed,
1428            )
1429        }
1430        .map_err(windows_error)?;
1431        let owner = unsafe { &*buffer.as_ptr().cast::<TOKEN_OWNER>() };
1432        let status = unsafe {
1433            SetSecurityInfo(
1434                HANDLE(file.as_raw_handle() as isize),
1435                SE_FILE_OBJECT,
1436                DACL_SECURITY_INFORMATION
1437                    | OWNER_SECURITY_INFORMATION
1438                    | PROTECTED_DACL_SECURITY_INFORMATION,
1439                owner.Owner,
1440                PSID::default(),
1441                Some(descriptor.dacl()?),
1442                None,
1443            )
1444        };
1445        if status == ERROR_SUCCESS {
1446            Ok(())
1447        } else {
1448            Err(io::Error::from_raw_os_error(status.0 as i32))
1449        }
1450    })();
1451    let _ = unsafe { CloseHandle(token) };
1452    result?;
1453    windows_validate_file_owner(file)?;
1454    windows_validate_exact_private_acl(file)
1455}
1456
1457#[cfg(target_os = "windows")]
1458fn windows_validate_exact_private_acl(file: &File) -> io::Result<()> {
1459    use std::os::windows::io::AsRawHandle;
1460    use windows::Win32::Foundation::{LocalFree, HANDLE, HLOCAL, PSID};
1461    use windows::Win32::Security::Authorization::{GetSecurityInfo, SE_FILE_OBJECT};
1462    use windows::Win32::Security::{
1463        GetAce, GetSecurityDescriptorControl, ACCESS_ALLOWED_ACE, ACL, DACL_SECURITY_INFORMATION,
1464        PSECURITY_DESCRIPTOR, SE_DACL_PROTECTED,
1465    };
1466    use windows::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
1467    const ACCESS_ALLOWED_ACE_TYPE_VALUE: u8 = 0;
1468
1469    let mut dacl = std::ptr::null_mut::<ACL>();
1470    let mut descriptor = PSECURITY_DESCRIPTOR::default();
1471    let status = unsafe {
1472        GetSecurityInfo(
1473            HANDLE(file.as_raw_handle() as isize),
1474            SE_FILE_OBJECT,
1475            DACL_SECURITY_INFORMATION,
1476            None,
1477            None,
1478            Some(&mut dacl),
1479            None,
1480            Some(&mut descriptor),
1481        )
1482    };
1483    if !status.is_ok() {
1484        return Err(io::Error::from_raw_os_error(status.0 as i32));
1485    }
1486    let result = (|| {
1487        if dacl.is_null() {
1488            return Err(permission_denied("private Windows ACL is absent"));
1489        }
1490        let mut control = 0u16;
1491        let mut revision = 0u32;
1492        unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) }
1493            .map_err(windows_io_error)?;
1494        if control & SE_DACL_PROTECTED.0 == 0 || unsafe { (*dacl).AceCount } != 1 {
1495            return Err(permission_denied(
1496                "private Windows ACL is not one protected owner ACE",
1497            ));
1498        }
1499        let mut raw_ace = std::ptr::null_mut();
1500        unsafe { GetAce(dacl, 0, &mut raw_ace) }.map_err(windows_io_error)?;
1501        let ace = unsafe { &*raw_ace.cast::<ACCESS_ALLOWED_ACE>() };
1502        if ace.Header.AceType != ACCESS_ALLOWED_ACE_TYPE_VALUE || ace.Mask != FILE_ALL_ACCESS.0 {
1503            return Err(permission_denied(
1504                "private Windows ACL does not grant exact owner full control",
1505            ));
1506        }
1507        let sid = PSID(std::ptr::addr_of!(ace.SidStart).cast_mut().cast());
1508        if windows_sid_string(sid)? != current_process_sid_string()? {
1509            return Err(permission_denied(
1510                "private Windows ACL is granted to another identity",
1511            ));
1512        }
1513        Ok(())
1514    })();
1515    unsafe {
1516        let _ = LocalFree(HLOCAL(descriptor.0));
1517    }
1518    result
1519}
1520
1521#[cfg(target_os = "windows")]
1522fn windows_create_private_directory(path: &Path) -> io::Result<()> {
1523    use windows::core::PCWSTR;
1524    use windows::Win32::Storage::FileSystem::CreateDirectoryW;
1525
1526    let descriptor = WindowsPrivateSecurityDescriptor::new()?;
1527    let attributes = descriptor.security_attributes();
1528    let wide = windows_path_wide(path)?;
1529    unsafe { CreateDirectoryW(PCWSTR(wide.as_ptr()), Some(&attributes)) }.map_err(windows_io_error)
1530}
1531
1532#[cfg(target_os = "windows")]
1533fn reject_windows_reparse_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
1534    use std::os::windows::fs::MetadataExt;
1535
1536    if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_VALUE != 0 {
1537        Err(permission_denied(
1538            "owner-private path cannot be a reparse point",
1539        ))
1540    } else {
1541        Ok(())
1542    }
1543}
1544
1545#[cfg(target_os = "windows")]
1546fn windows_ensure_private_dir(path: &Path) -> io::Result<()> {
1547    windows_ensure_private_dir_with_failure_injector(path, None)
1548}
1549
1550#[cfg(target_os = "windows")]
1551fn windows_ensure_private_dir_with_failure_injector(
1552    path: &Path,
1553    failures: Option<&PrivatePathDurabilityFailureInjector>,
1554) -> io::Result<()> {
1555    windows_ensure_private_dir_with_hook(path, failures, |_| Ok(()))
1556}
1557
1558#[cfg(target_os = "windows")]
1559fn windows_ensure_private_dir_with_hook<F>(
1560    path: &Path,
1561    failures: Option<&PrivatePathDurabilityFailureInjector>,
1562    mut after_validated_child: F,
1563) -> io::Result<()>
1564where
1565    F: FnMut(&Path) -> io::Result<()>,
1566{
1567    let mut current = std::path::PathBuf::new();
1568    let mut saw_directory = false;
1569    let mut hierarchy: Vec<(PathBuf, File)> = Vec::new();
1570    for component in path.components() {
1571        current.push(component.as_os_str());
1572        if !matches!(component, std::path::Component::Normal(_)) {
1573            if matches!(component, std::path::Component::ParentDir) {
1574                return Err(io::Error::new(
1575                    io::ErrorKind::InvalidInput,
1576                    "owner-private paths cannot contain parent traversal",
1577                ));
1578            }
1579            continue;
1580        }
1581        saw_directory = true;
1582        match std::fs::symlink_metadata(&current) {
1583            Ok(metadata) => {
1584                reject_windows_reparse_metadata(&metadata)?;
1585                if !metadata.is_dir() {
1586                    return Err(permission_denied(
1587                        "private path component is not a directory",
1588                    ));
1589                }
1590                let directory = if current == path {
1591                    windows_open_directory_for_hardening(&current)?
1592                } else {
1593                    windows_open_directory(&current)?
1594                };
1595                windows_validate_directory(&directory)?;
1596                if current == path {
1597                    windows_validate_hardenable_file_owner(&directory)?;
1598                    windows_harden_file_acl(&directory)?;
1599                    windows_revalidate_directory_path(&current, &directory)?;
1600                }
1601                // A prior create may have returned a transient parent-flush
1602                // error after installing this owner-private entry. Re-flush
1603                // its retained parent on retry before advancing. Do this only
1604                // below an owner-private parent: a normal user profile is
1605                // itself user-owned but its system-owned parent (for example
1606                // `C:\\Users`) does not grant generic write, so requiring a
1607                // flush there would reject every ordinary private tree.
1608                if windows_validate_file_owner(&directory).is_ok() {
1609                    let parent_path = normalized_parent(&current);
1610                    let parent_probe = windows_open_directory(parent_path)?;
1611                    match windows_validate_file_owner(&parent_probe) {
1612                        Ok(()) => {
1613                            let parent = windows_open_directory_for_durability(parent_path)?;
1614                            windows_flush_directory_metadata(&parent)?;
1615                            windows_revalidate_directory_path(parent_path, &parent)?;
1616                            windows_revalidate_directory_path(&current, &directory)?;
1617                        }
1618                        Err(error) if error.kind() == io::ErrorKind::PermissionDenied => {}
1619                        Err(error) => return Err(error),
1620                    }
1621                }
1622                after_validated_child(&current)?;
1623                let retained_access = if current == path {
1624                    windows::Win32::Storage::FileSystem::FILE_GENERIC_READ.0
1625                        | windows::Win32::Storage::FileSystem::WRITE_DAC.0
1626                        | windows::Win32::Storage::FileSystem::WRITE_OWNER.0
1627                } else {
1628                    windows::Win32::Storage::FileSystem::FILE_GENERIC_READ.0
1629                };
1630                let retained = windows_open_directory_with_access_and_share(
1631                    &current,
1632                    retained_access,
1633                    windows::Win32::Storage::FileSystem::FILE_SHARE_READ
1634                        | windows::Win32::Storage::FileSystem::FILE_SHARE_WRITE,
1635                )?;
1636                windows_validate_directory(&retained)?;
1637                if windows_file_identity(&directory)? != windows_file_identity(&retained)? {
1638                    return Err(permission_denied(
1639                        "validated private directory changed before retention",
1640                    ));
1641                }
1642                if current == path {
1643                    windows_harden_file_acl(&retained)?;
1644                    windows_revalidate_directory_path(&current, &retained)?;
1645                } else {
1646                    windows_revalidate_directory_path_identity(&current, &retained)?;
1647                }
1648                hierarchy.push((current.clone(), retained));
1649            }
1650            Err(error) if error.kind() == io::ErrorKind::NotFound => {
1651                let parent_path = normalized_parent(&current);
1652                let parent = windows_open_directory_for_durability(parent_path)?;
1653                windows_validate_directory(&parent)?;
1654                if let Some((retained_path, retained)) = hierarchy.last() {
1655                    if retained_path == parent_path
1656                        && windows_file_identity(retained)? != windows_file_identity(&parent)?
1657                    {
1658                        return Err(permission_denied(
1659                            "private directory parent changed before creation",
1660                        ));
1661                    }
1662                }
1663                if let Err(create_error) = windows_create_private_directory(&current) {
1664                    if create_error.kind() != io::ErrorKind::AlreadyExists {
1665                        return Err(create_error);
1666                    }
1667                }
1668                let directory = windows_open_directory_for_hardening(&current)?;
1669                windows_validate_directory(&directory)?;
1670                windows_validate_hardenable_file_owner(&directory)?;
1671                windows_harden_file_acl(&directory)?;
1672                windows_revalidate_directory_path(&current, &directory)?;
1673                after_validated_child(&current)?;
1674                let retained = windows_open_directory_with_access_and_share(
1675                    &current,
1676                    windows::Win32::Storage::FileSystem::FILE_GENERIC_READ.0
1677                        | windows::Win32::Storage::FileSystem::WRITE_DAC.0
1678                        | windows::Win32::Storage::FileSystem::WRITE_OWNER.0,
1679                    windows::Win32::Storage::FileSystem::FILE_SHARE_READ
1680                        | windows::Win32::Storage::FileSystem::FILE_SHARE_WRITE,
1681                )?;
1682                windows_validate_directory(&retained)?;
1683                if windows_file_identity(&directory)? != windows_file_identity(&retained)? {
1684                    return Err(permission_denied(
1685                        "created private directory changed before retention",
1686                    ));
1687                }
1688                windows_harden_file_acl(&retained)?;
1689                windows_revalidate_directory_path(&current, &retained)?;
1690                if let Some(failures) = failures {
1691                    failures.check(PrivatePathDurabilityFailurePoint::ParentDirectorySync)?;
1692                }
1693                windows_flush_directory_metadata(&parent)?;
1694                windows_revalidate_directory_path(parent_path, &parent)?;
1695                windows_revalidate_directory_path(&current, &retained)?;
1696                windows_validate_exact_private_acl(&retained)?;
1697                hierarchy.push((current.clone(), retained));
1698            }
1699            Err(error) => return Err(error),
1700        }
1701    }
1702    if !saw_directory {
1703        return Err(io::Error::new(
1704            io::ErrorKind::InvalidInput,
1705            "refusing to harden a filesystem root or current directory",
1706        ));
1707    }
1708    Ok(())
1709}
1710
1711/// Best-effort flush of a retained directory handle after a child metadata
1712/// change. Windows does not provide a portable directory-fsync primitive:
1713/// `FlushFileBuffers` rejects directory handles with `ERROR_ACCESS_DENIED` on
1714/// supported GitHub-hosted NTFS runners even when they were opened with
1715/// generic write and backup semantics. Preserve real I/O errors, but treat
1716/// that unsupported shape as the platform durability boundary.
1717#[cfg(target_os = "windows")]
1718fn windows_flush_directory_metadata(directory: &File) -> io::Result<()> {
1719    use std::os::windows::io::AsRawHandle;
1720    use windows::Win32::Foundation::{ERROR_ACCESS_DENIED, HANDLE, WIN32_ERROR};
1721    use windows::Win32::Storage::FileSystem::FlushFileBuffers;
1722
1723    match unsafe { FlushFileBuffers(HANDLE(directory.as_raw_handle() as isize)) } {
1724        Ok(()) => Ok(()),
1725        Err(error) if WIN32_ERROR::from_error(&error) == Some(ERROR_ACCESS_DENIED) => Ok(()),
1726        Err(error) => Err(windows_io_error(error)),
1727    }
1728}
1729
1730#[cfg(target_os = "windows")]
1731fn windows_validate_existing_directory_chain(path: &Path) -> io::Result<()> {
1732    let mut current = PathBuf::new();
1733    for component in path.components() {
1734        current.push(component.as_os_str());
1735        if !matches!(component, std::path::Component::Normal(_)) {
1736            if matches!(component, std::path::Component::ParentDir) {
1737                return Err(io::Error::new(
1738                    io::ErrorKind::InvalidInput,
1739                    "owner-private paths cannot contain parent traversal",
1740                ));
1741            }
1742            continue;
1743        }
1744        let metadata = std::fs::symlink_metadata(&current)?;
1745        reject_windows_reparse_metadata(&metadata)?;
1746        if !metadata.is_dir() {
1747            return Err(permission_denied(
1748                "private path component is not a directory",
1749            ));
1750        }
1751        let directory = windows_open_directory(&current)?;
1752        windows_validate_directory(&directory)?;
1753    }
1754    Ok(())
1755}
1756
1757#[cfg(target_os = "windows")]
1758fn windows_open_directory(path: &Path) -> io::Result<File> {
1759    use windows::Win32::Storage::FileSystem::FILE_GENERIC_READ;
1760
1761    windows_open_directory_with_access(path, FILE_GENERIC_READ.0)
1762}
1763
1764#[cfg(target_os = "windows")]
1765fn windows_open_directory_for_hardening(path: &Path) -> io::Result<File> {
1766    use windows::Win32::Storage::FileSystem::{FILE_GENERIC_READ, WRITE_DAC, WRITE_OWNER};
1767
1768    windows_open_directory_with_access(path, FILE_GENERIC_READ.0 | WRITE_DAC.0 | WRITE_OWNER.0)
1769}
1770
1771#[cfg(target_os = "windows")]
1772fn windows_open_directory_for_durability(path: &Path) -> io::Result<File> {
1773    use windows::Win32::Storage::FileSystem::{
1774        FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, WRITE_DAC,
1775    };
1776
1777    windows_open_directory_with_access_and_share(
1778        path,
1779        FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0 | WRITE_DAC.0,
1780        FILE_SHARE_READ | FILE_SHARE_WRITE,
1781    )
1782}
1783
1784#[cfg(target_os = "windows")]
1785fn windows_open_directory_with_access(path: &Path, desired_access: u32) -> io::Result<File> {
1786    use windows::Win32::Storage::FileSystem::{
1787        FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
1788    };
1789
1790    windows_open_directory_with_access_and_share(
1791        path,
1792        desired_access,
1793        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
1794    )
1795}
1796
1797#[cfg(target_os = "windows")]
1798fn windows_open_directory_with_access_and_share(
1799    path: &Path,
1800    desired_access: u32,
1801    share: windows::Win32::Storage::FileSystem::FILE_SHARE_MODE,
1802) -> io::Result<File> {
1803    use std::os::windows::io::FromRawHandle;
1804    use windows::core::PCWSTR;
1805    use windows::Win32::Foundation::HANDLE;
1806    use windows::Win32::Storage::FileSystem::{
1807        CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, OPEN_EXISTING,
1808    };
1809
1810    let wide = windows_path_wide(path)?;
1811    let handle = unsafe {
1812        CreateFileW(
1813            PCWSTR(wide.as_ptr()),
1814            desired_access,
1815            share,
1816            None,
1817            OPEN_EXISTING,
1818            FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
1819            HANDLE::default(),
1820        )
1821    }
1822    .map_err(windows_io_error)?;
1823    // SAFETY: CreateFileW returned a newly-owned valid handle.
1824    Ok(unsafe { File::from_raw_handle(handle.0 as *mut std::ffi::c_void) })
1825}
1826
1827#[cfg(target_os = "windows")]
1828fn windows_open_existing_file_for_hardening(path: &Path) -> io::Result<File> {
1829    use windows::Win32::Storage::FileSystem::{FILE_GENERIC_READ, WRITE_DAC, WRITE_OWNER};
1830
1831    windows_open_existing_file_with_access(path, FILE_GENERIC_READ.0 | WRITE_DAC.0 | WRITE_OWNER.0)
1832}
1833
1834#[cfg(target_os = "windows")]
1835fn windows_open_existing_file_with_access(path: &Path, desired_access: u32) -> io::Result<File> {
1836    use std::os::windows::io::FromRawHandle;
1837    use windows::core::PCWSTR;
1838    use windows::Win32::Foundation::HANDLE;
1839    use windows::Win32::Storage::FileSystem::{
1840        CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE,
1841        FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
1842    };
1843
1844    let wide = windows_path_wide(path)?;
1845    let handle = unsafe {
1846        CreateFileW(
1847            PCWSTR(wide.as_ptr()),
1848            desired_access,
1849            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
1850            None,
1851            OPEN_EXISTING,
1852            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
1853            HANDLE::default(),
1854        )
1855    }
1856    .map_err(windows_io_error)?;
1857    // SAFETY: CreateFileW returned a newly-owned valid handle.
1858    Ok(unsafe { File::from_raw_handle(handle.0 as *mut std::ffi::c_void) })
1859}
1860
1861#[cfg(target_os = "windows")]
1862fn windows_validate_directory(directory: &File) -> io::Result<()> {
1863    let metadata = directory.metadata()?;
1864    reject_windows_reparse_metadata(&metadata)?;
1865    if metadata.is_dir() {
1866        Ok(())
1867    } else {
1868        Err(permission_denied("private path is not a directory"))
1869    }
1870}
1871
1872#[cfg(target_os = "windows")]
1873fn windows_validate_file_owner(file: &File) -> io::Result<()> {
1874    let owner = windows_file_owner_sid_string(file)?;
1875    let process_owner = current_process_default_owner_sid_string()?;
1876    if owner == process_owner {
1877        Ok(())
1878    } else {
1879        Err(io::Error::new(
1880            io::ErrorKind::PermissionDenied,
1881            format!("private path owner {owner} does not match token owner {process_owner}"),
1882        ))
1883    }
1884}
1885
1886#[cfg(target_os = "windows")]
1887fn windows_validate_hardenable_file_owner(file: &File) -> io::Result<()> {
1888    // Accept only TokenOwner or TokenUser before hardening. The former is the
1889    // native Windows ownership identity (often BUILTIN\Administrators for an
1890    // elevated token); the latter covers entries produced by earlier CAR
1891    // versions. Hardening normalizes ownership to TokenOwner and the protected
1892    // one-ACE DACL to TokenUser.
1893    let owner = windows_file_owner_sid_string(file)?;
1894    let process_user = current_process_sid_string()?;
1895    let default_owner = current_process_default_owner_sid_string()?;
1896    if owner == process_user || owner == default_owner {
1897        Ok(())
1898    } else {
1899        Err(io::Error::new(
1900            io::ErrorKind::PermissionDenied,
1901            format!(
1902                "private path owner {owner} is neither process user {process_user} nor token default owner {default_owner}"
1903            ),
1904        ))
1905    }
1906}
1907
1908#[cfg(target_os = "windows")]
1909fn windows_file_owner_sid_string(file: &File) -> io::Result<String> {
1910    use std::os::windows::io::AsRawHandle;
1911    use windows::Win32::Foundation::{LocalFree, HANDLE, HLOCAL, PSID};
1912    use windows::Win32::Security::Authorization::{GetSecurityInfo, SE_FILE_OBJECT};
1913    use windows::Win32::Security::{OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR};
1914
1915    let mut owner = PSID::default();
1916    let mut descriptor = PSECURITY_DESCRIPTOR::default();
1917    let status = unsafe {
1918        GetSecurityInfo(
1919            HANDLE(file.as_raw_handle() as isize),
1920            SE_FILE_OBJECT,
1921            OWNER_SECURITY_INFORMATION,
1922            Some(&mut owner),
1923            None,
1924            None,
1925            None,
1926            Some(&mut descriptor),
1927        )
1928    };
1929    if status.is_err() {
1930        return Err(io::Error::other(status.to_hresult().message()));
1931    }
1932    let result = windows_sid_string(owner);
1933    unsafe {
1934        let _ = LocalFree(HLOCAL(descriptor.0));
1935    }
1936    result
1937}
1938
1939#[cfg(target_os = "windows")]
1940fn windows_sid_string(sid: windows::Win32::Foundation::PSID) -> io::Result<String> {
1941    use windows::core::PWSTR;
1942    use windows::Win32::Foundation::{LocalFree, HLOCAL};
1943    use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
1944
1945    let mut sid_text = PWSTR::null();
1946    unsafe { ConvertSidToStringSidW(sid, &mut sid_text) }
1947        .map_err(|error| io::Error::other(error.to_string()))?;
1948    let result = unsafe { sid_text.to_string() }
1949        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error));
1950    unsafe {
1951        let _ = LocalFree(HLOCAL(sid_text.0.cast()));
1952    }
1953    result
1954}
1955
1956#[cfg(target_os = "windows")]
1957fn windows_revalidate_directory_path(path: &Path, directory: &File) -> io::Result<()> {
1958    windows_validate_directory(directory)?;
1959    windows_validate_file_owner(directory)?;
1960    let by_path = windows_open_directory(path)?;
1961    windows_validate_directory(&by_path)?;
1962    windows_validate_file_owner(&by_path)?;
1963    if windows_file_identity(directory)? != windows_file_identity(&by_path)? {
1964        return Err(permission_denied(
1965            "private directory path changed during ACL hardening",
1966        ));
1967    }
1968    Ok(())
1969}
1970
1971#[cfg(target_os = "windows")]
1972fn windows_revalidate_directory_path_identity(path: &Path, directory: &File) -> io::Result<()> {
1973    windows_validate_directory(directory)?;
1974    let by_path = windows_open_directory(path)?;
1975    windows_validate_directory(&by_path)?;
1976    if windows_file_identity(directory)? != windows_file_identity(&by_path)? {
1977        return Err(permission_denied(
1978            "directory path changed before its retained handle was validated",
1979        ));
1980    }
1981    Ok(())
1982}
1983
1984#[cfg(target_os = "windows")]
1985fn windows_open_private(
1986    path: &Path,
1987    mode: PrivateOpenMode,
1988    failures: Option<&PrivatePathDurabilityFailureInjector>,
1989) -> io::Result<File> {
1990    use std::os::windows::io::FromRawHandle;
1991    use windows::core::PCWSTR;
1992    use windows::Win32::Foundation::HANDLE;
1993    use windows::Win32::Storage::FileSystem::{
1994        CreateFileW, CREATE_NEW, FILE_APPEND_DATA, FILE_ATTRIBUTE_NORMAL,
1995        FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_DELETE,
1996        FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, WRITE_DAC, WRITE_OWNER,
1997    };
1998
1999    let parent = normalized_parent(path);
2000    if matches!(mode, PrivateOpenMode::Append | PrivateOpenMode::CreateNew) {
2001        if parent != Path::new(".") {
2002            windows_ensure_private_dir_with_failure_injector(parent, failures)?;
2003        }
2004    } else if parent != Path::new(".") {
2005        windows_validate_existing_directory_chain(parent)?;
2006    }
2007    let durability_parent = if matches!(mode, PrivateOpenMode::Append | PrivateOpenMode::CreateNew)
2008    {
2009        let descriptor = windows_open_directory_for_durability(parent)?;
2010        windows_validate_directory(&descriptor)?;
2011        Some(descriptor)
2012    } else {
2013        None
2014    };
2015    let (desired_access, disposition) = match mode {
2016        PrivateOpenMode::Read => (
2017            FILE_GENERIC_READ.0 | WRITE_DAC.0 | WRITE_OWNER.0,
2018            OPEN_EXISTING,
2019        ),
2020        PrivateOpenMode::Append => (
2021            FILE_GENERIC_READ.0 | FILE_APPEND_DATA.0 | WRITE_DAC.0 | WRITE_OWNER.0,
2022            OPEN_EXISTING,
2023        ),
2024        PrivateOpenMode::Truncate => (
2025            FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0 | WRITE_DAC.0 | WRITE_OWNER.0,
2026            OPEN_EXISTING,
2027        ),
2028        PrivateOpenMode::CreateNew => (
2029            FILE_GENERIC_WRITE.0 | WRITE_DAC.0 | WRITE_OWNER.0,
2030            CREATE_NEW,
2031        ),
2032    };
2033    let descriptor = WindowsPrivateSecurityDescriptor::new()?;
2034    let attributes = descriptor.security_attributes();
2035    let wide = windows_path_wide(path)?;
2036    let open = |disposition| {
2037        unsafe {
2038            CreateFileW(
2039                PCWSTR(wide.as_ptr()),
2040                desired_access,
2041                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
2042                Some(&attributes),
2043                disposition,
2044                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
2045                HANDLE::default(),
2046            )
2047        }
2048        .map_err(windows_io_error)
2049    };
2050    let mut created = mode == PrivateOpenMode::CreateNew;
2051    let handle = match open(disposition) {
2052        Ok(handle) => handle,
2053        Err(error)
2054            if mode == PrivateOpenMode::Append && error.kind() == io::ErrorKind::NotFound =>
2055        {
2056            match open(CREATE_NEW) {
2057                Ok(handle) => {
2058                    created = true;
2059                    handle
2060                }
2061                Err(error) if error.kind() == io::ErrorKind::AlreadyExists => open(OPEN_EXISTING)?,
2062                Err(error) => return Err(error),
2063            }
2064        }
2065        Err(error) => return Err(error),
2066    };
2067    // SAFETY: CreateFileW returned a newly-owned valid handle.
2068    let file = unsafe { File::from_raw_handle(handle.0 as *mut std::ffi::c_void) };
2069    windows_validate_file(&file)?;
2070    windows_validate_hardenable_file_owner(&file)?;
2071    windows_harden_file_acl(&file)?;
2072    windows_revalidate_private_path(path, &file)?;
2073    if mode == PrivateOpenMode::Truncate {
2074        if parent != Path::new(".") {
2075            windows_validate_existing_directory_chain(parent)?;
2076        }
2077        file.set_len(0)?;
2078        windows_validate_file(&file)?;
2079        windows_validate_file_owner(&file)?;
2080        windows_revalidate_private_path(path, &file)?;
2081    }
2082    if let Some(parent_descriptor) = durability_parent.as_ref() {
2083        if created {
2084            if let Some(failures) = failures {
2085                failures.check(PrivatePathDurabilityFailurePoint::ParentDirectorySync)?;
2086            }
2087        }
2088        windows_flush_directory_metadata(parent_descriptor)?;
2089        windows_revalidate_directory_path(parent, parent_descriptor)?;
2090        windows_revalidate_private_path(path, &file)?;
2091    }
2092    Ok(file)
2093}
2094
2095#[cfg(target_os = "windows")]
2096fn windows_validate_file(file: &File) -> io::Result<()> {
2097    let metadata = file.metadata()?;
2098    reject_windows_reparse_metadata(&metadata)?;
2099    if !metadata.is_file() {
2100        return Err(permission_denied("private file is not a regular file"));
2101    }
2102    if windows_file_link_count(file)? != 1 {
2103        return Err(permission_denied(
2104            "private file must have exactly one hard link",
2105        ));
2106    }
2107    Ok(())
2108}
2109
2110#[cfg(target_os = "windows")]
2111fn windows_revalidate_private_path(path: &Path, file: &File) -> io::Result<()> {
2112    use std::os::windows::fs::OpenOptionsExt;
2113
2114    let by_path = std::fs::OpenOptions::new()
2115        .read(true)
2116        .custom_flags(0x0020_0000) // FILE_FLAG_OPEN_REPARSE_POINT
2117        .open(path)?;
2118    windows_validate_file(&by_path)?;
2119    windows_validate_file_owner(&by_path)?;
2120    if windows_file_identity(file)? != windows_file_identity(&by_path)? {
2121        return Err(permission_denied(
2122            "private file path no longer names the validated descriptor",
2123        ));
2124    }
2125    Ok(())
2126}
2127
2128#[cfg(target_os = "windows")]
2129fn windows_harden_private_tree(
2130    tree: &PrivateTree,
2131    selected: &[PathBuf],
2132) -> io::Result<PrivateTreeReport> {
2133    use std::collections::BTreeSet;
2134
2135    let mut report = PrivateTreeReport::default();
2136    if selected.is_empty() {
2137        windows_harden_file_acl(&tree.root_descriptor)?;
2138        tree.revalidate_root()?;
2139        report.directories_hardened = 1;
2140        return Ok(report);
2141    }
2142
2143    let mut visited = BTreeSet::new();
2144    visited.insert(windows_file_identity(&tree.root_descriptor)?);
2145    for relative in selected {
2146        tree.revalidate_root()?;
2147        windows_harden_selected_path(tree, relative, &mut visited, &mut report)?;
2148        tree.revalidate_root()?;
2149    }
2150    Ok(report)
2151}
2152
2153#[cfg(target_os = "windows")]
2154fn windows_harden_selected_path(
2155    tree: &PrivateTree,
2156    relative: &Path,
2157    visited: &mut std::collections::BTreeSet<(u32, u64)>,
2158    report: &mut PrivateTreeReport,
2159) -> io::Result<()> {
2160    let components = relative
2161        .components()
2162        .map(|component| component.as_os_str().to_os_string())
2163        .collect::<Vec<_>>();
2164    windows_harden_selected_components(
2165        tree,
2166        &tree.root_descriptor,
2167        &tree.root,
2168        &components,
2169        visited,
2170        report,
2171    )
2172}
2173
2174#[cfg(target_os = "windows")]
2175fn windows_harden_selected_components(
2176    tree: &PrivateTree,
2177    parent: &File,
2178    parent_path: &Path,
2179    components: &[std::ffi::OsString],
2180    visited: &mut std::collections::BTreeSet<(u32, u64)>,
2181    report: &mut PrivateTreeReport,
2182) -> io::Result<()> {
2183    let (component, remaining) = components.split_first().ok_or_else(|| {
2184        io::Error::new(
2185            io::ErrorKind::InvalidInput,
2186            "private-tree selection has no components",
2187        )
2188    })?;
2189    let path = parent_path.join(component);
2190    let entry = windows_open_entry_at_for_hardening(parent, component)?;
2191    if remaining.is_empty() {
2192        return windows_harden_tree_entry(tree, parent, component, &path, entry, visited, report);
2193    }
2194    windows_validate_directory(&entry)?;
2195    windows_validate_hardenable_file_owner(&entry)?;
2196    windows_harden_file_acl(&entry)?;
2197    windows_revalidate_directory_path(&path, &entry)?;
2198    windows_revalidate_entry_at(parent, component, &entry)?;
2199    if visited.insert(windows_file_identity(&entry)?) {
2200        report.directories_hardened += 1;
2201    }
2202    windows_harden_selected_components(tree, &entry, &path, remaining, visited, report)?;
2203    windows_revalidate_entry_at(parent, component, &entry)
2204}
2205
2206#[cfg(target_os = "windows")]
2207fn windows_harden_tree_entry(
2208    tree: &PrivateTree,
2209    parent: &File,
2210    name: &std::ffi::OsStr,
2211    path: &Path,
2212    entry: File,
2213    visited: &mut std::collections::BTreeSet<(u32, u64)>,
2214    report: &mut PrivateTreeReport,
2215) -> io::Result<()> {
2216    let metadata = entry.metadata()?;
2217    reject_windows_reparse_metadata(&metadata)?;
2218    windows_validate_hardenable_file_owner(&entry)?;
2219    if metadata.is_dir() {
2220        windows_validate_directory(&entry)?;
2221        windows_harden_file_acl(&entry)?;
2222        windows_revalidate_directory_path(path, &entry)?;
2223        windows_revalidate_entry_at(parent, name, &entry)?;
2224        if visited.insert(windows_file_identity(&entry)?) {
2225            report.directories_hardened += 1;
2226            for child in windows_directory_names(&entry)? {
2227                tree.revalidate_root()?;
2228                let child_path = path.join(&child);
2229                let child_entry = windows_open_entry_at_for_hardening(&entry, &child)?;
2230                windows_harden_tree_entry(
2231                    tree,
2232                    &entry,
2233                    &child,
2234                    &child_path,
2235                    child_entry,
2236                    visited,
2237                    report,
2238                )?;
2239            }
2240        }
2241        windows_revalidate_entry_at(parent, name, &entry)?;
2242    } else if metadata.is_file() {
2243        windows_validate_file(&entry)?;
2244        windows_harden_file_acl(&entry)?;
2245        windows_revalidate_private_path(path, &entry)?;
2246        windows_revalidate_entry_at(parent, name, &entry)?;
2247        if visited.insert(windows_file_identity(&entry)?) {
2248            report.files_hardened += 1;
2249        }
2250        windows_revalidate_entry_at(parent, name, &entry)?;
2251    } else {
2252        return Err(permission_denied(
2253            "private tree contains a reparse point or special file",
2254        ));
2255    }
2256    Ok(())
2257}
2258
2259#[cfg(target_os = "windows")]
2260fn windows_revalidate_entry_at(
2261    parent: &File,
2262    name: &std::ffi::OsStr,
2263    entry: &File,
2264) -> io::Result<()> {
2265    let by_name = windows_open_entry_at(parent, name)?;
2266    let metadata = by_name.metadata()?;
2267    reject_windows_reparse_metadata(&metadata)?;
2268    windows_validate_file_owner(&by_name)?;
2269    if metadata.is_file() {
2270        windows_validate_file(&by_name)?;
2271    } else if metadata.is_dir() {
2272        windows_validate_directory(&by_name)?;
2273    } else {
2274        return Err(permission_denied(
2275            "private tree contains a reparse point or special file",
2276        ));
2277    }
2278    if windows_file_identity(entry)? != windows_file_identity(&by_name)? {
2279        return Err(permission_denied(
2280            "private tree entry changed during descriptor validation",
2281        ));
2282    }
2283    Ok(())
2284}
2285
2286#[cfg(target_os = "windows")]
2287fn windows_open_entry_at(parent: &File, name: &std::ffi::OsStr) -> io::Result<File> {
2288    use windows::Win32::Storage::FileSystem::FILE_GENERIC_READ;
2289
2290    windows_open_entry_at_with_access(parent, name, FILE_GENERIC_READ)
2291}
2292
2293#[cfg(target_os = "windows")]
2294fn windows_open_entry_at_for_hardening(parent: &File, name: &std::ffi::OsStr) -> io::Result<File> {
2295    use windows::Win32::Storage::FileSystem::{FILE_GENERIC_READ, WRITE_DAC, WRITE_OWNER};
2296
2297    windows_open_entry_at_with_access(parent, name, FILE_GENERIC_READ | WRITE_DAC | WRITE_OWNER)
2298}
2299
2300#[cfg(target_os = "windows")]
2301fn windows_open_entry_at_with_access(
2302    parent: &File,
2303    name: &std::ffi::OsStr,
2304    desired_access: windows::Win32::Storage::FileSystem::FILE_ACCESS_RIGHTS,
2305) -> io::Result<File> {
2306    use windows::Win32::Storage::FileSystem::{
2307        FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
2308    };
2309
2310    windows_open_entry_at_with_access_and_share(
2311        parent,
2312        name,
2313        desired_access,
2314        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
2315    )
2316}
2317
2318#[cfg(target_os = "windows")]
2319fn windows_open_entry_at_with_access_and_share(
2320    parent: &File,
2321    name: &std::ffi::OsStr,
2322    desired_access: windows::Win32::Storage::FileSystem::FILE_ACCESS_RIGHTS,
2323    share: windows::Win32::Storage::FileSystem::FILE_SHARE_MODE,
2324) -> io::Result<File> {
2325    use std::os::windows::ffi::OsStrExt;
2326    use std::os::windows::io::{AsRawHandle, FromRawHandle};
2327    use windows::core::PWSTR;
2328    use windows::Wdk::Foundation::OBJECT_ATTRIBUTES;
2329    use windows::Wdk::Storage::FileSystem::{
2330        NtCreateFile, FILE_OPEN, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT,
2331    };
2332    use windows::Win32::Foundation::{RtlNtStatusToDosError, HANDLE, UNICODE_STRING};
2333    use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
2334    use windows::Win32::System::IO::IO_STATUS_BLOCK;
2335
2336    let mut wide = name.encode_wide().collect::<Vec<_>>();
2337    let byte_length = wide
2338        .len()
2339        .checked_mul(std::mem::size_of::<u16>())
2340        .and_then(|length| u16::try_from(length).ok())
2341        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "tree name is too long"))?;
2342    let unicode = UNICODE_STRING {
2343        Length: byte_length,
2344        MaximumLength: byte_length,
2345        Buffer: PWSTR(wide.as_mut_ptr()),
2346    };
2347    let attributes = OBJECT_ATTRIBUTES {
2348        Length: std::mem::size_of::<OBJECT_ATTRIBUTES>() as u32,
2349        RootDirectory: HANDLE(parent.as_raw_handle() as isize),
2350        ObjectName: &unicode,
2351        Attributes: 0x40, // OBJ_CASE_INSENSITIVE
2352        SecurityDescriptor: std::ptr::null(),
2353        SecurityQualityOfService: std::ptr::null(),
2354    };
2355    let mut handle = HANDLE::default();
2356    let mut status_block = IO_STATUS_BLOCK::default();
2357    let status = unsafe {
2358        NtCreateFile(
2359            &mut handle,
2360            desired_access,
2361            &attributes,
2362            &mut status_block,
2363            None,
2364            FILE_ATTRIBUTE_NORMAL,
2365            share,
2366            FILE_OPEN,
2367            FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
2368            None,
2369            0,
2370        )
2371    };
2372    if status.is_err() {
2373        let win32_error = unsafe { RtlNtStatusToDosError(status) };
2374        return Err(io::Error::from_raw_os_error(win32_error as i32));
2375    }
2376    // SAFETY: successful `NtCreateFile` returned a newly-owned handle.
2377    Ok(unsafe { File::from_raw_handle(handle.0 as *mut std::ffi::c_void) })
2378}
2379
2380#[cfg(target_os = "windows")]
2381fn windows_directory_names(directory: &File) -> io::Result<Vec<std::ffi::OsString>> {
2382    use std::os::windows::ffi::OsStringExt;
2383    use std::os::windows::io::AsRawHandle;
2384    use windows::Win32::Foundation::{ERROR_NO_MORE_FILES, HANDLE, WIN32_ERROR};
2385    use windows::Win32::Storage::FileSystem::{
2386        FileIdBothDirectoryInfo, FileIdBothDirectoryRestartInfo, GetFileInformationByHandleEx,
2387        FILE_ID_BOTH_DIR_INFO,
2388    };
2389
2390    let scan = directory.try_clone()?;
2391    windows_validate_directory(&scan)?;
2392    let mut buffer = vec![0u64; 8192];
2393    let mut restart = true;
2394    let mut names = Vec::new();
2395    loop {
2396        let class = if restart {
2397            FileIdBothDirectoryRestartInfo
2398        } else {
2399            FileIdBothDirectoryInfo
2400        };
2401        restart = false;
2402        let result = unsafe {
2403            GetFileInformationByHandleEx(
2404                HANDLE(scan.as_raw_handle() as isize),
2405                class,
2406                buffer.as_mut_ptr().cast(),
2407                (buffer.len() * std::mem::size_of::<u64>()) as u32,
2408            )
2409        };
2410        if let Err(error) = result {
2411            if WIN32_ERROR::from_error(&error) == Some(ERROR_NO_MORE_FILES) {
2412                break;
2413            }
2414            return Err(io::Error::other(error.to_string()));
2415        }
2416
2417        let buffer_bytes = buffer.len() * std::mem::size_of::<u64>();
2418        let mut offset = 0usize;
2419        loop {
2420            if offset + std::mem::size_of::<FILE_ID_BOTH_DIR_INFO>() > buffer_bytes {
2421                return Err(io::Error::new(
2422                    io::ErrorKind::InvalidData,
2423                    "directory enumeration returned a truncated entry",
2424                ));
2425            }
2426            let entry = unsafe {
2427                &*buffer
2428                    .as_ptr()
2429                    .cast::<u8>()
2430                    .add(offset)
2431                    .cast::<FILE_ID_BOTH_DIR_INFO>()
2432            };
2433            let name_units = usize::try_from(entry.FileNameLength / 2).map_err(|_| {
2434                io::Error::new(io::ErrorKind::InvalidData, "directory name is too long")
2435            })?;
2436            let name_offset = offset + std::mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
2437            let name_bytes = name_units.checked_mul(2).ok_or_else(|| {
2438                io::Error::new(io::ErrorKind::InvalidData, "directory name is too long")
2439            })?;
2440            if name_offset + name_bytes > buffer_bytes {
2441                return Err(io::Error::new(
2442                    io::ErrorKind::InvalidData,
2443                    "directory enumeration returned a truncated name",
2444                ));
2445            }
2446            let name_slice = unsafe {
2447                std::slice::from_raw_parts(
2448                    buffer.as_ptr().cast::<u8>().add(name_offset).cast::<u16>(),
2449                    name_units,
2450                )
2451            };
2452            if name_slice != [b'.' as u16] && name_slice != [b'.' as u16, b'.' as u16] {
2453                names.push(std::ffi::OsString::from_wide(name_slice));
2454            }
2455            if entry.NextEntryOffset == 0 {
2456                break;
2457            }
2458            let next = usize::try_from(entry.NextEntryOffset).map_err(|_| {
2459                io::Error::new(io::ErrorKind::InvalidData, "invalid directory entry offset")
2460            })?;
2461            if next == 0 || offset + next >= buffer_bytes {
2462                return Err(io::Error::new(
2463                    io::ErrorKind::InvalidData,
2464                    "invalid directory entry offset",
2465                ));
2466            }
2467            offset += next;
2468        }
2469    }
2470    names.sort();
2471    Ok(names)
2472}
2473
2474#[cfg(target_os = "windows")]
2475fn windows_file_identity(file: &File) -> io::Result<(u32, u64)> {
2476    let information = windows_file_information(file)?;
2477    Ok((
2478        information.dwVolumeSerialNumber,
2479        (u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow),
2480    ))
2481}
2482
2483#[cfg(target_os = "windows")]
2484fn windows_file_link_count(file: &File) -> io::Result<u32> {
2485    Ok(windows_file_information(file)?.nNumberOfLinks)
2486}
2487
2488#[cfg(target_os = "windows")]
2489fn windows_file_information(
2490    file: &File,
2491) -> io::Result<windows::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION> {
2492    use std::os::windows::io::AsRawHandle;
2493    use windows::Win32::Foundation::HANDLE;
2494    use windows::Win32::Storage::FileSystem::{
2495        GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
2496    };
2497
2498    let mut information = BY_HANDLE_FILE_INFORMATION::default();
2499    // SAFETY: the Rust `File` owns a valid handle for the duration of this call
2500    // and `information` is a writable output structure.
2501    unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as isize), &mut information) }
2502        .map_err(|error| io::Error::other(error.to_string()))?;
2503    Ok(information)
2504}
2505
2506#[cfg(target_os = "windows")]
2507fn windows_atomic_replace_private_file(temp: &Path, destination: &Path) -> io::Result<()> {
2508    windows_atomic_replace_private_file_with_hook(temp, destination, || Ok(()))
2509}
2510
2511#[cfg(target_os = "windows")]
2512fn windows_atomic_replace_private_file_with_hook<F>(
2513    temp: &Path,
2514    destination: &Path,
2515    before_rename: F,
2516) -> io::Result<()>
2517where
2518    F: FnOnce() -> io::Result<()>,
2519{
2520    use std::os::windows::ffi::OsStrExt;
2521    use std::os::windows::io::AsRawHandle;
2522    use windows::Win32::Foundation::HANDLE;
2523    use windows::Win32::Storage::FileSystem::{
2524        FileRenameInfoEx, SetFileInformationByHandle, DELETE, FILE_GENERIC_READ,
2525        FILE_GENERIC_WRITE, FILE_RENAME_INFO, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
2526        WRITE_DAC, WRITE_OWNER,
2527    };
2528
2529    const FILE_RENAME_FLAG_REPLACE_IF_EXISTS: u32 = 0x1;
2530    const FILE_RENAME_FLAG_POSIX_SEMANTICS: u32 = 0x2;
2531
2532    let parent_path = normalized_parent(destination);
2533    windows_validate_existing_directory_chain(parent_path)?;
2534    let parent = windows_error_stage(
2535        "open retained replacement parent",
2536        windows_open_directory_with_access_and_share(
2537            parent_path,
2538            FILE_GENERIC_READ.0 | WRITE_DAC.0 | WRITE_OWNER.0,
2539            FILE_SHARE_READ | FILE_SHARE_WRITE,
2540        ),
2541    )?;
2542    windows_error_stage(
2543        "validate retained replacement parent",
2544        windows_validate_directory(&parent),
2545    )?;
2546    windows_error_stage(
2547        "validate retained replacement parent owner",
2548        windows_validate_hardenable_file_owner(&parent),
2549    )?;
2550    windows_error_stage(
2551        "revalidate retained replacement parent before hardening",
2552        windows_revalidate_directory_path(parent_path, &parent),
2553    )?;
2554    windows_error_stage(
2555        "harden retained replacement parent",
2556        windows_harden_file_acl(&parent),
2557    )?;
2558    windows_error_stage(
2559        "revalidate retained replacement parent after hardening",
2560        windows_revalidate_directory_path(parent_path, &parent),
2561    )?;
2562
2563    let temp_name = temp.file_name().ok_or_else(|| {
2564        io::Error::new(io::ErrorKind::InvalidInput, "private temp has no file name")
2565    })?;
2566    let destination_name = destination.file_name().ok_or_else(|| {
2567        io::Error::new(
2568            io::ErrorKind::InvalidInput,
2569            "private destination has no file name",
2570        )
2571    })?;
2572    let temp_file = windows_error_stage(
2573        "open retained replacement source",
2574        windows_open_entry_at_with_access_and_share(
2575            &parent,
2576            temp_name,
2577            // `sync_all` below calls FlushFileBuffers, whose handle contract
2578            // requires write access even though publication does not modify the
2579            // bytes. Without FILE_GENERIC_WRITE every Windows atomic replace
2580            // stopped here with ERROR_ACCESS_DENIED before rename.
2581            FILE_GENERIC_READ | FILE_GENERIC_WRITE | WRITE_DAC | WRITE_OWNER | DELETE,
2582            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
2583        ),
2584    )?;
2585    windows_error_stage(
2586        "validate retained replacement source",
2587        windows_validate_file(&temp_file),
2588    )?;
2589    windows_error_stage(
2590        "validate retained replacement source owner",
2591        windows_validate_hardenable_file_owner(&temp_file),
2592    )?;
2593    windows_error_stage(
2594        "revalidate retained replacement source before hardening",
2595        windows_revalidate_entry_at(&parent, temp_name, &temp_file),
2596    )?;
2597    let temp_identity = windows_error_stage(
2598        "read retained replacement source identity",
2599        windows_file_identity(&temp_file),
2600    )?;
2601    windows_error_stage(
2602        "harden retained replacement source",
2603        windows_harden_file_acl(&temp_file),
2604    )?;
2605    windows_error_stage(
2606        "revalidate retained replacement source after hardening",
2607        windows_revalidate_entry_at(&parent, temp_name, &temp_file),
2608    )?;
2609    windows_error_stage("sync retained replacement source", temp_file.sync_all())?;
2610
2611    let destination_identity = match windows_open_entry_at(&parent, destination_name) {
2612        Ok(file) => {
2613            windows_validate_file(&file)?;
2614            windows_validate_file_owner(&file)?;
2615            Some(windows_file_identity(&file)?)
2616        }
2617        Err(error) if error.kind() == io::ErrorKind::NotFound => None,
2618        Err(error) => return Err(error),
2619    };
2620
2621    windows_error_stage("run replacement race hook", before_rename())?;
2622    windows_error_stage(
2623        "revalidate retained replacement parent before publication",
2624        windows_revalidate_directory_path(parent_path, &parent),
2625    )?;
2626    windows_error_stage(
2627        "revalidate retained replacement source before publication",
2628        windows_revalidate_entry_at(&parent, temp_name, &temp_file),
2629    )?;
2630    let current_destination_identity = match windows_open_entry_at(&parent, destination_name) {
2631        Ok(file) => {
2632            windows_validate_file(&file)?;
2633            windows_validate_file_owner(&file)?;
2634            Some(windows_file_identity(&file)?)
2635        }
2636        Err(error) if error.kind() == io::ErrorKind::NotFound => None,
2637        Err(error) => return Err(error),
2638    };
2639    if current_destination_identity != destination_identity {
2640        return Err(permission_denied(
2641            "private destination changed before handle-relative replacement",
2642        ));
2643    }
2644
2645    let destination_absolute = std::path::absolute(destination)?;
2646    let destination_wide = destination_absolute
2647        .as_os_str()
2648        .encode_wide()
2649        .collect::<Vec<_>>();
2650    if destination_wide.is_empty() || destination_wide.contains(&0) {
2651        return Err(io::Error::new(
2652            io::ErrorKind::InvalidInput,
2653            "private destination name is empty or contains NUL",
2654        ));
2655    }
2656    let name_bytes = destination_wide
2657        .len()
2658        .checked_mul(std::mem::size_of::<u16>())
2659        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file name is too long"))?;
2660    // Match std's FileRenameInfoEx layout: bytes through FileName, the UTF-16
2661    // path, and one trailing NUL excluded from FileNameLength.
2662    let buffer_bytes = std::mem::offset_of!(FILE_RENAME_INFO, FileName)
2663        .checked_add(name_bytes)
2664        .and_then(|size| size.checked_add(std::mem::size_of::<u16>()))
2665        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file name is too long"))?;
2666    let mut storage = vec![0u64; buffer_bytes.div_ceil(std::mem::size_of::<u64>())];
2667    let rename = storage.as_mut_ptr().cast::<FILE_RENAME_INFO>();
2668    let rename_result = unsafe {
2669        (*rename).Anonymous.Flags =
2670            FILE_RENAME_FLAG_REPLACE_IF_EXISTS | FILE_RENAME_FLAG_POSIX_SEMANTICS;
2671        // SetFileInformationByHandle's reliable Win32 shape is the same one
2672        // used by std: a full path with a null RootDirectory. The retained
2673        // no-delete-share parent handle pins the already-validated directory.
2674        (*rename).RootDirectory = HANDLE::default();
2675        (*rename).FileNameLength = u32::try_from(name_bytes)
2676            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "file name is too long"))?;
2677        std::ptr::copy_nonoverlapping(
2678            destination_wide.as_ptr(),
2679            std::ptr::addr_of_mut!((*rename).FileName).cast::<u16>(),
2680            destination_wide.len(),
2681        );
2682        SetFileInformationByHandle(
2683            HANDLE(temp_file.as_raw_handle() as isize),
2684            FileRenameInfoEx,
2685            rename.cast(),
2686            u32::try_from(buffer_bytes).map_err(|_| {
2687                io::Error::new(io::ErrorKind::InvalidInput, "rename buffer is too large")
2688            })?,
2689        )
2690    };
2691    windows_error_stage(
2692        "publish retained replacement source",
2693        rename_result.map_err(windows_io_error),
2694    )?;
2695
2696    let published = windows_error_stage(
2697        "open published replacement",
2698        windows_open_entry_at_for_hardening(&parent, destination_name),
2699    )?;
2700    windows_validate_file(&published)?;
2701    windows_validate_file_owner(&published)?;
2702    if windows_file_identity(&published)? != temp_identity {
2703        return Err(permission_denied(
2704            "published private file does not match the validated temp descriptor",
2705        ));
2706    }
2707    windows_harden_file_acl(&published)?;
2708    windows_revalidate_entry_at(&parent, destination_name, &published)?;
2709    windows_revalidate_directory_path(parent_path, &parent)
2710}
2711
2712#[cfg(not(any(unix, target_os = "windows")))]
2713fn generic_open_private(path: &Path, mode: PrivateOpenMode) -> io::Result<File> {
2714    if mode != PrivateOpenMode::Read {
2715        if let Some(parent) = path.parent().filter(|path| !path.as_os_str().is_empty()) {
2716            std::fs::create_dir_all(parent)?;
2717        }
2718    }
2719    let mut options = std::fs::OpenOptions::new();
2720    match mode {
2721        PrivateOpenMode::Read => {
2722            options.read(true);
2723        }
2724        PrivateOpenMode::Append => {
2725            options.read(true).append(true).create(true);
2726        }
2727        PrivateOpenMode::Truncate => {
2728            options.read(true).write(true);
2729        }
2730        PrivateOpenMode::CreateNew => {
2731            options.write(true).create_new(true);
2732        }
2733    }
2734    let file = options.open(path)?;
2735    revalidate_private_file(&file)?;
2736    if mode == PrivateOpenMode::Truncate {
2737        file.set_len(0)?;
2738        revalidate_private_file(&file)?;
2739    }
2740    Ok(file)
2741}
2742
2743#[cfg(not(any(unix, target_os = "windows")))]
2744fn generic_harden_private_tree(
2745    _tree: &PrivateTree,
2746    _selected: &[PathBuf],
2747) -> io::Result<PrivateTreeReport> {
2748    Err(io::Error::new(
2749        io::ErrorKind::Unsupported,
2750        "descriptor-root private-tree hardening is unsupported on this platform",
2751    ))
2752}
2753
2754#[cfg(target_os = "windows")]
2755fn harden_windows_acl(path: &Path) -> io::Result<()> {
2756    let metadata = std::fs::symlink_metadata(path)?;
2757    reject_windows_reparse_metadata(&metadata)?;
2758    let file = if metadata.is_dir() {
2759        windows_open_directory_for_hardening(path)?
2760    } else if metadata.is_file() {
2761        windows_open_existing_file_for_hardening(path)?
2762    } else {
2763        return Err(permission_denied(
2764            "owner-private path is not a regular file or directory",
2765        ));
2766    };
2767    windows_validate_hardenable_file_owner(&file)?;
2768    windows_harden_file_acl(&file)?;
2769    if metadata.is_dir() {
2770        windows_revalidate_directory_path(path, &file)
2771    } else {
2772        windows_validate_file(&file)?;
2773        windows_revalidate_private_path(path, &file)
2774    }
2775}
2776
2777#[cfg(target_os = "windows")]
2778fn current_process_default_owner_sid_string() -> std::io::Result<String> {
2779    use windows::Win32::Foundation::{CloseHandle, HANDLE};
2780    use windows::Win32::Security::{GetTokenInformation, TokenOwner, TOKEN_OWNER, TOKEN_QUERY};
2781    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
2782
2783    let windows_error = |error: windows::core::Error| std::io::Error::other(error.to_string());
2784    let mut token = HANDLE::default();
2785    unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }
2786        .map_err(windows_error)?;
2787
2788    let result = (|| {
2789        let mut needed = 0u32;
2790        let _ = unsafe { GetTokenInformation(token, TokenOwner, None, 0, &mut needed) };
2791        if needed == 0 {
2792            return Err(std::io::Error::last_os_error());
2793        }
2794        let words = (needed as usize).div_ceil(std::mem::size_of::<usize>());
2795        let mut buffer = vec![0usize; words];
2796        unsafe {
2797            GetTokenInformation(
2798                token,
2799                TokenOwner,
2800                Some(buffer.as_mut_ptr().cast()),
2801                needed,
2802                &mut needed,
2803            )
2804        }
2805        .map_err(windows_error)?;
2806        let owner = unsafe { &*buffer.as_ptr().cast::<TOKEN_OWNER>() };
2807        windows_sid_string(owner.Owner)
2808    })();
2809
2810    let _ = unsafe { CloseHandle(token) };
2811    result
2812}
2813
2814#[cfg(target_os = "windows")]
2815fn current_process_sid_string() -> std::io::Result<String> {
2816    use windows::core::PWSTR;
2817    use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
2818    use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
2819    use windows::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER};
2820    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
2821
2822    let windows_error = |error: windows::core::Error| std::io::Error::other(error.to_string());
2823    let mut token = HANDLE::default();
2824    unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }
2825        .map_err(windows_error)?;
2826
2827    let result = (|| {
2828        let mut needed = 0u32;
2829        let _ = unsafe { GetTokenInformation(token, TokenUser, None, 0, &mut needed) };
2830        if needed == 0 {
2831            return Err(std::io::Error::last_os_error());
2832        }
2833        let words = (needed as usize).div_ceil(std::mem::size_of::<usize>());
2834        let mut buffer = vec![0usize; words];
2835        unsafe {
2836            GetTokenInformation(
2837                token,
2838                TokenUser,
2839                Some(buffer.as_mut_ptr().cast()),
2840                needed,
2841                &mut needed,
2842            )
2843        }
2844        .map_err(windows_error)?;
2845        let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
2846        let mut sid_text = PWSTR::null();
2847        unsafe { ConvertSidToStringSidW(user.User.Sid, &mut sid_text) }.map_err(windows_error)?;
2848        let sid = unsafe { sid_text.to_string() }
2849            .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error));
2850        unsafe {
2851            let _ = LocalFree(HLOCAL(sid_text.0.cast()));
2852        }
2853        sid
2854    })();
2855
2856    let _ = unsafe { CloseHandle(token) };
2857    result
2858}
2859
2860#[cfg(test)]
2861mod tests {
2862    use super::*;
2863
2864    #[test]
2865    fn windows_private_paths_use_native_handle_bound_security_apis() {
2866        let source = include_str!("secure_path.rs");
2867        assert!(source.contains(&["Create", "DirectoryW"].concat()));
2868        assert!(source.contains(&["Create", "FileW"].concat()));
2869        assert!(source.contains(&["Set", "SecurityInfo"].concat()));
2870        assert!(source.contains(&["SetFileInformation", "ByHandle"].concat()));
2871        assert!(source.contains(&["Flush", "FileBuffers"].concat()));
2872        assert!(source.contains("FILE_GENERIC_WRITE.0 | WRITE_DAC.0"));
2873        assert!(!source.contains(&["Command::new(\"", "icacls", "\")"].concat()));
2874        assert!(!source.contains(&["Move", "FileExW("].concat()));
2875    }
2876
2877    #[test]
2878    fn harden_is_a_noop_on_a_missing_path_and_never_panics() {
2879        // The whole point is best-effort: a bad path must not panic the caller.
2880        harden_owner_only(Path::new("this/path/does/not/exist/xyz"));
2881    }
2882
2883    #[cfg(target_os = "windows")]
2884    fn assert_exact_protected_owner_acl(file: &File) {
2885        windows_validate_file_owner(file).unwrap();
2886        use std::os::windows::io::AsRawHandle;
2887        use windows::Win32::Foundation::{LocalFree, HANDLE, HLOCAL, PSID};
2888        use windows::Win32::Security::Authorization::{GetSecurityInfo, SE_FILE_OBJECT};
2889        use windows::Win32::Security::{
2890            GetAce, GetSecurityDescriptorControl, ACCESS_ALLOWED_ACE, ACL,
2891            DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, SE_DACL_PROTECTED,
2892        };
2893
2894        let mut dacl = std::ptr::null_mut::<ACL>();
2895        let mut descriptor = PSECURITY_DESCRIPTOR::default();
2896        let status = unsafe {
2897            GetSecurityInfo(
2898                HANDLE(file.as_raw_handle() as isize),
2899                SE_FILE_OBJECT,
2900                DACL_SECURITY_INFORMATION,
2901                None,
2902                None,
2903                Some(&mut dacl),
2904                None,
2905                Some(&mut descriptor),
2906            )
2907        };
2908        assert!(status.is_ok());
2909        assert!(!dacl.is_null());
2910
2911        let mut control = 0u16;
2912        let mut revision = 0u32;
2913        unsafe { GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) }.unwrap();
2914        assert_ne!(control & SE_DACL_PROTECTED.0, 0);
2915        assert_eq!(unsafe { (*dacl).AceCount }, 1);
2916
2917        let mut raw_ace = std::ptr::null_mut();
2918        unsafe { GetAce(dacl, 0, &mut raw_ace) }.unwrap();
2919        let ace = unsafe { &*raw_ace.cast::<ACCESS_ALLOWED_ACE>() };
2920        assert_eq!(ace.Header.AceType, 0, "ACE must be access-allowed");
2921        let sid = PSID(std::ptr::addr_of!(ace.SidStart).cast_mut().cast());
2922        let actual_sid = windows_sid_string(sid).unwrap();
2923        assert_eq!(actual_sid, current_process_sid_string().unwrap());
2924
2925        unsafe {
2926            let _ = LocalFree(HLOCAL(descriptor.0));
2927        }
2928    }
2929
2930    #[cfg(target_os = "windows")]
2931    #[test]
2932    fn created_files_and_directories_have_exact_protected_owner_acl() {
2933        let sandbox = tempfile::tempdir().unwrap();
2934        let directory = sandbox.path().join("private");
2935        ensure_private_dir(&directory).unwrap();
2936        let directory_handle = windows_open_directory(&directory).unwrap();
2937        assert_exact_protected_owner_acl(&directory_handle);
2938
2939        let path = directory.join("record.jsonl");
2940        let file = create_private_file(&path).unwrap();
2941        assert_exact_protected_owner_acl(&file);
2942    }
2943
2944    #[cfg(target_os = "windows")]
2945    #[test]
2946    fn windows_first_use_flush_failure_is_not_acknowledged_and_retry_flushes() {
2947        let sandbox = tempfile::tempdir().unwrap();
2948        let directory = sandbox.path().join("private").join("nested");
2949        let failures = PrivatePathDurabilityFailureInjector::default();
2950        failures.fail_next(PrivatePathDurabilityFailurePoint::ParentDirectorySync);
2951
2952        assert!(ensure_private_dir_with_failure_injector(&directory, &failures).is_err());
2953        ensure_private_dir_with_failure_injector(&directory, &failures).unwrap();
2954        // Exercise the existing-final handoff too: its retained no-delete
2955        // handle must carry WRITE_DAC because exact ACL hardening is repeated
2956        // after the identity comparison.
2957        ensure_private_dir_with_failure_injector(&directory, &failures).unwrap();
2958
2959        let path = directory.join("receipt.json");
2960        failures.fail_next(PrivatePathDurabilityFailurePoint::ParentDirectorySync);
2961        assert!(open_private_append_with_failure_injector(&path, &failures).is_err());
2962        let file = open_private_append_with_failure_injector(&path, &failures).unwrap();
2963        assert_exact_protected_owner_acl(&file);
2964    }
2965
2966    #[cfg(target_os = "windows")]
2967    #[test]
2968    fn windows_validated_directory_handoff_swap_fails_closed_and_retries() {
2969        let sandbox = tempfile::tempdir().unwrap();
2970        let directory = sandbox.path().join("private");
2971        ensure_private_dir(&directory).unwrap();
2972        let moved = sandbox.path().join("moved-private");
2973        let child = directory.join("child");
2974        let mut swapped = false;
2975
2976        let error = windows_ensure_private_dir_with_hook(&child, None, |validated| {
2977            if !swapped && validated == directory {
2978                std::fs::rename(&directory, &moved)?;
2979                ensure_private_dir(&directory)?;
2980                swapped = true;
2981            }
2982            Ok(())
2983        })
2984        .unwrap_err();
2985
2986        assert!(swapped, "the exact intermediate directory was substituted");
2987        assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
2988        assert!(!child.exists());
2989        std::fs::remove_dir(&directory).unwrap();
2990        std::fs::rename(&moved, &directory).unwrap();
2991        ensure_private_dir(&child).unwrap();
2992        let retained = windows_open_directory(&directory).unwrap();
2993        windows_validate_exact_private_acl(&retained).unwrap();
2994    }
2995
2996    #[cfg(target_os = "windows")]
2997    #[test]
2998    fn windows_replace_rejects_temp_name_substitution_before_publication() {
2999        use std::io::Write;
3000
3001        let root = tempfile::tempdir().unwrap();
3002        let destination = root.path().join("record.jsonl");
3003        let mut destination_file = create_private_file(&destination).unwrap();
3004        destination_file.write_all(b"old").unwrap();
3005        drop(destination_file);
3006        let temp = root.path().join("validated.tmp");
3007        let moved = root.path().join("moved.tmp");
3008        let mut file = create_private_file(&temp).unwrap();
3009        file.write_all(b"validated").unwrap();
3010        drop(file);
3011
3012        let error = windows_atomic_replace_private_file_with_hook(&temp, &destination, || {
3013            std::fs::rename(&temp, &moved)?;
3014            std::fs::write(&temp, b"substitute")?;
3015            Ok(())
3016        })
3017        .unwrap_err();
3018
3019        assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
3020        assert!(
3021            error
3022                .to_string()
3023                .contains("revalidate retained replacement source before publication"),
3024            "substitution must be rejected before publication: {error}"
3025        );
3026    }
3027
3028    #[cfg(target_os = "windows")]
3029    #[test]
3030    fn handle_relative_replace_blocks_parent_root_swap() {
3031        use std::io::Write;
3032
3033        let sandbox = tempfile::tempdir().unwrap();
3034        let root = sandbox.path().join("private-root");
3035        ensure_private_dir(&root).unwrap();
3036        let destination = root.join("record.jsonl");
3037        std::fs::write(&destination, b"old").unwrap();
3038        let temp = root.join("validated.tmp");
3039        let mut file = create_private_file(&temp).unwrap();
3040        file.write_all(b"validated").unwrap();
3041        drop(file);
3042        let moved_root = sandbox.path().join("moved-root");
3043
3044        windows_atomic_replace_private_file_with_hook(&temp, &destination, || {
3045            assert!(
3046                std::fs::rename(&root, &moved_root).is_err(),
3047                "retained no-delete-share parent handle must block root replacement"
3048            );
3049            Ok(())
3050        })
3051        .unwrap();
3052
3053        assert_eq!(std::fs::read(&destination).unwrap(), b"validated");
3054        assert!(!moved_root.exists());
3055    }
3056
3057    #[cfg(unix)]
3058    fn mode(path: &Path) -> u32 {
3059        use std::os::unix::fs::PermissionsExt;
3060        std::fs::symlink_metadata(path)
3061            .unwrap()
3062            .permissions()
3063            .mode()
3064            & 0o777
3065    }
3066
3067    #[cfg(unix)]
3068    #[test]
3069    fn private_creation_uses_owner_only_modes_from_first_open() {
3070        let root = tempfile::tempdir().unwrap();
3071        let private_dir = root.path().join("nested").join("private");
3072        ensure_private_dir(&private_dir).unwrap();
3073        assert_eq!(mode(&root.path().join("nested")), 0o700);
3074        assert_eq!(mode(&private_dir), 0o700);
3075
3076        let path = private_dir.join("record.jsonl");
3077        let file = create_private_file(&path).unwrap();
3078        assert_eq!(mode(&path), 0o600);
3079        revalidate_private_file(&file).unwrap();
3080    }
3081
3082    #[cfg(unix)]
3083    #[test]
3084    fn nested_private_creation_propagates_parent_sync_failure_and_retries() {
3085        let sandbox = tempfile::tempdir().unwrap();
3086        let nested = sandbox.path().join("private").join("nested");
3087        let failures = PrivatePathDurabilityFailureInjector::default();
3088        failures.fail_next(PrivatePathDurabilityFailurePoint::ParentDirectorySync);
3089
3090        let error = ensure_private_dir_with_failure_injector(&nested, &failures).unwrap_err();
3091        assert_eq!(error.kind(), io::ErrorKind::Other);
3092        assert!(
3093            !nested.exists(),
3094            "failed first component must not acknowledge the tree"
3095        );
3096
3097        ensure_private_dir_with_failure_injector(&nested, &failures).unwrap();
3098        assert_eq!(mode(&sandbox.path().join("private")), 0o700);
3099        assert_eq!(mode(&nested), 0o700);
3100    }
3101
3102    #[cfg(unix)]
3103    #[test]
3104    fn retained_intermediate_rename_is_rejected_before_the_next_child_create() {
3105        let sandbox = tempfile::tempdir().unwrap();
3106        let private = sandbox.path().join("private");
3107        ensure_private_dir(&private).unwrap();
3108        let intermediate = private.join("retained-intermediate");
3109        let moved = private.join("moved-intermediate");
3110        let target = intermediate.join("child");
3111        let mut renamed = false;
3112
3113        let error =
3114            unix_walk_directory_with_hook(&target, true, true, None, |validated_component| {
3115                if !renamed && validated_component == std::ffi::OsStr::new("retained-intermediate")
3116                {
3117                    std::fs::rename(&intermediate, &moved)?;
3118                    renamed = true;
3119                }
3120                Ok(())
3121            })
3122            .unwrap_err();
3123
3124        assert!(
3125            renamed,
3126            "the retained intermediate was renamed by the race hook"
3127        );
3128        assert!(matches!(
3129            error.kind(),
3130            io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
3131        ));
3132        assert!(
3133            !target.exists() && !moved.join("child").exists(),
3134            "the next child must not be created under the moved retained directory"
3135        );
3136
3137        std::fs::rename(&moved, &intermediate).unwrap();
3138        ensure_private_dir(&target).unwrap();
3139        assert!(
3140            target.is_dir(),
3141            "retry on the restored exact chain succeeds"
3142        );
3143    }
3144
3145    #[cfg(unix)]
3146    #[test]
3147    fn new_private_file_propagates_parent_sync_failure_and_append_retry_is_durable() {
3148        use std::io::Write;
3149
3150        let sandbox = tempfile::tempdir().unwrap();
3151        let directory = sandbox.path().join("private");
3152        ensure_private_dir(&directory).unwrap();
3153        let path = directory.join("journal.jsonl");
3154        let failures = PrivatePathDurabilityFailureInjector::default();
3155        failures.fail_next(PrivatePathDurabilityFailurePoint::ParentDirectorySync);
3156
3157        let error = open_private_append_with_failure_injector(&path, &failures).unwrap_err();
3158        assert_eq!(error.kind(), io::ErrorKind::Other);
3159        assert!(
3160            path.exists(),
3161            "the unacknowledged entry remains private for safe retry"
3162        );
3163        assert_eq!(mode(&path), 0o600);
3164
3165        let mut file = open_private_append_with_failure_injector(&path, &failures).unwrap();
3166        file.write_all(b"one\n").unwrap();
3167        file.sync_all().unwrap();
3168        assert_eq!(std::fs::read(&path).unwrap(), b"one\n");
3169    }
3170
3171    #[cfg(unix)]
3172    #[test]
3173    fn opening_owned_permissive_file_hardens_it() {
3174        use std::os::unix::fs::PermissionsExt;
3175
3176        let root = tempfile::tempdir().unwrap();
3177        let path = root.path().join("record.jsonl");
3178        std::fs::write(&path, b"old\n").unwrap();
3179        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
3180
3181        let _file = open_private_append(&path).unwrap();
3182        assert_eq!(mode(&path), 0o600);
3183    }
3184
3185    #[cfg(unix)]
3186    #[test]
3187    fn private_read_hardens_without_changing_content() {
3188        use std::io::Read;
3189        use std::os::unix::fs::PermissionsExt;
3190
3191        let root = tempfile::tempdir().unwrap();
3192        let path = root.path().join("record.jsonl");
3193        std::fs::write(&path, b"historical").unwrap();
3194        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
3195
3196        let mut file = open_private_read(&path).unwrap();
3197        let mut content = String::new();
3198        file.read_to_string(&mut content).unwrap();
3199        assert_eq!(content, "historical");
3200        assert_eq!(mode(&path), 0o600);
3201    }
3202
3203    #[cfg(unix)]
3204    #[test]
3205    fn atomic_replace_publishes_the_validated_private_inode() {
3206        use std::io::Write;
3207
3208        let root = tempfile::tempdir().unwrap();
3209        let destination = root.path().join("record.jsonl");
3210        std::fs::write(&destination, b"old").unwrap();
3211        let temp = root.path().join(".record.random.tmp");
3212        let mut file = create_private_file(&temp).unwrap();
3213        file.write_all(b"new").unwrap();
3214        file.sync_all().unwrap();
3215        drop(file);
3216
3217        atomic_replace_private_file(&temp, &destination).unwrap();
3218        assert_eq!(std::fs::read(&destination).unwrap(), b"new");
3219        assert_eq!(mode(&destination), 0o600);
3220        assert!(!temp.exists());
3221    }
3222
3223    #[cfg(unix)]
3224    #[test]
3225    fn atomic_replace_rejects_temp_path_substitution() {
3226        use std::io::Write;
3227
3228        let root = tempfile::tempdir().unwrap();
3229        let destination = root.path().join("record.jsonl");
3230        std::fs::write(&destination, b"old").unwrap();
3231        let temp = root.path().join(".record.random.tmp");
3232        let moved = root.path().join("validated.tmp");
3233        let mut file = create_private_file(&temp).unwrap();
3234        file.write_all(b"validated").unwrap();
3235        drop(file);
3236
3237        let result = unix_atomic_replace_private_file_with_hook(&temp, &destination, || {
3238            std::fs::rename(&temp, &moved)?;
3239            let mut substitute = create_private_file(&temp)?;
3240            substitute.write_all(b"substitute")?;
3241            Ok(())
3242        });
3243
3244        assert!(result.is_err());
3245        assert_eq!(std::fs::read(&destination).unwrap(), b"old");
3246        assert_eq!(std::fs::read(&moved).unwrap(), b"validated");
3247    }
3248
3249    #[cfg(unix)]
3250    #[test]
3251    fn path_revalidation_rejects_rename_and_substitution() {
3252        let root = tempfile::tempdir().unwrap();
3253        let path = root.path().join("record.jsonl");
3254        let file = open_private_append(&path).unwrap();
3255        revalidate_private_path(&path, &file).unwrap();
3256
3257        let moved = root.path().join("moved.jsonl");
3258        std::fs::rename(&path, &moved).unwrap();
3259        let _substitute = create_private_file(&path).unwrap();
3260        assert!(revalidate_private_path(&path, &file).is_err());
3261    }
3262
3263    #[cfg(unix)]
3264    #[test]
3265    fn private_tree_rejects_root_rename_and_replacement() {
3266        let sandbox = tempfile::tempdir().unwrap();
3267        let root = sandbox.path().join("car");
3268        std::fs::create_dir(&root).unwrap();
3269        let tree = PrivateTree::open(&root).unwrap();
3270
3271        let moved = sandbox.path().join("moved-car");
3272        std::fs::rename(&root, &moved).unwrap();
3273        std::fs::create_dir(&root).unwrap();
3274
3275        assert!(tree.revalidate_root().is_err());
3276        assert!(tree
3277            .harden_selected(&PrivateTreePolicy::root_only())
3278            .is_err());
3279    }
3280
3281    #[cfg(unix)]
3282    #[test]
3283    fn private_tree_retry_is_idempotent_and_does_not_touch_unselected_siblings() {
3284        use std::os::unix::fs::{symlink, PermissionsExt};
3285
3286        let sandbox = tempfile::tempdir().unwrap();
3287        let root = sandbox.path().join("car");
3288        std::fs::create_dir(&root).unwrap();
3289        let good = root.join("a-good.json");
3290        let blocked = root.join("b-blocked.json");
3291        let unrelated = root.join("unrelated.json");
3292        std::fs::write(&good, b"good").unwrap();
3293        symlink(&good, &blocked).unwrap();
3294        std::fs::write(&unrelated, b"unrelated").unwrap();
3295        for path in [&good, &unrelated] {
3296            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644)).unwrap();
3297        }
3298
3299        let tree = PrivateTree::open(&root).unwrap();
3300        let policy = PrivateTreePolicy::selected(["a-good.json", "b-blocked.json"]);
3301        assert!(tree.harden_selected(&policy).is_err());
3302        assert_eq!(mode(&good), 0o600);
3303        assert_eq!(mode(&unrelated), 0o644);
3304
3305        std::fs::remove_file(&blocked).unwrap();
3306        std::fs::write(&blocked, b"repaired").unwrap();
3307        std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o644)).unwrap();
3308        let report = tree.harden_selected(&policy).unwrap();
3309        assert_eq!(report.files_hardened, 2);
3310        assert_eq!(mode(&good), 0o600);
3311        assert_eq!(mode(&blocked), 0o600);
3312        assert_eq!(mode(&unrelated), 0o644);
3313
3314        let retry = tree.harden_selected(&policy).unwrap();
3315        assert_eq!(retry, report);
3316    }
3317
3318    #[cfg(unix)]
3319    #[test]
3320    fn private_tree_recurses_selected_directories_and_rejects_hardlinks() {
3321        use std::os::unix::fs::PermissionsExt;
3322
3323        let sandbox = tempfile::tempdir().unwrap();
3324        let root = sandbox.path().join("car");
3325        let selected = root.join("runs");
3326        std::fs::create_dir_all(&selected).unwrap();
3327        let victim = selected.join("victim.json");
3328        let alias = selected.join("alias.json");
3329        std::fs::write(&victim, b"run").unwrap();
3330        std::fs::hard_link(&victim, &alias).unwrap();
3331        std::fs::set_permissions(&selected, std::fs::Permissions::from_mode(0o755)).unwrap();
3332
3333        let tree = PrivateTree::open(&root).unwrap();
3334        let policy = PrivateTreePolicy::selected(["runs"]);
3335        assert!(tree.harden_selected(&policy).is_err());
3336        assert_eq!(mode(&selected), 0o700);
3337        assert_eq!(std::fs::read(&victim).unwrap(), b"run");
3338    }
3339
3340    #[cfg(unix)]
3341    #[test]
3342    fn private_tree_recursive_retry_has_stable_receipts_and_exact_modes() {
3343        use std::os::unix::fs::PermissionsExt;
3344
3345        let sandbox = tempfile::tempdir().unwrap();
3346        let root = sandbox.path().join("car");
3347        let nested = root.join("runs").join("today");
3348        std::fs::create_dir_all(&nested).unwrap();
3349        let run = nested.join("run.json");
3350        std::fs::write(&run, b"run").unwrap();
3351        std::fs::set_permissions(root.join("runs"), std::fs::Permissions::from_mode(0o755))
3352            .unwrap();
3353        std::fs::set_permissions(&nested, std::fs::Permissions::from_mode(0o755)).unwrap();
3354        std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o644)).unwrap();
3355
3356        let tree = PrivateTree::open(&root).unwrap();
3357        let policy = PrivateTreePolicy::selected(["runs"]);
3358        let first = tree.harden_selected(&policy).unwrap();
3359        let retry = tree.harden_selected(&policy).unwrap();
3360
3361        assert_eq!(first, retry);
3362        assert_eq!(first.directories_hardened, 2);
3363        assert_eq!(first.files_hardened, 1);
3364        assert_eq!(mode(&root), 0o700);
3365        assert_eq!(mode(&root.join("runs")), 0o700);
3366        assert_eq!(mode(&nested), 0o700);
3367        assert_eq!(mode(&run), 0o600);
3368    }
3369
3370    #[cfg(unix)]
3371    #[test]
3372    fn private_tree_rejects_parent_traversal_without_touching_external_file() {
3373        use std::os::unix::fs::PermissionsExt;
3374
3375        let sandbox = tempfile::tempdir().unwrap();
3376        let root = sandbox.path().join("car");
3377        std::fs::create_dir(&root).unwrap();
3378        let external = sandbox.path().join("external.json");
3379        std::fs::write(&external, b"external").unwrap();
3380        std::fs::set_permissions(&external, std::fs::Permissions::from_mode(0o644)).unwrap();
3381
3382        let tree = PrivateTree::open(&root).unwrap();
3383        let policy = PrivateTreePolicy::selected(["../external.json"]);
3384        assert!(tree.harden_selected(&policy).is_err());
3385        assert_eq!(mode(&external), 0o644);
3386        assert_eq!(std::fs::read(&external).unwrap(), b"external");
3387    }
3388
3389    #[cfg(unix)]
3390    #[test]
3391    fn overlapping_exact_selection_still_requires_the_nested_marker() {
3392        let sandbox = tempfile::tempdir().unwrap();
3393        let root = sandbox.path().join("car");
3394        let runs = root.join("runs");
3395        std::fs::create_dir_all(&runs).unwrap();
3396
3397        let tree = PrivateTree::open(&root).unwrap();
3398        let policy = PrivateTreePolicy::selected(["runs", "runs/.nobackup"]);
3399        assert_eq!(
3400            tree.harden_selected(&policy).unwrap_err().kind(),
3401            io::ErrorKind::NotFound
3402        );
3403
3404        std::fs::write(runs.join(".nobackup"), b"").unwrap();
3405        let report = tree.harden_selected(&policy).unwrap();
3406        assert_eq!(report.directories_hardened, 1);
3407        assert_eq!(report.files_hardened, 1);
3408    }
3409
3410    #[cfg(unix)]
3411    #[test]
3412    fn truncate_requires_an_existing_validated_file() {
3413        let root = tempfile::tempdir().unwrap();
3414        let existing = root.path().join("existing.json");
3415        std::fs::write(&existing, b"content").unwrap();
3416
3417        let file = open_private_truncate(&existing).unwrap();
3418        assert_eq!(file.metadata().unwrap().len(), 0);
3419        assert_eq!(mode(&existing), 0o600);
3420
3421        let missing = root.path().join("missing.json");
3422        assert_eq!(
3423            open_private_truncate(&missing).unwrap_err().kind(),
3424            io::ErrorKind::NotFound
3425        );
3426        assert!(!missing.exists());
3427    }
3428
3429    #[cfg(unix)]
3430    #[test]
3431    fn truncate_rejects_a_hardlink_without_touching_victim_bytes() {
3432        let root = tempfile::tempdir().unwrap();
3433        let victim = root.path().join("victim.json");
3434        let alias = root.path().join("alias.json");
3435        std::fs::write(&victim, b"must-survive").unwrap();
3436        std::fs::hard_link(&victim, &alias).unwrap();
3437
3438        assert!(open_private_truncate(&alias).is_err());
3439        assert_eq!(std::fs::read(&victim).unwrap(), b"must-survive");
3440    }
3441
3442    #[cfg(unix)]
3443    #[test]
3444    fn symlink_and_hardlink_files_are_rejected() {
3445        use std::os::unix::fs::symlink;
3446
3447        let root = tempfile::tempdir().unwrap();
3448        let target = root.path().join("target");
3449        std::fs::write(&target, b"target").unwrap();
3450
3451        let link = root.path().join("link");
3452        symlink(&target, &link).unwrap();
3453        assert!(open_private_append(&link).is_err());
3454
3455        let hardlink = root.path().join("hardlink");
3456        std::fs::hard_link(&target, &hardlink).unwrap();
3457        assert!(open_private_append(&hardlink).is_err());
3458    }
3459
3460    #[cfg(unix)]
3461    #[test]
3462    fn symlink_directory_component_is_rejected() {
3463        use std::os::unix::fs::symlink;
3464
3465        let root = tempfile::tempdir().unwrap();
3466        let real = root.path().join("real");
3467        std::fs::create_dir(&real).unwrap();
3468        let link = root.path().join("link");
3469        symlink(&real, &link).unwrap();
3470
3471        assert!(ensure_private_dir(&link.join("child")).is_err());
3472        assert!(create_private_file(&link.join("record")).is_err());
3473    }
3474
3475    #[test]
3476    fn create_private_file_never_reuses_an_existing_name() {
3477        let root = tempfile::tempdir().unwrap();
3478        let path = root.path().join("record");
3479        let _file = create_private_file(&path).unwrap();
3480        assert_eq!(
3481            create_private_file(&path).unwrap_err().kind(),
3482            std::io::ErrorKind::AlreadyExists
3483        );
3484    }
3485}