Skip to main content

runner_manager_platform/
runner_root_access.rs

1// owner: b2-windows-root-acl
2
3//! Who may write inside the **platform default** runner root, and how that is
4//! established.
5//!
6//! [`runner_root`](crate::runner_root) decides *where* runner workspaces go and
7//! deliberately mutates nothing: `02-target-architecture.md` keeps "directory
8//! creation and the narrowly scoped default-root ACL operation" as explicit
9//! application steps that happen after validation passes. This module is that
10//! step, and it is the only place in the workspace that creates or
11//! re-permissions a runner root.
12//!
13//! # The threat, stated exactly
14//!
15//! `04-security-recovery.md` lists it in one line: *"`%SystemDrive%\rman` is
16//! writable by unrelated local users."* That is not hypothetical, it is the
17//! **default** on every Windows host. The security descriptor of `C:\` carries
18//! an inherit-only ACE of roughly this shape:
19//!
20//! ```text
21//! (A;OICIIO;SDGXGWGR;;;AU)
22//! ```
23//!
24//! — *Authenticated Users*, delete plus generic read/write/execute, inherited
25//! by every child of `C:\`. A directory created there with inheritance left on
26//! is therefore writable by every account that can log in, including the
27//! account a hostile workflow's own leftovers could be running as. Runner
28//! workspaces are executable content that a later job re-enters, so this is a
29//! code-execution boundary rather than a tidiness one.
30//!
31//! The control is one character: `P`, the `SE_DACL_PROTECTED` flag, which
32//! severs inheritance. Everything else in this module exists to apply it
33//! **without ever widening anything**, to prove afterwards that it took, and to
34//! refuse rather than adopt a directory that was already open.
35//!
36//! # What is admitted, and why that is the minimum
37//!
38//! | Trustee | Rights | Why it cannot be dropped |
39//! |---|---|---|
40//! | `SY` — LocalSystem | Full control, inherited | A boot registration runs as LocalSystem and must create, materialize and clean attempt directories |
41//! | `BA` — Administrators | Full control, inherited | `07-security.md` already places a local administrator outside this threat model; without it an operator cannot clean up after a service account they are not logged in as |
42//! | the selected account | [`ADMITTED_RIGHTS`], inherited | A login task or a foreground daemon runs as an ordinary user whose token contains neither of the above |
43//!
44//! The third row is load-bearing in a way that is easy to miss. A login-mode
45//! registration is a Task Scheduler task rendered with
46//! `RunLevel = LeastPrivilege` (see [`crate::service::windows_scheduled_task_xml`]),
47//! so it runs under the operator's **filtered** token — in which the
48//! Administrators group is present but *deny-only*. A DACL naming only `SY` and
49//! `BA` therefore grants such a task nothing at all, even when the operator is
50//! an administrator. The explicit per-account ACE is what makes login mode work,
51//! and it is also why a mode change has to reconcile it: the account admitted
52//! for login mode is not the account boot mode needs.
53//!
54//! # Why the account gets modify rather than full control
55//!
56//! `FA` is `FILE_ALL_ACCESS`, which includes `WRITE_DAC` and `WRITE_OWNER` — the
57//! two rights that would let the admitted account undo the protection this
58//! module exists to apply. [`ADMITTED_RIGHTS`] is read, write, execute and
59//! delete, which is everything "create a child, materialize a runner into it,
60//! and clean it up again" needs and nothing that can re-open the root. Deleting
61//! a whole attempt tree works because the inherited ACE grants `DELETE` on every
62//! entry below, which is what `remove_dir_all` actually requires; the parent's
63//! `FILE_DELETE_CHILD` is a convenience this deliberately does not grant.
64//!
65//! # Custom roots are read, never rewritten
66//!
67//! An operator's `host set-runtime-root` path is theirs. This module offers no
68//! public function that applies a security descriptor to a caller-chosen path:
69//! [`ensure_default_root`] resolves [`crate::runner_root::default_runner_root`]
70//! itself and takes no path at all, and [`report`] — the entry point for a
71//! configured custom root — only reads. That is the whole of "custom roots are
72//! never re-ACLed", enforced by the shape of the API rather than by a check
73//! somebody has to remember to write.
74//!
75//! # Everything decidable is decided purely
76//!
77//! [`default_root_sddl`], [`grants_broad_write`], [`admits_exactly`] and
78//! [`redact`] are pure functions over text with no `cfg`, no privileges and no
79//! filesystem, for the reason this crate gives everywhere else: a Linux CI leg
80//! can assert the exact descriptor a Windows host will write, and the one test
81//! that needs a real DACL is the privileged one that has a real machine.
82
83use std::collections::{BTreeMap, BTreeSet};
84use std::fmt;
85use std::io;
86use std::path::{Path, PathBuf};
87
88#[cfg(windows)]
89use runner_manager_domain::path::LocalAbsolutePath;
90
91use crate::paths::AppPaths;
92#[cfg(windows)]
93use crate::runner_root::RootPreflight;
94use crate::runner_root::{RootOwner, RunnerRootError};
95
96// ---------------------------------------------------------------------------
97// The descriptor
98// ---------------------------------------------------------------------------
99
100/// The rights the selected login or foreground account is admitted with.
101///
102/// `FR` `FW` `FX` `SD` — `FILE_GENERIC_READ`, `FILE_GENERIC_WRITE`,
103/// `FILE_GENERIC_EXECUTE` and `DELETE`. See this module's documentation for why
104/// this is deliberately not `FA`.
105pub const ADMITTED_RIGHTS: &str = "FRFWFXSD";
106
107/// The inheritance flags every ACE this module writes carries.
108///
109/// `OI` `CI` — object inherit and container inherit, with no `IO`: the ACE
110/// applies to the root itself *and* propagates to every file and directory
111/// created below it. Both halves are required. Without the propagation a
112/// service could create `<root>\<attempt>` and then be unable to write inside
113/// it; without the ACE applying to the root itself it could not create the
114/// child in the first place.
115pub const INHERITANCE: &str = "OICI";
116
117/// Which account, beyond the two constants, the root must admit.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum RootAdmission {
120    /// A boot registration. LocalSystem is already `SY`, so nothing is added.
121    LocalSystem,
122    /// A login registration or a foreground daemon, named by its SID.
123    ///
124    /// A SID rather than a name for the reason [`crate::secrets`] gives at
125    /// length: a DACL that describes an account and hopes the description still
126    /// fits is a DACL that stops granting what it was written to grant.
127    Account(String),
128}
129
130impl RootAdmission {
131    /// The account this process is running as.
132    ///
133    /// This is the foreground-daemon and login-mode answer:
134    /// `03-migration-rollout.md` has both of those "attempt ordinary creation",
135    /// and ordinary creation by an ordinary account is exactly the case the
136    /// third ACE exists for.
137    ///
138    /// # Errors
139    /// [`RootAccessError::Identity`] when this process's own token cannot be
140    /// read, which is the one failure that leaves nothing sensible to admit.
141    #[cfg(windows)]
142    pub fn of_this_account() -> Result<Self, RootAccessError> {
143        crate::process::current_user_sid()
144            .map(Self::Account)
145            .map_err(|source| RootAccessError::Identity { source })
146    }
147
148    /// The SID this admission adds, when it adds one.
149    #[must_use]
150    pub fn sid(&self) -> Option<&str> {
151        match self {
152            Self::LocalSystem => None,
153            Self::Account(sid) => Some(sid),
154        }
155    }
156
157    /// Who the resulting descriptor admits, in the vocabulary `service status`
158    /// already uses.
159    #[must_use]
160    pub fn admits(&self) -> Vec<AdmittedTrustee> {
161        let mut admitted = vec![
162            AdmittedTrustee::LocalSystem,
163            AdmittedTrustee::Administrators,
164        ];
165        if matches!(self, Self::Account(_)) {
166            admitted.push(AdmittedTrustee::SelectedAccount);
167        }
168        admitted
169    }
170}
171
172/// One trustee a runner root admits, named without naming an account.
173///
174/// The task's scope note is *"add privileged inspection output without exposing
175/// identities beyond what existing service status already reports"*, and what
176/// `service status` already reports is [`crate::service::ServiceAccount`] —
177/// `NT AUTHORITY\SYSTEM`, or the words "the invoking user". So this is an enum
178/// of the same three ideas rather than a list of SIDs, and no caller can print
179/// one by accident.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
181pub enum AdmittedTrustee {
182    /// `SY`, `NT AUTHORITY\SYSTEM`.
183    LocalSystem,
184    /// `BA`, the local Administrators group.
185    Administrators,
186    /// The login or foreground account the registration selected.
187    SelectedAccount,
188}
189
190impl fmt::Display for AdmittedTrustee {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.write_str(match self {
193            Self::LocalSystem => "NT AUTHORITY\\SYSTEM",
194            Self::Administrators => "the local Administrators group",
195            Self::SelectedAccount => "the invoking user",
196        })
197    }
198}
199
200/// The security descriptor the default runner root is created and reconciled
201/// with, in SDDL.
202///
203/// Split out and public for the same reason [`crate::secrets`] splits out its
204/// own: a test asserts the exact string rather than inferring it from a file,
205/// and a reviewer reads one line instead of three API calls.
206#[must_use]
207pub fn default_root_sddl(admission: &RootAdmission) -> String {
208    // `P` first, because it is the entire control. `D:P` discards whatever the
209    // volume root would otherwise inherit into this directory.
210    let mut sddl = format!("D:P(A;{INHERITANCE};FA;;;SY)(A;{INHERITANCE};FA;;;BA)");
211    if let Some(sid) = admission.sid()
212        && !already_admitted(sid)
213    {
214        sddl.push_str(&format!("(A;{INHERITANCE};{ADMITTED_RIGHTS};;;{sid})"));
215    }
216    sddl
217}
218
219/// Whether the two constant ACEs already cover this SID.
220///
221/// A daemon running as LocalSystem reads its own SID as `S-1-5-18` and would
222/// otherwise add a third ACE for the trustee the first one names. The same
223/// applies to a caller that hands over the Administrators group.
224fn already_admitted(sid: &str) -> bool {
225    const COVERED: [&str; 4] = ["SY", "BA", SID_LOCAL_SYSTEM, SID_ADMINISTRATORS];
226    COVERED.iter().any(|known| sid.eq_ignore_ascii_case(known))
227}
228
229/// `NT AUTHORITY\SYSTEM`.
230const SID_LOCAL_SYSTEM: &str = "S-1-5-18";
231/// `BUILTIN\Administrators`.
232const SID_ADMINISTRATORS: &str = "S-1-5-32-544";
233
234// ---------------------------------------------------------------------------
235// Reading a descriptor back
236// ---------------------------------------------------------------------------
237
238/// The access mask bits that amount to "may change what is in this directory,
239/// or who may".
240///
241/// Spelled as bits rather than as SDDL abbreviations because the abbreviations
242/// are not self-describing: `LC` and `DC` are the directory-service names for
243/// `0x4` and `0x2`, which on a filesystem object are `FILE_ADD_SUBDIRECTORY`
244/// and `FILE_ADD_FILE` — and those two are exactly how `C:\` grants ordinary
245/// users the ability to create things. Matching on the letters would have
246/// missed them.
247///
248/// | Bit | Right |
249/// |---|---|
250/// | `0x1000_0000` | `GENERIC_ALL` |
251/// | `0x4000_0000` | `GENERIC_WRITE` |
252/// | `0x0008_0000` | `WRITE_OWNER` |
253/// | `0x0004_0000` | `WRITE_DAC` |
254/// | `0x0001_0000` | `DELETE` |
255/// | `0x0000_0100` | `FILE_WRITE_ATTRIBUTES` |
256/// | `0x0000_0040` | `FILE_DELETE_CHILD` |
257/// | `0x0000_0010` | `FILE_WRITE_EA` |
258/// | `0x0000_0004` | `FILE_APPEND_DATA` / `FILE_ADD_SUBDIRECTORY` |
259/// | `0x0000_0002` | `FILE_WRITE_DATA` / `FILE_ADD_FILE` |
260pub const WRITE_MASK: u32 = 0x1000_0000
261    | 0x4000_0000
262    | 0x0008_0000
263    | 0x0004_0000
264    | 0x0001_0000
265    | 0x0000_0100
266    | 0x0000_0040
267    | 0x0000_0010
268    | 0x0000_0004
269    | 0x0000_0002;
270
271/// The trustees that mean "more or less anybody who can log in here", in both
272/// the alias form SDDL is written in and the raw form the converter may hand
273/// back instead.
274///
275/// The same list [`crate::process`] uses for the *read* question, plus Guests.
276/// `CO` (CREATOR OWNER) is deliberately absent: inherited, it grants each
277/// child's own creator access to that child, which is not a grant to an
278/// unrelated user.
279const BROAD_TRUSTEES: &[&str] = &[
280    "WD",           // Everyone
281    "S-1-1-0",      // Everyone
282    "AU",           // Authenticated Users
283    "S-1-5-11",     // Authenticated Users
284    "BU",           // Builtin Users
285    "S-1-5-32-545", // Builtin Users
286    "BG",           // Guests
287    "S-1-5-32-546", // Guests
288    "DU",           // Domain Users
289    "IU",           // Interactive
290    "S-1-5-4",      // Interactive
291    "AN",           // Anonymous
292    "S-1-5-7",      // Anonymous
293    "WR",           // Write Restricted
294    "LU",           // Performance Log Users
295];
296
297/// One access-control entry, as SDDL spells it.
298///
299/// `(type;flags;rights;object;inherit_object;trustee)`, and the three fields
300/// this module has an opinion about.
301struct Ace<'a> {
302    kind: &'a str,
303    rights: &'a str,
304    trustee: &'a str,
305}
306
307/// Every ACE of the `D:` part of a security descriptor.
308///
309/// `None` when there is no `D:` at all, which Windows reads as "everyone, full
310/// control" and which therefore may never be confused with "an empty list of
311/// ACEs".
312fn aces(descriptor: &str) -> Option<Vec<Ace<'_>>> {
313    let body = descriptor.split("D:").nth(1)?;
314    // The other spelling of the same thing. `NO_ACCESS_CONTROL` is how SDDL
315    // renders a **NULL** DACL, which grants everyone everything — so it is `D:`
316    // present and no access control at all, not `D:` present with no entries.
317    // Read as a flags field it parses to zero ACEs, which would read back as
318    // the narrowest possible directory rather than the widest.
319    if body
320        .split('(')
321        .next()
322        .is_some_and(|flags| flags.contains("NO_ACCESS_CONTROL"))
323    {
324        return None;
325    }
326    Some(
327        body.split('(')
328            .skip(1)
329            .filter_map(|ace| {
330                let ace = ace.split(')').next()?;
331                let fields: Vec<&str> = ace.split(';').collect();
332                Some(Ace {
333                    kind: fields.first()?.trim(),
334                    rights: fields.get(2)?.trim(),
335                    trustee: fields.get(5)?.trim(),
336                })
337            })
338            .collect(),
339    )
340}
341
342impl Ace<'_> {
343    /// Whether this entry grants rather than denies or audits.
344    ///
345    /// Every allow type starts with `A` (`A`, `OA`, `XA`), and so does the audit
346    /// type `AU`. Audit entries live in the `S:` part and cannot appear here,
347    /// but treating one as a grant if it somehow did is the direction that fails
348    /// closed, and it is the rule [`crate::process`] already applies to the read
349    /// question.
350    fn is_allow(&self) -> bool {
351        self.kind.starts_with('A') || (self.kind.starts_with('X') && self.kind.contains('A'))
352    }
353
354    /// Whether this entry grants anything in [`WRITE_MASK`].
355    ///
356    /// An unreadable rights field counts as granting write. A descriptor this
357    /// cannot parse is a descriptor this cannot vouch for, and the caller's
358    /// response to `true` is to refuse rather than to widen.
359    fn grants_write(&self) -> bool {
360        rights_mask(self.rights).is_none_or(|mask| mask & WRITE_MASK != 0)
361    }
362}
363
364/// The access mask an SDDL rights field denotes.
365///
366/// `None` for a field this does not recognise, which every caller treats as
367/// "assume the worst".
368fn rights_mask(field: &str) -> Option<u32> {
369    if field.is_empty() {
370        return Some(0);
371    }
372    if !field.is_ascii() {
373        return None;
374    }
375    if let Some(hex) = field
376        .strip_prefix("0x")
377        .or_else(|| field.strip_prefix("0X"))
378    {
379        return u32::from_str_radix(hex, 16).ok();
380    }
381    if !field.len().is_multiple_of(2) {
382        return None;
383    }
384    let mut mask = 0u32;
385    for index in (0..field.len()).step_by(2) {
386        let token = field[index..index + 2].to_ascii_uppercase();
387        mask |= match token.as_str() {
388            // Generic.
389            "GA" => 0x1000_0000,
390            "GR" => 0x8000_0000,
391            "GW" => 0x4000_0000,
392            "GX" => 0x2000_0000,
393            // Standard.
394            "SD" => 0x0001_0000,
395            "RC" => 0x0002_0000,
396            "WD" => 0x0004_0000,
397            "WO" => 0x0008_0000,
398            // Object-specific bits, under their directory-service names. On a
399            // filesystem object these are the FILE_* rights of the same value.
400            "CC" => 0x0000_0001,
401            "DC" => 0x0000_0002,
402            "LC" => 0x0000_0004,
403            "SW" => 0x0000_0008,
404            "RP" => 0x0000_0010,
405            "WP" => 0x0000_0020,
406            "DT" => 0x0000_0040,
407            "LO" => 0x0000_0080,
408            "CR" => 0x0000_0100,
409            // File and directory.
410            "FA" => 0x001F_01FF,
411            "FR" => 0x0012_0089,
412            "FW" => 0x0012_0116,
413            "FX" => 0x0012_00A0,
414            // Registry, which cannot name a directory but is cheap to accept.
415            "KA" => 0x000F_003F,
416            "KR" | "KX" => 0x0002_0019,
417            "KW" => 0x0002_0006,
418            _ => return None,
419        };
420    }
421    Some(mask)
422}
423
424/// Whether a DACL lets a local user unrelated to this product write inside the
425/// object it protects.
426///
427/// This is the security preflight `04-security-recovery.md` requires and the
428/// reason an existing root can fail an install. It is the *write* counterpart of
429/// the read question [`crate::process::permissions_summary`] answers, and the
430/// two differ in more than the mask: a directory whose DACL is merely readable
431/// is a diagnostic, while one that is writable is an execution boundary.
432///
433/// Inheritance flags are ignored on purpose. An inherit-only broad ACE grants
434/// nothing on the root and everything on the attempt directories created below
435/// it, which is the half that matters.
436#[must_use]
437pub fn grants_broad_write(descriptor: &str) -> bool {
438    let Some(aces) = aces(descriptor) else {
439        // No DACL is not an empty DACL. Windows treats an object with no
440        // discretionary access control as granting everyone everything, and
441        // this is not a question to be optimistic about.
442        return true;
443    };
444    aces.iter().any(|ace| {
445        ace.is_allow()
446            && BROAD_TRUSTEES
447                .iter()
448                .any(|broad| ace.trustee.eq_ignore_ascii_case(broad))
449            && ace.grants_write()
450    })
451}
452
453/// Whether a DACL carries `SE_DACL_PROTECTED`, so that nothing is inherited into
454/// it from the volume root.
455#[must_use]
456pub fn is_protected(descriptor: &str) -> bool {
457    descriptor.split("D:").nth(1).is_some_and(|body| {
458        body.chars()
459            .take_while(|character| *character != '(')
460            .any(|character| character == 'P')
461    })
462}
463
464/// Every trustee a DACL grants write access to, canonicalised.
465#[must_use]
466pub fn write_trustees(descriptor: &str) -> BTreeSet<String> {
467    write_grants(descriptor).into_keys().collect()
468}
469
470/// The same, with what each trustee is granted.
471///
472/// A rights field this cannot parse contributes `u32::MAX` rather than nothing,
473/// so a descriptor that cannot be read is a descriptor that never compares
474/// equal to the one this module writes — which costs a rewrite and cannot cost
475/// an under-reconciled root.
476fn write_grants(descriptor: &str) -> BTreeMap<String, u32> {
477    let mut grants: BTreeMap<String, u32> = BTreeMap::new();
478    for ace in aces(descriptor).unwrap_or_default() {
479        if ace.is_allow() && ace.grants_write() {
480            *grants.entry(canonical_trustee(ace.trustee)).or_default() |=
481                rights_mask(ace.rights).unwrap_or(u32::MAX);
482        }
483    }
484    grants
485}
486
487/// One spelling for the two trustees that have a fixed SID.
488///
489/// Only those two. An account SID is machine-specific and Windows renders some
490/// of them back as aliases it chose — the built-in Administrator's
491/// `S-1-5-21-…-500` reads back as `LA`, which is the round trip that already
492/// cost [`crate::secrets`] a bug — so no attempt is made to canonicalise one.
493/// [`admits_exactly`] is written to be safe when that comparison fails.
494fn canonical_trustee(trustee: &str) -> String {
495    let upper = trustee.to_ascii_uppercase();
496    match upper.as_str() {
497        "SY" => SID_LOCAL_SYSTEM.to_owned(),
498        "BA" => SID_ADMINISTRATORS.to_owned(),
499        _ => upper,
500    }
501}
502
503/// Whether a DACL is already exactly what [`default_root_sddl`] would write.
504///
505/// Used only to skip a rewrite that would change nothing. A false negative
506/// costs one `SetNamedSecurityInfoW` — which is why an alias Windows substituted
507/// for an account SID is allowed to produce one — and a false positive would
508/// leave the root under-reconciled after a mode change, which is why the
509/// comparison is equality rather than "contains what is needed".
510///
511/// The rights are compared as well as the trustees, and that is not
512/// fastidiousness. A root that already names `SY`, `BA` and the selected
513/// account but grants the third `FA` matches on trustees alone, and `FA`
514/// carries the `WRITE_DAC` and `WRITE_OWNER` that [`ADMITTED_RIGHTS`] exists to
515/// withhold — so accepting it would leave the admitted account able to undo the
516/// protection this module applied. Masks rather than text, because Windows
517/// renders `FRFWFXSD` back as `0x1301bf`.
518#[must_use]
519pub fn admits_exactly(descriptor: &str, admission: &RootAdmission) -> bool {
520    if !is_protected(descriptor) {
521        return false;
522    }
523    // Both constants are spellings `rights_mask` recognises, so neither
524    // fallback is reachable; they are the same fail-closed one `write_grants`
525    // documents, so an unreadable expectation could only ever cost a rewrite.
526    let full_control = rights_mask("FA").unwrap_or(u32::MAX);
527    let mut expected: BTreeMap<String, u32> = [
528        (SID_LOCAL_SYSTEM.to_owned(), full_control),
529        (SID_ADMINISTRATORS.to_owned(), full_control),
530    ]
531    .into_iter()
532    .collect();
533    if let Some(sid) = admission.sid() {
534        // `or_default` and `|=` rather than `insert`, because a daemon running
535        // as LocalSystem names a SID the first entry already carries, and
536        // `default_root_sddl` writes no second ACE for it.
537        *expected.entry(canonical_trustee(sid)).or_default() |=
538            rights_mask(ADMITTED_RIGHTS).unwrap_or(u32::MAX);
539    }
540    write_grants(descriptor) == expected
541}
542
543/// A security descriptor with account SIDs reduced to the fact that they are
544/// account SIDs.
545///
546/// `S-1-5-21-<machine or domain>-<rid>` identifies a machine and a user. The
547/// well-known trustees do not identify anything — `SY` and `BA` are the same
548/// two words on every host — so they survive, and what an operator sees is the
549/// *shape* of the access control without a new identity in it.
550#[must_use]
551pub fn redact(descriptor: &str) -> String {
552    /// The authority every machine-local and domain account SID starts with.
553    const PREFIX: &str = "S-1-5-21-";
554
555    let mut out = String::with_capacity(descriptor.len());
556    let mut rest = descriptor;
557    while let Some(start) = rest.find(PREFIX) {
558        out.push_str(&rest[..start]);
559        out.push_str("S-1-5-21-<account>");
560        // Past the prefix before looking for the end of the sub-authorities.
561        // Resuming at `start` would find `S` — neither a digit nor a dash —
562        // conclude that the SID is zero characters long, and match the same
563        // prefix again on the next pass, forever.
564        let tail = &rest[start + PREFIX.len()..];
565        let end = tail
566            .find(|character: char| !character.is_ascii_digit() && character != '-')
567            .unwrap_or(tail.len());
568        rest = &tail[end..];
569    }
570    out.push_str(rest);
571    out
572}
573
574// ---------------------------------------------------------------------------
575// Errors
576// ---------------------------------------------------------------------------
577
578/// Why the default runner root could not be created, inspected, or reconciled.
579///
580/// Every variant is printed straight at an operator, so each says what to do
581/// next. None carries an account SID: a message an operator pastes into an issue
582/// should not be the thing that publishes their machine's identifiers, which is
583/// what [`redact`] is applied for before a descriptor reaches one of these.
584#[derive(Debug, thiserror::Error)]
585pub enum RootAccessError {
586    /// The default root could not be resolved, or failed `b1`'s preflight.
587    #[error("{source}")]
588    Resolve {
589        /// What `b1` reported.
590        #[source]
591        source: Box<RunnerRootError>,
592    },
593
594    /// This process's own account could not be identified.
595    #[error(
596        "this account's identity could not be read, so the runner root cannot be given the \
597         access a login-mode registration needs: {source}"
598    )]
599    Identity {
600        /// What reading the process token reported.
601        #[source]
602        source: io::Error,
603    },
604
605    /// The root already exists and is open to ordinary local users.
606    ///
607    /// The refusal is the point. Tightening the directory instead would silently
608    /// adopt whatever is already inside one that any local account could have
609    /// created and filled, and a runner root's contents are executed.
610    #[error(
611        "{} already exists and grants write access to ordinary local users, so it is not a \
612         safe place to run jobs: its access control is {dacl}. This is what a directory \
613         created below {} with inheritance left on looks like, and it is refused rather \
614         than tightened because the contents of a directory anybody could write cannot be \
615         trusted. Remove or empty it and run this again, or point the runner root somewhere \
616         this account controls with `{remediation}`.",
617        path.display(),
618        volume.display()
619    )]
620    BroadExistingAccess {
621        /// The root.
622        path: PathBuf,
623        /// Its DACL, in SDDL, with account SIDs redacted.
624        dacl: String,
625        /// The volume the inherited grant would have come from.
626        volume: PathBuf,
627        /// The command that configures a different root.
628        remediation: String,
629    },
630
631    /// The root does not exist and could not be created.
632    #[error(
633        "the default runner root {} could not be created: {source}. Create it as an \
634         administrator, or configure a directory this account owns with `{remediation}`.",
635        path.display()
636    )]
637    Create {
638        /// The root.
639        path: PathBuf,
640        /// What the operating system reported.
641        #[source]
642        source: io::Error,
643        /// The command that configures a different root.
644        remediation: String,
645    },
646
647    /// The root exists but its access control could not be read.
648    #[error(
649        "the access control of the default runner root {} could not be read: {source}. \
650         Without it there is no way to tell whether unrelated local users can write there, \
651         so this fails closed. Read it as an administrator, or configure a directory this \
652         account owns with `{remediation}`.",
653        path.display()
654    )]
655    Inspect {
656        /// The root.
657        path: PathBuf,
658        /// What the operating system reported.
659        #[source]
660        source: io::Error,
661        /// The command that configures a different root.
662        remediation: String,
663    },
664
665    /// The root exists and is not open, but could not be reconciled.
666    #[error(
667        "the access control of the default runner root {} could not be applied: {source}. \
668         Changing a directory's access control needs WRITE_DAC, which this account has \
669         only as its owner or as an administrator — an elevated shell is the usual answer. \
670         Otherwise configure a directory this account owns with `{remediation}`.",
671        path.display()
672    )]
673    Apply {
674        /// The root.
675        path: PathBuf,
676        /// What the operating system reported.
677        #[source]
678        source: io::Error,
679        /// The command that configures a different root.
680        remediation: String,
681    },
682}
683
684impl RootAccessError {
685    /// The root the failure is about, when it is about one.
686    #[must_use]
687    pub fn path(&self) -> Option<&Path> {
688        match self {
689            Self::Resolve { .. } | Self::Identity { .. } => None,
690            Self::BroadExistingAccess { path, .. }
691            | Self::Create { path, .. }
692            | Self::Inspect { path, .. }
693            | Self::Apply { path, .. } => Some(path),
694        }
695    }
696}
697
698/// The command an operator runs to move the runner root elsewhere.
699///
700/// Every caller outside the tests is `reconcile`, which is Windows-only and is
701/// the only place that builds an error carrying one. Named without an intra-doc
702/// link for exactly that reason: off Windows there is no such item to link to.
703#[cfg_attr(
704    not(windows),
705    allow(
706        dead_code,
707        reason = "only `reconcile`, which is Windows-only, builds an error that carries a remedy"
708    )
709)]
710fn remediation() -> String {
711    RootOwner::Host.remediation()
712}
713
714// ---------------------------------------------------------------------------
715// What happened
716// ---------------------------------------------------------------------------
717
718/// What creating or reconciling the default runner root amounted to.
719///
720/// Carried out of `service install` and `service set-start-mode` so a command
721/// can print it. It names trustees by [`AdmittedTrustee`] rather than by SID, so
722/// printing the whole value adds no identity to the output.
723#[derive(Debug, Clone, PartialEq, Eq)]
724pub enum RootAccessSummary {
725    /// Not a Windows host. Nothing was created and nothing was re-permissioned.
726    ///
727    /// macOS and Linux keep the runner root the application-data runtime
728    /// directory has always been, and its permissions are the ones
729    /// [`AppPaths`] already establishes. There is no inherited-broad-grant
730    /// problem to solve, and inventing one would move live workspaces for no
731    /// reason (`02-target-architecture.md`, "Platform defaults").
732    NotApplicable,
733    /// The root did not exist and was created with its access control applied by
734    /// the call that created it.
735    Created {
736        /// The root.
737        path: PathBuf,
738        /// Who it admits.
739        admits: Vec<AdmittedTrustee>,
740    },
741    /// The root existed and already admitted exactly the right trustees.
742    AlreadyReconciled {
743        /// The root.
744        path: PathBuf,
745        /// Who it admits.
746        admits: Vec<AdmittedTrustee>,
747    },
748    /// The root existed and its access control was rewritten.
749    Reconciled {
750        /// The root.
751        path: PathBuf,
752        /// Who it admits now.
753        admits: Vec<AdmittedTrustee>,
754    },
755}
756
757impl RootAccessSummary {
758    /// The root, when there was one.
759    #[must_use]
760    pub fn path(&self) -> Option<&Path> {
761        match self {
762            Self::NotApplicable => None,
763            Self::Created { path, .. }
764            | Self::AlreadyReconciled { path, .. }
765            | Self::Reconciled { path, .. } => Some(path),
766        }
767    }
768
769    /// Whether this operation created the directory.
770    #[must_use]
771    pub const fn created(&self) -> bool {
772        matches!(self, Self::Created { .. })
773    }
774}
775
776impl fmt::Display for RootAccessSummary {
777    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
778        let (verb, path, admits) = match self {
779            Self::NotApplicable => {
780                // Said without claiming *which* root it is about. This is both
781                // the macOS and Linux answer, where the root is the runtime
782                // directory `AppPaths` already permissions, and the Windows
783                // answer for an operation that moved nothing — and on Windows
784                // the runner root is emphatically not the runtime directory.
785                return f.write_str(
786                    "The runner root's access control was not created or changed by this \
787                     operation.",
788                );
789            }
790            Self::Created { path, admits } => ("was created admitting", path, admits),
791            Self::AlreadyReconciled { path, admits } => ("already admitted", path, admits),
792            Self::Reconciled { path, admits } => ("was reconciled to admit", path, admits),
793        };
794        let names: Vec<String> = admits.iter().map(ToString::to_string).collect();
795        write!(
796            f,
797            "The runner root {} {verb} {}, and inherits nothing from the volume above it, so \
798             unrelated local users cannot write there.",
799            path.display(),
800            names.join(", ")
801        )
802    }
803}
804
805/// A [`RootAccessSummary`] plus what it would take to undo.
806///
807/// The task requires directory and ACL work to be "transactional where current
808/// service installation rollback supports it". `service install` already rolls
809/// back a registration when the record cannot be written, so this is the same
810/// idea for the two effects this module has:
811///
812/// * a directory this call created can be removed again, and
813/// * a descriptor this call replaced can be written back, because the previous
814///   one was read first and kept.
815///
816/// What cannot be undone is a directory that was already there — and, per the
817/// same requirement, that is *reported* rather than pretended about. See
818/// [`Reversal`].
819#[derive(Debug, Clone)]
820pub struct RootAccessChange {
821    summary: RootAccessSummary,
822    /// Compiled where it is read rather than allowed where it is not.
823    ///
824    /// Only [`Self::revert`]'s Windows arm ever reads this, and only
825    /// [`reconcile`] ever fills it. On a platform with no descriptor to put
826    /// back the field is not there to be dead, so there is nothing to allow.
827    /// [`crate::process`] states the rule for this shape of problem: an
828    /// allowance leaves the lint's premise true and silences the report, while
829    /// a `cfg` makes the premise false instead.
830    #[cfg(windows)]
831    previous_dacl: Option<String>,
832}
833
834impl RootAccessChange {
835    /// The nothing that happens on a platform without this problem.
836    #[must_use]
837    pub const fn not_applicable() -> Self {
838        Self {
839            summary: RootAccessSummary::NotApplicable,
840            #[cfg(windows)]
841            previous_dacl: None,
842        }
843    }
844
845    /// What happened, in a form a command can print.
846    #[must_use]
847    pub const fn summary(&self) -> &RootAccessSummary {
848        &self.summary
849    }
850
851    /// Undoes as much of this change as can be undone, and says what could not
852    /// be.
853    ///
854    /// Deliberately infallible in the type: a rollback runs while another
855    /// failure is already being reported, and a rollback that could itself
856    /// return `Err` would either hide that failure or replace it. The
857    /// [`Reversal`] says what happened and the caller folds it into the message
858    /// it was already writing.
859    #[must_use]
860    pub fn revert(&self) -> Reversal {
861        #[cfg(windows)]
862        {
863            match &self.summary {
864                RootAccessSummary::NotApplicable | RootAccessSummary::AlreadyReconciled { .. } => {
865                    Reversal::NothingToUndo
866                }
867                RootAccessSummary::Created { path, .. } => match std::fs::remove_dir(path) {
868                    Ok(()) => Reversal::Removed { path: path.clone() },
869                    Err(source) if source.kind() == io::ErrorKind::NotFound => {
870                        Reversal::NothingToUndo
871                    }
872                    Err(source) => Reversal::Retained {
873                        path: path.clone(),
874                        detail: format!(
875                            "the directory this operation created could not be removed again \
876                             ({source}); it is empty unless something else has written to it, \
877                             and removing it by hand is safe"
878                        ),
879                    },
880                },
881                RootAccessSummary::Reconciled { path, .. } => {
882                    let Some(previous) = self.previous_dacl.as_deref() else {
883                        return Reversal::NothingToUndo;
884                    };
885                    match sys::write_dacl(path, previous) {
886                        Ok(()) => Reversal::Restored { path: path.clone() },
887                        Err(source) => Reversal::Retained {
888                            path: path.clone(),
889                            detail: format!(
890                                "this directory existed before this operation and could not be \
891                                 removed by it; its previous access control could not be put \
892                                 back either ({source}), so it now carries the access control \
893                                 this operation applied"
894                            ),
895                        },
896                    }
897                }
898            }
899        }
900        #[cfg(not(windows))]
901        {
902            Reversal::NothingToUndo
903        }
904    }
905}
906
907/// What undoing a [`RootAccessChange`] achieved.
908#[derive(Debug, Clone, PartialEq, Eq)]
909pub enum Reversal {
910    /// There was nothing to undo, or nothing had been changed.
911    NothingToUndo,
912    /// A directory this operation created was removed again.
913    Removed {
914        /// The directory that is gone.
915        path: PathBuf,
916    },
917    /// A descriptor this operation replaced was written back.
918    Restored {
919        /// The directory whose access control is as it was.
920        path: PathBuf,
921    },
922    /// Something is left behind, and here is exactly what.
923    ///
924    /// This is the "report any non-reversible existing directory state
925    /// explicitly" half of the requirement. It is never silent.
926    Retained {
927        /// The directory that remains.
928        path: PathBuf,
929        /// What about it could not be undone.
930        detail: String,
931    },
932}
933
934impl fmt::Display for Reversal {
935    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
936        match self {
937            Self::NothingToUndo => f.write_str("the runner root was left as it was found"),
938            Self::Removed { path } => {
939                write!(f, "the runner root {} was removed again", path.display())
940            }
941            Self::Restored { path } => write!(
942                f,
943                "the previous access control of {} was restored",
944                path.display()
945            ),
946            Self::Retained { path, detail } => write!(f, "{}: {detail}", path.display()),
947        }
948    }
949}
950
951// ---------------------------------------------------------------------------
952// Reading a root without touching it
953// ---------------------------------------------------------------------------
954
955/// What a runner root's access control amounts to, said without naming an
956/// account.
957///
958/// The read-only half of this module, and the whole of what a **custom**
959/// operator root ever gets: `03-migration-rollout.md` refuses to move or
960/// re-permission a directory the operator chose, so a configured root is
961/// preflighted and described, never rewritten.
962#[derive(Debug, Clone, PartialEq, Eq)]
963pub enum RootAccessReport {
964    /// Not a Windows host, so there is no DACL to describe.
965    NotApplicable,
966    /// The directory is not there.
967    Absent,
968    /// The directory is there and its access control could not be read.
969    Unreadable {
970        /// Why not.
971        detail: String,
972    },
973    /// The directory is there and this is what it grants.
974    Present {
975        /// Its DACL in SDDL, with account SIDs redacted by [`redact`].
976        dacl: String,
977        /// Whether it inherits nothing from the volume above it.
978        protected: bool,
979        /// Whether ordinary local users can write inside it.
980        broad_write: bool,
981    },
982}
983
984impl fmt::Display for RootAccessReport {
985    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
986        match self {
987            Self::NotApplicable => f.write_str("no Windows access control applies"),
988            Self::Absent => f.write_str("does not exist yet"),
989            Self::Unreadable { detail } => {
990                write!(f, "exists, but its access control cannot be read: {detail}")
991            }
992            Self::Present {
993                dacl,
994                protected,
995                broad_write,
996            } => write!(
997                f,
998                "{}; {}; {dacl}",
999                if *broad_write {
1000                    "ordinary local users can write there"
1001                } else {
1002                    "no ordinary local user can write there"
1003                },
1004                if *protected {
1005                    "inherits nothing from the volume above"
1006                } else {
1007                    "inherits from the volume above"
1008                }
1009            ),
1010        }
1011    }
1012}
1013
1014/// Describes a runner root's access control **without changing it**.
1015///
1016/// This is what a configured custom root gets, and what `service status` reports
1017/// about the default one. It creates nothing, permissions nothing, and is safe
1018/// to call on a path this product does not own.
1019#[must_use]
1020pub fn report(path: &Path) -> RootAccessReport {
1021    #[cfg(windows)]
1022    {
1023        // `Path::exists` answers "absent" to every question it cannot answer,
1024        // including "this account may not traverse the parent". That is the one
1025        // answer this must not give for a directory that is there, because
1026        // `Absent` reads as "nothing to worry about yet" while the truthful
1027        // outcome is `Unreadable`, which says so.
1028        match std::fs::symlink_metadata(path) {
1029            Ok(_) => {}
1030            Err(source) if source.kind() == io::ErrorKind::NotFound => {
1031                return RootAccessReport::Absent;
1032            }
1033            Err(source) => {
1034                return RootAccessReport::Unreadable {
1035                    detail: source.to_string(),
1036                };
1037            }
1038        }
1039        // The reader `crate::process` already carries, rather than a second
1040        // descriptor round trip of this module's own.
1041        match crate::process::permissions_summary(path) {
1042            Ok(summary) => RootAccessReport::Present {
1043                protected: is_protected(&summary.description),
1044                broad_write: grants_broad_write(&summary.description),
1045                dacl: redact(&summary.description),
1046            },
1047            Err(source) => RootAccessReport::Unreadable {
1048                detail: source.to_string(),
1049            },
1050        }
1051    }
1052    #[cfg(not(windows))]
1053    {
1054        let _ = path;
1055        RootAccessReport::NotApplicable
1056    }
1057}
1058
1059// ---------------------------------------------------------------------------
1060// The operation
1061// ---------------------------------------------------------------------------
1062
1063/// Creates or reconciles **the platform default** runner root.
1064///
1065/// Takes no path, and that is the design: there is no argument through which a
1066/// caller could aim this at an operator's configured directory. See this
1067/// module's documentation.
1068///
1069/// The order is the contract:
1070///
1071/// 1. resolve the platform default;
1072/// 2. run `b1`'s operational preflight, which mutates nothing;
1073/// 3. if the leaf is missing, create it **with its descriptor applied by the
1074///    call that creates it**, so there is no window in which it exists carrying
1075///    the volume's inherited grants;
1076/// 4. if it is already there, read its DACL and refuse if ordinary local users
1077///    can write inside it;
1078/// 5. otherwise reconcile the descriptor to admit exactly `SY`, `BA` and the
1079///    selected account, skipping the write when it already does.
1080///
1081/// # Errors
1082/// Any [`RootAccessError`]. In particular [`RootAccessError::BroadExistingAccess`]
1083/// when the directory is already open, which is a refusal rather than a repair.
1084pub fn ensure_default_root(
1085    paths: &AppPaths,
1086    admission: &RootAdmission,
1087) -> Result<RootAccessChange, RootAccessError> {
1088    #[cfg(windows)]
1089    {
1090        let root = crate::runner_root::default_runner_root(paths).map_err(|source| {
1091            RootAccessError::Resolve {
1092                source: Box::new(source),
1093            }
1094        })?;
1095        reconcile(paths, &root, admission)
1096    }
1097    #[cfg(not(windows))]
1098    {
1099        let _ = (paths, admission);
1100        Ok(RootAccessChange::not_applicable())
1101    }
1102}
1103
1104/// [`ensure_default_root`] against a root the caller names.
1105///
1106/// `pub(crate)` and nothing more, so no consumer of this crate can reach it,
1107/// and Windows-only because every caller is: [`ensure_default_root`] and
1108/// [`crate::service::ServiceOperations`] both reach it from a Windows arm, so
1109/// on the other two platforms it is not there to be dead rather than dead and
1110/// allowed. [`RootAccessChange::not_applicable`] is what those platforms
1111/// return instead.
1112///
1113/// The only caller that passes a path other than the platform default is
1114/// [`crate::service::ServiceOperations::with_runner_root`], whose override is
1115/// honoured for a [`crate::service::ServiceIdentity::fixture`] registration or
1116/// under `cfg(test)` and is otherwise ignored in favour of
1117/// [`ensure_default_root`]. A shipped binary therefore honours the fixture name
1118/// alone, and a fixture name cannot be the product's — which is what keeps a
1119/// released `service install` pointed at the platform default whatever it is
1120/// handed. A privileged smoke test uses it to exercise a directory it owns
1121/// instead of the real `C:\rman`.
1122#[cfg(windows)]
1123pub(crate) fn reconcile(
1124    paths: &AppPaths,
1125    root: &LocalAbsolutePath,
1126    admission: &RootAdmission,
1127) -> Result<RootAccessChange, RootAccessError> {
1128    let checked = RootPreflight::new(paths)
1129        .check(&RootOwner::Host, root)
1130        .map_err(|source| RootAccessError::Resolve {
1131            source: Box::new(source),
1132        })?;
1133    let desired = default_root_sddl(admission);
1134    let path = root.as_path().to_path_buf();
1135
1136    if checked.leaf_to_create().is_some() {
1137        match sys::create_with_dacl(&path, &desired) {
1138            Ok(()) => {
1139                return Ok(RootAccessChange {
1140                    summary: RootAccessSummary::Created {
1141                        path,
1142                        admits: admission.admits(),
1143                    },
1144                    previous_dacl: None,
1145                });
1146            }
1147            // Another process created it between the preflight and here.
1148            // Fall through and treat it as the pre-existing directory it now
1149            // is, which applies the same refusal to it as to any other.
1150            Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
1151            Err(source) => {
1152                return Err(RootAccessError::Create {
1153                    path,
1154                    source,
1155                    remediation: remediation(),
1156                });
1157            }
1158        }
1159    }
1160
1161    let current = sys::read_dacl(&path).map_err(|source| RootAccessError::Inspect {
1162        path: path.clone(),
1163        source,
1164        remediation: remediation(),
1165    })?;
1166
1167    if grants_broad_write(&current) {
1168        return Err(RootAccessError::BroadExistingAccess {
1169            dacl: redact(&current),
1170            volume: volume_of(&path),
1171            path,
1172            remediation: remediation(),
1173        });
1174    }
1175
1176    if admits_exactly(&current, admission) {
1177        return Ok(RootAccessChange {
1178            summary: RootAccessSummary::AlreadyReconciled {
1179                path,
1180                admits: admission.admits(),
1181            },
1182            previous_dacl: None,
1183        });
1184    }
1185
1186    sys::write_dacl(&path, &desired).map_err(|source| RootAccessError::Apply {
1187        path: path.clone(),
1188        source,
1189        remediation: remediation(),
1190    })?;
1191    Ok(RootAccessChange {
1192        summary: RootAccessSummary::Reconciled {
1193            path,
1194            admits: admission.admits(),
1195        },
1196        previous_dacl: Some(current),
1197    })
1198}
1199
1200/// Creates a directory carrying an exact descriptor, for this crate's own tests
1201/// only.
1202///
1203/// [`crate::service`]'s unit tests need a runner root that is deliberately open
1204/// to ordinary local users, and no temporary directory is: `%TEMP%` is
1205/// per-account, so everything created below it is already narrow. Building the
1206/// case by hand is the only way to reach the refusal that matters, and it is
1207/// the same call [`reconcile`] makes.
1208#[cfg(all(test, windows))]
1209pub(crate) fn create_with_descriptor_for_tests(path: &Path, sddl: &str) -> io::Result<()> {
1210    sys::create_with_dacl(path, sddl)
1211}
1212
1213/// The volume a path sits on, for the message that explains where an inherited
1214/// grant came from.
1215///
1216/// Windows-only because that message is: [`reconcile`] is the only caller, and
1217/// it is the item this file compiles where it is used rather than allows where
1218/// it is not.
1219#[cfg(windows)]
1220fn volume_of(path: &Path) -> PathBuf {
1221    path.ancestors()
1222        .last()
1223        .map_or_else(|| path.to_path_buf(), Path::to_path_buf)
1224}
1225
1226// ---------------------------------------------------------------------------
1227// Windows
1228// ---------------------------------------------------------------------------
1229
1230#[cfg(windows)]
1231mod sys {
1232    //! The three calls this module makes, and nothing else.
1233    //!
1234    //! Every one of them goes through SDDL rather than through
1235    //! `SetEntriesInAclW` and a hand-built ACL. That is the same choice
1236    //! [`crate::secrets`] and [`crate::process`] made, for the same two reasons:
1237    //! the descriptor a reviewer reads is the descriptor the host applies, and
1238    //! the unsafe surface is one conversion instead of an allocation protocol.
1239
1240    use std::ffi::OsStr;
1241    use std::io;
1242    use std::os::windows::ffi::OsStrExt;
1243    use std::path::Path;
1244
1245    use windows::Win32::Foundation::{ERROR_SUCCESS, HLOCAL, LocalFree};
1246    use windows::Win32::Security::Authorization::{
1247        ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, SE_FILE_OBJECT,
1248        SetNamedSecurityInfoW,
1249    };
1250    use windows::Win32::Security::{
1251        ACL, DACL_SECURITY_INFORMATION, GetSecurityDescriptorDacl,
1252        PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
1253        UNPROTECTED_DACL_SECURITY_INFORMATION,
1254    };
1255    use windows::Win32::Storage::FileSystem::CreateDirectoryW;
1256    use windows::core::PCWSTR;
1257
1258    /// A NUL-terminated wide string, as every `…W` entry point wants one.
1259    fn to_wide(value: &OsStr) -> Vec<u16> {
1260        value.encode_wide().chain(std::iter::once(0)).collect()
1261    }
1262
1263    /// A `windows` error as the `io::Error` this module's callers report.
1264    ///
1265    /// The facility is unwrapped rather than passed through, and that is not
1266    /// cosmetic. `windows-rs` reports a Win32 failure as `HRESULT_FROM_WIN32`,
1267    /// so `ERROR_ALREADY_EXISTS` arrives as `0x8007_00B7` — and
1268    /// `io::Error::from_raw_os_error` classifies the *Win32* code, which means
1269    /// the wrapped form reads back as [`io::ErrorKind::Uncategorized`] where
1270    /// the bare `183` reads back as [`io::ErrorKind::AlreadyExists`].
1271    /// [`super::reconcile`] branches on exactly that kind to absorb a directory
1272    /// created between the preflight and the creation, so leaving the facility
1273    /// on would make that branch unreachable.
1274    fn io_error(error: &windows::core::Error) -> io::Error {
1275        /// The high half `HRESULT_FROM_WIN32` puts in front of a Win32 code.
1276        const FACILITY_WIN32: u32 = 0x8007_0000;
1277
1278        let hresult = error.code().0;
1279        let bits = hresult.cast_unsigned();
1280        if bits & 0xFFFF_0000 == FACILITY_WIN32 {
1281            io::Error::from_raw_os_error((bits & 0x0000_FFFF).cast_signed())
1282        } else {
1283            io::Error::from_raw_os_error(hresult)
1284        }
1285    }
1286
1287    /// A security descriptor built from SDDL, freed when it goes out of scope.
1288    ///
1289    /// A guard rather than a `LocalFree` at each exit: the two callers below
1290    /// both have several, and a leak on the error path is exactly the kind of
1291    /// thing that is never noticed.
1292    struct Descriptor(PSECURITY_DESCRIPTOR);
1293
1294    impl Descriptor {
1295        fn from_sddl(sddl: &str) -> io::Result<Self> {
1296            let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
1297            let mut descriptor = PSECURITY_DESCRIPTOR(std::ptr::null_mut());
1298            // SAFETY: `wide` is NUL-terminated and outlives the call, which
1299            // fills `descriptor` with a LocalAlloc'd block this guard frees.
1300            unsafe {
1301                ConvertStringSecurityDescriptorToSecurityDescriptorW(
1302                    PCWSTR(wide.as_ptr()),
1303                    SDDL_REVISION_1,
1304                    &mut descriptor,
1305                    None,
1306                )
1307            }
1308            .map_err(|error| io_error(&error))?;
1309            Ok(Self(descriptor))
1310        }
1311
1312        /// The DACL inside it.
1313        ///
1314        /// The pointer is into the descriptor this guard owns, so it is only
1315        /// valid while `self` is.
1316        fn dacl(&self) -> io::Result<*const ACL> {
1317            let mut present = windows::core::BOOL(0);
1318            let mut acl: *mut ACL = std::ptr::null_mut();
1319            let mut defaulted = windows::core::BOOL(0);
1320            // SAFETY: `self.0` is a valid descriptor for the lifetime of `self`;
1321            // the three out-parameters are live locals.
1322            unsafe { GetSecurityDescriptorDacl(self.0, &mut present, &mut acl, &mut defaulted) }
1323                .map_err(|error| io_error(&error))?;
1324            if !present.as_bool() || acl.is_null() {
1325                return Err(io::Error::new(
1326                    io::ErrorKind::InvalidData,
1327                    "the security descriptor built from SDDL carries no DACL",
1328                ));
1329            }
1330            Ok(acl.cast_const())
1331        }
1332    }
1333
1334    impl Drop for Descriptor {
1335        fn drop(&mut self) {
1336            // SAFETY: LocalAlloc'd by the conversion above, freed exactly once.
1337            unsafe {
1338                let _ = LocalFree(Some(HLOCAL(self.0.0)));
1339            }
1340        }
1341    }
1342
1343    /// Creates a directory that carries its access control from the moment it
1344    /// exists.
1345    ///
1346    /// Not `create_dir` followed by a descriptor write. The gap between those
1347    /// two is a directory sitting on `C:\` with the volume's inherited grants,
1348    /// and a local account that loses that race gets a workspace root it can
1349    /// write into. The same reasoning `crate::process::RestrictiveHandoff`
1350    /// applies to the JIT configuration file applies here.
1351    pub(super) fn create_with_dacl(path: &Path, sddl: &str) -> io::Result<()> {
1352        let descriptor = Descriptor::from_sddl(sddl)?;
1353        let attributes = SECURITY_ATTRIBUTES {
1354            nLength: u32::try_from(size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(u32::MAX),
1355            lpSecurityDescriptor: descriptor.0.0,
1356            bInheritHandle: windows::core::BOOL(0),
1357        };
1358        let wide = to_wide(path.as_os_str());
1359        // SAFETY: `wide` is NUL-terminated, and `attributes` points at a
1360        // descriptor that outlives the call.
1361        unsafe { CreateDirectoryW(PCWSTR(wide.as_ptr()), Some(&raw const attributes)) }
1362            .map_err(|error| io_error(&error))
1363    }
1364
1365    /// The DACL a directory carries, in SDDL.
1366    ///
1367    /// Goes through the reader [`crate::process`] already has rather than
1368    /// repeating its `GetNamedSecurityInfoW` round trip: one implementation of
1369    /// "read a DACL back" means one place where the descriptor is freed
1370    /// correctly, and it is already exercised by that module's own tests.
1371    pub(super) fn read_dacl(path: &Path) -> io::Result<String> {
1372        crate::process::permissions_summary(path)
1373            .map(|summary| summary.description)
1374            .map_err(|error| io::Error::other(error.to_string()))
1375    }
1376
1377    /// Replaces a directory's DACL, honouring whether the SDDL asks for
1378    /// protection.
1379    ///
1380    /// The flag is read from the descriptor rather than hard-coded, because this
1381    /// is also the call that puts back a DACL that was **not** protected when a
1382    /// reconciliation is rolled back. Writing that one back as protected would
1383    /// leave the directory in a third state that was never true.
1384    pub(super) fn write_dacl(path: &Path, sddl: &str) -> io::Result<()> {
1385        let descriptor = Descriptor::from_sddl(sddl)?;
1386        let acl = descriptor.dacl()?;
1387        let information = DACL_SECURITY_INFORMATION
1388            | if super::is_protected(sddl) {
1389                PROTECTED_DACL_SECURITY_INFORMATION
1390            } else {
1391                UNPROTECTED_DACL_SECURITY_INFORMATION
1392            };
1393        let wide = to_wide(path.as_os_str());
1394        // SAFETY: `wide` is NUL-terminated; `acl` points into `descriptor`,
1395        // which is still alive; the two SID parameters are deliberately absent,
1396        // so ownership is not touched.
1397        let status = unsafe {
1398            SetNamedSecurityInfoW(
1399                PCWSTR(wide.as_ptr()),
1400                SE_FILE_OBJECT,
1401                information,
1402                None,
1403                None,
1404                Some(acl),
1405                None,
1406            )
1407        };
1408        if status == ERROR_SUCCESS {
1409            Ok(())
1410        } else {
1411            Err(io::Error::from_raw_os_error(
1412                i32::try_from(status.0).unwrap_or(i32::MAX),
1413            ))
1414        }
1415    }
1416
1417    #[cfg(test)]
1418    mod tests {
1419        use std::io;
1420
1421        /// [`super::super::reconcile`] absorbs a directory created between the
1422        /// preflight and the creation by matching on
1423        /// [`io::ErrorKind::AlreadyExists`]. That branch is reachable only if
1424        /// [`super::io_error`] unwraps `HRESULT_FROM_WIN32`, so the mapping is
1425        /// pinned here against a failure Windows itself produced rather than
1426        /// against a constant this file chose.
1427        #[test]
1428        fn a_directory_that_already_exists_reads_back_as_already_exists() {
1429            let directory = tempfile::tempdir().expect("a temporary directory");
1430            let error = super::create_with_dacl(directory.path(), "D:P(A;OICI;FA;;;SY)")
1431                .expect_err("creating a directory that is already there fails");
1432            assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}");
1433        }
1434    }
1435}
1436
1437// ---------------------------------------------------------------------------
1438// Tests
1439// ---------------------------------------------------------------------------
1440
1441#[cfg(test)]
1442mod tests {
1443    use super::*;
1444
1445    /// The DACL of `C:\` on a stock Windows host, near enough. The ACE that
1446    /// matters is the last one: inherit-only, Authenticated Users, delete plus
1447    /// generic write. Everything created below `C:\` without protection gets it.
1448    const VOLUME_ROOT: &str = "D:PAI(A;;FA;;;SY)(A;OICIIO;GA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;BA)\
1449                               (A;;0x1200a9;;;BU)(A;OICIIO;GXGR;;;BU)(A;;LC;;;BU)(A;CI;DC;;;BU)\
1450                               (A;;0x1301bf;;;AU)(A;OICIIO;SDGXGWGR;;;AU)";
1451
1452    /// What a directory created below `C:\` with inheritance left on ends up
1453    /// carrying, as `GetNamedSecurityInfoW` renders it back.
1454    const INHERITED_FROM_VOLUME: &str =
1455        "D:AI(A;OICIID;GA;;;SY)(A;OICIID;GA;;;BA)(A;OICIID;GXGR;;;BU)(A;OICIID;SDGXGWGR;;;AU)";
1456
1457    fn account() -> RootAdmission {
1458        RootAdmission::Account("S-1-5-21-1-2-3-1001".to_owned())
1459    }
1460
1461    // -- the descriptor -----------------------------------------------------
1462
1463    #[test]
1464    fn a_boot_root_admits_system_and_administrators_and_nothing_else() {
1465        assert_eq!(
1466            default_root_sddl(&RootAdmission::LocalSystem),
1467            "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)"
1468        );
1469    }
1470
1471    #[test]
1472    fn a_login_root_admits_the_selected_account_with_modify_rather_than_full_control() {
1473        assert_eq!(
1474            default_root_sddl(&account()),
1475            "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1001)"
1476        );
1477        // The two rights that would let the admitted account undo the
1478        // protection are exactly the two it does not get.
1479        let mask = rights_mask(ADMITTED_RIGHTS).expect("the constant parses");
1480        assert_eq!(mask & 0x0004_0000, 0, "WRITE_DAC must not be granted");
1481        assert_eq!(mask & 0x0008_0000, 0, "WRITE_OWNER must not be granted");
1482        // And everything create-materialize-clean needs is.
1483        for (bit, name) in [
1484            (0x0000_0002, "FILE_WRITE_DATA"),
1485            (0x0000_0004, "FILE_APPEND_DATA"),
1486            (0x0001_0000, "DELETE"),
1487            (0x0000_0001, "FILE_READ_DATA"),
1488        ] {
1489            assert_ne!(mask & bit, 0, "{name} must be granted");
1490        }
1491    }
1492
1493    #[test]
1494    fn a_daemon_running_as_local_system_does_not_add_an_ace_for_itself() {
1495        // `current_user_sid()` under a boot service answers S-1-5-18, which the
1496        // first ACE already names.
1497        let as_system = RootAdmission::Account("S-1-5-18".to_owned());
1498        assert_eq!(
1499            default_root_sddl(&as_system),
1500            default_root_sddl(&RootAdmission::LocalSystem)
1501        );
1502        let as_administrators = RootAdmission::Account("s-1-5-32-544".to_owned());
1503        assert_eq!(
1504            default_root_sddl(&as_administrators),
1505            default_root_sddl(&RootAdmission::LocalSystem)
1506        );
1507    }
1508
1509    #[test]
1510    fn every_ace_the_default_writes_is_inherited_by_children() {
1511        // A service that could create `<root>\<attempt>` but not write inside it
1512        // would satisfy a descriptor review and fail the first job.
1513        for admission in [RootAdmission::LocalSystem, account()] {
1514            let sddl = default_root_sddl(&admission);
1515            for ace in sddl.split('(').skip(1) {
1516                let flags = ace.split(';').nth(1).expect("an ACE has a flags field");
1517                assert_eq!(flags, INHERITANCE, "in {sddl}");
1518            }
1519        }
1520    }
1521
1522    // -- the security preflight ---------------------------------------------
1523
1524    #[test]
1525    fn the_descriptor_this_module_writes_grants_no_broad_write() {
1526        for admission in [RootAdmission::LocalSystem, account()] {
1527            let sddl = default_root_sddl(&admission);
1528            assert!(!grants_broad_write(&sddl), "{sddl}");
1529            assert!(is_protected(&sddl), "{sddl}");
1530        }
1531    }
1532
1533    #[test]
1534    fn a_root_that_inherited_the_volumes_grants_is_broadly_writable() {
1535        // The whole reason this module exists, in one assertion.
1536        assert!(grants_broad_write(INHERITED_FROM_VOLUME));
1537        assert!(!is_protected(INHERITED_FROM_VOLUME));
1538        assert!(grants_broad_write(VOLUME_ROOT));
1539    }
1540
1541    #[test]
1542    fn the_directory_service_spellings_of_create_file_and_create_folder_are_caught() {
1543        // `LC` is 0x4 — FILE_ADD_SUBDIRECTORY — and `DC` is 0x2 —
1544        // FILE_ADD_FILE. Matching on the letters rather than the bits would
1545        // have read them as "list children" and "delete child" and missed the
1546        // grant entirely.
1547        assert!(grants_broad_write("D:P(A;OICI;LC;;;BU)"));
1548        assert!(grants_broad_write("D:P(A;OICI;DC;;;BU)"));
1549        // `CC` is 0x1, which on a directory is FILE_LIST_DIRECTORY: a read.
1550        assert!(!grants_broad_write("D:P(A;OICI;CC;;;BU)"));
1551    }
1552
1553    #[test]
1554    fn a_broad_read_only_grant_is_not_a_write_grant() {
1555        // Deliberately different from `process::permissions_summary`'s question.
1556        // A world-readable runner root is untidy; a world-writable one is a
1557        // code-execution boundary, and only the second refuses an install.
1558        assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;FR;;;WD)"));
1559        assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;GR;;;AU)"));
1560        assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICI;FX;;;BU)"));
1561    }
1562
1563    #[test]
1564    fn a_hexadecimal_rights_field_is_read_as_bits() {
1565        // 0x1301bf is the "modify" mask Windows writes at `C:\` for
1566        // Authenticated Users; it contains FILE_WRITE_DATA.
1567        assert!(grants_broad_write("D:P(A;;0x1301bf;;;AU)"));
1568        // 0x1200a9 is read-and-execute, which is not a write grant.
1569        assert!(!grants_broad_write("D:P(A;;0x1200a9;;;BU)"));
1570    }
1571
1572    #[test]
1573    fn a_deny_ace_naming_everyone_is_a_tightening_not_a_leak() {
1574        assert!(!grants_broad_write("D:P(D;OICI;FA;;;WD)(A;OICI;FA;;;SY)"));
1575    }
1576
1577    #[test]
1578    fn an_unparseable_or_missing_descriptor_fails_closed() {
1579        assert!(
1580            grants_broad_write("O:BAG:BA"),
1581            "no DACL is not an empty DACL"
1582        );
1583        assert!(grants_broad_write("D:P(A;OICI;QQ;;;WD)"), "unknown rights");
1584        assert!(grants_broad_write("D:P(A;OICI;FAX;;;AU)"), "odd length");
1585        assert!(grants_broad_write("D:P(A;OICI;0xzz;;;AU)"), "bad hex");
1586        assert!(
1587            grants_broad_write("D:NO_ACCESS_CONTROL"),
1588            "a NULL DACL grants everyone everything; read as a flags field it would otherwise \
1589             parse to zero ACEs and be adopted as the narrowest directory on the machine"
1590        );
1591        assert!(!is_protected("D:NO_ACCESS_CONTROL"));
1592        assert!(write_trustees("D:NO_ACCESS_CONTROL").is_empty());
1593    }
1594
1595    #[test]
1596    fn a_creator_owner_grant_is_not_a_grant_to_an_unrelated_user() {
1597        // Inherited, `CO` gives each child's creator rights over that child.
1598        assert!(!grants_broad_write("D:P(A;OICI;FA;;;SY)(A;OICIIO;GA;;;CO)"));
1599    }
1600
1601    #[test]
1602    fn an_inherit_only_broad_ace_still_counts() {
1603        // It grants nothing on the root and everything on every attempt
1604        // directory created below it, which is the half that matters.
1605        assert!(grants_broad_write("D:P(A;OICIIO;GW;;;AU)"));
1606    }
1607
1608    // -- reconciliation ------------------------------------------------------
1609
1610    #[test]
1611    fn a_root_already_carrying_this_modules_descriptor_needs_no_rewrite() {
1612        for admission in [RootAdmission::LocalSystem, account()] {
1613            let sddl = default_root_sddl(&admission);
1614            assert!(admits_exactly(&sddl, &admission), "{sddl}");
1615        }
1616    }
1617
1618    #[test]
1619    fn a_mode_change_is_visible_as_a_descriptor_that_no_longer_matches() {
1620        // Boot to login must add the account, and login to boot must drop it:
1621        // `04-security-recovery.md` requires the selected identity to be
1622        // reconciled when service mode changes, in both directions.
1623        let boot = default_root_sddl(&RootAdmission::LocalSystem);
1624        let login = default_root_sddl(&account());
1625        assert!(!admits_exactly(&boot, &account()));
1626        assert!(!admits_exactly(&login, &RootAdmission::LocalSystem));
1627    }
1628
1629    #[test]
1630    fn a_root_that_admits_a_second_account_does_not_match() {
1631        let extra = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1001)\
1632                     (A;OICI;FRFWFXSD;;;S-1-5-21-1-2-3-1002)";
1633        assert!(!admits_exactly(extra, &account()));
1634    }
1635
1636    #[test]
1637    fn a_root_that_grants_the_account_full_control_is_reconciled_rather_than_accepted() {
1638        // The trustees are exactly right and the rights are not: `FA` carries
1639        // `WRITE_DAC` and `WRITE_OWNER`, the two the admitted account must not
1640        // have, because either one lets it undo the protection. Matching on
1641        // trustees alone would adopt this and leave the root re-openable by the
1642        // account it exists to constrain.
1643        let too_much = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-1-2-3-1001)";
1644        assert!(!grants_broad_write(too_much), "no broad trustee is named");
1645        assert_eq!(
1646            write_trustees(too_much),
1647            write_trustees(&default_root_sddl(&account()))
1648        );
1649        assert!(!admits_exactly(too_much, &account()), "{too_much}");
1650    }
1651
1652    #[test]
1653    fn windows_own_spelling_of_the_admitted_rights_still_matches() {
1654        // `FRFWFXSD` is not a form the converter hands back; it renders the
1655        // same mask as `0x1301bf`. Comparing the text rather than the bits
1656        // would rewrite the descriptor on every single install.
1657        let rendered = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1301bf;;;S-1-5-21-1-2-3-1001)";
1658        assert!(admits_exactly(rendered, &account()), "{rendered}");
1659    }
1660
1661    #[test]
1662    fn an_unprotected_root_never_matches_however_narrow_it_looks() {
1663        let narrow = "D:AI(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)";
1664        assert!(!grants_broad_write(narrow), "nothing broad is granted");
1665        assert!(
1666            !admits_exactly(narrow, &RootAdmission::LocalSystem),
1667            "but it still inherits, so it is reconciled rather than accepted"
1668        );
1669    }
1670
1671    #[test]
1672    fn the_two_well_known_trustees_compare_equal_in_either_spelling() {
1673        let aliases = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
1674        let sids = "D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)";
1675        assert_eq!(write_trustees(aliases), write_trustees(sids));
1676        assert!(admits_exactly(sids, &RootAdmission::LocalSystem));
1677    }
1678
1679    #[test]
1680    fn an_account_alias_windows_substituted_is_reconciled_rather_than_trusted() {
1681        // Windows renders the built-in Administrator's S-1-5-21-…-500 back as
1682        // `LA` — the round trip that already cost the secret store a bug. This
1683        // module cannot resolve that, so it declines to claim a match and pays
1684        // for one redundant descriptor write instead.
1685        let substituted = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;LA)";
1686        assert!(!grants_broad_write(substituted));
1687        assert!(!admits_exactly(
1688            substituted,
1689            &RootAdmission::Account("S-1-5-21-1-2-3-500".to_owned())
1690        ));
1691    }
1692
1693    // -- reporting -----------------------------------------------------------
1694
1695    #[test]
1696    fn redaction_removes_the_machine_and_the_user_from_an_account_sid() {
1697        let redacted = redact(
1698            "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-4004-77-9-1001)",
1699        );
1700        assert_eq!(
1701            redacted,
1702            "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FRFWFXSD;;;S-1-5-21-<account>)"
1703        );
1704        assert!(!redacted.contains("4004"), "{redacted}");
1705        assert!(!redacted.contains("1001"), "{redacted}");
1706    }
1707
1708    #[test]
1709    fn redaction_keeps_the_well_known_trustees_that_identify_nobody() {
1710        // `SY` and `S-1-5-18` are the same two words on every host, and
1711        // `service status` already prints the first of them.
1712        let descriptor = "D:P(A;OICI;FA;;;S-1-5-18)(A;OICI;FA;;;S-1-5-32-544)";
1713        assert_eq!(redact(descriptor), descriptor);
1714    }
1715
1716    #[test]
1717    fn redaction_handles_several_accounts_and_a_trailing_one() {
1718        assert_eq!(
1719            redact("(A;;FA;;;S-1-5-21-1-2-3-1001)(A;;FA;;;S-1-5-21-9-8-7-1002)"),
1720            "(A;;FA;;;S-1-5-21-<account>)(A;;FA;;;S-1-5-21-<account>)"
1721        );
1722        assert_eq!(redact("S-1-5-21-1-2-3-1001"), "S-1-5-21-<account>");
1723    }
1724
1725    #[test]
1726    fn a_summary_names_trustees_without_naming_an_account() {
1727        let summary = RootAccessSummary::Created {
1728            path: PathBuf::from("C:\\rman"),
1729            admits: account().admits(),
1730        };
1731        let rendered = summary.to_string();
1732        assert!(rendered.contains("C:\\rman"), "{rendered}");
1733        assert!(rendered.contains("the invoking user"), "{rendered}");
1734        assert!(!rendered.contains("S-1-5-21"), "{rendered}");
1735        assert!(summary.created());
1736    }
1737
1738    #[test]
1739    fn a_boot_summary_does_not_claim_to_admit_an_invoking_user() {
1740        let rendered = RootAccessSummary::Reconciled {
1741            path: PathBuf::from("C:\\rman"),
1742            admits: RootAdmission::LocalSystem.admits(),
1743        }
1744        .to_string();
1745        assert!(!rendered.contains("the invoking user"), "{rendered}");
1746        assert!(rendered.contains("NT AUTHORITY\\SYSTEM"), "{rendered}");
1747    }
1748
1749    #[test]
1750    fn a_reversal_that_left_something_behind_says_so() {
1751        let retained = Reversal::Retained {
1752            path: PathBuf::from("C:\\rman"),
1753            detail: "it existed before this operation".to_owned(),
1754        };
1755        assert!(retained.to_string().contains("existed before"));
1756        assert_ne!(retained, Reversal::NothingToUndo);
1757    }
1758
1759    #[test]
1760    fn a_not_applicable_change_reverts_to_nothing() {
1761        let change = RootAccessChange::not_applicable();
1762        assert_eq!(change.summary(), &RootAccessSummary::NotApplicable);
1763        assert_eq!(change.revert(), Reversal::NothingToUndo);
1764        assert_eq!(change.summary().path(), None);
1765    }
1766
1767    #[test]
1768    fn the_broad_access_refusal_names_the_path_the_volume_and_the_remedy() {
1769        let error = RootAccessError::BroadExistingAccess {
1770            path: PathBuf::from("C:\\rman"),
1771            dacl: redact(INHERITED_FROM_VOLUME),
1772            volume: PathBuf::from("C:\\"),
1773            remediation: remediation(),
1774        };
1775        let message = error.to_string();
1776        assert!(message.contains("C:\\rman"), "{message}");
1777        assert!(message.contains("host set-runtime-root"), "{message}");
1778        assert!(
1779            message.contains("refused rather than tightened"),
1780            "an operator has to be told why it was not simply fixed: {message}"
1781        );
1782        assert_eq!(error.path(), Some(Path::new("C:\\rman")));
1783    }
1784
1785    // -- platform behaviour --------------------------------------------------
1786
1787    #[cfg(not(windows))]
1788    #[test]
1789    fn nothing_is_created_or_re_permissioned_off_windows() {
1790        let root = tempfile::tempdir().expect("a temporary directory");
1791        let paths = AppPaths::rooted_at(root.path());
1792        let change =
1793            ensure_default_root(&paths, &RootAdmission::LocalSystem).expect("a no-op succeeds");
1794        assert_eq!(change.summary(), &RootAccessSummary::NotApplicable);
1795        assert_eq!(report(root.path()), RootAccessReport::NotApplicable);
1796    }
1797
1798    #[cfg(windows)]
1799    #[test]
1800    fn a_directory_this_process_created_is_reported_as_narrow() {
1801        let root = tempfile::tempdir().expect("a temporary directory");
1802        let directory = root.path().join("narrow");
1803        let admission = RootAdmission::of_this_account().expect("this process has an account");
1804        let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
1805            .expect("a local absolute path");
1806        // A separate application-data tree, so `b1`'s overlap check has nothing
1807        // to object to.
1808        let elsewhere = tempfile::tempdir().expect("a second temporary directory");
1809        let app_paths = AppPaths::rooted_at(elsewhere.path());
1810
1811        let change = reconcile(&app_paths, &checked, &admission).expect("creation succeeds");
1812        assert!(change.summary().created(), "{:?}", change.summary());
1813
1814        match report(&directory) {
1815            RootAccessReport::Present {
1816                protected,
1817                broad_write,
1818                dacl,
1819            } => {
1820                assert!(protected, "{dacl}");
1821                assert!(!broad_write, "{dacl}");
1822                assert!(!dacl.contains("S-1-5-21-1"), "unredacted account: {dacl}");
1823            }
1824            other => panic!("expected a readable descriptor, got {other:?}"),
1825        }
1826        let after_creation = read_back(&directory);
1827
1828        // A second pass creates nothing, and reverting it removes nothing,
1829        // because there is nothing of this call's to remove.
1830        //
1831        // Whether it also *rewrites* the descriptor is a property of the host
1832        // rather than of this code, so it is deliberately not asserted here.
1833        // `admits_exactly` compares SDDL text, and Windows renders an account
1834        // whose RID has an alias back as that alias -- the built-in
1835        // administrator's `S-1-5-21-...-500` reads back as `LA`. This module
1836        // declines to claim a match it cannot resolve and pays for one
1837        // redundant write instead, which is what a CI host running as that
1838        // account did while a developer host running as an ordinary one did
1839        // not. Both outcomes are correct; only removing the directory would
1840        // not be.
1841        // `an_account_alias_windows_substituted_is_reconciled_rather_than_trusted`
1842        // pins that decision purely. What holds on every host, and is asserted
1843        // instead, is that neither outcome changes anything: the directory
1844        // survives and still carries the descriptor creation wrote.
1845        let again = reconcile(&app_paths, &checked, &admission).expect("a second pass succeeds");
1846        assert!(!again.summary().created(), "{:?}", again.summary());
1847        // Read back BEFORE reverting. A rewriting second pass keeps the
1848        // descriptor it read as `previous_dacl`, so reverting puts
1849        // `after_creation` back whatever it wrote — asserting only afterwards
1850        // would pass however wrong that write had been.
1851        //
1852        // Compared by what it grants rather than by its exact text, for the
1853        // reason `reverting_a_reconciliation_puts_the_previous_descriptor_back`
1854        // gives: `SetNamedSecurityInfoW` records that it ran the
1855        // auto-inheritance algorithm by adding `AI` to the control flags, so a
1856        // host that took the rewriting branch reads back as `D:PAI` where
1857        // `CreateDirectoryW` wrote `D:P`. Demanding the same characters would
1858        // fail on exactly the host the comment above describes.
1859        assert_same_grants(
1860            &read_back(&directory),
1861            &after_creation,
1862            "a second pass must leave the descriptor granting what creation wrote",
1863        );
1864        let reversal = again.revert();
1865        assert!(
1866            matches!(
1867                reversal,
1868                Reversal::NothingToUndo | Reversal::Restored { .. }
1869            ),
1870            "a second pass has nothing of its own to undo: {reversal:?}"
1871        );
1872        assert!(directory.is_dir(), "the second pass must not remove it");
1873        assert_same_grants(
1874            &read_back(&directory),
1875            &after_creation,
1876            "and reverting it must leave those grants alone",
1877        );
1878
1879        // The child a runner attempt would be, created and cleaned as this
1880        // account.
1881        let child = directory.join("s1");
1882        std::fs::create_dir(&child).expect("a child below the root");
1883        std::fs::write(child.join("marker"), b"job").expect("content inside the child");
1884        std::fs::remove_dir_all(&child).expect("the child is removable again");
1885    }
1886
1887    #[cfg(windows)]
1888    #[test]
1889    fn an_existing_broad_directory_is_refused_rather_than_tightened() {
1890        let root = tempfile::tempdir().expect("a temporary directory");
1891        let directory = root.path().join("open");
1892        // Explicitly broad, without depending on what the temp directory
1893        // happens to inherit on this machine.
1894        sys::create_with_dacl(&directory, "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;WD)")
1895            .expect("a deliberately open directory");
1896        let before = read_back(&directory);
1897        assert!(grants_broad_write(&before), "{before}");
1898
1899        let elsewhere = tempfile::tempdir().expect("a second temporary directory");
1900        let app_paths = AppPaths::rooted_at(elsewhere.path());
1901        let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
1902            .expect("a local absolute path");
1903
1904        let error = reconcile(&app_paths, &checked, &RootAdmission::LocalSystem)
1905            .expect_err("an open directory is refused");
1906        assert!(
1907            matches!(error, RootAccessError::BroadExistingAccess { .. }),
1908            "{error}"
1909        );
1910        // And it was refused rather than repaired: the directory is untouched.
1911        assert_eq!(read_back(&directory), before);
1912    }
1913
1914    #[cfg(windows)]
1915    #[test]
1916    fn reverting_a_reconciliation_puts_the_previous_descriptor_back() {
1917        let root = tempfile::tempdir().expect("a temporary directory");
1918        let directory = root.path().join("narrow");
1919        let admission = RootAdmission::of_this_account().expect("this process has an account");
1920        let sid = admission
1921            .sid()
1922            .expect("an ordinary account has a SID")
1923            .to_owned();
1924        // Narrow enough to pass the preflight, but not what this module writes.
1925        // Named for this account so that an unelevated test run can still read
1926        // the descriptor back; the previous DACL is restorable either way,
1927        // because the owner of a directory implicitly holds WRITE_DAC.
1928        sys::create_with_dacl(&directory, &format!("D:P(A;OICI;FA;;;{sid})"))
1929            .expect("a narrow directory");
1930        let before = read_back(&directory);
1931        assert!(!grants_broad_write(&before), "{before}");
1932
1933        let elsewhere = tempfile::tempdir().expect("a second temporary directory");
1934        let app_paths = AppPaths::rooted_at(elsewhere.path());
1935        let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
1936            .expect("a local absolute path");
1937
1938        let change =
1939            reconcile(&app_paths, &checked, &admission).expect("a narrow directory is reconciled");
1940        assert!(matches!(
1941            change.summary(),
1942            RootAccessSummary::Reconciled { .. }
1943        ));
1944        assert_ne!(read_back(&directory), before, "it was actually rewritten");
1945
1946        assert_eq!(
1947            change.revert(),
1948            Reversal::Restored {
1949                path: directory.clone()
1950            }
1951        );
1952        // Compared by what it grants rather than by its exact text.
1953        // `SetNamedSecurityInfoW` records that it ran the auto-inheritance
1954        // algorithm by adding `AI` to the control flags, so a descriptor
1955        // written back through it reads as `D:PAI` where the original —
1956        // applied by `CreateDirectoryW` — read as `D:P`. The protection and
1957        // every ACE are identical, which is what "put back" has to mean.
1958        let after = read_back(&directory);
1959        assert_eq!(aces_of(&after), aces_of(&before), "{after} vs {before}");
1960        assert!(is_protected(&after), "{after}");
1961        assert!(
1962            directory.is_dir(),
1963            "a pre-existing directory is never removed"
1964        );
1965    }
1966
1967    /// A descriptor from its first ACE onwards, for a comparison that ignores
1968    /// the control flags Windows maintains for itself.
1969    #[cfg(windows)]
1970    fn aces_of(descriptor: &str) -> &str {
1971        descriptor
1972            .find('(')
1973            .map_or(descriptor, |start| &descriptor[start..])
1974    }
1975
1976    /// Two descriptors grant the same thing, whoever wrote them.
1977    ///
1978    /// The same comparison [`aces_of`] exists for: every ACE identical and the
1979    /// protection still in force, without demanding the `AI` control flag that
1980    /// `SetNamedSecurityInfoW` adds and `CreateDirectoryW` does not.
1981    #[cfg(windows)]
1982    fn assert_same_grants(actual: &str, expected: &str, context: &str) {
1983        assert_eq!(
1984            aces_of(actual),
1985            aces_of(expected),
1986            "{context}: {actual} vs {expected}"
1987        );
1988        assert!(is_protected(actual), "{context}: {actual}");
1989    }
1990
1991    #[cfg(windows)]
1992    #[test]
1993    fn reverting_a_creation_removes_the_directory_it_created() {
1994        let root = tempfile::tempdir().expect("a temporary directory");
1995        let directory = root.path().join("created");
1996        let elsewhere = tempfile::tempdir().expect("a second temporary directory");
1997        let app_paths = AppPaths::rooted_at(elsewhere.path());
1998        let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
1999            .expect("a local absolute path");
2000        // The login and foreground admission, which is the one an unelevated
2001        // caller can roll back: `ADMITTED_RIGHTS` carries `SD`, and creating a
2002        // directory does not by itself confer the right to delete it again.
2003        let admission = RootAdmission::of_this_account().expect("this process has an account");
2004
2005        let change = reconcile(&app_paths, &checked, &admission).expect("creation succeeds");
2006        assert!(directory.is_dir());
2007        assert_eq!(
2008            change.revert(),
2009            Reversal::Removed {
2010                path: directory.clone()
2011            }
2012        );
2013        assert!(!directory.exists(), "the rollback is a real rollback");
2014    }
2015
2016    #[cfg(windows)]
2017    #[test]
2018    fn a_rollback_that_cannot_finish_reports_what_it_left_behind() {
2019        // The boot admission names only `SY` and `BA`, so an unelevated caller
2020        // creating one cannot delete it again — the owner of a directory holds
2021        // WRITE_DAC implicitly but not DELETE. That is a real outcome rather
2022        // than a contrived one, and the requirement is that it is *reported*
2023        // rather than swallowed. An elevated run has `BA` and takes the
2024        // `Removed` branch above, so both are accepted here.
2025        let root = tempfile::tempdir().expect("a temporary directory");
2026        let directory = root.path().join("boot-owned");
2027        let elsewhere = tempfile::tempdir().expect("a second temporary directory");
2028        let app_paths = AppPaths::rooted_at(elsewhere.path());
2029        let checked = LocalAbsolutePath::new(directory.to_str().expect("a unicode temp path"))
2030            .expect("a local absolute path");
2031
2032        let change = reconcile(&app_paths, &checked, &RootAdmission::LocalSystem)
2033            .expect("creation succeeds");
2034        match change.revert() {
2035            Reversal::Removed { path } => assert_eq!(path, directory),
2036            Reversal::Retained { path, detail } => {
2037                assert_eq!(path, directory);
2038                assert!(
2039                    detail.contains("removing it by hand is safe"),
2040                    "a non-reversible state must say what to do about it: {detail}"
2041                );
2042            }
2043            other => panic!("expected a removal or an explicit retention, got {other:?}"),
2044        }
2045    }
2046
2047    #[cfg(windows)]
2048    #[test]
2049    fn a_custom_root_is_described_without_being_changed() {
2050        // `report` is the whole of what a configured operator root gets.
2051        let root = tempfile::tempdir().expect("a temporary directory");
2052        let directory = root.path().join("operators-own");
2053        sys::create_with_dacl(&directory, "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;WD)")
2054            .expect("an operator directory this product did not create");
2055        let before = read_back(&directory);
2056
2057        match report(&directory) {
2058            RootAccessReport::Present { broad_write, .. } => assert!(broad_write),
2059            other => panic!("expected a readable descriptor, got {other:?}"),
2060        }
2061        assert_eq!(read_back(&directory), before, "reporting must not rewrite");
2062    }
2063
2064    #[cfg(windows)]
2065    #[test]
2066    fn an_absent_directory_reports_absent() {
2067        let root = tempfile::tempdir().expect("a temporary directory");
2068        assert_eq!(
2069            report(&root.path().join("nothing")),
2070            RootAccessReport::Absent
2071        );
2072    }
2073
2074    #[cfg(windows)]
2075    fn read_back(path: &Path) -> String {
2076        sys::read_dacl(path).expect("this process can read the descriptor it just wrote")
2077    }
2078}