From 012e3073c48d7e29ca9ed871975237c7ac5ecc7f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 21:06:58 -0400 Subject: [PATCH 01/36] docs(namespace-request): GetFullPathNameW is not lexical, and say why it stays The module doc claimed the call is lexical and then, in the next sentence, described it resolving against the process current directory. Those disagree: a lexical canonicalizer is a pure function of its input, and this reads mutable process state -- the current directory, and for a drive-relative path the per-drive current directory in the hidden `=C:` environment variables. "Touches no filesystem" is the claim that holds. Swept `lexical` across the workspace rather than fixing the reported site: 6 statements in this crate, of which the probes checklist item named one. The identical sentence was also in path.rs, with four more restatements in doc examples, tests, an acceptance comment and DESIGN-NOTES. One further site in windows-platform-probes is left to its own commit, since this crate is release-managed and that one is not this crate's to change. D-18 records the decision half. A genuinely lexical canonicalizer exists -- PathCchCanonicalizeEx, or PathAllocCanonicalize -- and is cheaper, but it is the wrong call: resolving against the current directory AT SUBMISSION is the property this crate buys, and a lexical call would leave a relative path relative for a worker to resolve later against a directory any thread may have changed. It also records that whether the call enters the kernel is NOT established -- the PEB and environment block are process memory, which is a statement about the data sources, not a measurement. Completed item: M2.6 (part 1 of 2): Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide whether it is still the call `prepare` wants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 44 ++++++++++++++- .../src/full_path.rs | 53 ++++++++++++++++--- .../src/full_path/tests.rs | 3 +- .../windows-namespace-request-sys/src/path.rs | 9 ++-- .../tests/acceptance/operations.rs | 2 +- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index a911d2830..01863d1f9 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -534,11 +534,53 @@ proven correct -- a bad trade on a crate whose buffers are handed to the kernel. If it is ever done, it must be done together with the size passed to Win32, never to one side alone. +## D-18: `GetFullPathNameW` is not lexical, and the genuinely lexical alternative is the wrong call + +**The correction.** This crate described `GetFullPathNameW` as **lexical** in +six places, in a sentence that then went on to say it resolves against the +process current directory. Those two claims disagree: a lexical canonicalizer is +a pure function of its input, and this reads mutable process state -- the current +directory, and for a drive-relative path such as `C:foo` the per-drive current +directory Windows keeps in the hidden `=C:` environment variables. The claim +that holds is **touches no filesystem**. + +The wrong word had spread beyond where it was reported. The consuming probe's +checklist item named [full_path.rs](src/full_path.rs) only; a sweep for the term +found the identical sentence in [path.rs](src/path.rs), plus four further +restatements across doc examples, tests, an acceptance comment and this file. +The reported site was a sample, not the population -- which is the standing +lesson, met again. + +**The decision: keep `GetFullPathNameW`.** A genuinely lexical canonicalizer +exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and is cheaper, +reading no process state at all. It is the wrong call here, and for the property +rather than the price: resolving against the current directory *at submission* is +what this crate is buying. A lexical canonicalizer would leave a relative path +relative, so its meaning would be settled on the worker at execution time, +against a current directory any thread may have changed in between -- which is +exactly the race preparation exists to close. The cheaper call is cheaper because +it does less, and the part it omits is the part wanted. + +Recorded with the alternative named so the next reader does not re-derive it. If +the reasoning is ever wrong -- a consumer wanting a pure string operation, having +resolved relativity another way -- the cheaper call is named here. + +**Whether it enters the kernel: not established, and said so.** Nothing it is +documented to consult requires a transition; the current directory lives in the +PEB and the `=C:` variables in the process environment block, both ordinary +process memory. Windows does not document the implementation, so that is a +statement about the data sources rather than a measurement of the call. +`probe-request-cost` measures roughly 212 ns per resolution on x86_64, which is +consistent with user-mode work and does not by itself establish it. The +distinction is kept deliberately: two successive descriptions of this call in +that probe were each wrong in the same direction, by naming a mechanism the +evidence did not reach. + ## Open, and inherited rather than introduced - **Path resolution under a captured identity.** A path must be resolved on the calling thread, because the process current directory is mutable by any - thread -- but `GetFullPathNameW` is lexical and never expands a drive letter, + thread -- but `GetFullPathNameW` never expands a drive letter, and drive-letter resolution follows the *impersonated* token's logon session. So a root resolved on a submitter and opened on a worker under a captured token can name a different device. The workspace has this as an open decision; diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index b3f79a06b..a94159004 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -7,15 +7,56 @@ //! //! # What it solves, and what it leaves standing //! -//! This call is **lexical**. It resolves relative components and `.`/`..` -//! against the process current directory, and it touches no filesystem: it will -//! happily resolve a path to something that does not exist. +//! This call **touches no filesystem**: it will happily resolve a path to +//! something that does not exist. It is **not** lexical, and the difference is +//! the whole reason this entry exists. It resolves relative components and +//! `.`/`..` against the *process current directory*, and for a drive-relative +//! path such as `C:foo` against the per-drive current directory Windows keeps +//! in the hidden `=C:` environment variables. Both are process state, so the +//! same input string resolves to different outputs in the same process at +//! different times. +//! +//! Calling it lexical -- as an earlier revision of this doc did, in the sentence +//! immediately before the one describing the current directory it reads -- gets +//! that exactly backwards. A lexical canonicalizer is a pure function of its +//! input; this reads mutable process state, which is precisely the property +//! being bought. //! //! So it solves exactly one problem -- the process current directory is shared //! mutable state that any thread can change, so a relative path means something //! different depending on *when* it is resolved. Performing this on the //! submitting thread pins that meaning. //! +//! # Why not a genuinely lexical canonicalizer +//! +//! One exists: `PathCchCanonicalizeEx`, or `PathAllocCanonicalize`. Either is +//! cheaper and reads no process state at all. +//! +//! **They are the wrong call here, and the reason is the property above rather +//! than cost.** Resolving against the current directory *at submission* is what +//! this crate is buying. A lexical canonicalizer would leave a relative path +//! still relative, so its meaning would be decided on the worker thread at +//! execution time, against a current directory any thread may have changed in +//! between -- reintroducing exactly the race preparation exists to close. The +//! cheaper call is cheaper because it does less, and the part it does not do is +//! the part wanted. +//! +//! Recorded so the next reader does not re-derive it. If this reasoning is ever +//! wrong -- for a consumer that genuinely wants a pure string operation and has +//! resolved relativity some other way -- the cheaper call is named here. +//! +//! # Whether it can enter the kernel +//! +//! Nothing it is documented to consult requires a transition. The process +//! current directory lives in the PEB and the `=C:` variables in the process +//! environment block; both are ordinary process memory. Windows does not +//! document the implementation, so this is a statement about the data sources, +//! not a measurement of the call -- a distinction worth keeping, because two +//! successive descriptions of this call in a consuming probe were each wrong in +//! the same direction, by naming a mechanism the evidence did not reach. +//! `probe-request-cost` measures roughly 212 ns per resolution on x86_64, which +//! is consistent with user-mode work and does not by itself establish it. +//! //! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` //! never expands a drive letter, and a drive letter is resolved against the @@ -103,7 +144,7 @@ impl From for FullPathError { /// use windows_namespace_request_sys::full_path::ResolveFullPath; /// use wtf_string::Wtf16String; /// -/// // Lexical: `.` and `..` are resolved without touching the filesystem. +/// // `.` and `..` are resolved without touching the filesystem. /// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\Windows\System32\..\.\Temp")) /// .perform()? /// .to_string_lossy(); @@ -118,8 +159,8 @@ impl From for FullPathError { /// use windows_namespace_request_sys::full_path::ResolveFullPath; /// use wtf_string::Wtf16String; /// -/// // A path to nothing resolves perfectly happily, because the call is -/// // lexical. A consumer wanting a verified path wants an open plus +/// // A path to nothing resolves perfectly happily, because the call +/// // touches no filesystem. A consumer wanting a verified path wants an open plus /// // GetFinalPathNameByHandleW instead. /// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\no-such-directory\..\file.txt")) /// .perform()? diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index a8d734ea0..282889d7f 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -2,7 +2,8 @@ //! Tests for the `GetFullPathNameW` entry. //! -//! The negatives matter more than the positives here: this call is lexical, and +//! The negatives matter more than the positives here: this call touches no +//! filesystem, and //! a suite that only ever resolved existing paths would leave a reader //! believing it verifies something. diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 2ef29b2ed..6950f022c 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -15,8 +15,11 @@ //! //! # A resolved path is not a session-independent path //! -//! `GetFullPathNameW` is **lexical**. It resolves relative components and -//! `.`/`..` and never expands a drive letter, and a drive letter resolves +//! `GetFullPathNameW` **touches no filesystem**, and it is not lexical: it +//! resolves relative components and `.`/`..` against process state -- the +//! current directory, and for a drive-relative path the per-drive current +//! directory in the `=C:` environment variables. What it never does is expand +//! a drive letter, and a drive letter resolves //! against the *logon session* of whatever token is in effect. So a path //! prepared on a submitting thread and opened on a worker under a captured //! token from another session can name a different device. Preparation closes @@ -177,7 +180,7 @@ impl std::error::Error for PathError { /// Preparation resolves against the *process* current directory, on the /// calling thread, which is what stops the meaning of a relative path changing /// between submission and execution. It does **not** expand a drive letter, -/// because `GetFullPathNameW` is lexical and never does -- and a drive letter +/// because `GetFullPathNameW` never does -- and a drive letter /// is resolved against the logon session of the token in effect at open time. /// A prepared path carried to a worker running under a captured token from /// another logon session can therefore still name a different device. That diff --git a/crates/windows-namespace-request-sys/tests/acceptance/operations.rs b/crates/windows-namespace-request-sys/tests/acceptance/operations.rs index d6b2dc6e0..513c15c2c 100644 --- a/crates/windows-namespace-request-sys/tests/acceptance/operations.rs +++ b/crates/windows-namespace-request-sys/tests/acceptance/operations.rs @@ -275,7 +275,7 @@ fn watcher_getvolumeinformationbyhandle_shape_is_reachable() { #[test] fn enumeration_getfullpathname_shape_is_reachable() { - // path.rs:149 -- lexical resolution with a null file-part out-param. + // path.rs:149 -- resolution with a null file-part out-param. let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\Windows\System32\..\.\Temp")) .perform() .expect("the enumeration crate's resolution shape") From 80acadb94452f01cc43ad0933bb7716ebef38520 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 21:07:43 -0400 Subject: [PATCH 02/36] docs(probes): finish the GetFullPathNameW sweep and point at the decision The workspace sweep for `lexical` found one site in this crate that the namespace-request correction could not touch: device_map's printed prose called path preparation "lexical resolution" while explaining why it does not close the drive-letter hazard. The sentence is right about the hazard and wrong about the mechanism, in the same direction as the two corrections before it. request_cost's module doc already declined to name a mechanism, which was honest but left the question open one layer down. It now links D-18 in the owning crate, which closes it: what the call does, why it is kept over the cheaper PathCchCanonicalizeEx, and that whether it enters the kernel is not established. Completed item: M2.6 (part 2 of 2): Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide whether it is still the call `prepare` wants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 2 +- crates/windows-platform-probes/src/bin/device_map.rs | 2 +- crates/windows-platform-probes/src/request_cost.rs | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index e7360a478..37c5ac390 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -88,7 +88,7 @@ speculative list to extend by imagination -- a fourth is added when a fourth con explicitly after being pointed at the question. Nothing in the suite decides it either way, which is itself the argument for the oracle. -- [ ] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide +- [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide whether it is still the call `prepare` wants. Two successive descriptions in the cost probe were each wrong in the same direction: *a syscall cost*, which a timing loop cannot establish, and then *lexical*, which it also is not. The probe now states the cost and declines the mechanism, which is diff --git a/crates/windows-platform-probes/src/bin/device_map.rs b/crates/windows-platform-probes/src/bin/device_map.rs index bb11ba70f..332a95a5b 100644 --- a/crates/windows-platform-probes/src/bin/device_map.rs +++ b/crates/windows-platform-probes/src/bin/device_map.rs @@ -119,7 +119,7 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " different device -- which is why lexical resolution does not" + " different device -- which is why resolving the path does not" ); let _ = writeln!(out, " close that hazard."); } else { diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 1f7bedf52..ec95b9505 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -75,6 +75,13 @@ //! submission is the property being bought. What survives either way is the //! part that matters: an allocator cannot remove it. //! +//! The owning crate now settles both halves rather than leaving them to be +//! re-derived from a probe: see `windows-namespace-request-sys`' +//! [DESIGN-NOTES.md](../../windows-namespace-request-sys/DESIGN-NOTES.md) -> +//! `D-18`, which states what the call actually does, records keeping it over +//! the cheaper lexical alternative, and says plainly that whether it enters the +//! kernel is not established. +//! //! The two schemes that might reduce it recover different halves. **Inline //! storage** removes the allocation and copy, which is what //! `clone_prepared_units` measures, and cannot touch the resolution at all. From 04ef09659327805e946f2d48aed1ec75ac58466f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 22:31:24 -0400 Subject: [PATCH 03/36] docs: finish the GetFullPathNameW sweep across the workspace The two commits before this corrected the crate that owns the call and the probe that consumes it. Sweeping `lexical` across the whole workspace found four more statements of the same wrong fact, three of which neither commit touched -- including one in a file the first commit had already edited. - `full_path/tests.rs` still said "the call is lexical" in a second comment forty lines below the one that was fixed. The reported site is a sample, not the population, and that holds even when the population is one file. - The root DESIGN-NOTES stated it as a premise of a design corollary. The corollary's conclusion is right and unchanged -- a drive letter survives submission-time canonicalisation -- but it was resting on a false reason, so it now gives the true one and links D-18 for the rest. - Root CHECKLIST.md M20 used it as a live premise inside an OPEN milestone, which is the most expensive of the four: a reader executing that work would have inherited the wrong reason for a correct conclusion. - CHECKLIST-thread-ambient.md stated it twice, in the entry catalogue table and in M26.5. Both are completed records, and the correction is confined to the technical premise; the request each recorded is untouched, and M26.5's is in fact what D-18 now satisfies. Deliberately not touched: the 2026-08-27 design session uses "lexical canonicalisation" generically and correctly, and a Tier 3 transcript is a raw record rather than a normative statement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 9 +++++---- CHECKLIST.md | 4 ++-- DESIGN-NOTES.md | 10 +++++++--- .../src/full_path/tests.rs | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 1f0dd6377..442b01d74 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -160,7 +160,7 @@ pool, or async anywhere near it. The family grows by one entry per Win32 call. | 6 | `GetFileInformationByHandle` (non-Ex) | watcher | `BY_HANDLE_FILE_INFORMATION`; a distinct call, not a class of entry 5 | | 7 | `GetFinalPathNameByHandleW` | watcher directly, Globazog via `std::fs::canonicalize` | `VOLUME_NAME_DOS \| FILE_NAME_NORMALIZED` | | 8 | `GetVolumeInformationByHandleW` | watcher | handle-based, not the path-based `GetVolumeInformationW` | -| 9 | `GetFullPathNameW` | enumeration | lexical only | +| 9 | `GetFullPathNameW` | enumeration | no filesystem access | Four audit findings that shape the milestones below, recorded because each contradicts an assumption the first draft of this plan was written on. @@ -417,9 +417,10 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the filesystem name. Handle-based; the path-based `GetVolumeInformationW` is deliberately not in round one because no audited consumer calls it. -- [x] **M26.5** -- The `GetFullPathNameW` entry. Lexical only: it resolves relative components and `.`/`..` - and never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and - its documentation must say which problem it solves and which it leaves standing. +- [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it resolves relative components + and `.`/`..` against the process current directory, and never expands a drive letter, so it does + **not** close the session-relative hazard from M20.1, and its documentation must say which problem it + solves and which it leaves standing. - [x] **M26.6** -- Acceptance, in **two** parts, because the audit had two purposes and checking only the first is how the coverage question got missed once already. diff --git a/CHECKLIST.md b/CHECKLIST.md index be63f3ad9..4df84e5d4 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -59,8 +59,8 @@ they are deliberately separate from building the new facility, which cannot star - [ ] **M20.1** -- Decide what the namespace facility does with a session-relative drive letter, and record it as a decision rather than leaving the absence of one implicit. Path resolution follows the impersonated token's logon session (measured: under a token from another logon session with unchanged - local access, the global `C:` resolved and a `subst` letter did not), and `GetFullPathNameW` is lexical - so submission-time canonicalisation does not expand the letter. `QueryDosDeviceW` distinguishes a real + local access, the global `C:` resolved and a `subst` letter did not), and `GetFullPathNameW` never + expands a drive letter, so submission-time canonicalisation does not expand it either. `QueryDosDeviceW` distinguishes a real local volume, a `subst`, and a network mapping cheaply, so detection is settled and only the response is open: expand to a session-independent form at submission, or reject at admission with a typed error. Expansion is not uniform -- a network mapping becomes a UNC path, a local volume becomes a device path diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 5cb85f68c..ddd7bc56e 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1459,9 +1459,13 @@ Two corollaries that decide the design: - **Submission-time canonicalisation does not close this.** The path must be resolved on the calling thread at submission, because the process current directory is mutable by any thread and even perfect remoting would be racy -- - but `GetFullPathNameW` is *lexical*. It resolves relative components and - `.`/`..` and never expands a drive letter, so the "canonical" path still - carries a session-relative reference. + but `GetFullPathNameW` never expands a drive letter, so the "canonical" path + still carries a session-relative reference. (It is not *lexical* either, which + matters elsewhere but not here: it resolves against the process current + directory, which is exactly the property submission-time resolution buys. See + `windows-namespace-request-sys`' + [DESIGN-NOTES.md](crates/windows-namespace-request-sys/DESIGN-NOTES.md) -> + `D-18`.) - **The extended-length prefix does not help.** `\\?\Z:\dir` still resolves `Z:` through the device map; the prefix skips Win32 normalisation, not object-manager resolution. Only UNC, `\\?\Volume{GUID}\`, and diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 282889d7f..73dbd498b 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -46,8 +46,8 @@ fn an_already_absolute_path_is_returned_unchanged() { #[test] fn a_path_that_does_not_exist_resolves_perfectly_happily() { - // The call is lexical and touches no filesystem. A consumer wanting a - // verified path wants an open plus GetFinalPathNameByHandleW. + // The call touches no filesystem. A consumer wanting a verified path wants + // an open plus GetFinalPathNameByHandleW. assert_eq!( resolve(r"C:\no-such-directory\..\nothing-here.txt"), r"C:\nothing-here.txt" From bec2dad9cf079933d992e782968e16ab3846cf0b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 22:48:30 -0400 Subject: [PATCH 04/36] docs(namespace-request): quote the cost figure for what it measures Self-review caught this, and it is the same defect D-18 exists to correct, committed inside the paragraph warning against it. The doc said `probe-request-cost` "measures roughly 212 ns per resolution". It does not. The probe reports `prepare_short_path` as an explicit construct-and-drop cycle -- an allocation, this resolution and a drop together -- and says in its own output that it declines to say what the total is made of. Attributing it to the resolution names a mechanism the measurement does not isolate, which is exactly what "a syscall cost" and "lexical" each did before it. Verified by running the probe rather than by reading the number: it prints 208.0 ns for `prepare_short_path` on this x86_64 host under the heading "the first four are construct-and-drop cycles". The figure now bounds the call from above and claims nothing else. Swept both restatements. The probes CHECKLIST said "~212 ns for a build ... and declines to say what that is made of", which was accurate already; it was the restatement in this crate that drifted, in both the module doc and D-18. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-namespace-request-sys/DESIGN-NOTES.md | 10 ++++++++-- crates/windows-namespace-request-sys/src/full_path.rs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 01863d1f9..88ae23c34 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -570,8 +570,14 @@ documented to consult requires a transition; the current directory lives in the PEB and the `=C:` variables in the process environment block, both ordinary process memory. Windows does not document the implementation, so that is a statement about the data sources rather than a measurement of the call. -`probe-request-cost` measures roughly 212 ns per resolution on x86_64, which is -consistent with user-mode work and does not by itself establish it. The +The figure that exists is quoted for what it is and no more: `probe-request-cost` +measures a *construct-and-drop cycle* for a short prepared path at roughly 210 ns +on x86_64 -- an allocation, this resolution and a drop together. The probe +deliberately declines to decompose that total, so it bounds this call from above +and says nothing about what it costs alone, still less about whether any part of +it entered the kernel. Quoting it as a per-resolution cost, as a first draft of +this decision did, would have been the same defect this decision is correcting: +attributing a measured number to a mechanism the measurement does not isolate. The distinction is kept deliberately: two successive descriptions of this call in that probe were each wrong in the same direction, by naming a mechanism the evidence did not reach. diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index a94159004..57d67bb76 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -54,8 +54,14 @@ //! not a measurement of the call -- a distinction worth keeping, because two //! successive descriptions of this call in a consuming probe were each wrong in //! the same direction, by naming a mechanism the evidence did not reach. -//! `probe-request-cost` measures roughly 212 ns per resolution on x86_64, which -//! is consistent with user-mode work and does not by itself establish it. +//! +//! So the figure that exists is quoted for what it is and no more: +//! `probe-request-cost` measures a *construct-and-drop cycle* for a short +//! prepared path at roughly 210 ns on x86_64. That is an allocation, this +//! resolution and a drop together -- the probe deliberately declines to say how +//! the total divides, so it bounds this call from above and says nothing about +//! what it costs alone, still less about whether any part of it entered the +//! kernel. //! //! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` From 4e3a27789631fe5dc265eacff15c718bc78c378c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 22:55:05 -0400 Subject: [PATCH 05/36] docs: separate what GetFullPathNameW does lexically from what it roots Independent review of the previous commits found the correction had overshot: saying the call resolves `.`/`..` "against the process current directory" attributes process-state dependence to the one operation that has none. That is the same imprecision D-18 was written to fix, running the other way. Measured rather than reasoned. With the process CWD set to C:\Windows and then C:\Users, a fully-qualified `C:\a\..\b` resolves to `C:\b` both times, while `rel.txt` follows the CWD. So the call does two separable things: it collapses `.`/`..` and normalizes separators, which IS lexical and reads no process state; and it roots a path that is not fully qualified, which reads the current directory or the drive's own `=C:` entry. It is not lexical AS A WHOLE, and only the rooting half is the property submission-time resolution buys -- which is what D-18's decision actually turns on. Corrected in all four places that had merged the two: the module doc, path.rs, the root DESIGN-NOTES and M26.5. Two further review findings: - D-18 undercounted its own sweep as "six places" and "four further restatements". The real figure is nine sites across five files, three of them in full_path.rs alone. A paragraph whose point is "the reported site was a sample, not the population" should not itself undercount the population. - M2.6 was checked off while its body still read as pending -- quoting the module doc text this change deletes, and saying the correction "is queued rather than taken". Rather than restate it in the past tense, the item is archived per the completed-item rule (2411 characters, so large) with a one-line stub pointing at it, and the archive records the resolution above the request it preserves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 8 ++-- DESIGN-NOTES.md | 7 ++-- .../DESIGN-NOTES.md | 36 ++++++++++------ .../src/full_path.rs | 31 +++++++++----- .../windows-namespace-request-sys/src/path.rs | 10 +++-- crates/windows-platform-probes/CHECKLIST.md | 31 +------------- .../COMPLETED-CHECKLIST.md | 42 +++++++++++++++++++ 7 files changed, 100 insertions(+), 65 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 442b01d74..2963f8976 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -417,10 +417,10 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the filesystem name. Handle-based; the path-based `GetVolumeInformationW` is deliberately not in round one because no audited consumer calls it. -- [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it resolves relative components - and `.`/`..` against the process current directory, and never expands a drive letter, so it does - **not** close the session-relative hazard from M20.1, and its documentation must say which problem it - solves and which it leaves standing. +- [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it collapses `.`/`..` + lexically and roots a path that is not fully qualified against the process current directory, and + never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and + its documentation must say which problem it solves and which it leaves standing. - [x] **M26.6** -- Acceptance, in **two** parts, because the audit had two purposes and checking only the first is how the coverage question got missed once already. diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index ddd7bc56e..5932b4564 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1460,9 +1460,10 @@ Two corollaries that decide the design: resolved on the calling thread at submission, because the process current directory is mutable by any thread and even perfect remoting would be racy -- but `GetFullPathNameW` never expands a drive letter, so the "canonical" path - still carries a session-relative reference. (It is not *lexical* either, which - matters elsewhere but not here: it resolves against the process current - directory, which is exactly the property submission-time resolution buys. See + still carries a session-relative reference. (It is not *lexical* as a whole + either, which matters elsewhere but not here: it collapses `.`/`..` lexically + but roots a path that is not fully qualified against the process current + directory, and that rooting is the property submission-time resolution buys. See `windows-namespace-request-sys`' [DESIGN-NOTES.md](crates/windows-namespace-request-sys/DESIGN-NOTES.md) -> `D-18`.) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 88ae23c34..25b6f45fe 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -537,19 +537,29 @@ to one side alone. ## D-18: `GetFullPathNameW` is not lexical, and the genuinely lexical alternative is the wrong call **The correction.** This crate described `GetFullPathNameW` as **lexical** in -six places, in a sentence that then went on to say it resolves against the -process current directory. Those two claims disagree: a lexical canonicalizer is -a pure function of its input, and this reads mutable process state -- the current -directory, and for a drive-relative path such as `C:foo` the per-drive current -directory Windows keeps in the hidden `=C:` environment variables. The claim -that holds is **touches no filesystem**. - -The wrong word had spread beyond where it was reported. The consuming probe's -checklist item named [full_path.rs](src/full_path.rs) only; a sweep for the term -found the identical sentence in [path.rs](src/path.rs), plus four further -restatements across doc examples, tests, an acceptance comment and this file. -The reported site was a sample, not the population -- which is the standing -lesson, met again. +**nine places across five files**, in a sentence that then went on to say it +resolves against the process current directory. Those two claims disagree, and +the call is better described as doing two separable things. It collapses +`.`/`..` and normalizes separators, which *is* lexical -- `C:\a\..\b` becomes +`C:\b` whatever the current directory is, and whether or not `C:\a` exists. It +*also* **roots** a path that is not fully qualified, and that reads mutable +process state: the current directory, or for a drive-relative path such as +`C:foo` that drive's own current directory, which Windows keeps in the hidden +`=C:` environment variables. So the call is not lexical *as a whole*, and the +claim that holds unqualified is **touches no filesystem**. + +Keeping the two halves apart matters, because the decision below turns on the +rooting half alone. Saying the call resolves `.`/`..` "against the current +directory" -- as a first draft of this correction did -- attributes process-state +dependence to the one operation that has none, which is the same imprecision +running the other way. + +The wrong word had spread well beyond where it was reported. The consuming +probe's checklist item named [full_path.rs](src/full_path.rs) only; that file +held three of the nine on its own, `path.rs` two more, and the rest were spread +across doc examples, [tests.rs](src/full_path/tests.rs), an acceptance comment +and this file. The reported site was a sample, not the population -- which is the +standing lesson, met again. **The decision: keep `GetFullPathNameW`.** A genuinely lexical canonicalizer exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and is cheaper, diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 57d67bb76..1c54ccf93 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -8,18 +8,27 @@ //! # What it solves, and what it leaves standing //! //! This call **touches no filesystem**: it will happily resolve a path to -//! something that does not exist. It is **not** lexical, and the difference is -//! the whole reason this entry exists. It resolves relative components and -//! `.`/`..` against the *process current directory*, and for a drive-relative -//! path such as `C:foo` against the per-drive current directory Windows keeps -//! in the hidden `=C:` environment variables. Both are process state, so the -//! same input string resolves to different outputs in the same process at -//! different times. +//! something that does not exist. //! -//! Calling it lexical -- as an earlier revision of this doc did, in the sentence -//! immediately before the one describing the current directory it reads -- gets -//! that exactly backwards. A lexical canonicalizer is a pure function of its -//! input; this reads mutable process state, which is precisely the property +//! It does **two** things, and keeping them apart is the whole reason this +//! entry exists: +//! +//! 1. It collapses `.` and `..` and normalizes separators. This part *is* +//! lexical -- pure string work over the input, reading no process state. +//! `C:\a\..\b` becomes `C:\b` whatever the current directory happens to be, +//! and whether or not `C:\a` exists. +//! 2. It **roots** a path that is not fully qualified, and that part reads +//! mutable process state. A relative path like `rel.txt` is rooted at the +//! *process current directory*; a drive-relative path like `C:foo` is rooted +//! at that drive's own current directory, which Windows keeps in the hidden +//! `=C:` environment variables. +//! +//! So the call is **not** lexical as a whole, and describing it that way -- as +//! an earlier revision of this doc did, in the sentence immediately before the +//! one describing the current directory it reads -- loses exactly the half that +//! matters here. A fully-qualified input resolves to the same output every +//! time; an input that is not fully qualified resolves to different outputs in +//! the same process at different times, and pinning *that* is the property //! being bought. //! //! So it solves exactly one problem -- the process current directory is shared diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 6950f022c..bec0f5634 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -15,10 +15,12 @@ //! //! # A resolved path is not a session-independent path //! -//! `GetFullPathNameW` **touches no filesystem**, and it is not lexical: it -//! resolves relative components and `.`/`..` against process state -- the -//! current directory, and for a drive-relative path the per-drive current -//! directory in the `=C:` environment variables. What it never does is expand +//! `GetFullPathNameW` **touches no filesystem**. It collapses `.`/`..` +//! lexically, and it *additionally* roots a path that is not fully qualified +//! against process state -- the current directory, or for a drive-relative path +//! that drive's own current directory in the `=C:` environment variables. It is +//! therefore not a lexical call as a whole, which is what makes resolving on +//! the submitting thread meaningful. What it never does is expand //! a drive letter, and a drive letter resolves //! against the *logon session* of whatever token is in effect. So a path //! prepared on a submitting thread and opened on a worker under a captured diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 37c5ac390..46baff817 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -89,36 +89,7 @@ speculative list to extend by imagination -- a fourth is added when a fourth con itself the argument for the oracle. - [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide - whether it is still the call `prepare` wants. Two successive descriptions in the cost probe were - each wrong in the same direction: *a syscall cost*, which a timing loop cannot establish, and then - *lexical*, which it also is not. The probe now states the cost and declines the mechanism, which is - honest but leaves the question open one layer down. - - [../windows-namespace-request-sys/src/full_path.rs](../windows-namespace-request-sys/src/full_path.rs) - carries the same imprecision, and is the crate that owns the answer: its module doc says "This call - is **lexical**. It resolves relative components and `.`/`..` against the process current - directory". Those two sentences disagree -- consulting the current directory is process state, and - for a drive-relative path (`C:foo`) it also reads the per-drive current directory held in the - `=C:` environment variables. "Touches no filesystem" is the claim that holds; "lexical" is not. - - **The mono-repo rule says fix the layer, so the correction belongs in - `windows-namespace-request-sys`, not in the probe that consumes it.** It is queued rather than - taken because that crate is outside this peel and is release-managed, so a docs change there is its - own commit with its own scope. - - The decision half is the part worth an engineer's attention rather than a sweep. A genuinely - lexical canonicalizer exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and would be - cheaper, with no process state read at all. **It is very likely the wrong call anyway**, because - resolving against the current directory *at submission* is the property the namespace design is - buying: the CWD is shared mutable state, so a relative path means something different depending on - when it is resolved, and pinning that on the submitting thread is the whole point. Record that - conclusion explicitly, with the alternative named, so the next reader does not re-derive it -- and - if it is wrong, the cheaper call is sitting there. - - Also worth settling while the question is open: whether `GetFullPathNameW` can enter the kernel at - all on any path this crate takes. The probe measured ~212 ns for a build on x86_64 and declines to - say what that is made of; the owning crate could say, and a reader of either would then stop - guessing. + whether it is still the call `prepare` wants. -> [completed 2026-09-09](COMPLETED-CHECKLIST.md#m26) - [ ] **M2.7** -- Decide whether the other nine probe steps in CI should carry `if: '!cancelled()'`, and apply or record the decision. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 02129fa4a..4436b0ce3 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -93,3 +93,45 @@ piece of work rather than a correction to that one. The reason any of it works is that Rust's `Stdout` wraps a `LineWriter` and flushes at each newline even when redirected -- had stdout been block-buffered this milestone would have needed a per-line flush too. + +## Moved 2026-09-09 22:54:01 -04:00 -- M2.6: what `GetFullPathNameW` does, and whether it stays + +**Resolved.** The correction and the decision both landed in the owning crate as `D-18` in +[../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md): +`GetFullPathNameW` collapses `.`/`..` lexically but roots a path that is not fully qualified against +process state, so it is not a lexical call as a whole; `PathCchCanonicalizeEx` is genuinely lexical, +cheaper, and the wrong call, because rooting at submission is the property being bought. Whether it +enters the kernel is recorded as NOT established. The item's body below is the request as it was +written, and quotes the module doc as it read before the correction. + +- [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide + whether it is still the call `prepare` wants. Two successive descriptions in the cost probe were + each wrong in the same direction: *a syscall cost*, which a timing loop cannot establish, and then + *lexical*, which it also is not. The probe now states the cost and declines the mechanism, which is + honest but leaves the question open one layer down. + + [../windows-namespace-request-sys/src/full_path.rs](../windows-namespace-request-sys/src/full_path.rs) + carries the same imprecision, and is the crate that owns the answer: its module doc says "This call + is **lexical**. It resolves relative components and `.`/`..` against the process current + directory". Those two sentences disagree -- consulting the current directory is process state, and + for a drive-relative path (`C:foo`) it also reads the per-drive current directory held in the + `=C:` environment variables. "Touches no filesystem" is the claim that holds; "lexical" is not. + + **The mono-repo rule says fix the layer, so the correction belongs in + `windows-namespace-request-sys`, not in the probe that consumes it.** It is queued rather than + taken because that crate is outside this peel and is release-managed, so a docs change there is its + own commit with its own scope. + + The decision half is the part worth an engineer's attention rather than a sweep. A genuinely + lexical canonicalizer exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and would be + cheaper, with no process state read at all. **It is very likely the wrong call anyway**, because + resolving against the current directory *at submission* is the property the namespace design is + buying: the CWD is shared mutable state, so a relative path means something different depending on + when it is resolved, and pinning that on the submitting thread is the whole point. Record that + conclusion explicitly, with the alternative named, so the next reader does not re-derive it -- and + if it is wrong, the cheaper call is sitting there. + + Also worth settling while the question is open: whether `GetFullPathNameW` can enter the kernel at + all on any path this crate takes. The probe measured ~212 ns for a build on x86_64 and declines to + say what that is made of; the owning crate could say, and a reader of either would then stop + guessing. From f2807a724199100df8a021d260184b9b3709c442 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 23:12:58 -0400 Subject: [PATCH 06/36] docs(namespace-request): say what the probe actually establishes about cost Second review round. The previous commit over-corrected the first one, in the opposite direction, and both errors were about the same number. The first draft called ~210 ns a per-resolution cost, attributing to this call a total that also contains an allocation and a drop. Correcting that, the second draft said the probe "declines to decompose" the total. It does decompose: the report states that recycling an already-resolved path pays ~42 ns against ~210 ns, and that the ~168 ns difference is the resolution rather than the allocation -- which is exactly why recycling is the only one of its two candidate optimizations that can touch it. What the probe withholds is the MECHANISM, not the division. So the doc now quotes ~168 ns as this call's measured share, and says the number is silent on whether any of it entered the kernel. Also corrects which label the ~210 ns belongs to. It is the whole request build; `prepare_short_path` is a different, lower figure. Measured over three runs on this x86_64 host: prepare 200-206 ns, build 208-218 ns, clone 43-69 ns. An earlier single run had read 287 ns for the build, which was noise -- these probes are not deterministic, which is why the repo's own control for them exists. Two further fixes: - The two-part split omitted a third rooting form. `\foo` takes only the current DRIVE from process state, yielding `C:\foo` -- not the current directory's subtree, which the previous wording implied. Measured under two different current directories. The lexical half now also names trailing dot/space trimming and legacy device mapping (`CON` -> `\\.\CON`), which a path-preparing crate's callers can hit. - D-18's sweep narrative counted the doc examples twice. The nine/three/two totals were right; the breakdown now adds to nine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 30 +++++++++------ .../src/full_path.rs | 38 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 25b6f45fe..edc7c7d8a 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -556,10 +556,10 @@ running the other way. The wrong word had spread well beyond where it was reported. The consuming probe's checklist item named [full_path.rs](src/full_path.rs) only; that file -held three of the nine on its own, `path.rs` two more, and the rest were spread -across doc examples, [tests.rs](src/full_path/tests.rs), an acceptance comment -and this file. The reported site was a sample, not the population -- which is the -standing lesson, met again. +held three of the nine on its own -- the module doc and both doc examples -- +with two more in `path.rs`, two in [tests.rs](src/full_path/tests.rs), one in an +acceptance comment and one in this file. The reported site was a sample, not the +population -- which is the standing lesson, met again. **The decision: keep `GetFullPathNameW`.** A genuinely lexical canonicalizer exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and is cheaper, @@ -580,14 +580,20 @@ documented to consult requires a transition; the current directory lives in the PEB and the `=C:` variables in the process environment block, both ordinary process memory. Windows does not document the implementation, so that is a statement about the data sources rather than a measurement of the call. -The figure that exists is quoted for what it is and no more: `probe-request-cost` -measures a *construct-and-drop cycle* for a short prepared path at roughly 210 ns -on x86_64 -- an allocation, this resolution and a drop together. The probe -deliberately declines to decompose that total, so it bounds this call from above -and says nothing about what it costs alone, still less about whether any part of -it entered the kernel. Quoting it as a per-resolution cost, as a first draft of -this decision did, would have been the same defect this decision is correcting: -attributing a measured number to a mechanism the measurement does not isolate. The +The figures that exist say more than a bound, and are worth quoting exactly. On +x86_64 `probe-request-cost` measures building an open request as a +construct-and-drop cycle at roughly 210 ns, and cloning an *already resolved* +path at roughly 42 ns. It attributes the difference -- about 168 ns -- to this +resolution rather than to the allocation, which is why recycling a resolved path +is the only one of the two candidate optimizations that can touch it. What the +probe declines to name is the **mechanism**, not the division. + +Two drafts of this decision got that wrong in opposite directions, which is why +it is spelled out. The first quoted the whole figure as a per-resolution cost, +attributing to this call a total that includes an allocation and a drop. The +second over-corrected, saying the probe "declines to decompose" -- it does +decompose, and names ~168 ns as the resolution's share; what it withholds is +whether any of that is a kernel transition. The distinction is kept deliberately: two successive descriptions of this call in that probe were each wrong in the same direction, by naming a mechanism the evidence did not reach. diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 1c54ccf93..112a14889 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -13,14 +13,20 @@ //! It does **two** things, and keeping them apart is the whole reason this //! entry exists: //! -//! 1. It collapses `.` and `..` and normalizes separators. This part *is* -//! lexical -- pure string work over the input, reading no process state. -//! `C:\a\..\b` becomes `C:\b` whatever the current directory happens to be, -//! and whether or not `C:\a` exists. +//! 1. It rewrites the string. `.` and `..` are collapsed, `/` becomes `\`, +//! trailing dots and spaces are trimmed, and a legacy device name is mapped +//! into the device namespace (`CON` becomes `\\.\CON`, which is worth +//! knowing for a crate that prepares paths). This part *is* lexical -- pure +//! string work over the input, reading no process state. `C:\a\..\b` becomes +//! `C:\b` whatever the current directory happens to be, and whether or not +//! `C:\a` exists. //! 2. It **roots** a path that is not fully qualified, and that part reads -//! mutable process state. A relative path like `rel.txt` is rooted at the -//! *process current directory*; a drive-relative path like `C:foo` is rooted -//! at that drive's own current directory, which Windows keeps in the hidden +//! mutable process state. There are three such forms, and they read +//! different state: a relative path like `rel.txt` is rooted at the *process +//! current directory*; a root-relative path like `\foo` takes only the +//! *current drive* from it, giving `C:\foo` rather than the current +//! directory's subtree; and a drive-relative path like `C:foo` is rooted at +//! that drive's own current directory, which Windows keeps in the hidden //! `=C:` environment variables. //! //! So the call is **not** lexical as a whole, and describing it that way -- as @@ -64,13 +70,17 @@ //! successive descriptions of this call in a consuming probe were each wrong in //! the same direction, by naming a mechanism the evidence did not reach. //! -//! So the figure that exists is quoted for what it is and no more: -//! `probe-request-cost` measures a *construct-and-drop cycle* for a short -//! prepared path at roughly 210 ns on x86_64. That is an allocation, this -//! resolution and a drop together -- the probe deliberately declines to say how -//! the total divides, so it bounds this call from above and says nothing about -//! what it costs alone, still less about whether any part of it entered the -//! kernel. +//! The figures that exist say more than a bound, and are worth quoting exactly. +//! On x86_64 `probe-request-cost` measures building an open request as a +//! construct-and-drop cycle at roughly 210 ns, and cloning an *already +//! resolved* path at roughly 42 ns. It attributes the difference -- about +//! 168 ns -- to this resolution rather than to the allocation, which is why +//! recycling a resolved path is the only one of the two candidate +//! optimizations that can touch it. +//! +//! What the probe declines to name is the **mechanism**, not the division. So +//! roughly 168 ns is this call's measured share on that host, and nothing in +//! that number says whether any part of it entered the kernel. //! //! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` From 6160ef96bb4e61a651b725bdaa03f4df85a8802e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Wed, 9 Sep 2026 23:45:41 -0400 Subject: [PATCH 07/36] docs(namespace-request): make the cost a bound, and fix three path claims Third review round. The pattern is now explicit in D-18: each of the three previous drafts of the cost paragraph named a mechanism the evidence did not reach, in a different way. COST. The last draft subtracted `clone_prepared_units` from `build_open_request` and called the ~168 ns remainder "this call's measured share". The subtraction does not isolate the call. `build_open_request` runs the whole preparation step plus the OpenFile builder chain, and `prepare` makes TWO heap allocations of its own -- a copy of the input and a MAX_PATH output buffer -- against the clone's one. So the gap is dominated by allocator work the same sentence was busy excluding. Measured, five runs on this x86_64 host: prepare 211.5, build 218.4, clone 47.0, so the builder chain is ~7 ns and the gap ~165 ns (the 168 was subtracting two separately rounded figures). Timed directly with the input marshalled and the output buffer pre-allocated, so no allocation is in the loop, the call itself is ~110 ns over 200k iterations -- about two thirds of the gap. The doc now states the probe's number as an upper bound, says plainly that no instrument here isolates the call, and labels the direct measurement as not being a probe output. PATHS. Three further corrections, each measured: - A legacy device name SHORT-CIRCUITS the rooting rather than adding to the rewriting. `CON` -> `\\.\CON` and is not rooted at all, so it was a counterexample to the unconditional "roots a path that is not fully qualified" and to the claim that unqualified inputs vary with process state -- `CON` is unqualified and invariant. It is exact-match only: `CON.txt` and `a\CON` root normally, and `\CON` -> `Q:\CON`. - `\foo` takes the ROOT of the current directory, not its drive. With the current directory set to \\localhost\C$\Windows it yields \\localhost\C$\foo, which has no drive letter at all. This crate handles \\?\UNC\ paths, so the UNC case is in scope. - The root DESIGN-NOTES and M26.5 still said rooting was "against the process current directory", which the drive-relative case disproves: setting =C: while leaving the current directory alone changes what `C:foo` resolves to. Both now match D-18 and path.rs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 7 ++- DESIGN-NOTES.md | 6 +- .../DESIGN-NOTES.md | 34 ++++++----- .../src/full_path.rs | 59 ++++++++++++------- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 2963f8976..8e1662228 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -418,9 +418,10 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the because no audited consumer calls it. - [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it collapses `.`/`..` - lexically and roots a path that is not fully qualified against the process current directory, and - never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and - its documentation must say which problem it solves and which it leaves standing. + lexically and roots a path that is not fully qualified against process state -- the current + directory, or for a drive-relative path that drive's own -- and never expands a drive letter, so it + does **not** close the session-relative hazard from M20.1, and its documentation must say which + problem it solves and which it leaves standing. - [x] **M26.6** -- Acceptance, in **two** parts, because the audit had two purposes and checking only the first is how the coverage question got missed once already. diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 5932b4564..621495357 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1462,8 +1462,10 @@ Two corollaries that decide the design: but `GetFullPathNameW` never expands a drive letter, so the "canonical" path still carries a session-relative reference. (It is not *lexical* as a whole either, which matters elsewhere but not here: it collapses `.`/`..` lexically - but roots a path that is not fully qualified against the process current - directory, and that rooting is the property submission-time resolution buys. See + but roots a path that is not fully qualified against process state -- the + current directory, or for a drive-relative path that drive's own current + directory, which moves independently of it -- and that rooting is the property + submission-time resolution buys. See `windows-namespace-request-sys`' [DESIGN-NOTES.md](crates/windows-namespace-request-sys/DESIGN-NOTES.md) -> `D-18`.) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index edc7c7d8a..732b27ca6 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -580,20 +580,26 @@ documented to consult requires a transition; the current directory lives in the PEB and the `=C:` variables in the process environment block, both ordinary process memory. Windows does not document the implementation, so that is a statement about the data sources rather than a measurement of the call. -The figures that exist say more than a bound, and are worth quoting exactly. On -x86_64 `probe-request-cost` measures building an open request as a -construct-and-drop cycle at roughly 210 ns, and cloning an *already resolved* -path at roughly 42 ns. It attributes the difference -- about 168 ns -- to this -resolution rather than to the allocation, which is why recycling a resolved path -is the only one of the two candidate optimizations that can touch it. What the -probe declines to name is the **mechanism**, not the division. - -Two drafts of this decision got that wrong in opposite directions, which is why -it is spelled out. The first quoted the whole figure as a per-resolution cost, -attributing to this call a total that includes an allocation and a drop. The -second over-corrected, saying the probe "declines to decompose" -- it does -decompose, and names ~168 ns as the resolution's share; what it withholds is -whether any of that is a kernel transition. The +What `probe-request-cost` produces is a **bound, not this call's cost**. On +x86_64 it measures building an open request as a construct-and-drop cycle at +roughly 210 ns and cloning an already-resolved path at roughly 45 ns. The +~165 ns between them is what recycling recovers, and no more than that: the gap +spans the whole preparation step, which makes **two** heap allocations this +crate performs -- a copy of the input and a `MAX_PATH` output buffer -- against +the clone's one, plus the builder chain. Timed on its own with no allocation in +the loop, the call is about **110 ns** on this host, roughly two thirds of the +gap; that is a direct measurement taken for this note, not a probe output, and +no instrument in this repository isolates the call. + +**Three drafts of this paragraph were wrong in three different ways, which is +why it is now spelled out.** The first quoted ~210 ns as a per-resolution cost, +attributing to this call a total containing an allocation and a drop. The second +over-corrected to "the probe declines to decompose" -- it does decompose, and +prints the build-minus-clone split itself. The third took that split at face +value and called ~168 ns "this call's measured share", which credits the call +with the allocator work the same sentence excludes, and overstates it by about +half. Each draft named a mechanism the evidence did not reach, which is the +defect this decision exists to correct. The distinction is kept deliberately: two successive descriptions of this call in that probe were each wrong in the same direction, by naming a mechanism the evidence did not reach. diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 112a14889..606977950 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -13,10 +13,8 @@ //! It does **two** things, and keeping them apart is the whole reason this //! entry exists: //! -//! 1. It rewrites the string. `.` and `..` are collapsed, `/` becomes `\`, -//! trailing dots and spaces are trimmed, and a legacy device name is mapped -//! into the device namespace (`CON` becomes `\\.\CON`, which is worth -//! knowing for a crate that prepares paths). This part *is* lexical -- pure +//! 1. It rewrites the string. `.` and `..` are collapsed, `/` becomes `\`, and +//! trailing dots and spaces are trimmed. This part *is* lexical -- pure //! string work over the input, reading no process state. `C:\a\..\b` becomes //! `C:\b` whatever the current directory happens to be, and whether or not //! `C:\a` exists. @@ -24,18 +22,28 @@ //! mutable process state. There are three such forms, and they read //! different state: a relative path like `rel.txt` is rooted at the *process //! current directory*; a root-relative path like `\foo` takes only the -//! *current drive* from it, giving `C:\foo` rather than the current -//! directory's subtree; and a drive-relative path like `C:foo` is rooted at -//! that drive's own current directory, which Windows keeps in the hidden -//! `=C:` environment variables. +//! *root* of that directory, giving `C:\foo` rather than its subtree -- and +//! `\\server\share\foo` when the current directory is a UNC path, which is +//! why this says root and not drive; and a drive-relative path like `C:foo` +//! is rooted at that drive's own current directory, which Windows keeps in +//! the hidden `=C:` environment variables and which moves independently of +//! the process current directory. +//! +//! **One input short-circuits both.** An input that is *exactly* a legacy device +//! name resolves into the device namespace and is not rooted at all: `CON` +//! becomes `\\.\CON`, not a file under the current directory. It is exact-match +//! only -- `CON.txt` and `a\CON` are rooted normally, and `\CON` becomes +//! `Q:\CON` for a current directory on `Q:`. A crate that prepares paths on a +//! caller's behalf should know that `prepare("CON")` hands back a device. //! //! So the call is **not** lexical as a whole, and describing it that way -- as //! an earlier revision of this doc did, in the sentence immediately before the //! one describing the current directory it reads -- loses exactly the half that //! matters here. A fully-qualified input resolves to the same output every -//! time; an input that is not fully qualified resolves to different outputs in -//! the same process at different times, and pinning *that* is the property -//! being bought. +//! time; an input that is rooted resolves to different outputs in the same +//! process at different times, and pinning *that* is the property being bought. +//! (Not every unqualified input is rooted, which is the point of the device +//! short-circuit above: `CON` is unqualified and yet invariant.) //! //! So it solves exactly one problem -- the process current directory is shared //! mutable state that any thread can change, so a relative path means something @@ -70,17 +78,26 @@ //! successive descriptions of this call in a consuming probe were each wrong in //! the same direction, by naming a mechanism the evidence did not reach. //! -//! The figures that exist say more than a bound, and are worth quoting exactly. -//! On x86_64 `probe-request-cost` measures building an open request as a -//! construct-and-drop cycle at roughly 210 ns, and cloning an *already -//! resolved* path at roughly 42 ns. It attributes the difference -- about -//! 168 ns -- to this resolution rather than to the allocation, which is why -//! recycling a resolved path is the only one of the two candidate -//! optimizations that can touch it. +//! The figure the repo's own instrument produces is a **bound, not this call's +//! cost**, and the difference matters. On x86_64 `probe-request-cost` measures +//! building an open request as a construct-and-drop cycle at roughly 210 ns and +//! cloning an already-resolved path at roughly 45 ns. The ~165 ns between them +//! is what recycling a resolved path recovers, and that is all it is: the gap +//! covers the whole preparation step, which makes **two** heap allocations this +//! crate's own code performs -- a copy of the input and a MAX_PATH output +//! buffer -- against the clone's one, plus the builder chain. Attributing the +//! gap to this call, as a draft of this doc did, credits `GetFullPathNameW` +//! with allocator work the same sentence is busy excluding. +//! +//! Timed on its own -- input already marshalled, output buffer pre-allocated, +//! so no allocation is in the loop -- the call costs about **110 ns** on this +//! host, roughly two thirds of that gap. That measurement is a direct one taken +//! for this note and is *not* something the probe reports; no instrument in +//! this repository isolates the call, and the honest reading of +//! `probe-request-cost` alone is an upper bound. //! -//! What the probe declines to name is the **mechanism**, not the division. So -//! roughly 168 ns is this call's measured share on that host, and nothing in -//! that number says whether any part of it entered the kernel. +//! What the probe declines to name is the **mechanism**. No figure here says +//! whether any part of the call entered the kernel. //! //! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` From c61ea2d168b85427becc4f24997d385c5d04c7bd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 00:24:38 -0400 Subject: [PATCH 08/36] docs: qualify the rooting clause everywhere, and put each fact in its own module Fourth review round. Two findings, and they share a root cause: facts were landing in whichever file was open rather than the file that owns them. ROOTING IS NOT UNCONDITIONAL, AND FIVE SITES STILL SAID IT WAS. The previous commit's own message called `CON` a counterexample to "roots a path that is not fully qualified" -- then left that exact clause standing in path.rs, the root DESIGN-NOTES, M26.5, D-18 and the probes archive, two of which it rewrote in the same commit. The refuted statement outnumbered its correction five to one. All now say "most paths". THE DEVICE FORM IS LOOSER THAN "EXACT MATCH". Measured: `CON:`, `CON::`, `NUL:`, `LPT1:`, `AUX:` map to the device namespace, as do `CON.`, `CON ` and `con` -- the trailing colon is part of the form, the step-1 trimming runs first, and matching is case-insensitive. Only a name with something after it (`CON.txt`, `a\CON`, `CON:x`) roots normally. Saying "exact-match only" would have told a reader `prepare("CON:")` gives a file path; it gives a device. PLACEMENT. The `prepare("CON")` warning was in full_path.rs, which documents `ResolveFullPath`; `prepare` lives in path.rs, whose own docs said nothing about it. Moved. D-18, the durable record, mentioned neither the device short-circuit nor the root-vs-drive distinction -- both existed only in a module doc. Added. Also mine, found while checking the reviewer's allocation claim: the cost paragraph attributed "two heap allocations -- a copy of the input and a MAX_PATH output buffer" to this crate while sitting in full_path.rs. That is `path::resolve`'s shape, which is what the probe measures. `ResolveFullPath` takes its input already owned and allocates one buffer per attempt, with a growth loop `resolve` does not have. The paragraph now names which entry point it is describing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 2 +- DESIGN-NOTES.md | 2 +- .../DESIGN-NOTES.md | 22 +++++++++--- .../src/full_path.rs | 36 ++++++++++++------- .../windows-namespace-request-sys/src/path.rs | 18 ++++++++-- .../COMPLETED-CHECKLIST.md | 2 +- 6 files changed, 59 insertions(+), 23 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 8e1662228..1460f33cb 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -418,7 +418,7 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the because no audited consumer calls it. - [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it collapses `.`/`..` - lexically and roots a path that is not fully qualified against process state -- the current + lexically and roots most paths that are not fully qualified against process state -- the current directory, or for a drive-relative path that drive's own -- and never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and its documentation must say which problem it solves and which it leaves standing. diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 621495357..b99d45b9b 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1462,7 +1462,7 @@ Two corollaries that decide the design: but `GetFullPathNameW` never expands a drive letter, so the "canonical" path still carries a session-relative reference. (It is not *lexical* as a whole either, which matters elsewhere but not here: it collapses `.`/`..` lexically - but roots a path that is not fully qualified against process state -- the + but roots most paths that are not fully qualified against process state -- the current directory, or for a drive-relative path that drive's own current directory, which moves independently of it -- and that rooting is the property submission-time resolution buys. See diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 732b27ca6..20e7e8a9b 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -542,11 +542,23 @@ resolves against the process current directory. Those two claims disagree, and the call is better described as doing two separable things. It collapses `.`/`..` and normalizes separators, which *is* lexical -- `C:\a\..\b` becomes `C:\b` whatever the current directory is, and whether or not `C:\a` exists. It -*also* **roots** a path that is not fully qualified, and that reads mutable -process state: the current directory, or for a drive-relative path such as -`C:foo` that drive's own current directory, which Windows keeps in the hidden -`=C:` environment variables. So the call is not lexical *as a whole*, and the -claim that holds unqualified is **touches no filesystem**. +*also* **roots** most paths that are not fully qualified, and that reads mutable +process state. Three forms read three different pieces of it: a relative path +takes the current directory; a root-relative path like `\foo` takes only that +directory's *root*, which is `\\server\share\` when the current directory is a +UNC path and so is not a drive at all; and a drive-relative path such as +`C:foo` takes that drive's own current directory, which Windows keeps in the +hidden `=C:` environment variables and which moves independently of the process +current directory. So the call is not lexical *as a whole*, and the claim that +holds unqualified is **touches no filesystem**. + +**"Most" rather than "every", because a legacy device name short-circuits the +rooting.** `CON` resolves to `\\.\CON` and is not rooted, so it is an +unqualified input that is nonetheless invariant -- which is why the rooting +clause cannot be stated unconditionally. The form is looser than exact match: +`CON:`, `CON.`, `con` and `CONIN$` all map, while `CON.txt`, `a\CON` and +`CON:x` root normally. This matters to a crate that prepares paths on a +caller's behalf: `prepare("CON")` returns a device. Keeping the two halves apart matters, because the decision below turns on the rooting half alone. Saying the call resolves `.`/`..` "against the current diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 606977950..aedfe5134 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -29,12 +29,19 @@ //! the hidden `=C:` environment variables and which moves independently of //! the process current directory. //! -//! **One input short-circuits both.** An input that is *exactly* a legacy device -//! name resolves into the device namespace and is not rooted at all: `CON` -//! becomes `\\.\CON`, not a file under the current directory. It is exact-match -//! only -- `CON.txt` and `a\CON` are rooted normally, and `\CON` becomes -//! `Q:\CON` for a current directory on `Q:`. A crate that prepares paths on a -//! caller's behalf should know that `prepare("CON")` hands back a device. +//! **A whole class of input short-circuits both.** When the input names a +//! legacy device and nothing else, it resolves into the device namespace and is +//! not rooted at all: `CON` becomes `\\.\CON`, not a file under the current +//! directory. +//! +//! "And nothing else" is doing real work, and is looser than it first looks. A +//! trailing colon is part of the form, so `CON:` and `CON::` map too; the +//! trimming in step 1 happens first, so `CON.` and `CON ` map as well; and the +//! match is case-insensitive, so `con` does. What does *not* map is a name with +//! anything after it -- `CON.txt`, `a\CON` and `CON:x` are all rooted normally, +//! and `\CON` becomes `Q:\CON` for a current directory on `Q:`. The device set +//! is the legacy one (`CON`, `NUL`, `PRN`, `AUX`, `COM1`-`9`, `LPT1`-`9`, and +//! the console pair `CONIN$`/`CONOUT$`), not an open-ended list. //! //! So the call is **not** lexical as a whole, and describing it that way -- as //! an earlier revision of this doc did, in the sentence immediately before the @@ -82,12 +89,17 @@ //! cost**, and the difference matters. On x86_64 `probe-request-cost` measures //! building an open request as a construct-and-drop cycle at roughly 210 ns and //! cloning an already-resolved path at roughly 45 ns. The ~165 ns between them -//! is what recycling a resolved path recovers, and that is all it is: the gap -//! covers the whole preparation step, which makes **two** heap allocations this -//! crate's own code performs -- a copy of the input and a MAX_PATH output -//! buffer -- against the clone's one, plus the builder chain. Attributing the -//! gap to this call, as a draft of this doc did, credits `GetFullPathNameW` -//! with allocator work the same sentence is busy excluding. +//! is what recycling a resolved path recovers, and that is all it is. +//! +//! **That probe exercises [`crate::path::prepare`], not this module**, and the +//! two have different allocation shapes -- which is itself why the gap cannot be +//! read as this call's cost. `prepare` copies the input and then allocates a +//! `MAX_PATH` output buffer, so two allocations against the clone's one, and the +//! builder chain sits on top. [`ResolveFullPath`] takes its input already owned +//! and allocates one buffer per attempt instead. Either way the allocator work +//! is the crate's, not `GetFullPathNameW`'s, and attributing the gap to the call +//! -- as a draft of this doc did -- credits it with the work the same sentence +//! is busy excluding. //! //! Timed on its own -- input already marshalled, output buffer pre-allocated, //! so no allocation is in the loop -- the call costs about **110 ns** on this diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index bec0f5634..62e094ab9 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -16,11 +16,23 @@ //! # A resolved path is not a session-independent path //! //! `GetFullPathNameW` **touches no filesystem**. It collapses `.`/`..` -//! lexically, and it *additionally* roots a path that is not fully qualified -//! against process state -- the current directory, or for a drive-relative path +//! lexically, and it *additionally* roots most paths that are not fully +//! qualified against process state -- the current directory, or for a +//! root-relative path that directory's *root*, or for a drive-relative path //! that drive's own current directory in the `=C:` environment variables. It is //! therefore not a lexical call as a whole, which is what makes resolving on -//! the submitting thread meaningful. What it never does is expand +//! the submitting thread meaningful. +//! +//! **"Most" rather than "every", because a legacy device name short-circuits +//! the rooting entirely.** [`prepare`] hands the input to that call without +//! device handling of its own, so `prepare("CON")` yields `\\.\CON` -- a device, +//! not a file under the current directory -- and the same holds for `CON:`, +//! `NUL`, `LPT1:` and the rest of the legacy set. A caller passing through an +//! untrusted name should know that. Anything with more after it (`CON.txt`, +//! `a\CON`) roots normally. The full shape is in +//! [`crate::full_path`], which documents the call itself. +//! +//! What it never does is expand //! a drive letter, and a drive letter resolves //! against the *logon session* of whatever token is in effect. So a path //! prepared on a submitting thread and opened on a worker under a captured diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 4436b0ce3..18a8cecd3 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -98,7 +98,7 @@ piece of work rather than a correction to that one. **Resolved.** The correction and the decision both landed in the owning crate as `D-18` in [../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md): -`GetFullPathNameW` collapses `.`/`..` lexically but roots a path that is not fully qualified against +`GetFullPathNameW` collapses `.`/`..` lexically but roots most paths that are not fully qualified against process state, so it is not a lexical call as a whole; `PathCchCanonicalizeEx` is genuinely lexical, cheaper, and the wrong call, because rooting at submission is the property being bought. Whether it enters the kernel is recorded as NOT established. The item's body below is the request as it was From b778ea293648b5f748c4fb983c97083d1fa00c73 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 01:03:35 -0400 Subject: [PATCH 09/36] docs(namespace-request): drop the unmeasured cost claim, narrow the contract, cover it with tests Addresses the Copilot review feedback on #86, plus a fifth-round finding. REMOVED THE "CHEAPER" CLAIM. Copilot flagged this in six separate reviews and was right every time: nothing here benchmarks PathCchCanonicalizeEx or PathAllocCanonicalize, Microsoft documents behaviour rather than relative cost, and PathAllocCanonicalize allocates its own result. It was a guess wearing the clothes of a measurement, in a decision whose entire subject is not doing that -- and it survived four rounds of review because I kept reading past it. The decision never needed it; rooting semantics decide it alone. Also notes that PATHCCH_ALLOW_LONG_PATHS makes PathCchCanonicalizeEx consult process long-path state, so "reads no process state" was wrong too. NARROWED "TOUCHES NO FILESYSTEM". What Microsoft documents is that the function does not verify the result is valid or names an existing file -- about verification, not I/O. Observation cannot close that gap: resolving under a directory that does not exist shows no CHECK was made, not that no filesystem was touched. Now stated as the documented guarantee at all seven sites, which is what the crate actually relies on. THE DEVICE SET WAS INCOMPLETE, AND SAID IT WAS CLOSED. COM/LPT accept SUPERSCRIPT digits: COM^1, COM^2, COM^3 (U+00B9/B2/B3) and the LPT forms all resolve to devices. My own earlier test appeared to show otherwise; re-run with explicit code points it was unambiguous, and the first result had been garbled console output I misread. Those are exactly the members a hand-written denylist omits, and path.rs tells callers to guard untrusted names -- so the doc now warns against building a filter from it. Also drops the claim that trimming happens before the device check, which was an undocumented ordering. TESTS, so none of this rests on prose again. Seven new cases pin every documented form: relative rooting, root-relative taking the ROOT (asserted as a relation, so it holds under any current directory), the device short-circuit, all its accepted spellings, the superscripts, the rooted-normally control, and fully-qualified invariance. Verified non-vacuous by injecting COM0 into the superscript case and watching it go red. CONVENTIONS. The M2.6 stub is one line; the archive has a date group plus an anchored item heading with a completion stamp; and D-18's draft chronology moves to a new Tier 2 DESIGN-RATIONALE.md, leaving Tier 1 the decision and the constraint it carries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 4 +- .../DESIGN-NOTES.md | 58 ++++---- .../DESIGN-RATIONALE.md | 100 ++++++++++++++ .../src/full_path.rs | 66 ++++++--- .../src/full_path/tests.rs | 129 +++++++++++++++++- .../windows-namespace-request-sys/src/path.rs | 2 +- crates/windows-platform-probes/CHECKLIST.md | 3 +- .../COMPLETED-CHECKLIST.md | 14 +- .../src/request_cost.rs | 5 +- 9 files changed, 317 insertions(+), 64 deletions(-) create mode 100644 crates/windows-namespace-request-sys/DESIGN-RATIONALE.md diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 1460f33cb..5e6dfcf41 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -160,7 +160,7 @@ pool, or async anywhere near it. The family grows by one entry per Win32 call. | 6 | `GetFileInformationByHandle` (non-Ex) | watcher | `BY_HANDLE_FILE_INFORMATION`; a distinct call, not a class of entry 5 | | 7 | `GetFinalPathNameByHandleW` | watcher directly, Globazog via `std::fs::canonicalize` | `VOLUME_NAME_DOS \| FILE_NAME_NORMALIZED` | | 8 | `GetVolumeInformationByHandleW` | watcher | handle-based, not the path-based `GetVolumeInformationW` | -| 9 | `GetFullPathNameW` | enumeration | no filesystem access | +| 9 | `GetFullPathNameW` | enumeration | result not verified | Four audit findings that shape the milestones below, recorded because each contradicts an assumption the first draft of this plan was written on. @@ -417,7 +417,7 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the filesystem name. Handle-based; the path-based `GetVolumeInformationW` is deliberately not in round one because no audited consumer calls it. -- [x] **M26.5** -- The `GetFullPathNameW` entry. Touches no filesystem: it collapses `.`/`..` +- [x] **M26.5** -- The `GetFullPathNameW` entry. Does not verify its result: it collapses `.`/`..` lexically and roots most paths that are not fully qualified against process state -- the current directory, or for a drive-relative path that drive's own -- and never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and its documentation must say which diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 20e7e8a9b..511cf7521 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -550,7 +550,7 @@ UNC path and so is not a drive at all; and a drive-relative path such as `C:foo` takes that drive's own current directory, which Windows keeps in the hidden `=C:` environment variables and which moves independently of the process current directory. So the call is not lexical *as a whole*, and the claim that -holds unqualified is **touches no filesystem**. +holds unqualified is that it **does not verify what it produces** -- the documented guarantee, which is narrower than the "touches no filesystem" an earlier draft claimed and which observation cannot establish. **"Most" rather than "every", because a legacy device name short-circuits the rooting.** `CON` resolves to `\\.\CON` and is not rooted, so it is an @@ -573,19 +573,28 @@ with two more in `path.rs`, two in [tests.rs](src/full_path/tests.rs), one in an acceptance comment and one in this file. The reported site was a sample, not the population -- which is the standing lesson, met again. -**The decision: keep `GetFullPathNameW`.** A genuinely lexical canonicalizer -exists -- `PathCchCanonicalizeEx`, or `PathAllocCanonicalize` -- and is cheaper, -reading no process state at all. It is the wrong call here, and for the property -rather than the price: resolving against the current directory *at submission* is -what this crate is buying. A lexical canonicalizer would leave a relative path -relative, so its meaning would be settled on the worker at execution time, -against a current directory any thread may have changed in between -- which is -exactly the race preparation exists to close. The cheaper call is cheaper because -it does less, and the part it omits is the part wanted. - -Recorded with the alternative named so the next reader does not re-derive it. If -the reasoning is ever wrong -- a consumer wanting a pure string operation, having -resolved relativity another way -- the cheaper call is named here. +**The decision: keep `GetFullPathNameW`.** Two canonicalizers that do not root +exist -- `PathCchCanonicalizeEx` and `PathAllocCanonicalize`. They are the wrong +call here for a semantic reason: resolving against the current directory *at +submission* is what this crate is buying. A canonicalizer that does not root +would leave a relative path relative, so its meaning would be settled on the +worker at execution time, against a current directory any thread may have +changed in between -- which is exactly the race preparation exists to close. What +they omit is the part that is wanted. + +**No cost comparison is claimed, and that is deliberate.** An earlier draft of +this decision called the alternatives "cheaper". Nothing here benchmarks them, +Microsoft documents behaviour rather than relative cost, and +`PathAllocCanonicalize` allocates its own result -- so the word was a guess +wearing the clothes of a measurement, in a decision whose whole subject is not +doing that. It is also unnecessary: the rooting semantics decide this alone. +Neither is reliably free of process state either, since +`PATHCCH_ALLOW_LONG_PATHS` makes `PathCchCanonicalizeEx` consult the process +long-path setting unless the FORCE variant is used. + +Recorded with the alternatives named so the next reader does not re-derive it. +If the reasoning is ever wrong -- a consumer wanting a pure string operation, +having resolved relativity another way -- they are named here. **Whether it enters the kernel: not established, and said so.** Nothing it is documented to consult requires a transition; the current directory lives in the @@ -603,18 +612,15 @@ the loop, the call is about **110 ns** on this host, roughly two thirds of the gap; that is a direct measurement taken for this note, not a probe output, and no instrument in this repository isolates the call. -**Three drafts of this paragraph were wrong in three different ways, which is -why it is now spelled out.** The first quoted ~210 ns as a per-resolution cost, -attributing to this call a total containing an allocation and a drop. The second -over-corrected to "the probe declines to decompose" -- it does decompose, and -prints the build-minus-clone split itself. The third took that split at face -value and called ~168 ns "this call's measured share", which credits the call -with the allocator work the same sentence excludes, and overstates it by about -half. Each draft named a mechanism the evidence did not reach, which is the -defect this decision exists to correct. The -distinction is kept deliberately: two successive descriptions of this call in -that probe were each wrong in the same direction, by naming a mechanism the -evidence did not reach. +**The constraint this decision carries, and not just its conclusion:** state +only what the evidence reaches. Seven drafts of this entry each named a +mechanism it did not -- the call's nature, what a number measured, what the +alternatives cost, whether any filesystem was touched. The wordings differ; the +error does not. A reader taking only "it is not lexical" away from D-18 has the +answer without the thing that kept producing wrong ones. + +The drafts themselves, and why each failed, are Tier 2: +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) -> `D-18`. ## Open, and inherited rather than introduced diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md new file mode 100644 index 000000000..cdeb72801 --- /dev/null +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -0,0 +1,100 @@ +# Design rationale: windows-namespace-request-sys + +Tier 2. How the decisions in [DESIGN-NOTES.md](DESIGN-NOTES.md) were reached -- +alternatives considered, drafts that were wrong, and the reasoning that got +discarded along the way. Consulted for "why", never for "what is true now": if +this file and Tier 1 disagree, **Tier 1 wins**. + +Cross-referenced by decision ID. + +## D-18: `GetFullPathNameW` is not lexical + +The decision is in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> `D-18`. What follows is +the record of getting there, which is unusually worth keeping because the same +mistake recurred five times in five different wordings. + +### The shape of the error, which never changed + +Every wrong draft did the same thing: **named a mechanism the evidence did not +reach.** The subject moved -- what the call does, what a number measures, which +alternatives cost less -- but the failure did not. That is why the decision +records the constraint rather than only the conclusion: a reader who takes only +"it is not lexical" away from D-18 has the answer without the thing that keeps +producing wrong answers. + +### The drafts + +1. **"This call is lexical."** The original text, contradicted by its own next + sentence, which said it resolves against the process current directory. A + lexical canonicalizer is a pure function of its input; this reads mutable + process state. + +2. **"It resolves relative components and `.`/`..` against the process current + directory."** The first correction, which overshot. Collapsing `.`/`..` is + pure string work and reads no process state at all -- `C:\a\..\b` becomes + `C:\b` under any current directory, and whether or not `C:\a` exists. Only + *rooting* reads process state. Measured under two different current + directories. + +3. **"The probe measures roughly 212 ns per resolution."** It does not. The + probe reports a construct-and-drop cycle whose total contains an allocation + and a drop, and its own output says so. + +4. **"The probe deliberately declines to decompose that total."** An + over-correction of (3). It does decompose: the report states that recycling + an already-resolved path pays ~42 ns against ~210 ns and attributes the + difference to the resolution. What the probe withholds is the **mechanism** -- + whether any of it is a kernel transition -- not the division. + +5. **"Roughly 168 ns is this call's measured share."** Taking (4)'s split at face + value. The subtraction spans the whole preparation step, which makes two heap + allocations of the crate's own against the clone's one, plus a builder chain. + Timed directly with no allocation in the loop, the call is about 110 ns -- + the draft overstated it by roughly half. + +6. **"Either alternative is cheaper."** Said of `PathCchCanonicalizeEx` and + `PathAllocCanonicalize` from the first commit onwards, and never measured. + Nothing in this repository benchmarks them, Microsoft documents behaviour + rather than relative cost, and `PathAllocCanonicalize` allocates its own + result. Removed rather than substantiated, because the decision rests on + rooting semantics and never needed it. + +7. **"Touches no filesystem."** The claim that survived longest, because it is + nearly right. What Microsoft documents is that the function does not verify + that the resulting path is valid or names an existing file -- a statement + about *verification*, not about I/O. Observation cannot close the gap: + resolving a path under a directory that does not exist shows no check was + made, not that no filesystem was touched. + +### Two facts that were measured, then asserted too narrowly + +Both were found by review after the correction had already shipped, and both +were *enumerations* -- which is the form this kind of error likes. + +- **The rooting forms.** Stated first as one case (the current directory), then + two, and finally three: a relative path takes the current directory, a + root-relative path takes only that directory's *root* (which is + `\\server\share\` under a UNC current directory, so "current drive" was + wrong), and a drive-relative path takes that drive's own current directory + from `=C:`. + +- **The device set.** The short-circuit was first described as "exact-match + only", which `CON:` disproves; then enumerated as `CON`/`NUL`/`PRN`/`AUX`/ + `COM1`-`9`/`LPT1`-`9`/`CONIN$`/`CONOUT$`, which omits the superscript + spellings `COM^1`, `COM^2`, `COM^3` (U+00B9, U+00B2, U+00B3) and their `LPT` + equivalents. Those are exactly the members a hand-written denylist misses, and + the documentation asserted a closed list without them until a review measured + it. The tests in `full_path/tests.rs` now pin every documented spelling so the + next omission fails CI instead of a review. + +### Why the alternatives were never seriously in contention + +`PathCchCanonicalizeEx` and `PathAllocCanonicalize` canonicalize without +rooting, which sounds like a strictly smaller job and therefore an easy win. It +is the wrong trade for this crate: a path that is still relative has its meaning +settled on the worker thread at execution time, against a current directory any +thread may have changed in between. That is precisely the race preparation +exists to close, so the "smaller job" omits the part being bought. + +Recorded here rather than only in Tier 1 so the next reader who notices the +cheaper-looking API does not have to re-derive why it was declined. diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index aedfe5134..8aaf3966b 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -7,8 +7,17 @@ //! //! # What it solves, and what it leaves standing //! -//! This call **touches no filesystem**: it will happily resolve a path to -//! something that does not exist. +//! This call **does not verify what it produces**: it will happily resolve a +//! path to something that does not exist, and it reports no error for one. +//! +//! That is the documented guarantee, and it is deliberately narrower than +//! "touches no filesystem", which earlier revisions of this doc claimed. +//! Microsoft specifies that the function does not verify that the resulting +//! path and file name are valid or that they name an existing file; it does not +//! specify that no I/O occurs. Observation cannot close that gap either -- +//! resolving a path under a directory that does not exist shows no *check* was +//! made, not that no filesystem was touched. The narrower claim is the one this +//! crate relies on, and it is sufficient: a caller wanting existence must open. //! //! It does **two** things, and keeping them apart is the whole reason this //! entry exists: @@ -34,14 +43,24 @@ //! not rooted at all: `CON` becomes `\\.\CON`, not a file under the current //! directory. //! -//! "And nothing else" is doing real work, and is looser than it first looks. A -//! trailing colon is part of the form, so `CON:` and `CON::` map too; the -//! trimming in step 1 happens first, so `CON.` and `CON ` map as well; and the -//! match is case-insensitive, so `con` does. What does *not* map is a name with -//! anything after it -- `CON.txt`, `a\CON` and `CON:x` are all rooted normally, -//! and `\CON` becomes `Q:\CON` for a current directory on `Q:`. The device set -//! is the legacy one (`CON`, `NUL`, `PRN`, `AUX`, `COM1`-`9`, `LPT1`-`9`, and -//! the console pair `CONIN$`/`CONOUT$`), not an open-ended list. +//! "And nothing else" is doing real work, and is looser than it first looks. +//! These all reach a device: a bare name (`CON`), a trailing colon (`CON:`, +//! `CON::`), trailing dots or spaces (`CON.`, `CON `), and any casing +//! (`con`). These do not, and root normally: anything with more of a path +//! around it (`CON.txt`, `a\CON`, `.\CON`, `CON:x`), and `\CON`, which +//! becomes `Q:\CON` for a current directory on `Q:`. +//! +//! **Do not build a name filter from the list below.** The accepted names are +//! `CON`, `NUL`, `PRN`, `AUX`, `CONIN$`, `CONOUT$`, and `COM`/`LPT` +//! followed by a single digit -- where "digit" includes the *superscripts* +//! `COM^1`, `COM^2` and `COM^3` (U+00B9, U+00B2, U+00B3) as well as `1`-`9`. +//! An exhaustive scan of the character after `COM` accepts exactly +//! U+0031-U+0039, U+00B2, U+00B3 and U+00B9 on the tested build; `COM0` and +//! `COM10` are not devices. The superscripts are precisely the sort of member a +//! hand-written denylist omits, and this documentation asserted a list without +//! them until a review measured it -- so treat the set as *observed on one +//! build*, and prefer letting this call answer the question over reimplementing +//! its judgement. //! //! So the call is **not** lexical as a whole, and describing it that way -- as //! an earlier revision of this doc did, in the sentence immediately before the @@ -59,21 +78,28 @@ //! //! # Why not a genuinely lexical canonicalizer //! -//! One exists: `PathCchCanonicalizeEx`, or `PathAllocCanonicalize`. Either is -//! cheaper and reads no process state at all. +//! Two exist: `PathCchCanonicalizeEx` and `PathAllocCanonicalize`. Both +//! canonicalize the string without rooting it. //! -//! **They are the wrong call here, and the reason is the property above rather -//! than cost.** Resolving against the current directory *at submission* is what +//! **They are the wrong call here, and the reason is a semantic difference, not +//! a cost one.** Resolving against the current directory *at submission* is what //! this crate is buying. A lexical canonicalizer would leave a relative path //! still relative, so its meaning would be decided on the worker thread at //! execution time, against a current directory any thread may have changed in -//! between -- reintroducing exactly the race preparation exists to close. The -//! cheaper call is cheaper because it does less, and the part it does not do is -//! the part wanted. +//! between -- reintroducing exactly the race preparation exists to close. What +//! they omit is the part that is wanted. +//! +//! **No cost comparison is claimed, deliberately.** Nothing in this repository +//! benchmarks either alternative, Microsoft documents behaviour rather than +//! relative cost, and `PathAllocCanonicalize` allocates its own result -- so +//! "cheaper" would be a guess. It is also not needed: the decision rests on the +//! rooting semantics alone. Nor is either one reliably free of process state, +//! since `PATHCCH_ALLOW_LONG_PATHS` makes `PathCchCanonicalizeEx` consult the +//! process long-path setting unless the FORCE variant is used. //! //! Recorded so the next reader does not re-derive it. If this reasoning is ever //! wrong -- for a consumer that genuinely wants a pure string operation and has -//! resolved relativity some other way -- the cheaper call is named here. +//! resolved relativity some other way -- the alternatives are named here. //! //! # Whether it can enter the kernel //! @@ -214,8 +240,8 @@ impl From for FullPathError { /// use wtf_string::Wtf16String; /// /// // A path to nothing resolves perfectly happily, because the call -/// // touches no filesystem. A consumer wanting a verified path wants an open plus -/// // GetFinalPathNameByHandleW instead. +/// // does not verify it. A consumer wanting a verified path wants an open +/// // plus GetFinalPathNameByHandleW instead. /// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\no-such-directory\..\file.txt")) /// .perform()? /// .to_string_lossy(); diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 73dbd498b..c0fd9b445 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -2,10 +2,9 @@ //! Tests for the `GetFullPathNameW` entry. //! -//! The negatives matter more than the positives here: this call touches no -//! filesystem, and -//! a suite that only ever resolved existing paths would leave a reader -//! believing it verifies something. +//! The negatives matter more than the positives here: this call does not verify +//! what it produces, and a suite that only ever resolved existing paths would +//! leave a reader believing it does. use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; use wtf_string::Wtf16String; @@ -46,8 +45,9 @@ fn an_already_absolute_path_is_returned_unchanged() { #[test] fn a_path_that_does_not_exist_resolves_perfectly_happily() { - // The call touches no filesystem. A consumer wanting a verified path wants - // an open plus GetFinalPathNameByHandleW. + // Resolution does not verify the result: no error, and no check that any + // component exists. A consumer wanting a verified path wants an open plus + // GetFinalPathNameByHandleW. assert_eq!( resolve(r"C:\no-such-directory\..\nothing-here.txt"), r"C:\nothing-here.txt" @@ -215,3 +215,120 @@ fn a_resolution_performs_the_same_way_on_another_thread() { assert_eq!(resolved, r"C:\Windows\System32"); } + +/// The current directory this process is running in, as `GetFullPathNameW` +/// would use it. +/// +/// Read through the same API rather than `std::env::current_dir`, because the +/// two can disagree: `std` normalizes, and what these tests need is exactly the +/// string the call under test roots against. +fn current_directory() -> String { + // A lone `.` is rooted at the current directory and then collapses to it. + resolve(".") +} + +#[test] +fn a_relative_path_is_rooted_at_the_process_current_directory() { + // The rooting half of the documented contract. Asserted as a RELATION to + // the current directory rather than against a literal, so it holds on any + // machine and in any working directory. + let expected = format!(r"{}\rel.txt", current_directory().trim_end_matches('\\')); + assert_eq!(resolve("rel.txt"), expected); +} + +#[test] +fn a_root_relative_path_takes_the_root_and_not_the_whole_directory() { + // `\foo` is documented as taking only the ROOT of the current directory, + // which is what distinguishes it from an ordinary relative path. Calling it + // "the current drive" was wrong -- under a UNC current directory there is + // no drive at all -- so this pins the property the doc actually claims. + let cwd = current_directory(); + let resolved = resolve(r"\foo"); + + assert!( + resolved.ends_with(r"\foo"), + "a root-relative path keeps its component: {resolved}" + ); + assert!( + cwd.starts_with(resolved.trim_end_matches(r"foo")), + "and is rooted at a PREFIX of the current directory ({cwd}), not under it: {resolved}" + ); + // The distinguishing property: it does NOT include the current directory's + // subtree, so unless the current directory is itself the root, the two differ. + if cwd.trim_end_matches('\\').len() > resolved.trim_end_matches(r"\foo").len() { + assert_ne!( + resolved, + format!(r"{}\foo", cwd.trim_end_matches('\\')), + "a root-relative path is not the same as a relative one" + ); + } +} + +#[test] +fn a_legacy_device_name_short_circuits_rooting() { + // The exception to "roots a path that is not fully qualified", and the one + // a caller passing an untrusted name has to know about: these do not become + // files under the current directory. + for name in [ + "CON", "NUL", "PRN", "AUX", "CONIN$", "CONOUT$", "COM1", "LPT9", + ] { + let resolved = resolve(name); + assert!( + resolved.starts_with(r"\\.\"), + "{name} names a device, so it must not be rooted: {resolved}" + ); + } +} + +#[test] +fn the_device_form_accepts_trailing_colons_dots_spaces_and_any_casing() { + // Every spelling the module doc claims reaches a device. A filter written + // from a narrower reading of the rule would let these through. + for spelling in ["CON", "CON:", "CON::", "CON.", "CON ", "con", "cOn:"] { + let resolved = resolve(spelling); + assert!( + resolved.starts_with(r"\\.\"), + "{spelling:?} is a device spelling: {resolved}" + ); + } +} + +#[test] +fn superscript_digits_are_device_names_too() { + // The members a hand-written denylist omits, and which this crate's own + // documentation asserted did not exist until a review measured them. If a + // future Windows build stops accepting them this test says so, which is the + // whole reason it is here rather than left as prose. + for spelling in ["COM\u{00b9}", "COM\u{00b2}", "COM\u{00b3}", "LPT\u{00b9}"] { + let resolved = resolve(spelling); + assert!( + resolved.starts_with(r"\\.\"), + "{spelling:?} uses a superscript digit and still names a device: {resolved}" + ); + } +} + +#[test] +fn a_device_name_with_anything_around_it_is_rooted_normally() { + // The other half, and the one that keeps the rule from being read as "any + // input containing a device name". Without these the test above would pass + // just as well against an implementation that mapped far too much. + for spelling in [ + "CON.txt", r"a\CON", r".\CON", "CON:x", "COM0", "COM10", "CONIN", + ] { + let resolved = resolve(spelling); + assert!( + !resolved.starts_with(r"\\.\"), + "{spelling:?} is not a bare device name, so it must be rooted: {resolved}" + ); + } +} + +#[test] +fn a_fully_qualified_path_is_unaffected_by_the_current_directory() { + // The lexical half, stated as the invariance the rooting half lacks: this + // is what makes "not lexical as a whole" a claim about the OTHER half only. + // `C:\a` need not exist, which is the same fact the existence test pins. + assert_eq!(resolve(r"C:\a\..\b"), r"C:\b"); + assert_eq!(resolve("C:/a/b//c"), r"C:\a\b\c"); +} diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 62e094ab9..41e9b5136 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -15,7 +15,7 @@ //! //! # A resolved path is not a session-independent path //! -//! `GetFullPathNameW` **touches no filesystem**. It collapses `.`/`..` +//! `GetFullPathNameW` **does not verify what it produces**. It collapses `.`/`..` //! lexically, and it *additionally* roots most paths that are not fully //! qualified against process state -- the current directory, or for a //! root-relative path that directory's *root*, or for a drive-relative path diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 46baff817..9177d2033 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -88,8 +88,7 @@ speculative list to extend by imagination -- a fourth is added when a fourth con explicitly after being pointed at the question. Nothing in the suite decides it either way, which is itself the argument for the oracle. -- [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide - whether it is still the call `prepare` wants. -> [completed 2026-09-09](COMPLETED-CHECKLIST.md#m26) +- [x] **M2.6** -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. -> [completed 2026-09-09](COMPLETED-CHECKLIST.md#m26) - [ ] **M2.7** -- Decide whether the other nine probe steps in CI should carry `if: '!cancelled()'`, and apply or record the decision. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 18a8cecd3..a8509f630 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -94,15 +94,19 @@ piece of work rather than a correction to that one. newline even when redirected -- had stdout been block-buffered this milestone would have needed a per-line flush too. -## Moved 2026-09-09 22:54:01 -04:00 -- M2.6: what `GetFullPathNameW` does, and whether it stays +## Moved 2026-09-09 22:54:01 -04:00 -- M2.6: what `GetFullPathNameW` does, and whether it stays + +### M2.6 -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. *(completed 2026-09-09 22:54:01 -04:00)* **Resolved.** The correction and the decision both landed in the owning crate as `D-18` in [../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md): `GetFullPathNameW` collapses `.`/`..` lexically but roots most paths that are not fully qualified against -process state, so it is not a lexical call as a whole; `PathCchCanonicalizeEx` is genuinely lexical, -cheaper, and the wrong call, because rooting at submission is the property being bought. Whether it -enters the kernel is recorded as NOT established. The item's body below is the request as it was -written, and quotes the module doc as it read before the correction. +process state, so it is not a lexical call as a whole; `PathCchCanonicalizeEx` does not root, and is +the wrong call for that reason, because rooting at submission is the property being bought. No cost +comparison is claimed -- the item below asked whether the alternative "would be cheaper", and the +answer recorded in D-18 is that nothing measures it, so the decision rests on semantics alone. +Whether the call enters the kernel is recorded as NOT established. The item's body below is the +request as it was written, and quotes the module doc as it read before the correction. - [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide whether it is still the call `prepare` wants. Two successive descriptions in the cost probe were diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index ec95b9505..dbc1972d3 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -79,8 +79,9 @@ //! re-derived from a probe: see `windows-namespace-request-sys`' //! [DESIGN-NOTES.md](../../windows-namespace-request-sys/DESIGN-NOTES.md) -> //! `D-18`, which states what the call actually does, records keeping it over -//! the cheaper lexical alternative, and says plainly that whether it enters the -//! kernel is not established. +//! the canonicalizers that do not root, and says plainly that whether it enters +//! the kernel is not established. It claims no cost comparison against those +//! alternatives, because nothing here measures them. //! //! The two schemes that might reduce it recover different halves. **Inline //! storage** removes the allocation and copy, which is what From b22a0df086b78a02d0bde9c698b47c017da22bd4 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 01:23:41 -0400 Subject: [PATCH 10/36] docs: finish the contract sweep into the probe crate, and stop a test overclaiming Sixth review round. Three findings, all the same failure as R4: the fix reached the reported site and its immediate neighbour, not the population. THE DOC EXAMPLE THE PREVIOUS COMMIT EDITED STILL CARRIED THE REFUTED CLAIM. full_path.rs:227 said `.` and `..` resolve "without touching the filesystem" -- two hundred lines below a module doc that now says exactly why that claim cannot be made. b778ea2 edited that line (dropping a `Lexical:` prefix) and kept the rest, while rewriting the sibling example six lines down. So the two adjacent examples for one type stated contradictory contracts, and the one docs.rs shows first was the wrong one. THE PROBE CRATE STILL PRINTED IT, AND A TEST CLAIMED TO PIN IT. The emitted report said "touches no filesystem", and tests.rs asserted the drive-letter measurement "pins the 'touches no filesystem' claim itself ... if that claim ever stops holding, this fails first". It does not and cannot: succeeding against a volume that does not exist shows the volume was never CONSULTED -- an absence of verification, not of I/O. That comment is now honest about what it pins, which is the thing the probe actually needs. TIER 2 PROMISED COVERAGE IT DID NOT HAVE. DESIGN-RATIONALE named the omitted device spellings as COM^1..3 "and their LPT equivalents", then said the tests "pin every documented spelling so the next omission fails CI". The test carried four of the six -- LPT^2 and LPT^3 were named in the sentence and absent from the loop. An enumeration omitting two members, inside a paragraph about an enumeration omitting members. Both added. Also mine, from checking my own new tests rather than assuming them: the root-relative test guarded its one distinguishing assertion behind a length comparison, so with the current directory at a drive root -- where a root-relative and a relative path coincide -- the guard was false and the test passed having asserted almost nothing. It now computes the root explicitly (handling UNC, where there is no drive letter) and asserts equality outright. Verified by sabotaging the rooting rule and watching it go red, then verifying the restore, having been caught earlier this session by a Move-Item restore that left cargo with a stale mtime and a stale binary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path.rs | 3 +- .../src/full_path/tests.rs | 53 +++++++++++++------ .../src/bin/request_cost.rs | 4 +- crates/windows-platform-probes/src/tests.rs | 19 ++++--- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 8aaf3966b..2d7fcbecb 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -224,7 +224,8 @@ impl From for FullPathError { /// use windows_namespace_request_sys::full_path::ResolveFullPath; /// use wtf_string::Wtf16String; /// -/// // `.` and `..` are resolved without touching the filesystem. +/// // `.` and `..` are collapsed as string work, with no component verified: +/// // this holds whether or not `C:\Windows\System32` exists. /// let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\Windows\System32\..\.\Temp")) /// .perform()? /// .to_string_lossy(); diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index c0fd9b445..506b143a9 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -245,23 +245,37 @@ fn a_root_relative_path_takes_the_root_and_not_the_whole_directory() { let cwd = current_directory(); let resolved = resolve(r"\foo"); - assert!( - resolved.ends_with(r"\foo"), - "a root-relative path keeps its component: {resolved}" - ); - assert!( - cwd.starts_with(resolved.trim_end_matches(r"foo")), - "and is rooted at a PREFIX of the current directory ({cwd}), not under it: {resolved}" + // Computed, not inferred. An earlier version of this test derived the root + // by trimming the result it was checking, and guarded its one distinguishing + // assertion behind a length comparison -- so with the current directory at a + // drive root, where a root-relative path and a relative one coincide, the + // guard was false and the test passed having asserted almost nothing. A + // check whose strength depends on where it runs is the vacuous pass this + // repository keeps paying for. + let root = root_of(&cwd); + assert_eq!( + resolved, + format!(r"{root}foo"), + "a root-relative path is rooted at the root of the current directory \ + ({cwd}, root {root}), and carries none of its subtree" ); - // The distinguishing property: it does NOT include the current directory's - // subtree, so unless the current directory is itself the root, the two differ. - if cwd.trim_end_matches('\\').len() > resolved.trim_end_matches(r"\foo").len() { - assert_ne!( - resolved, - format!(r"{}\foo", cwd.trim_end_matches('\\')), - "a root-relative path is not the same as a relative one" - ); +} + +/// The root of an absolute Windows path, including its trailing separator. +/// +/// C:\a\b gives C:\, and \\server\share\a gives \\server\share\ -- which +/// is why this returns a *root* rather than a drive: under a UNC current +/// directory there is no drive letter to return. +fn root_of(path: &str) -> String { + if let Some(rest) = path.strip_prefix(r"\\") { + // server\share, then everything after it. + let mut parts = rest.splitn(3, '\\'); + let server = parts.next().unwrap_or_default(); + let share = parts.next().unwrap_or_default(); + return format!(r"\\{server}\{share}\"); } + let (drive, _) = path.split_at(2); + format!(r"{drive}\") } #[test] @@ -299,7 +313,14 @@ fn superscript_digits_are_device_names_too() { // documentation asserted did not exist until a review measured them. If a // future Windows build stops accepting them this test says so, which is the // whole reason it is here rather than left as prose. - for spelling in ["COM\u{00b9}", "COM\u{00b2}", "COM\u{00b3}", "LPT\u{00b9}"] { + for spelling in [ + "COM\u{00b9}", + "COM\u{00b2}", + "COM\u{00b3}", + "LPT\u{00b9}", + "LPT\u{00b2}", + "LPT\u{00b3}", + ] { let resolved = resolve(spelling); assert!( resolved.starts_with(r"\\.\"), diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index 6a2f45edf..4f898f7c5 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -313,11 +313,11 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " touches no filesystem -- and it is not an allocation, so most of the" + " verifies nothing it produces -- and it is not an allocation, so most" ); let _ = writeln!( out, - " cost above is work no allocation scheme can remove. Whether any of" + " of the cost above is work no allocation scheme can remove. Whether any" ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index e95abd376..5b3ec7158 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4358,15 +4358,20 @@ fn a_small_handshake_completes_and_reports_a_positive_round_trip() { // // `request_cost::measure` builds its long-path sample on a hard-coded `C:`, and // two review passes read that as a portability bug: a machine with no `C:` -// volume would panic on the `expect` rather than measure. It would not, and the -// reason is the same fact the probe's own headline conclusion rests on -- that -// `GetFullPathNameW` resolves a fully-qualified path without touching the -// filesystem. A volume that does not exist is therefore not consulted. +// volume would panic on the `expect` rather than measure. It would not, because +// resolving a fully-qualified path consults no volume -- the same fact the +// probe's account of its own nanoseconds rests on. // // That was an argument, and an argument is what a reviewer had to disbelieve. -// This is the measurement. It also pins the "touches no filesystem" claim -// itself, which nothing else here does: if that claim ever stops holding, the -// probe's account of where its nanoseconds go is wrong, and this fails first. +// This is the measurement. +// +// **It pins less than an earlier version of this comment claimed.** That version +// said it pinned "touches no filesystem". It cannot: succeeding against a volume +// that does not exist shows the volume was never *consulted*, which is an +// absence of verification, not an absence of I/O. No black-box test can +// establish the latter, and the owning crate's `D-18` now says so. What this +// does pin is exactly what the probe needs -- that a missing volume does not +// make the measurement fail. #[test] fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { From 745efb18818c819ab32392fcbb36021f6ca0c5cd Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 01:27:02 -0400 Subject: [PATCH 11/36] test(namespace-request): cover the drive-relative rooting form Copilot review 5162979152. Of its two suppressed comments, one was already fixed 13 minutes after the review was cut; this is the other, and it was real. The docs give drive-relative paths a distinct, process-state-dependent rule -- rooted at THAT DRIVE's current directory, from the hidden `=X:` variables, moving independently of the process one -- and the tests added last round covered relative, root-relative, device and fully-qualified inputs but not that. Four of the five documented forms, with the omitted one being the least believable rule of the five. The same enumeration-omission this PR keeps finding, now in the coverage rather than the prose. Pinned without mutating process state. Setting a `=X:` variable is the direct test but is process-global, and these tests share a process, so the rule is asserted through its two observable consequences instead: - On the drive the process is already on, that drive's recorded directory IS the process current directory, so `Q:foo` lands exactly where `foo` does. - On a drive never visited there is no recorded directory, so `X:foo` roots at `X:\` and carries none of the process current directory -- which is what makes this a different rule and not a spelling of the relative one. The drive need not exist; nothing consults the volume. A UNC current directory has no drive letter, so the test returns early and says so, rather than passing silently. Verified non-vacuous by asserting the wrong rule -- that an unvisited drive roots at the process directory -- and confirming it goes red, then confirming the restore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 506b143a9..ed34db37e 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -353,3 +353,51 @@ fn a_fully_qualified_path_is_unaffected_by_the_current_directory() { assert_eq!(resolve(r"C:\a\..\b"), r"C:\b"); assert_eq!(resolve("C:/a/b//c"), r"C:\a\b\c"); } + +#[test] +fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() { + // The third rooting form, and the one whose rule is least believable: a + // drive-relative path is rooted at *that drive's* current directory, which + // Windows records per drive and which moves independently of the process + // current directory. + // + // Pinned without mutating anything. Setting a `=X:` variable would be the + // direct test, but it is process-global and these tests share a process, so + // the two observable consequences are asserted instead: + let cwd = current_directory(); + let Some(drive) = cwd.chars().next().filter(|c| c.is_ascii_alphabetic()) else { + // A UNC current directory has no drive letter, so neither consequence + // is expressible. Skipping is visible here rather than silently passing. + return; + }; + + // 1. For the drive the process is ALREADY on, that drive's recorded + // directory is the process current directory -- so a drive-relative path + // lands exactly where a plain relative one does. + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{}\foo", cwd.trim_end_matches('\\')), + "on the current drive, the per-drive directory is the process one" + ); + + // 2. For a drive the process has never visited there is no recorded + // directory, so it roots at that drive's root -- NOT under the process + // current directory, which is what makes this a different rule rather + // than a spelling of the relative one. The drive need not exist: nothing + // consults the volume. + let other = if drive.eq_ignore_ascii_case(&'X') { + 'Y' + } else { + 'X' + }; + let resolved = resolve(&format!("{other}:foo")); + assert_eq!( + resolved, + format!(r"{other}:\foo"), + "an unvisited drive roots at its own root" + ); + assert!( + !resolved.starts_with(cwd.trim_end_matches('\\')), + "and carries none of the process current directory ({cwd}): {resolved}" + ); +} From fd32f0703198f968fcc5b4735f1ff2f31d82811a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 01:31:43 -0400 Subject: [PATCH 12/36] test(namespace-request): make the drive-relative test independent of the host Self-review of last round's commit, before the reviewer reported. The test I added to close the coverage gap was itself environment-dependent, in a published crate whose tests run on machines we do not control. It asserted `X:foo` == `X:\foo` for a drive other than the current one. Measured: that holds only while the chosen drive DOES NOT EXIST. If it exists and the process inherited a `=X:` entry -- which a parent shell sets merely by visiting the drive -- then `X:foo` resolves under that recorded directory instead. Setting `=X:` for a non-existent X: changed nothing; setting `=C:` on this host, where C: exists, moved `C:foo` to `C:\Windows\System32\foo` with the process directory untouched. So a developer with a mapped or subst'd X: would have hit a red test on a green tree. The assertion is now the property that actually holds regardless: a drive-relative path roots on ITS drive, keeps its component, and carries none of the process current directory. That is what distinguishes it from the relative form, which is the whole point of the test; the exact string depends on host state and is no longer claimed. Two smaller things in the same file: - The UNC early return was silent. A test that quietly does nothing is indistinguishable from one that passed, so it now prints why it skipped. - `root_of` called `split_at(2)` on an invariant stated nowhere. Its only caller passes a resolved absolute path so it cannot panic today, but the precondition is now asserted and says what it wants rather than failing on an index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index ed34db37e..89c778546 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -274,6 +274,13 @@ fn root_of(path: &str) -> String { let share = parts.next().unwrap_or_default(); return format!(r"\\{server}\{share}\"); } + // Stated rather than assumed: the only caller passes a resolved absolute + // path, which is always at least `C:\`, but `split_at` would panic on an + // index rather than say why. + assert!( + path.len() >= 2 && path.is_char_boundary(2), + "root_of expects an absolute path, got {path:?}" + ); let (drive, _) = path.split_at(2); format!(r"{drive}\") } @@ -366,8 +373,14 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // the two observable consequences are asserted instead: let cwd = current_directory(); let Some(drive) = cwd.chars().next().filter(|c| c.is_ascii_alphabetic()) else { - // A UNC current directory has no drive letter, so neither consequence - // is expressible. Skipping is visible here rather than silently passing. + // A UNC current directory has no drive letter, so there is no + // drive-relative form to exercise. Announced rather than returned + // silently: a test that quietly does nothing is indistinguishable from + // one that passed, which is the failure mode this suite keeps meeting. + eprintln!( + "SKIPPED a_drive_relative_path_...: current directory {cwd} is UNC, \ + so it has no drive letter" + ); return; }; @@ -380,21 +393,34 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() "on the current drive, the per-drive directory is the process one" ); - // 2. For a drive the process has never visited there is no recorded - // directory, so it roots at that drive's root -- NOT under the process - // current directory, which is what makes this a different rule rather - // than a spelling of the relative one. The drive need not exist: nothing - // consults the volume. + // 2. For a DIFFERENT drive the path roots on that drive, carrying none of + // the process current directory. That is what makes this a distinct rule + // rather than a spelling of the relative one. + // + // Asserted as "on that drive" rather than as the exact string + // `X:\foo`, because the precise answer depends on environment this test + // must not assume. Measured: if the drive EXISTS and the process + // inherited a `=X:` entry for it -- which a parent shell sets simply by + // visiting it -- then `X:foo` resolves under that recorded directory + // instead of the drive root. (For a drive that does not exist the entry + // is ignored, which is why this passed locally.) A published crate's + // tests run on machines its authors do not control, and an earlier + // version of this assertion demanded the drive-root form outright, so a + // mapped `X:` would have failed it. let other = if drive.eq_ignore_ascii_case(&'X') { 'Y' } else { 'X' }; let resolved = resolve(&format!("{other}:foo")); - assert_eq!( - resolved, - format!(r"{other}:\foo"), - "an unvisited drive roots at its own root" + assert!( + resolved.starts_with(&format!(r"{other}:\")), + "a drive-relative path roots on ITS drive, whatever that drive's \ + recorded directory happens to be: {resolved}" + ); + assert!( + resolved.ends_with(r"\foo"), + "and keeps the component it was given: {resolved}" ); assert!( !resolved.starts_with(cwd.trim_end_matches('\\')), From 990f2e5f5bb4feb0611f2ed5b1935980c0bae92e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 01:40:50 -0400 Subject: [PATCH 13/36] test(namespace-request): fix the case dependence, the vacuous assert, and the two-arm rule Seventh review round, on the commit that fixed the sixth. Three findings on the test plus one on the docs it pins, all reproduced here before fixing. CASE WAS STILL NOT THE TEST'S TO CHOOSE. Last commit fixed the PATH dependence and left a case dependence in the replacement: `starts_with` is a byte comparison against a hard-coded uppercase letter, but the drive letter comes back as Windows recorded it, not as it was typed. Reproduced exactly: `subst X: Q:\github`, visit it with a lowercase `cd`, and the test fails on `x:\windows-threadpool-sys\foo`. Compared case-insensitively now; verified load-bearing by restoring the byte comparison under the same subst and watching it fail, then confirming the restore. That is the same defect as the commit it fixes -- a host-dependent assertion in a published crate -- one layer down. The commit message even named "a developer with a mapped or subst'd X:" as the motivating case, and the fix still failed it when the drive was visited in lowercase. THE THIRD ASSERTION COULD NOT FAIL. `!resolved.starts_with(cwd)` compared a path on `other` against one on the current drive; the letters differ by construction, so it was always false. It carried the test's headline claim while asserting nothing the prefix check had not already forced. Removed, with a note saying why rather than leaving a gap someone re-fills. THE DOCS CONTRADICTED THE TEST, in the same diff. They said the per-drive directory "moves independently of the process current directory"; the test's own current-drive assertion says the opposite. Both are right about different arms: for another drive Windows reads the `=X:` entry, for the CURRENT drive it ignores it and the process directory wins. Measured -- setting `=Q:` while on `Q:` changes nothing, while setting `=C:` from `Q:` moves `C:foo`. All three sites now state both arms. THE UNC SKIP WAS NOT ANNOUNCED. libtest captures stderr for passing tests, so the eprintln the last commit added is invisible without --nocapture; the silent-skip defect it claimed to fix was unchanged. Restructured instead so the other-drive arm runs unconditionally -- under a UNC current directory every letter is "other" -- and only the current-drive arm is conditional. The test can no longer no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 8 +- .../src/full_path.rs | 6 +- .../src/full_path/tests.rs | 93 +++++++++---------- .../windows-namespace-request-sys/src/path.rs | 4 +- 4 files changed, 54 insertions(+), 57 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 511cf7521..51ca94fd6 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -547,9 +547,11 @@ process state. Three forms read three different pieces of it: a relative path takes the current directory; a root-relative path like `\foo` takes only that directory's *root*, which is `\\server\share\` when the current directory is a UNC path and so is not a drive at all; and a drive-relative path such as -`C:foo` takes that drive's own current directory, which Windows keeps in the -hidden `=C:` environment variables and which moves independently of the process -current directory. So the call is not lexical *as a whole*, and the claim that +`C:foo` takes that drive's own current directory. That rule has two arms: +for a drive other than the current one Windows reads the hidden `=C:` entry +recorded for it, which moves independently of the process current directory; +for the *current* drive the entry is ignored and the process current directory +wins. Measured -- setting `=Q:` while the process is on `Q:` changes nothing. So the call is not lexical *as a whole*, and the claim that holds unqualified is that it **does not verify what it produces** -- the documented guarantee, which is narrower than the "touches no filesystem" an earlier draft claimed and which observation cannot establish. **"Most" rather than "every", because a legacy device name short-circuits the diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 2d7fcbecb..258a9c2ac 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -35,8 +35,10 @@ //! `\\server\share\foo` when the current directory is a UNC path, which is //! why this says root and not drive; and a drive-relative path like `C:foo` //! is rooted at that drive's own current directory, which Windows keeps in -//! the hidden `=C:` environment variables and which moves independently of -//! the process current directory. +//! the hidden `=C:` environment variables. That entry is what is read for a +//! drive *other* than the current one, and it moves independently of the +//! process current directory; for the current drive it is ignored and the +//! process current directory wins. //! //! **A whole class of input short-circuits both.** When the input names a //! legacy device and nothing else, it resolves into the device namespace and is diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 89c778546..71d7bfd12 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -363,58 +363,38 @@ fn a_fully_qualified_path_is_unaffected_by_the_current_directory() { #[test] fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() { - // The third rooting form, and the one whose rule is least believable: a - // drive-relative path is rooted at *that drive's* current directory, which - // Windows records per drive and which moves independently of the process - // current directory. + // The third rooting form. A drive-relative path is rooted at *that drive's* + // current directory -- and the rule has two arms, which is the part that + // gets missed: // - // Pinned without mutating anything. Setting a `=X:` variable would be the - // direct test, but it is process-global and these tests share a process, so - // the two observable consequences are asserted instead: - let cwd = current_directory(); - let Some(drive) = cwd.chars().next().filter(|c| c.is_ascii_alphabetic()) else { - // A UNC current directory has no drive letter, so there is no - // drive-relative form to exercise. Announced rather than returned - // silently: a test that quietly does nothing is indistinguishable from - // one that passed, which is the failure mode this suite keeps meeting. - eprintln!( - "SKIPPED a_drive_relative_path_...: current directory {cwd} is UNC, \ - so it has no drive letter" - ); - return; - }; - - // 1. For the drive the process is ALREADY on, that drive's recorded - // directory is the process current directory -- so a drive-relative path - // lands exactly where a plain relative one does. - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{}\foo", cwd.trim_end_matches('\\')), - "on the current drive, the per-drive directory is the process one" - ); - - // 2. For a DIFFERENT drive the path roots on that drive, carrying none of - // the process current directory. That is what makes this a distinct rule - // rather than a spelling of the relative one. + // * For a drive OTHER than the current one, Windows reads the hidden + // =X: entry recorded for it. + // * For the CURRENT drive the entry is ignored entirely and the process + // current directory wins. Measured: setting =Q: while the process is + // on Q: changes nothing. // - // Asserted as "on that drive" rather than as the exact string - // `X:\foo`, because the precise answer depends on environment this test - // must not assume. Measured: if the drive EXISTS and the process - // inherited a `=X:` entry for it -- which a parent shell sets simply by - // visiting it -- then `X:foo` resolves under that recorded directory - // instead of the drive root. (For a drive that does not exist the entry - // is ignored, which is why this passed locally.) A published crate's - // tests run on machines its authors do not control, and an earlier - // version of this assertion demanded the drive-root form outright, so a - // mapped `X:` would have failed it. - let other = if drive.eq_ignore_ascii_case(&'X') { - 'Y' - } else { - 'X' + // Pinned without mutating anything, since =X: is process-global and these + // tests share a process. + let cwd = current_directory(); + let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); + + // The other-drive arm needs no drive letter from the current directory -- + // under a UNC current directory every letter is "other" -- so it runs + // unconditionally and this test never degenerates to a silent skip. + let other = match cwd_drive { + Some(d) if d.eq_ignore_ascii_case(&'X') => 'Y', + _ => 'X', }; let resolved = resolve(&format!("{other}:foo")); + + // Compared case-insensitively, because the case is not this test's to + // choose: the letter comes back as Windows recorded it, not as it was + // typed. This host returns q:\... for an uppercase Q: input, because the + // shell was started with a lowercase cd. An earlier version compared bytes + // and would have failed on a drive visited in lowercase. + let prefix = format!(r"{other}:\"); assert!( - resolved.starts_with(&format!(r"{other}:\")), + resolved.len() >= prefix.len() && resolved[..prefix.len()].eq_ignore_ascii_case(&prefix), "a drive-relative path roots on ITS drive, whatever that drive's \ recorded directory happens to be: {resolved}" ); @@ -422,8 +402,19 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() resolved.ends_with(r"\foo"), "and keeps the component it was given: {resolved}" ); - assert!( - !resolved.starts_with(cwd.trim_end_matches('\\')), - "and carries none of the process current directory ({cwd}): {resolved}" - ); + // Deliberately NOT asserting that esolved avoids the current directory: + // other differs from the current drive by construction, so such a check + // cannot fail, and an earlier version carried the headline claim's message + // on an assertion that could never go red. + + // The current-drive arm, where the process directory wins over any =X:. + // Only expressible when the current directory has a drive letter at all. + if let Some(drive) = cwd_drive { + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{}\foo", cwd.trim_end_matches('\\')), + "on the current drive, the per-drive entry is ignored and the \ + process directory is used" + ); + } } diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 41e9b5136..624529bd6 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -19,7 +19,9 @@ //! lexically, and it *additionally* roots most paths that are not fully //! qualified against process state -- the current directory, or for a //! root-relative path that directory's *root*, or for a drive-relative path -//! that drive's own current directory in the `=C:` environment variables. It is +//! naming another drive that drive's own current directory from the `=C:` +//! environment variables (for the current drive the process directory is used +//! and that entry is ignored). It is //! therefore not a lexical call as a whole, which is what makes resolving on //! the submitting thread meaningful. //! From df751874c8391486b23369b42e694d35ab0d8ef5 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 02:02:59 -0400 Subject: [PATCH 14/36] docs: GetFullPathNameW does touch the filesystem, and writes the environment Eighth review round, and the finding overturns what this branch has been asserting since its first commit. MEASURED. Resolving a drive-relative path for a drive that is NOT the current one validates that drive's `=X:` entry against the filesystem: =X: = C:\definitely-not-here -> X:\foo and =X: REWRITTEN to X:\ =X: = C:\Windows (a directory) -> C:\Windows\foo honoured verbatim =X: = C:\Windows\notepad.exe -> X:\foo and =X: REWRITTEN to X:\ So it distinguishes an existing directory from an existing FILE from a missing one -- a filesystem query, not a syntax or drive-existence check -- and it WRITES the process environment block as a side effect of what reads like a pure query. For the current drive neither happens. Three claims in this branch die with that: - "Whether it can enter the kernel is not established." It is established, in the affirmative, for this form. The section is replaced by the measurement. - "Observation cannot close that gap." Observation closed it the moment anyone looked at the right form. Four drafts reasoned from where the data lives -- PEB, environment block, therefore process memory -- and the reasoning was sound and the conclusion false. That is the failure D-18 exists to name, and this is its cleanest instance: the earlier drafts at least knew they were asserting a mechanism, this one thought it was declining to. - "That part READS mutable process state." It also writes it. The entry is also honoured VERBATIM and is not constrained to its own drive, so "that drive's own current directory" is the convention, not a guarantee. The test's assertion required an `X:\` prefix and carried a message claiming robustness "whatever that drive's recorded directory happens to be" -- the one case that breaks it. It now asserts what is invariant: a drive-relative path for another drive does not use the process current directory. Verified non-vacuous by resolving a relative path instead and watching it fire. The test comment also said it "pins without mutating anything". False on a host with a stale inherited entry: the resolve itself rewrites it. Said so. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHECKLIST.md | 36 +++++++ .../DESIGN-NOTES.md | 41 ++++---- .../DESIGN-RATIONALE.md | 31 +++++- crates/windows-namespace-request-sys/PLANS.md | 1 + .../src/full_path.rs | 81 ++++++++++------ .../src/full_path/tests.rs | 95 +++++++++++++++---- .../windows-namespace-request-sys/src/path.rs | 11 ++- .../src/bin/request_cost.rs | 10 +- crates/windows-platform-probes/src/tests.rs | 21 ++-- 9 files changed, 240 insertions(+), 87 deletions(-) create mode 100644 crates/windows-namespace-request-sys/CHECKLIST.md diff --git a/crates/windows-namespace-request-sys/CHECKLIST.md b/crates/windows-namespace-request-sys/CHECKLIST.md new file mode 100644 index 000000000..56cec1b23 --- /dev/null +++ b/crates/windows-namespace-request-sys/CHECKLIST.md @@ -0,0 +1,36 @@ +# Checklist: windows-namespace-request-sys + +Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md), and how they were +reached in [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md). This crate's *creation* is +tracked separately, in the workspace +[CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) milestones +M24-M26; that file is feature-scoped and is deleted when its feature completes, +so durable follow-up work for the crate belongs here instead. + +## NR-1 -- Pin the per-drive current directory arm of drive-relative rooting + +- [ ] **NR-1.1** -- Decide how this crate's tests may control process-global state, + then pin the `=X:` arm of drive-relative rooting. + + **The gap, stated exactly.** [D-18](DESIGN-NOTES.md#d-18) documents a two-arm + rule: a drive-relative path like `C:foo` is rooted at *that drive's* recorded + current directory (the hidden `=C:` entry) for a drive other than the current + one, while for the current drive the entry is ignored and the process current + directory wins. `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` + in [tests.rs](src/full_path/tests.rs) pins the second arm and only bounds the + first: when the chosen drive has no `=X:` entry, an implementation that always + used the drive root would pass every assertion. + + **Why it was not simply written.** Pinning it needs a controlled `=X:`, and + both routes cost something this item should decide rather than assume. Setting + the variable in-process mutates state shared by every test thread, which is + the hazard [DESIGN-NOTES.md](../windows-file-watcher/DESIGN-NOTES.md) records + for this workspace's single-process test model. Spawning a child with a + crafted environment avoids that but makes it an integration test and needs the + `Win32_System_Environment` feature as a dev-dependency. + + Measured facts the work can rely on, so they are not rediscovered: setting + `=X:` for a drive that does **not** exist has no effect; setting it for an + existing non-current drive does move that drive's resolution; and + `SetCurrentDirectoryW` does not maintain these entries -- a parent shell + writes them. diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 51ca94fd6..adf245d41 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -568,12 +568,9 @@ directory" -- as a first draft of this correction did -- attributes process-stat dependence to the one operation that has none, which is the same imprecision running the other way. -The wrong word had spread well beyond where it was reported. The consuming -probe's checklist item named [full_path.rs](src/full_path.rs) only; that file -held three of the nine on its own -- the module doc and both doc examples -- -with two more in `path.rs`, two in [tests.rs](src/full_path/tests.rs), one in an -acceptance comment and one in this file. The reported site was a sample, not the -population -- which is the standing lesson, met again. +The wrong word had spread well beyond the one file that was reported. The sweep +that found the rest, and its arithmetic, are Tier 2: +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) -> `D-18`. **The decision: keep `GetFullPathNameW`.** Two canonicalizers that do not root exist -- `PathCchCanonicalizeEx` and `PathAllocCanonicalize`. They are the wrong @@ -598,21 +595,23 @@ Recorded with the alternatives named so the next reader does not re-derive it. If the reasoning is ever wrong -- a consumer wanting a pure string operation, having resolved relativity another way -- they are named here. -**Whether it enters the kernel: not established, and said so.** Nothing it is -documented to consult requires a transition; the current directory lives in the -PEB and the `=C:` variables in the process environment block, both ordinary -process memory. Windows does not document the implementation, so that is a -statement about the data sources rather than a measurement of the call. -What `probe-request-cost` produces is a **bound, not this call's cost**. On -x86_64 it measures building an open request as a construct-and-drop cycle at -roughly 210 ns and cloning an already-resolved path at roughly 45 ns. The -~165 ns between them is what recycling recovers, and no more than that: the gap -spans the whole preparation step, which makes **two** heap allocations this -crate performs -- a copy of the input and a `MAX_PATH` output buffer -- against -the clone's one, plus the builder chain. Timed on its own with no allocation in -the loop, the call is about **110 ns** on this host, roughly two thirds of the -gap; that is a direct measurement taken for this note, not a probe output, and -no instrument in this repository isolates the call. +**It does touch the filesystem, on one form -- measured, after four drafts said +otherwise.** Resolving a drive-relative path for a drive that is *not* the +current one validates that drive's `=X:` entry against the filesystem: an entry +naming an existing directory is honoured **verbatim** (and need not be on that +drive -- with `=X:` set to `C:\Windows`, `X:foo` is `C:\Windows\foo`), while a +missing directory or an existing *file* is rejected and the entry is +**rewritten** to the drive root. The rewrite mutates the process environment +block as a side effect of what reads like a pure query. For the current drive +the entry is neither consulted nor rewritten. + +Earlier drafts concluded the opposite by reasoning that the current directory +lives in the PEB and the `=X:` variables in the environment block, so both are +ordinary process memory. The reasoning was sound and the conclusion wrong. That +is the failure this decision exists to name: a mechanism argued from where the +data lives rather than measured. It also means the guarantee this crate relies +on has to be the narrow one -- the call does not *verify* what it produces -- +because the broad one is not merely unproven but false. **The constraint this decision carries, and not just its conclusion:** state only what the evidence reaches. Seven drafts of this entry each named a diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index cdeb72801..cc9547f83 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -62,9 +62,22 @@ producing wrong answers. 7. **"Touches no filesystem."** The claim that survived longest, because it is nearly right. What Microsoft documents is that the function does not verify that the resulting path is valid or names an existing file -- a statement - about *verification*, not about I/O. Observation cannot close the gap: - resolving a path under a directory that does not exist shows no check was - made, not that no filesystem was touched. + about *verification*, not about I/O. + +8. **"Observation cannot close that gap."** The correction to (7), and wrong in + a more interesting way: observation *did* close it, in the affirmative + direction, the moment anyone looked at the right form. Resolving `X:foo` for + a non-current drive distinguishes an existing directory from an existing + *file* from a missing one, and rewrites the `=X:` entry when it is not a + directory. That is a filesystem query and a write to the process environment, + in a call four drafts had described as reading process memory. + + The reasoning behind those drafts was sound -- the current directory is in + the PEB, the `=X:` variables are in the environment block, both are process + memory -- and the conclusion was false. It is the cleanest example in this + file of the standing failure: a mechanism argued from where the data lives + rather than measured. Every earlier draft at least *knew* it was asserting a + mechanism; this one thought it was declining to. ### Two facts that were measured, then asserted too narrowly @@ -84,9 +97,19 @@ were *enumerations* -- which is the form this kind of error likes. spellings `COM^1`, `COM^2`, `COM^3` (U+00B9, U+00B2, U+00B3) and their `LPT` equivalents. Those are exactly the members a hand-written denylist misses, and the documentation asserted a closed list without them until a review measured - it. The tests in `full_path/tests.rs` now pin every documented spelling so the + it. The tests in [tests.rs](src/full_path/tests.rs) now pin every documented spelling so the next omission fails CI instead of a review. +### The sweep, and its arithmetic + +The wrong word had spread well beyond where it was reported. The consuming +probe's checklist item named [full_path.rs](src/full_path.rs) only; that file +held **three of the nine** on its own -- the module doc and both doc examples -- +with two more in [path.rs](src/path.rs), two in +[tests.rs](src/full_path/tests.rs), one in an acceptance comment and one in +[DESIGN-NOTES.md](DESIGN-NOTES.md). Nine across five files, from a report naming +one. The reported site was a sample, not the population. + ### Why the alternatives were never seriously in contention `PathCchCanonicalizeEx` and `PathAllocCanonicalize` canonicalize without diff --git a/crates/windows-namespace-request-sys/PLANS.md b/crates/windows-namespace-request-sys/PLANS.md index de5fb4ac0..a28fe24c4 100644 --- a/crates/windows-namespace-request-sys/PLANS.md +++ b/crates/windows-namespace-request-sys/PLANS.md @@ -10,4 +10,5 @@ source-component. That file is not the workspace | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| +| [CHECKLIST.md](CHECKLIST.md) | not started | NR-1: pin the `=X:` arm of drive-relative rooting, once it is decided how this crate's tests may control process-global state. The other arm and the surrounding contract are already pinned; this one is bounded, not pinned. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-18), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | **This crate's part (M24-M26) is complete**: the foundations (owned handle duplication, security attributes, path preparation, the faithful-execution contract), the four handle-producing entries, the five query entries, a test seam, and an acceptance pass over both operation and scenario coverage. The checklist itself stays open for M27 (`windows-platform-probes`) and the `M26+` items gated on this branch merging with `main` -- including `M26+.3`, the merge-or-delete decision on this crate's duplicated path preparation. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 258a9c2ac..cf6fce6f3 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -14,10 +14,15 @@ //! "touches no filesystem", which earlier revisions of this doc claimed. //! Microsoft specifies that the function does not verify that the resulting //! path and file name are valid or that they name an existing file; it does not -//! specify that no I/O occurs. Observation cannot close that gap either -- -//! resolving a path under a directory that does not exist shows no *check* was -//! made, not that no filesystem was touched. The narrower claim is the one this -//! crate relies on, and it is sufficient: a caller wanting existence must open. +//! specify that no I/O occurs. +//! +//! **And on one form it demonstrably does touch the filesystem.** Resolving a +//! drive-relative path for a drive other than the current one validates that +//! drive's recorded entry against the filesystem, and rewrites it when the +//! entry does not name an existing directory -- see "The drive-relative form +//! writes process state" below. So the narrow guarantee is the one to rely on +//! precisely because the broad one is false, not merely unproven. A caller +//! wanting existence must still open. //! //! It does **two** things, and keeping them apart is the whole reason this //! entry exists: @@ -27,18 +32,20 @@ //! string work over the input, reading no process state. `C:\a\..\b` becomes //! `C:\b` whatever the current directory happens to be, and whether or not //! `C:\a` exists. -//! 2. It **roots** a path that is not fully qualified, and that part reads -//! mutable process state. There are three such forms, and they read -//! different state: a relative path like `rel.txt` is rooted at the *process -//! current directory*; a root-relative path like `\foo` takes only the -//! *root* of that directory, giving `C:\foo` rather than its subtree -- and -//! `\\server\share\foo` when the current directory is a UNC path, which is -//! why this says root and not drive; and a drive-relative path like `C:foo` -//! is rooted at that drive's own current directory, which Windows keeps in -//! the hidden `=C:` environment variables. That entry is what is read for a -//! drive *other* than the current one, and it moves independently of the -//! process current directory; for the current drive it is ignored and the -//! process current directory wins. +//! 2. It **roots** a path that is not fully qualified, using mutable process +//! state -- and on one form it also *changes* that state. There are three +//! such forms: +//! +//! * A relative path like `rel.txt` is rooted at the *process current +//! directory*. +//! * A root-relative path like `\foo` takes only the *root* of that +//! directory, giving `C:\foo` rather than its subtree -- and +//! `\\server\share\foo` when the current directory is a UNC path, which +//! is why this says root and not drive. +//! * A drive-relative path like `C:foo` is rooted at the entry Windows +//! keeps for that drive in the hidden `=C:` environment variables. For +//! the *current* drive that entry is ignored and the process current +//! directory wins. //! //! **A whole class of input short-circuits both.** When the input names a //! legacy device and nothing else, it resolves into the device namespace and is @@ -103,15 +110,35 @@ //! wrong -- for a consumer that genuinely wants a pure string operation and has //! resolved relativity some other way -- the alternatives are named here. //! -//! # Whether it can enter the kernel +//! # The drive-relative form writes process state, and touches the filesystem +//! +//! Measured, and it overturns what four earlier revisions of this doc asserted. +//! Resolving `X:foo` for a drive that is **not** the current one does not +//! merely read the `=X:` entry: +//! +//! * The entry is honoured **verbatim** when it names an existing directory -- +//! including a directory on a *different* drive. With `=X:` set to +//! `C:\Windows`, `X:foo` resolves to `C:\Windows\foo`. So "that drive's own +//! current directory" describes the convention, not a guarantee. +//! * Otherwise the entry is **rewritten** to the drive root and that is used. +//! Both a missing directory and an existing *file* are rejected this way, so +//! the check is a filesystem query rather than a syntax or drive-existence +//! test -- and the rewrite mutates the process environment block as a side +//! effect of what reads like a pure query. //! -//! Nothing it is documented to consult requires a transition. The process -//! current directory lives in the PEB and the `=C:` variables in the process -//! environment block; both are ordinary process memory. Windows does not -//! document the implementation, so this is a statement about the data sources, -//! not a measurement of the call -- a distinction worth keeping, because two -//! successive descriptions of this call in a consuming probe were each wrong in -//! the same direction, by naming a mechanism the evidence did not reach. +//! For the current drive neither happens: the entry is not consulted and not +//! rewritten. +//! +//! This is why the "does not verify what it produces" guarantee above is worth +//! stating narrowly. The broad reading -- that the call touches no filesystem -- +//! is not merely unproven, it is false here. Earlier revisions said the +//! opposite, reasoning that the current directory lives in the PEB and the +//! `=X:` variables in the environment block and that both are ordinary process +//! memory. The reasoning was sound and the conclusion wrong, which is the +//! standing hazard this crate keeps meeting: a mechanism argued from the data +//! sources rather than measured. +//! +//! # What a resolution costs //! //! The figure the repo's own instrument produces is a **bound, not this call's //! cost**, and the difference matters. On x86_64 `probe-request-cost` measures @@ -135,11 +162,7 @@ //! for this note and is *not* something the probe reports; no instrument in //! this repository isolates the call, and the honest reading of //! `probe-request-cost` alone is an upper bound. -//! -//! What the probe declines to name is the **mechanism**. No figure here says -//! whether any part of the call entered the kernel. -//! -//! It does **not** solve the session-relative drive-letter hazard, and saying +//!//! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` //! never expands a drive letter, and a drive letter is resolved against the //! logon session of whatever token is in effect at open time. A path resolved diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 71d7bfd12..684775f43 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -368,13 +368,29 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // gets missed: // // * For a drive OTHER than the current one, Windows reads the hidden - // =X: entry recorded for it. + // `=X:` entry recorded for it. // * For the CURRENT drive the entry is ignored entirely and the process - // current directory wins. Measured: setting =Q: while the process is - // on Q: changes nothing. + // current directory wins. Measured: setting `=Q:` while the process is + // on `Q:` changes nothing. // - // Pinned without mutating anything, since =X: is process-global and these - // tests share a process. + // **This test does not mutate `=X:`, but the call it exercises may.** + // Measured: resolving `X:foo` for a non-current drive validates that + // drive's entry against the filesystem and REWRITES it to `X:\` when it + // does not name an existing directory. So on a host that inherited a stale + // entry, merely running this test changes the process environment. That is + // a property of the call, documented in the module doc; it is noted here so + // the next reader does not take "reads process state" at face value, as + // four revisions of that doc did. + // + // **What that leaves unpinned, stated rather than glossed:** when the chosen + // drive has no `=X:` entry, an implementation that always used the drive + // root would satisfy every assertion below, and the current-drive arm cannot + // separate the two rules either because there the entry is ignored by + // design. Pinning the entry-reading arm needs a controlled `=X:`, which + // means either mutating process-global state that other test threads share + // or spawning a child process -- a decision about this crate's test shape + // rather than something to slip in here. Queued as `NR-1.1` in this + // crate's CHECKLIST.md. let cwd = current_directory(); let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); @@ -389,25 +405,27 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // Compared case-insensitively, because the case is not this test's to // choose: the letter comes back as Windows recorded it, not as it was - // typed. This host returns q:\... for an uppercase Q: input, because the - // shell was started with a lowercase cd. An earlier version compared bytes + // typed. This host returns `q:\...` for an uppercase `Q:` input, because the + // shell was started with a lowercase `cd`. An earlier version compared bytes // and would have failed on a drive visited in lowercase. - let prefix = format!(r"{other}:\"); + // Asserted against the process current directory, not against the drive + // letter. The entry is honoured VERBATIM when it names an existing + // directory, and is not constrained to live on that drive -- with `=X:` + // set to `C:\Windows`, `X:foo` is `C:\Windows\foo`. An earlier version + // required the result to start with `X:\` and carried a message claiming + // robustness "whatever that drive's recorded directory happens to be", + // which is exactly the case that broke it. assert!( - resolved.len() >= prefix.len() && resolved[..prefix.len()].eq_ignore_ascii_case(&prefix), - "a drive-relative path roots on ITS drive, whatever that drive's \ - recorded directory happens to be: {resolved}" + !resolved.eq_ignore_ascii_case(&format!(r"{}\foo", cwd.trim_end_matches('\\'))), + "a drive-relative path for another drive does not use the process \ + current directory ({cwd}): {resolved}" ); assert!( resolved.ends_with(r"\foo"), "and keeps the component it was given: {resolved}" ); - // Deliberately NOT asserting that esolved avoids the current directory: - // other differs from the current drive by construction, so such a check - // cannot fail, and an earlier version carried the headline claim's message - // on an assertion that could never go red. - // The current-drive arm, where the process directory wins over any =X:. + // The current-drive arm, where the process directory wins over any `=X:`. // Only expressible when the current directory has a drive letter at all. if let Some(drive) = cwd_drive { assert_eq!( @@ -418,3 +436,48 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() ); } } + +#[test] +fn trailing_dots_and_spaces_are_trimmed_from_ordinary_components() { + // The module doc says the rewrite trims trailing dots and spaces. Until now + // that was only exercised through a final `.` component (which is the + // separate `.`-collapsing rule) and through device spellings (which take + // the short-circuit and never reach the ordinary path). Neither pins this. + // + // Every case measured before being written down. + for (input, expected) in [ + (r"C:\name.", r"C:\name"), + (r"C:\name ", r"C:\name"), + (r"C:\name...", r"C:\name"), + (r"C:\name ", r"C:\name"), + (r"C:\name. ", r"C:\name"), + // Trimming applies per component, not only at the end of the path. + (r"C:\a.\b", r"C:\a\b"), + // An extension is not special: the trailing dot goes, the rest stays. + (r"C:\name.txt.", r"C:\name.txt"), + ] { + assert_eq!(resolve(input), expected, "trimming {input:?}"); + } +} + +#[test] +fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { + // Strengthens the device-negative control. Asserting only "not `\\.\`" is + // too weak: an implementation that returned every relative input unchanged + // would satisfy it while rooting nothing. These assert the full resolved + // path, so the rooting guarantee is actually covered. + // + // `.\CON` is excluded deliberately -- the `.` component collapses, so its + // expected form is the bare name, which the loop below would have to + // special-case. It is covered by the device-negative test instead. + let base = current_directory(); + let base = base.trim_end_matches('\\'); + for name in ["CON.txt", "CONIN", "COM0", "COM10", r"a\CON"] { + assert_eq!( + resolve(name), + format!(r"{base}\{name}"), + "{name:?} is not a bare device name, so it roots under the current \ + directory rather than merely avoiding the device namespace" + ); + } +} diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 624529bd6..33ba9eb13 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -15,13 +15,16 @@ //! //! # A resolved path is not a session-independent path //! -//! `GetFullPathNameW` **does not verify what it produces**. It collapses `.`/`..` +//! `GetFullPathNameW` **does not verify what it produces** -- though for a +//! drive-relative path naming another drive it does query the filesystem, and +//! may rewrite that drive's recorded entry (see [`crate::full_path`]). It +//! collapses `.`/`..` //! lexically, and it *additionally* roots most paths that are not fully //! qualified against process state -- the current directory, or for a //! root-relative path that directory's *root*, or for a drive-relative path -//! naming another drive that drive's own current directory from the `=C:` -//! environment variables (for the current drive the process directory is used -//! and that entry is ignored). It is +//! naming another drive the entry recorded for it in the `=C:` environment +//! variables -- honoured verbatim, so it need not even be on that drive (for the +//! current drive the process directory is used and the entry is ignored). It is //! therefore not a lexical call as a whole, which is what makes resolving on //! the submitting thread meaningful. //! diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index 4f898f7c5..a9f94b6ed 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -313,15 +313,19 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " verifies nothing it produces -- and it is not an allocation, so most" + " verifies nothing it produces. The gap between building and cloning" ); let _ = writeln!( out, - " of the cost above is work no allocation scheme can remove. Whether any" + " bounds that resolution from above; it is not the call's own cost," ); let _ = writeln!( out, - " it enters the kernel is not something this run measured." + " because it also spans this crate's allocations and the builder chain." + ); + let _ = writeln!( + out, + " Whether any of it enters the kernel is not something this run measured." ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 5b3ec7158..ec05116c9 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4358,20 +4358,21 @@ fn a_small_handshake_completes_and_reports_a_positive_round_trip() { // // `request_cost::measure` builds its long-path sample on a hard-coded `C:`, and // two review passes read that as a portability bug: a machine with no `C:` -// volume would panic on the `expect` rather than measure. It would not, because -// resolving a fully-qualified path consults no volume -- the same fact the -// probe's account of its own nanoseconds rests on. +// volume would panic on the `expect` rather than measure. It does not -- and +// that, the observable outcome, is the whole of what is claimed here. // // That was an argument, and an argument is what a reviewer had to disbelieve. // This is the measurement. // -// **It pins less than an earlier version of this comment claimed.** That version -// said it pinned "touches no filesystem". It cannot: succeeding against a volume -// that does not exist shows the volume was never *consulted*, which is an -// absence of verification, not an absence of I/O. No black-box test can -// establish the latter, and the owning crate's `D-18` now says so. What this -// does pin is exactly what the probe needs -- that a missing volume does not -// make the measurement fail. +// **It pins less than two earlier versions of this comment claimed.** The first +// said it pinned "touches no filesystem"; the second said it showed the volume +// was "never consulted". Neither follows. A black-box success is equally +// compatible with a consultation whose failure is ignored, so nothing here +// reaches the mechanism -- which is the overreach the owning crate's `D-18` +// exists to remove, committed twice in the comment describing it. +// +// What this pins is the observable outcome, and it is exactly what the probe +// needs: the absence of a volume does not make preparation fail. #[test] fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { From edc9cfbca95b4a53e912c1a923b4dfd1e13f2cb8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 05:21:08 -0400 Subject: [PATCH 15/36] test(namespace-request): pin the per-drive entry arm, and stop understating the write Ninth review round, self-review ahead of the reviewer. Two findings, and the first dissolves a blocker I had recorded an hour earlier. THE ENTRY IS CREATED, NOT MERELY REWRITTEN. Last round measured that resolving a drive-relative path rewrites a stale `=X:` entry. It does more: with the entry UNSET, resolving creates it. So the environment write happens on a pristine host, not only on one carrying stale state, and the docs and the test comment both understated it. Also confirmed it is not a one-time cache fill -- two different stale values were each rewritten. That fact closed NR-1.1, the item created last round. It deferred pinning the entry-reading arm because a controlled `=X:` meant mutating process-global state shared by other test threads. But the call under test already writes that state on every resolve, so a test that sets it first adds no hazard that resolving alone did not. Nothing was left to decide, so the work is done rather than queued, and the checklist records why. `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one` now pins all three behaviours: an entry naming an existing directory is honoured verbatim onto a DIFFERENT drive; one naming nothing is rejected for the drive root; and the call writes the entry back, creating it when absent. Drive `W` keeps it from racing the sibling test's `X`/`Y`. Verified by clearing the entry to model the "always use the drive root" implementation the test exists to exclude, and watching it go red. THE SIBLING TEST'S ASSERTION WAS NEAR-VACUOUS -- the same defect round seven removed, respelled. `!resolved.eq_ignore_ascii_case(cwd\foo)` can fire only when the entry happens to equal the process directory exactly; on any ordinary host it is unfalsifiable. Worse, I "verified" it by resolving a relative path instead, which sabotages the test rather than the code under test and demonstrated nothing. It now asserts only what survives every entry value, and says plainly that it bounds the arm rather than pinning it. Needs `Win32_System_Environment` as a DEV-dependency: `std::env` cannot address these keys at all, since it rejects a key containing `=`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CHECKLIST.md | 29 +--- .../COMPLETED-CHECKLIST.md | 37 +++++ .../COMPLETED-PLANS.md | 5 + .../windows-namespace-request-sys/Cargo.toml | 11 ++ .../DESIGN-NOTES.md | 3 +- crates/windows-namespace-request-sys/PLANS.md | 1 - .../src/full_path.rs | 4 +- .../src/full_path/tests.rs | 140 ++++++++++++++---- 8 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md create mode 100644 crates/windows-namespace-request-sys/COMPLETED-PLANS.md diff --git a/crates/windows-namespace-request-sys/CHECKLIST.md b/crates/windows-namespace-request-sys/CHECKLIST.md index 56cec1b23..fd0864cd5 100644 --- a/crates/windows-namespace-request-sys/CHECKLIST.md +++ b/crates/windows-namespace-request-sys/CHECKLIST.md @@ -7,30 +7,5 @@ tracked separately, in the workspace M24-M26; that file is feature-scoped and is deleted when its feature completes, so durable follow-up work for the crate belongs here instead. -## NR-1 -- Pin the per-drive current directory arm of drive-relative rooting - -- [ ] **NR-1.1** -- Decide how this crate's tests may control process-global state, - then pin the `=X:` arm of drive-relative rooting. - - **The gap, stated exactly.** [D-18](DESIGN-NOTES.md#d-18) documents a two-arm - rule: a drive-relative path like `C:foo` is rooted at *that drive's* recorded - current directory (the hidden `=C:` entry) for a drive other than the current - one, while for the current drive the entry is ignored and the process current - directory wins. `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` - in [tests.rs](src/full_path/tests.rs) pins the second arm and only bounds the - first: when the chosen drive has no `=X:` entry, an implementation that always - used the drive root would pass every assertion. - - **Why it was not simply written.** Pinning it needs a controlled `=X:`, and - both routes cost something this item should decide rather than assume. Setting - the variable in-process mutates state shared by every test thread, which is - the hazard [DESIGN-NOTES.md](../windows-file-watcher/DESIGN-NOTES.md) records - for this workspace's single-process test model. Spawning a child with a - crafted environment avoids that but makes it an integration test and needs the - `Win32_System_Environment` feature as a dev-dependency. - - Measured facts the work can rely on, so they are not rediscovered: setting - `=X:` for a drive that does **not** exist has no effect; setting it for an - existing non-current drive does move that drive's resolution; and - `SetCurrentDirectoryW` does not maintain these entries -- a parent shell - writes them. +No open milestones. Completed work is in +[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md new file mode 100644 index 000000000..58708d427 --- /dev/null +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -0,0 +1,37 @@ +# Completed checklists: windows-namespace-request-sys + +Append-only. Newest groups at the bottom. + +## Moved 2026-09-10 -- NR-1: the per-drive current-directory arm, pinned + +### NR-1.1 -- Pin the `=X:` arm of drive-relative rooting. *(completed 2026-09-10)* + +- [x] **NR-1.1** -- Decide how this crate's tests may control process-global + state, then pin the `=X:` arm of drive-relative rooting. + + **Completed in the round that raised it, because the blocker turned out not to + exist.** The item was written to defer the work: pinning the arm needs a + controlled `=X:`, and the two routes to one -- mutating process-global state + that other test threads share, or spawning a child process with a crafted + environment -- looked like a decision about this crate's test shape rather than + something to take in passing. + + Measurement dissolved the first route's objection within the hour. + `GetFullPathNameW` **itself** writes the `=X:` entry on every drive-relative + resolution, creating it when absent. The code under test already mutates that + state, so a test that sets it first introduces no hazard that resolving alone + did not, and there was nothing left to decide. + + `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one` + in [tests.rs](src/full_path/tests.rs) now pins all three behaviours: an entry + naming an existing directory is honoured verbatim (onto a *different* drive, + which is what makes "that drive's own current directory" a convention rather + than a guarantee); an entry naming nothing is rejected in favour of the drive + root; and the call writes the entry back, creating it on a host that had none. + It uses drive `W` so it cannot race the sibling test's `X`/`Y` under libtest's + thread-per-test model. + + The sibling test + `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` + keeps its weaker form and now says so: without controlling the entry it can + only bound the arm. diff --git a/crates/windows-namespace-request-sys/COMPLETED-PLANS.md b/crates/windows-namespace-request-sys/COMPLETED-PLANS.md new file mode 100644 index 000000000..8a3ca5ac6 --- /dev/null +++ b/crates/windows-namespace-request-sys/COMPLETED-PLANS.md @@ -0,0 +1,5 @@ +# Completed plans: windows-namespace-request-sys + +| Path to CHECKLIST.md | Completion Date | Brief description | Design Notes | +|---|---|---|---| +| [CHECKLIST.md](CHECKLIST.md) | 2026-09-10 | NR-1: pinned the `=X:` arm of drive-relative rooting. Raised as deferred work and completed the same round, because the blocker dissolved on measurement -- `GetFullPathNameW` writes the per-drive entry itself, so a test that sets it adds no hazard. The test now pins verbatim honouring, drive-root fallback, and the write-back. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-18), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) | diff --git a/crates/windows-namespace-request-sys/Cargo.toml b/crates/windows-namespace-request-sys/Cargo.toml index 0dc6fa943..ade033ea0 100644 --- a/crates/windows-namespace-request-sys/Cargo.toml +++ b/crates/windows-namespace-request-sys/Cargo.toml @@ -39,6 +39,17 @@ wtf-string = { version = "0.1.0", path = "../wtf-string" } [dev-dependencies] windows-thread-ambient-sys = { version = "0.2.0", path = "../windows-thread-ambient-sys" } +# Test-only, for the hidden `=X:` per-drive current-directory entries. Rust's +# `std::env` cannot reach them at all -- it rejects a key containing `=` -- and +# they are the state `GetFullPathNameW` reads and rewrites when it roots a +# drive-relative path, so a test that does not control them can only bound that +# behaviour rather than pin it. A dev-dependency, so no consumer inherits the +# feature. +[dev-dependencies.windows-sys] +version = "0.61.2" +default-features = false +features = ["Win32_System_Environment"] + [dependencies.windows-sys] version = "0.61.2" default-features = false diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index adf245d41..ebfcacfef 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -601,7 +601,8 @@ current one validates that drive's `=X:` entry against the filesystem: an entry naming an existing directory is honoured **verbatim** (and need not be on that drive -- with `=X:` set to `C:\Windows`, `X:foo` is `C:\Windows\foo`), while a missing directory or an existing *file* is rejected and the entry is -**rewritten** to the drive root. The rewrite mutates the process environment +**written** to the drive root -- created when absent, so this happens on a +pristine host too. The rewrite mutates the process environment block as a side effect of what reads like a pure query. For the current drive the entry is neither consulted nor rewritten. diff --git a/crates/windows-namespace-request-sys/PLANS.md b/crates/windows-namespace-request-sys/PLANS.md index a28fe24c4..de5fb4ac0 100644 --- a/crates/windows-namespace-request-sys/PLANS.md +++ b/crates/windows-namespace-request-sys/PLANS.md @@ -10,5 +10,4 @@ source-component. That file is not the workspace | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | not started | NR-1: pin the `=X:` arm of drive-relative rooting, once it is decided how this crate's tests may control process-global state. The other arm and the surrounding contract are already pinned; this one is bounded, not pinned. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-18), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | **This crate's part (M24-M26) is complete**: the foundations (owned handle duplication, security attributes, path preparation, the faithful-execution contract), the four handle-producing entries, the five query entries, a test seam, and an acceptance pass over both operation and scenario coverage. The checklist itself stays open for M27 (`windows-platform-probes`) and the `M26+` items gated on this branch merging with `main` -- including `M26+.3`, the merge-or-delete decision on this crate's duplicated path preparation. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index cf6fce6f3..b40a1a4e8 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -120,7 +120,9 @@ //! including a directory on a *different* drive. With `=X:` set to //! `C:\Windows`, `X:foo` resolves to `C:\Windows\foo`. So "that drive's own //! current directory" describes the convention, not a guarantee. -//! * Otherwise the entry is **rewritten** to the drive root and that is used. +//! * Otherwise the entry is **written** to the drive root and that is used -- +//! created when absent, so this happens on a pristine host and not only on +//! one carrying a stale entry. //! Both a missing directory and an existing *file* are rejected this way, so //! the check is a filesystem query rather than a syntax or drive-existence //! test -- and the rewrite mutates the process environment block as a side diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 684775f43..2305cb312 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -375,22 +375,19 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // // **This test does not mutate `=X:`, but the call it exercises may.** // Measured: resolving `X:foo` for a non-current drive validates that - // drive's entry against the filesystem and REWRITES it to `X:\` when it - // does not name an existing directory. So on a host that inherited a stale - // entry, merely running this test changes the process environment. That is + // drive's entry against the filesystem and WRITES it to `X:\` when it does + // not name an existing directory -- creating it when absent, so merely + // running this test changes the process environment on ANY host, not just + // one carrying a stale entry. That is // a property of the call, documented in the module doc; it is noted here so // the next reader does not take "reads process state" at face value, as // four revisions of that doc did. // - // **What that leaves unpinned, stated rather than glossed:** when the chosen - // drive has no `=X:` entry, an implementation that always used the drive - // root would satisfy every assertion below, and the current-drive arm cannot - // separate the two rules either because there the entry is ignored by - // design. Pinning the entry-reading arm needs a controlled `=X:`, which - // means either mutating process-global state that other test threads share - // or spawning a child process -- a decision about this crate's test shape - // rather than something to slip in here. Queued as `NR-1.1` in this - // crate's CHECKLIST.md. + // **This test only BOUNDS the other-drive arm**, because it does not control + // the entry: with no `=X:` set, an implementation that always used the + // drive root would satisfy everything here. The arm is pinned properly by + // `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one`, + // which sets the entry and uses drive `W` so the two cannot race. let cwd = current_directory(); let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); @@ -408,21 +405,15 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // typed. This host returns `q:\...` for an uppercase `Q:` input, because the // shell was started with a lowercase `cd`. An earlier version compared bytes // and would have failed on a drive visited in lowercase. - // Asserted against the process current directory, not against the drive - // letter. The entry is honoured VERBATIM when it names an existing - // directory, and is not constrained to live on that drive -- with `=X:` - // set to `C:\Windows`, `X:foo` is `C:\Windows\foo`. An earlier version - // required the result to start with `X:\` and carried a message claiming - // robustness "whatever that drive's recorded directory happens to be", - // which is exactly the case that broke it. - assert!( - !resolved.eq_ignore_ascii_case(&format!(r"{}\foo", cwd.trim_end_matches('\\'))), - "a drive-relative path for another drive does not use the process \ - current directory ({cwd}): {resolved}" - ); + // Only what is invariant without controlling the entry. An earlier version + // required the result to start with `X:\`, which the verbatim rule breaks; + // its replacement compared against the current directory, which `other` + // differs from by construction, so it could fire only if the entry happened + // to equal the process directory exactly -- the same vacuity, respelled. + // What survives every entry value is that the component is carried through. assert!( resolved.ends_with(r"\foo"), - "and keeps the component it was given: {resolved}" + "the component is carried through whatever the entry holds: {resolved}" ); // The current-drive arm, where the process directory wins over any `=X:`. @@ -481,3 +472,102 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { ); } } + +/// Reads one of the hidden `=X:` per-drive current-directory entries. +/// +/// Through Win32 rather than `std::env`, which rejects a key containing `=` +/// outright and so cannot address these at all. +fn drive_entry(drive: char) -> Option { + let name = Wtf16String::from(format!("={drive}:").as_str()); + let mut buffer = vec![0u16; 1024]; + // SAFETY: `name` is NUL-terminated, and `buffer` is writable for the length + // passed. + let written = unsafe { + windows_sys::Win32::System::Environment::GetEnvironmentVariableW( + name.as_terminated_ptr(), + buffer.as_mut_ptr(), + u32::try_from(buffer.len()).unwrap_or(u32::MAX), + ) + }; + (written != 0).then(|| String::from_utf16_lossy(&buffer[..written as usize])) +} + +/// Sets or clears one of the hidden `=X:` entries. +fn set_drive_entry(drive: char, value: Option<&str>) { + let name = Wtf16String::from(format!("={drive}:").as_str()); + let value = value.map(Wtf16String::from); + let value_ptr = value + .as_ref() + .map_or(core::ptr::null(), Wtf16String::as_terminated_ptr); + // SAFETY: both pointers are NUL-terminated; a null value clears the entry. + let ok = unsafe { + windows_sys::Win32::System::Environment::SetEnvironmentVariableW( + name.as_terminated_ptr(), + value_ptr, + ) + }; + assert!(ok != 0, "set ={drive}: entry"); +} + +#[test] +fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one() { + // The arm the sibling test can only BOUND. Without controlling the entry, + // an implementation that always used the drive root would satisfy every + // assertion there, because a host with no `=X:` entry cannot tell the two + // rules apart. + // + // **Controlling it is not the hazard it looks like**, and that is what + // unblocked this test. The objection was that `=X:` is process-global while + // these tests share a process -- but `GetFullPathNameW` *itself* writes the + // entry on every drive-relative resolution, creating it when absent. The + // code under test already mutates this state, so a test that sets it first + // introduces no hazard that resolving alone did not. + // + // Drive `W` is used rather than the `X`/`Y` of the sibling test, so the two + // cannot race under libtest's thread-per-test model. + const DRIVE: char = 'W'; + let restore = drive_entry(DRIVE); + + // The current directory is a real directory that is certainly NOT on drive + // W, which is exactly what makes it the right probe: if the entry is + // honoured verbatim, a `W:`-relative path resolves onto another drive + // entirely. + let cwd = current_directory(); + let cwd = cwd.trim_end_matches('\\'); + + set_drive_entry(DRIVE, Some(cwd)); + assert_eq!( + resolve(&format!("{DRIVE}:foo")), + format!(r"{cwd}\foo"), + "an entry naming an existing directory is honoured verbatim, even onto \ + a different drive -- so \"that drive's own current directory\" is the \ + convention the entry usually holds, not a guarantee about the result" + ); + + // An entry that does not name an existing directory is rejected, and the + // call rewrites it to the drive root rather than leaving it stale. + set_drive_entry(DRIVE, Some(r"C:\no-such-directory-for-this-test")); + assert_eq!( + resolve(&format!("{DRIVE}:foo")), + format!(r"{DRIVE}:\foo"), + "an entry that names nothing is rejected in favour of the drive root" + ); + assert_eq!( + drive_entry(DRIVE).as_deref(), + Some(format!(r"{DRIVE}:\").as_str()), + "and the call REWROTE the entry: this is a query that mutates the \ + process environment block" + ); + + // Absent entirely, the entry is created rather than merely read. + set_drive_entry(DRIVE, None); + assert_eq!(drive_entry(DRIVE), None, "precondition: entry cleared"); + let _ = resolve(&format!("{DRIVE}:foo")); + assert_eq!( + drive_entry(DRIVE).as_deref(), + Some(format!(r"{DRIVE}:\").as_str()), + "resolving created the entry on a host that had none" + ); + + set_drive_entry(DRIVE, restore.as_deref()); +} From 7bffb514a81edcdd47182842e6ad1e4e3813e8ab Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 05:34:19 -0400 Subject: [PATCH 16/36] docs: the drive-entry gate is shape AND existence, not existence instead of shape Ninth review round. Three findings, and the middle one is the same error as the round it corrects, committed inside the correction. THE GATE IS NOT PURELY A FILESYSTEM CHECK. Last round measured that an entry naming a missing directory or an existing FILE is rejected, and concluded "the check is a filesystem query rather than a syntax or drive-existence test". The word "rather" was doing unearned work. Measured: every value below names the SAME existing directory, and only the first is accepted. C:\Windows\System32 accepted C:/Windows/System32 rejected C:\Windows\System32\. rejected C:\Windows\System32\..\System32 rejected \\?\C:\Windows\System32 rejected So shape gates it independently of existence, and acceptance is literal: `C:\Windows\` yields `C:\Windows\\foo`, unnormalised at the join. Ruling an alternative out is a strictly stronger claim than establishing the one you measured, and three observations of the existence check said nothing about shape. Corrected in full_path.rs, D-18 and path.rs, recorded as draft 9 in Tier 2, and pinned by a new test so the next draft cannot restate it from memory -- with a control asserting the same directory IS accepted in canonical form, so the rejections cannot be blamed on the directory. A BROKEN DOC COMMENT REACHED RENDERED RUSTDOC. Splicing the new sections in `df75187` produced `//!//! It does **not** solve...`, which merged two sections and leaked a literal `//!` into the published module doc of a release-managed crate. Fixed; verified by grepping the generated HTML, which now contains no `//!` at all. TWO STATEMENTS DESCRIBED D-18 AS LEAVING THE KERNEL QUESTION OPEN, after the same branch closed it. Both were added by this branch, so both are corrected in place rather than amended -- append-only protects landed history, not a paragraph still in review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 20 +++++--- .../DESIGN-RATIONALE.md | 18 +++++++ .../src/full_path.rs | 32 ++++++++---- .../src/full_path/tests.rs | 50 +++++++++++++++++++ .../windows-namespace-request-sys/src/path.rs | 5 +- .../COMPLETED-CHECKLIST.md | 4 +- .../src/request_cost.rs | 10 ++-- 7 files changed, 116 insertions(+), 23 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index ebfcacfef..cca96bdb1 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -597,12 +597,20 @@ having resolved relativity another way -- they are named here. **It does touch the filesystem, on one form -- measured, after four drafts said otherwise.** Resolving a drive-relative path for a drive that is *not* the -current one validates that drive's `=X:` entry against the filesystem: an entry -naming an existing directory is honoured **verbatim** (and need not be on that -drive -- with `=X:` set to `C:\Windows`, `X:foo` is `C:\Windows\foo`), while a -missing directory or an existing *file* is rejected and the entry is -**written** to the drive root -- created when absent, so this happens on a -pristine host too. The rewrite mutates the process environment +current one checks that drive's `=X:` entry against the filesystem *and* against +a required shape. An accepted entry is used **verbatim** and need not be on that +drive -- with `=X:` set to `C:\Windows`, `X:foo` is `C:\Windows\foo`. Anything +rejected is replaced by the drive root, and the entry is **written** back there +-- created when absent, so this happens on a pristine host too. + +Both halves of the gate were measured, and one of them refuted a draft of this +very decision. Existence matters: a missing directory and an existing *file* are +each rejected. Shape matters too, and independently -- `C:/Windows/System32`, +`C:\Windows\System32\.`, `C:\Windows\System32\..\System32` and +`\\?\C:\Windows\System32` were all rejected while naming the same existing +directory that `C:\Windows\System32` was accepted for. So the draft calling this +"a filesystem query rather than a syntax test" named a mechanism the evidence +contradicts: it is both, and the list is observation rather than specification. The rewrite mutates the process environment block as a side effect of what reads like a pure query. For the current drive the entry is neither consulted nor rewritten. diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index cc9547f83..68b5968cc 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -79,6 +79,24 @@ producing wrong answers. rather than measured. Every earlier draft at least *knew* it was asserting a mechanism; this one thought it was declining to. +9. **"A filesystem query rather than a syntax test."** Written in the paragraph + correcting (8), and wrong the same way within a single round. Having found + that an entry naming a missing directory or an existing *file* is rejected, + the draft concluded the gate was existence and not shape. Measured, shape + gates it too and independently: `C:/Windows/System32`, + `C:\Windows\System32\.`, `C:\Windows\System32\..\System32` and + `\\?\C:\Windows\System32` are each rejected while naming the same existing + directory that `C:\Windows\System32` is accepted for. Acceptance is also + literal -- `C:\Windows\` yields `C:\Windows\\foo`, with no normalisation at + the join. + + The tell was the word *rather*. Ruling an alternative out is a strictly + stronger claim than establishing the one you measured, and needs its own + evidence; three observations of the existence check said nothing about + shape. Both halves are now pinned by + `a_rejected_drive_entry_is_replaced_by_the_drive_root` so the next draft + cannot restate this from memory. + ### Two facts that were measured, then asserted too narrowly Both were found by review after the correction had already shipped, and both diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index b40a1a4e8..0a8b7f9b3 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -116,17 +116,28 @@ //! Resolving `X:foo` for a drive that is **not** the current one does not //! merely read the `=X:` entry: //! -//! * The entry is honoured **verbatim** when it names an existing directory -- -//! including a directory on a *different* drive. With `=X:` set to -//! `C:\Windows`, `X:foo` resolves to `C:\Windows\foo`. So "that drive's own -//! current directory" describes the convention, not a guarantee. +//! * An **accepted** entry is used **verbatim**, including a directory on a +//! *different* drive. With `=X:` set to `C:\Windows`, `X:foo` resolves to +//! `C:\Windows\foo`, so "that drive's own current directory" describes the +//! convention the entry usually holds, not a guarantee about the result. +//! Verbatim really means verbatim: `C:\Windows\` yields `C:\Windows\\foo`, +//! with no normalisation at the join. //! * Otherwise the entry is **written** to the drive root and that is used -- //! created when absent, so this happens on a pristine host and not only on -//! one carrying a stale entry. -//! Both a missing directory and an existing *file* are rejected this way, so -//! the check is a filesystem query rather than a syntax or drive-existence -//! test -- and the rewrite mutates the process environment block as a side -//! effect of what reads like a pure query. +//! one carrying a stale entry. The write mutates the process environment +//! block as a side effect of what reads like a pure query. +//! +//! **Acceptance needs both a shape and an existence check, and the observed +//! necessary conditions are worth listing because they are not guessable.** An +//! entry naming a directory that exists is still rejected unless it is already +//! in fully-qualified `X:\...` form: measured on one build, `C:/Windows/System32`, +//! `C:\Windows\System32\.`, `C:\Windows\System32\..\System32` and +//! `\\?\C:\Windows\System32` were each rejected while naming the same existing +//! directory that `C:\Windows\System32` was accepted for. An existing *file* and +//! a missing directory are rejected too, so existence is checked as well -- but +//! saying the gate is "a filesystem query rather than a syntax test", as a draft +//! of this doc did, states a mechanism the evidence contradicts. It is both, and +//! this list is a set of observations rather than a specification. //! //! For the current drive neither happens: the entry is not consulted and not //! rewritten. @@ -164,7 +175,8 @@ //! for this note and is *not* something the probe reports; no instrument in //! this repository isolates the call, and the honest reading of //! `probe-request-cost` alone is an upper bound. -//!//! It does **not** solve the session-relative drive-letter hazard, and saying +//! +//! It does **not** solve the session-relative drive-letter hazard, and saying //! so plainly matters more than the part it does solve. `GetFullPathNameW` //! never expands a drive letter, and a drive letter is resolved against the //! logon session of whatever token is in effect at open time. A path resolved diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 2305cb312..6ec260039 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -571,3 +571,53 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( set_drive_entry(DRIVE, restore.as_deref()); } + +#[test] +fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { + // Acceptance needs BOTH a shape and an existence check, and a draft of the + // module doc claimed it was "a filesystem query rather than a syntax test" + // -- having measured only the existence half. Every value below names an + // existing directory, so anything rejected here is rejected on shape alone. + // + // Pinned because the distinction is not guessable and the doc asserts it. + const DRIVE: char = 'V'; + let restore = drive_entry(DRIVE); + + let accepted = current_directory(); + let accepted = accepted.trim_end_matches('\\'); + + // The control: this exact directory IS accepted in canonical form, so the + // rejections below cannot be blamed on the directory itself. + set_drive_entry(DRIVE, Some(accepted)); + assert_eq!( + resolve(&format!("{DRIVE}:foo")), + format!(r"{accepted}\foo"), + "control: the same directory in canonical form is accepted" + ); + + // Same directory, spellings that are not fully-qualified `X:\...` form. + // Each names something that exists; each is rejected anyway. + for spelling in [ + accepted.replace('\\', "/"), + format!(r"{accepted}\."), + format!( + r"{accepted}\..\{}", + accepted.rsplit('\\').next().unwrap_or("") + ), + format!(r"\\?\{accepted}"), + ] { + set_drive_entry(DRIVE, Some(&spelling)); + assert_eq!( + resolve(&format!("{DRIVE}:foo")), + format!(r"{DRIVE}:\foo"), + "{spelling:?} names an existing directory but is rejected on shape" + ); + assert_eq!( + drive_entry(DRIVE).as_deref(), + Some(format!(r"{DRIVE}:\").as_str()), + "and the rejected entry is written back as the drive root" + ); + } + + set_drive_entry(DRIVE, restore.as_deref()); +} diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 33ba9eb13..ff2d62634 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -23,8 +23,9 @@ //! qualified against process state -- the current directory, or for a //! root-relative path that directory's *root*, or for a drive-relative path //! naming another drive the entry recorded for it in the `=C:` environment -//! variables -- honoured verbatim, so it need not even be on that drive (for the -//! current drive the process directory is used and the entry is ignored). It is +//! variables -- used verbatim when accepted, so it need not even be on that +//! drive, and replaced by the drive root when not (for the current drive the +//! process directory is used and the entry is ignored). It is //! therefore not a lexical call as a whole, which is what makes resolving on //! the submitting thread meaningful. //! diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index a8509f630..6835b8751 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -105,7 +105,9 @@ process state, so it is not a lexical call as a whole; `PathCchCanonicalizeEx` d the wrong call for that reason, because rooting at submission is the property being bought. No cost comparison is claimed -- the item below asked whether the alternative "would be cheaper", and the answer recorded in D-18 is that nothing measures it, so the decision rests on semantics alone. -Whether the call enters the kernel is recorded as NOT established. The item's body below is the +The mechanism question this item raised is answered rather than left open: resolving a +drive-relative path for another drive checks that drive's recorded entry against the filesystem +and writes the entry back, so the call does touch the filesystem on that form. The item's body below is the request as it was written, and quotes the module doc as it read before the correction. - [x] **M2.6** -- Say precisely what `GetFullPathNameW` does, in the crate that owns it, and decide diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index dbc1972d3..5bcfd92f7 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -78,10 +78,12 @@ //! The owning crate now settles both halves rather than leaving them to be //! re-derived from a probe: see `windows-namespace-request-sys`' //! [DESIGN-NOTES.md](../../windows-namespace-request-sys/DESIGN-NOTES.md) -> -//! `D-18`, which states what the call actually does, records keeping it over -//! the canonicalizers that do not root, and says plainly that whether it enters -//! the kernel is not established. It claims no cost comparison against those -//! alternatives, because nothing here measures them. +//! `D-18`, which states what the call actually does and records keeping it over +//! the canonicalizers that do not root. It claims no cost comparison against +//! those alternatives, because nothing here measures them -- but it does settle +//! the mechanism question this module once left open: resolving a +//! drive-relative path for another drive checks that drive's recorded entry +//! against the filesystem, and writes the entry back. //! //! The two schemes that might reduce it recover different halves. **Inline //! storage** removes the allocation and copy, which is what From 7411afd0158979e28999fe80c0498b7f541ab299 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 08:23:48 -0400 Subject: [PATCH 17/36] test(namespace-request): stop deriving the drive-entry probes from the current directory Self-review ahead of the round-ten reviewer, and it is the rounds 6-8 defect again: a test whose strength depends on where it runs, this time in the two tests written to fix that very class. Both new drive-entry tests built their probe values from `current_directory()`, trimming the trailing separator. Run from a drive root that turns `C:\` into `C:` -- a DRIVE-RELATIVE value, not a directory -- and since an accepted entry is joined literally, `W:foo` resolved to `C:foo` rather than `C:\foo`. Measured by running the test binary directly with a chosen working directory: cwd=Q:\github\... 11 passed (what CI does, so it looked fine) cwd=C:\ 2 FAILED They passed only because CI runs from a repository checkout. The probes now come from a created directory under %TEMP%, which is guaranteed to exist, to be in canonical `X:\...` form, and to be neither a drive root nor the process current directory. Verified from `C:\`, `Q:\`, `C:\Windows` and a UNC working directory (`\\localhost\C$\Windows`, launched through .NET since cmd cannot hold a UNC cwd): 11 passed in every one. Re-confirmed non-vacuous afterwards by substituting the canonical spelling into the rejection loop and watching it go red. Cleanup is a plain statement, not a drop guard, and says why: neither the directory removal nor the entry restore runs on panic, but both leftovers are states the system already produces -- the call writes that entry on every drive-relative resolution regardless -- and a guard that wrote during unwinding could panic and abort, replacing a diagnosable failure with one that explains nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 6ec260039..ece721068 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -473,6 +473,31 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { } } +/// A directory that certainly exists, is in canonical `X:\...` form, and is +/// neither a drive root nor the process current directory. +/// +/// **Derived from the temp directory rather than from the current directory, +/// which is not a detail.** An earlier version of these tests built their probe +/// values by trimming the trailing separator off `current_directory()`. Run +/// from a drive root that turns `C:\` into `C:` -- a *drive-relative* value, not +/// a directory -- and since an accepted entry is joined literally, the entry +/// resolved to `C:foo` and both tests failed. They passed only because CI runs +/// from a repository checkout. A test whose strength depends on where it is run +/// is the vacuous pass this suite keeps paying for. +/// +/// The caller removes it, and the entry restore beside it is a plain statement +/// rather than a drop guard. Neither runs if the test panics -- which leaves a +/// directory under `%TEMP%` and an `=X:` entry reading `X:\`. Both are states +/// the system already produces on its own: the call under test writes that +/// entry on every drive-relative resolution anyway. A guard that wrote during +/// unwinding could panic and abort the process, replacing a diagnosable failure +/// with one that explains nothing, so the trade is one-sided. +fn probe_directory(tag: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("wnrs-{}-{tag}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create the probe directory"); + dir +} + /// Reads one of the hidden `=X:` per-drive current-directory entries. /// /// Through Win32 rather than `std::env`, which rejects a key containing `=` @@ -528,17 +553,16 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( const DRIVE: char = 'W'; let restore = drive_entry(DRIVE); - // The current directory is a real directory that is certainly NOT on drive - // W, which is exactly what makes it the right probe: if the entry is - // honoured verbatim, a `W:`-relative path resolves onto another drive - // entirely. - let cwd = current_directory(); - let cwd = cwd.trim_end_matches('\\'); + // A real directory that is certainly NOT on drive W, which is what makes it + // the right probe: if the entry is honoured verbatim, a `W:`-relative path + // resolves onto another drive entirely. + let probe_owned = probe_directory("verbatim"); + let probe = probe_owned.to_str().expect("the temp path is UTF-8"); - set_drive_entry(DRIVE, Some(cwd)); + set_drive_entry(DRIVE, Some(probe)); assert_eq!( resolve(&format!("{DRIVE}:foo")), - format!(r"{cwd}\foo"), + format!(r"{probe}\foo"), "an entry naming an existing directory is honoured verbatim, even onto \ a different drive -- so \"that drive's own current directory\" is the \ convention the entry usually holds, not a guarantee about the result" @@ -570,6 +594,7 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( ); set_drive_entry(DRIVE, restore.as_deref()); + let _ = std::fs::remove_dir(&probe_owned); } #[test] @@ -583,8 +608,8 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { const DRIVE: char = 'V'; let restore = drive_entry(DRIVE); - let accepted = current_directory(); - let accepted = accepted.trim_end_matches('\\'); + let probe_owned = probe_directory("shape"); + let accepted = probe_owned.to_str().expect("the temp path is UTF-8"); // The control: this exact directory IS accepted in canonical form, so the // rejections below cannot be blamed on the directory itself. @@ -620,4 +645,5 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { } set_drive_entry(DRIVE, restore.as_deref()); + let _ = std::fs::remove_dir(&probe_owned); } From 640df05985979e63b1c9ca7b51e9aa25cee10722 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 08:32:34 -0400 Subject: [PATCH 18/36] test(namespace-request): choose the probe drive letter at runtime Tenth review round. The same host-dependence, one layer finer than the fix that preceded it. `7411afd` removed the drive-ROOT and UNC dependence by sourcing probe directories from %TEMP%. It left the drive-LETTER dependence: both tests hard-coded `W` and `V` and then asserted that resolving `W:foo` honours the entry set for it. That is only true for a drive other than the current one -- on the current drive the entry is ignored and the process directory wins, which is the two-arm rule these very tests document. `W` and `V` are among the letters a mapped network drive or a `subst` most often takes. Reproduced rather than reasoned: `subst W: Q:\github`, suite launched from `W:\`, and the verbatim test fails asserting `W:\foo` against the %TEMP% path. The sibling test already had exactly this guard, so the omission was inconsistent within one file. The letters are now chosen at runtime against the current drive, in disjoint pairs -- `W`/`U`, `V`/`T`, beside the sibling's `X`/`Y` -- so no two tests can land on the same letter and race. A fallback is reachable only when the current drive is the preferred letter, and the fallback differs from it, so the choice is always off the current drive. Verified from `W:\`, `V:\`, `U:\`, `T:\`, `C:\`, `Q:\`, `C:\Windows` and a UNC working directory: 11 passed in every one. Worth recording the secondary effect the reviewer noted: run from `V:`, every spelling in the rejection loop resolves to `V:\foo`, so all four rejection assertions would have passed while measuring nothing about shape. The control assertion -- that the same directory in canonical form IS accepted -- is what turned that vacuous pass into a visible failure. It earned its place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index ece721068..1df998229 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -498,6 +498,27 @@ fn probe_directory(tag: &str) -> std::path::PathBuf { dir } +/// A drive letter to probe with, which is certainly not the current drive. +/// +/// **The letter cannot be a constant, for the reason these tests exist.** An +/// entry is only consulted for a drive *other* than the current one -- on the +/// current drive it is ignored and the process directory wins -- so a test that +/// hard-codes `W` asserts something false when run from `W:`. Measured: with +/// `subst W:` and the suite launched from `W:\`, the verbatim test failed. +/// `W` and `V` are exactly the letters a mapped network drive or a `subst` +/// tends to take. +/// +/// Each caller passes a disjoint pair, so two tests can never land on the same +/// letter and race under libtest's thread-per-test model. +fn probe_drive(preferred: char, fallback: char) -> char { + let cwd = current_directory(); + match cwd.chars().next().filter(char::is_ascii_alphabetic) { + Some(current) if current.eq_ignore_ascii_case(&preferred) => fallback, + // A UNC current directory has no drive letter, so nothing collides. + _ => preferred, + } +} + /// Reads one of the hidden `=X:` per-drive current-directory entries. /// /// Through Win32 rather than `std::env`, which rejects a key containing `=` @@ -548,10 +569,10 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // code under test already mutates this state, so a test that sets it first // introduces no hazard that resolving alone did not. // - // Drive `W` is used rather than the `X`/`Y` of the sibling test, so the two - // cannot race under libtest's thread-per-test model. - const DRIVE: char = 'W'; - let restore = drive_entry(DRIVE); + // `W` (or `U` when the suite runs from `W:`) keeps this clear of the + // sibling tests' `X`/`Y` and `V`/`T`, so none of them can race. + let drive = probe_drive('W', 'U'); + let restore = drive_entry(drive); // A real directory that is certainly NOT on drive W, which is what makes it // the right probe: if the entry is honoured verbatim, a `W:`-relative path @@ -559,9 +580,9 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( let probe_owned = probe_directory("verbatim"); let probe = probe_owned.to_str().expect("the temp path is UTF-8"); - set_drive_entry(DRIVE, Some(probe)); + set_drive_entry(drive, Some(probe)); assert_eq!( - resolve(&format!("{DRIVE}:foo")), + resolve(&format!("{drive}:foo")), format!(r"{probe}\foo"), "an entry naming an existing directory is honoured verbatim, even onto \ a different drive -- so \"that drive's own current directory\" is the \ @@ -570,30 +591,30 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // An entry that does not name an existing directory is rejected, and the // call rewrites it to the drive root rather than leaving it stale. - set_drive_entry(DRIVE, Some(r"C:\no-such-directory-for-this-test")); + set_drive_entry(drive, Some(r"C:\no-such-directory-for-this-test")); assert_eq!( - resolve(&format!("{DRIVE}:foo")), - format!(r"{DRIVE}:\foo"), + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), "an entry that names nothing is rejected in favour of the drive root" ); assert_eq!( - drive_entry(DRIVE).as_deref(), - Some(format!(r"{DRIVE}:\").as_str()), + drive_entry(drive).as_deref(), + Some(format!(r"{drive}:\").as_str()), "and the call REWROTE the entry: this is a query that mutates the \ process environment block" ); // Absent entirely, the entry is created rather than merely read. - set_drive_entry(DRIVE, None); - assert_eq!(drive_entry(DRIVE), None, "precondition: entry cleared"); - let _ = resolve(&format!("{DRIVE}:foo")); + set_drive_entry(drive, None); + assert_eq!(drive_entry(drive), None, "precondition: entry cleared"); + let _ = resolve(&format!("{drive}:foo")); assert_eq!( - drive_entry(DRIVE).as_deref(), - Some(format!(r"{DRIVE}:\").as_str()), + drive_entry(drive).as_deref(), + Some(format!(r"{drive}:\").as_str()), "resolving created the entry on a host that had none" ); - set_drive_entry(DRIVE, restore.as_deref()); + set_drive_entry(drive, restore.as_deref()); let _ = std::fs::remove_dir(&probe_owned); } @@ -605,17 +626,17 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { // existing directory, so anything rejected here is rejected on shape alone. // // Pinned because the distinction is not guessable and the doc asserts it. - const DRIVE: char = 'V'; - let restore = drive_entry(DRIVE); + let drive = probe_drive('V', 'T'); + let restore = drive_entry(drive); let probe_owned = probe_directory("shape"); let accepted = probe_owned.to_str().expect("the temp path is UTF-8"); // The control: this exact directory IS accepted in canonical form, so the // rejections below cannot be blamed on the directory itself. - set_drive_entry(DRIVE, Some(accepted)); + set_drive_entry(drive, Some(accepted)); assert_eq!( - resolve(&format!("{DRIVE}:foo")), + resolve(&format!("{drive}:foo")), format!(r"{accepted}\foo"), "control: the same directory in canonical form is accepted" ); @@ -631,19 +652,19 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { ), format!(r"\\?\{accepted}"), ] { - set_drive_entry(DRIVE, Some(&spelling)); + set_drive_entry(drive, Some(&spelling)); assert_eq!( - resolve(&format!("{DRIVE}:foo")), - format!(r"{DRIVE}:\foo"), + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), "{spelling:?} names an existing directory but is rejected on shape" ); assert_eq!( - drive_entry(DRIVE).as_deref(), - Some(format!(r"{DRIVE}:\").as_str()), + drive_entry(drive).as_deref(), + Some(format!(r"{drive}:\").as_str()), "and the rejected entry is written back as the drive root" ); } - set_drive_entry(DRIVE, restore.as_deref()); + set_drive_entry(drive, restore.as_deref()); let _ = std::fs::remove_dir(&probe_owned); } From 79b3a6da9f2597d5a3e0f8938cb213847f7822fb Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 08:46:22 -0400 Subject: [PATCH 19/36] test(namespace-request): enforce the probe directory's form instead of assuming it Eleventh review round, and the third distinct source of host-dependence in the same two tests. `7411afd` traded a current-directory dependence for a %TEMP% one. `std::env::temp_dir()` is %TMP%/%TEMP% verbatim and carries no guarantee of a drive letter, so with temp redirected to a share the probe is a UNC path -- which GetFullPathNameW rejects as an entry ON SHAPE, the very rule the test was written to pin. Folder redirection makes that an ordinary configuration. Reproduced before fixing: with TMP/TEMP set to \\localhost\C$\Windows\Temp and the current directory untouched, both drive tests failed asserting `W:\foo` against the UNC probe. The helper now enforces the form its own doc claims. It uses the temp directory only when that is drive-rooted, and otherwise falls back to %SystemRoot% -- guaranteed to exist, canonical, not a drive root, and needing no write permission, which matters on exactly the hosts that redirect temp. A `created` flag carries whether there is anything to clean up. Verified across TEMP = \\localhost\C$\Windows\Temp, \\127.0.0.1\C$\Users, C:\, C:\Windows\Temp and Q:\ -- the drive tests pass in all five. Two OTHER tests fail under \\127.0.0.1\C$\Users, and they are not mine and not a regression: handle/tests.rs and request/tests.rs create real fixture files and get PermissionDenied from an unwritable temp directory. Also corrected two comments left naming drive `W` after `640df05` made the letter dynamic, and re-added `probe_drive`, which a range replacement in this change had swallowed -- caught by the compiler, which is the cheap case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 100 ++++++++++++------ 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 1df998229..f0ea56267 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -387,7 +387,8 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // the entry: with no `=X:` set, an implementation that always used the // drive root would satisfy everything here. The arm is pinned properly by // `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one`, - // which sets the entry and uses drive `W` so the two cannot race. + // which sets the entry and draws its letter from a disjoint pair, so the + // two cannot race. let cwd = current_directory(); let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); @@ -473,31 +474,62 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { } } -/// A directory that certainly exists, is in canonical `X:\...` form, and is -/// neither a drive root nor the process current directory. +/// A directory that exists, is in canonical `X:\...` form, and is neither a +/// drive root nor the process current directory. /// -/// **Derived from the temp directory rather than from the current directory, -/// which is not a detail.** An earlier version of these tests built their probe -/// values by trimming the trailing separator off `current_directory()`. Run -/// from a drive root that turns `C:\` into `C:` -- a *drive-relative* value, not -/// a directory -- and since an accepted entry is joined literally, the entry -/// resolved to `C:foo` and both tests failed. They passed only because CI runs -/// from a repository checkout. A test whose strength depends on where it is run -/// is the vacuous pass this suite keeps paying for. +/// **The form is enforced, not assumed, and that distinction has now cost two +/// rounds.** An earlier version derived the probe from `current_directory()`, +/// which yields `C:` at a drive root -- drive-relative, not a directory -- and +/// both tests failed there. Its replacement used `std::env::temp_dir()`, which +/// is `%TMP%`/`%TEMP%` verbatim and carries no guarantee of a drive letter: with +/// temp redirected to a share, the probe is a UNC path, which +/// `GetFullPathNameW` rejects as an entry *on shape* -- the very rule the +/// caller is trying to pin. Folder redirection makes that an ordinary +/// configuration, not a contrived one. /// -/// The caller removes it, and the entry restore beside it is a plain statement -/// rather than a drop guard. Neither runs if the test panics -- which leaves a -/// directory under `%TEMP%` and an `=X:` entry reading `X:\`. Both are states -/// the system already produces on its own: the call under test writes that -/// entry on every drive-relative resolution anyway. A guard that wrote during -/// unwinding could panic and abort the process, replacing a diagnosable failure -/// with one that explains nothing, so the trade is one-sided. -fn probe_directory(tag: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("wnrs-{}-{tag}", std::process::id())); - std::fs::create_dir_all(&dir).expect("create the probe directory"); - dir -} +/// So the temp directory is used only when it is drive-rooted, and otherwise +/// the fallback is `%SystemRoot%`, which is guaranteed to exist, to be +/// canonical, and not to be a drive root. Nothing is created in the fallback +/// case, so [`ProbeDir::created`] tells the caller whether to clean up. +struct ProbeDir { + path: std::path::PathBuf, + created: bool, +} + +fn probe_directory(tag: &str) -> ProbeDir { + let drive_rooted = |p: &std::path::Path| { + let s = p.as_os_str().to_string_lossy().into_owned(); + let mut chars = s.chars(); + matches!( + (chars.next(), chars.next(), chars.next()), + (Some(d), Some(':'), Some('\\')) if d.is_ascii_alphabetic() + ) + }; + let temp = std::env::temp_dir(); + if drive_rooted(&temp) { + let path = temp.join(format!("wnrs-{}-{tag}", std::process::id())); + std::fs::create_dir_all(&path).expect("create the probe directory"); + return ProbeDir { + path, + created: true, + }; + } + + // Not creating anything here, so no write permission is needed on a host + // whose temp directory is redirected off a drive letter. + let system_root = std::env::var("SystemRoot").expect("SystemRoot is always set on Windows"); + let path = std::path::PathBuf::from(system_root); + assert!( + drive_rooted(&path), + "the fallback probe must be drive-rooted: {}", + path.display() + ); + ProbeDir { + path, + created: false, + } +} /// A drive letter to probe with, which is certainly not the current drive. /// /// **The letter cannot be a constant, for the reason these tests exist.** An @@ -574,11 +606,11 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( let drive = probe_drive('W', 'U'); let restore = drive_entry(drive); - // A real directory that is certainly NOT on drive W, which is what makes it - // the right probe: if the entry is honoured verbatim, a `W:`-relative path - // resolves onto another drive entirely. - let probe_owned = probe_directory("verbatim"); - let probe = probe_owned.to_str().expect("the temp path is UTF-8"); + // A real directory that is certainly NOT on the probe drive, which is what + // makes it the right probe: if the entry is honoured verbatim, a + // drive-relative path resolves onto another drive entirely. + let probe_dir = probe_directory("verbatim"); + let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); set_drive_entry(drive, Some(probe)); assert_eq!( @@ -615,7 +647,9 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( ); set_drive_entry(drive, restore.as_deref()); - let _ = std::fs::remove_dir(&probe_owned); + if probe_dir.created { + let _ = std::fs::remove_dir(&probe_dir.path); + } } #[test] @@ -629,8 +663,8 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { let drive = probe_drive('V', 'T'); let restore = drive_entry(drive); - let probe_owned = probe_directory("shape"); - let accepted = probe_owned.to_str().expect("the temp path is UTF-8"); + let probe_dir = probe_directory("shape"); + let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); // The control: this exact directory IS accepted in canonical form, so the // rejections below cannot be blamed on the directory itself. @@ -666,5 +700,7 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { } set_drive_entry(drive, restore.as_deref()); - let _ = std::fs::remove_dir(&probe_owned); + if probe_dir.created { + let _ = std::fs::remove_dir(&probe_dir.path); + } } From 092bda82dfe7a8b1adeff781b640866edbbcd2f0 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 10:37:12 -0400 Subject: [PATCH 20/36] fix(namespace-request): grow the entry buffer, and stop the probe contradicting itself Six GitHub Copilot reviews since the last pass. Their live findings, and the first is a real panic. BUFFER: `GetEnvironmentVariableW` reports units WRITTEN on success and capacity REQUIRED on an undersized buffer. `drive_entry` treated the two alike against a fixed 1024-unit buffer, so a legitimate per-drive entry longer than that sliced out of range and panicked -- before the test could restore the process state it had borrowed. It now grows and retries. Reproduced by sabotage: `range end index 1204 out of range for slice of length 256`. A new test plants a 1200-unit entry and asserts it round-trips, so the growth path is exercised rather than asserted. Restoration also went through `String::from_utf16_lossy`, which would replace an unpaired surrogate and write back something the process did not start with. It now carries WTF-16 units end to end. THE PROBE CONTRADICTED ITSELF IN ITS OWN OUTPUT. One paragraph said the build-minus-clone gap "is not the call's own cost, because it also spans this crate's allocations and the builder chain"; five lines later the same report called that saving "the Win32 resolution, not an allocation". Both were emitted every run. The report now says the saving is the RESOLUTION STEP, allocations included, throughout -- and the module doc, which still carried the old attribution, with it. IT ALSO CLAIMED WORK IT DOES NOT DO. The report described `prepare` as resolving against the working directory, but both timed samples are fully qualified, so no rooting happens in the measured loop. It now says the rooting is the motivation for resolving at submission and is not what the numbers contain. "THAT DRIVE'S OWN CURRENT DIRECTORY" survived in four documents after D-18 established the entry is used verbatim and may name a directory on another drive. All now say the entry recorded for that drive. Also: D-18 still said observation "cannot establish" the broader claim, which Tier 2 records as wrong -- it does not need to establish it, the drive-relative form disproves it. The archive rationale overstated the write-back as happening on every resolution, when an accepted entry is left alone and the current-drive form touches nothing. And `COM^1` is now written as a Rust escape, since a reader copying a caret gets a filename, not a device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 3 +- DESIGN-NOTES.md | 4 +- .../COMPLETED-CHECKLIST.md | 12 ++- .../DESIGN-NOTES.md | 10 +- .../DESIGN-RATIONALE.md | 4 +- .../src/full_path.rs | 5 +- .../src/full_path/tests.rs | 92 +++++++++++++++---- .../src/bin/request_cost.rs | 22 +++-- .../src/request_cost.rs | 26 ++++-- 9 files changed, 127 insertions(+), 51 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 5e6dfcf41..418c81008 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -419,7 +419,8 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the - [x] **M26.5** -- The `GetFullPathNameW` entry. Does not verify its result: it collapses `.`/`..` lexically and roots most paths that are not fully qualified against process state -- the current - directory, or for a drive-relative path that drive's own -- and never expands a drive letter, so it + directory, or for a drive-relative path the entry recorded for that drive -- and never expands a + drive letter, so it does **not** close the session-relative hazard from M20.1, and its documentation must say which problem it solves and which it leaves standing. diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index b99d45b9b..014c3aad3 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1463,8 +1463,8 @@ Two corollaries that decide the design: still carries a session-relative reference. (It is not *lexical* as a whole either, which matters elsewhere but not here: it collapses `.`/`..` lexically but roots most paths that are not fully qualified against process state -- the - current directory, or for a drive-relative path that drive's own current - directory, which moves independently of it -- and that rooting is the property + current directory, or for a drive-relative path the entry recorded for that + drive, which moves independently of it -- and that rooting is the property submission-time resolution buys. See `windows-namespace-request-sys`' [DESIGN-NOTES.md](crates/windows-namespace-request-sys/DESIGN-NOTES.md) -> diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md index 58708d427..792c9a58c 100644 --- a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -17,10 +17,14 @@ Append-only. Newest groups at the bottom. something to take in passing. Measurement dissolved the first route's objection within the hour. - `GetFullPathNameW` **itself** writes the `=X:` entry on every drive-relative - resolution, creating it when absent. The code under test already mutates that - state, so a test that sets it first introduces no hazard that resolving alone - did not, and there was nothing left to decide. + `GetFullPathNameW` **itself** writes the `=X:` entry -- when resolving for a + drive other than the current one, and when the recorded entry is absent or + rejected, in which case it is written as the drive root. (An accepted entry is + left alone, and the current-drive form touches nothing.) The code under test + therefore already mutates that state on the very path these tests exercise, so + a test that sets it first introduces no hazard that resolving alone did not, + and there was nothing left to decide. Isolation across the tests comes from + their disjoint drive letters. `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one` in [tests.rs](src/full_path/tests.rs) now pins all three behaviours: an entry diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index cca96bdb1..d5090c68d 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -547,12 +547,18 @@ process state. Three forms read three different pieces of it: a relative path takes the current directory; a root-relative path like `\foo` takes only that directory's *root*, which is `\\server\share\` when the current directory is a UNC path and so is not a drive at all; and a drive-relative path such as -`C:foo` takes that drive's own current directory. That rule has two arms: +`C:foo` takes the entry recorded for that drive -- usually that drive's own +current directory, though the entry is used verbatim and an accepted one may +name a directory on another drive entirely. That rule has two arms: for a drive other than the current one Windows reads the hidden `=C:` entry recorded for it, which moves independently of the process current directory; for the *current* drive the entry is ignored and the process current directory wins. Measured -- setting `=Q:` while the process is on `Q:` changes nothing. So the call is not lexical *as a whole*, and the claim that -holds unqualified is that it **does not verify what it produces** -- the documented guarantee, which is narrower than the "touches no filesystem" an earlier draft claimed and which observation cannot establish. +holds unqualified is that it **does not verify what it produces** -- the +documented guarantee, and narrower than the "touches no filesystem" an earlier +draft claimed. A black-box success cannot establish that broader claim, and it +did not need to: the drive-relative form *disproves* it outright, as the +measurement below records. **"Most" rather than "every", because a legacy device name short-circuits the rooting.** `CON` resolves to `\\.\CON` and is not rooted, so it is an diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 68b5968cc..3f9ad4cf3 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -106,13 +106,13 @@ were *enumerations* -- which is the form this kind of error likes. two, and finally three: a relative path takes the current directory, a root-relative path takes only that directory's *root* (which is `\\server\share\` under a UNC current directory, so "current drive" was - wrong), and a drive-relative path takes that drive's own current directory + wrong), and a drive-relative path takes the entry recorded for that drive from `=C:`. - **The device set.** The short-circuit was first described as "exact-match only", which `CON:` disproves; then enumerated as `CON`/`NUL`/`PRN`/`AUX`/ `COM1`-`9`/`LPT1`-`9`/`CONIN$`/`CONOUT$`, which omits the superscript - spellings `COM^1`, `COM^2`, `COM^3` (U+00B9, U+00B2, U+00B3) and their `LPT` + spellings `COM\u{00b9}`, `COM\u{00b2}`, `COM\u{00b3}` and their `LPT` equivalents. Those are exactly the members a hand-written denylist misses, and the documentation asserted a closed list without them until a review measured it. The tests in [tests.rs](src/full_path/tests.rs) now pin every documented spelling so the diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 0a8b7f9b3..e52430826 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -62,7 +62,10 @@ //! **Do not build a name filter from the list below.** The accepted names are //! `CON`, `NUL`, `PRN`, `AUX`, `CONIN$`, `CONOUT$`, and `COM`/`LPT` //! followed by a single digit -- where "digit" includes the *superscripts* -//! `COM^1`, `COM^2` and `COM^3` (U+00B9, U+00B2, U+00B3) as well as `1`-`9`. +//! `COM\u{00b9}`, `COM\u{00b2}` and `COM\u{00b3}` as well as `1`-`9`. Those are +//! written as Rust escapes deliberately: spelled `COM^1` with a caret, as an +//! earlier revision had them, a reader copying the text gets an ordinary +//! filename rather than a device. //! An exhaustive scan of the character after `COM` accepts exactly //! U+0031-U+0039, U+00B2, U+00B3 and U+00B9 on the tested build; `COM0` and //! `COM10` are not devices. The superscripts are precisely the sort of member a diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index f0ea56267..3cfaa6b4e 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -555,28 +555,53 @@ fn probe_drive(preferred: char, fallback: char) -> char { /// /// Through Win32 rather than `std::env`, which rejects a key containing `=` /// outright and so cannot address these at all. -fn drive_entry(drive: char) -> Option { +fn drive_entry(drive: char) -> Option { let name = Wtf16String::from(format!("={drive}:").as_str()); - let mut buffer = vec![0u16; 1024]; - // SAFETY: `name` is NUL-terminated, and `buffer` is writable for the length - // passed. - let written = unsafe { - windows_sys::Win32::System::Environment::GetEnvironmentVariableW( - name.as_terminated_ptr(), - buffer.as_mut_ptr(), - u32::try_from(buffer.len()).unwrap_or(u32::MAX), - ) - }; - (written != 0).then(|| String::from_utf16_lossy(&buffer[..written as usize])) + // Start small and grow to whatever Windows asks for. The API's two return + // conventions differ: on success it reports the units written EXCLUDING the + // terminator, and on an undersized buffer it reports the capacity REQUIRED + // INCLUDING it. Treating the second as the first indexes past the buffer and + // panics -- while trying to preserve a legitimate long entry, so the failure + // would land before the test could restore the process state it borrowed. + let mut buffer = vec![0u16; 256]; + loop { + // SAFETY: the name is NUL-terminated and the buffer is writable for + // the length passed. + let written = unsafe { + windows_sys::Win32::System::Environment::GetEnvironmentVariableW( + name.as_terminated_ptr(), + buffer.as_mut_ptr(), + u32::try_from(buffer.len()).unwrap_or(u32::MAX), + ) + }; + let written = written as usize; + if written == 0 { + return None; + } + if written < buffer.len() { + // Kept as WTF-16 units rather than going through String: a lossy + // conversion would replace an unpaired surrogate, so restoring the + // entry afterwards would write back something the process did not + // start with. + return Some(Wtf16String::from_units(&buffer[..written])); + } + buffer = vec![0u16; written]; + } } - /// Sets or clears one of the hidden `=X:` entries. fn set_drive_entry(drive: char, value: Option<&str>) { + set_drive_entry_units(drive, value.map(Wtf16String::from).as_ref()); +} + +/// [`set_drive_entry`], taking the exact units a [`drive_entry`] read returned. +/// +/// Restoration goes through this rather than through `&str`, so an entry +/// containing an unpaired surrogate is put back byte for byte. +fn set_drive_entry_units(drive: char, value: Option<&Wtf16String>) { let name = Wtf16String::from(format!("={drive}:").as_str()); - let value = value.map(Wtf16String::from); let value_ptr = value .as_ref() - .map_or(core::ptr::null(), Wtf16String::as_terminated_ptr); + .map_or(core::ptr::null(), |v| v.as_terminated_ptr()); // SAFETY: both pointers are NUL-terminated; a null value clears the entry. let ok = unsafe { windows_sys::Win32::System::Environment::SetEnvironmentVariableW( @@ -630,7 +655,7 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( "an entry that names nothing is rejected in favour of the drive root" ); assert_eq!( - drive_entry(drive).as_deref(), + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), Some(format!(r"{drive}:\").as_str()), "and the call REWROTE the entry: this is a query that mutates the \ process environment block" @@ -641,12 +666,12 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( assert_eq!(drive_entry(drive), None, "precondition: entry cleared"); let _ = resolve(&format!("{drive}:foo")); assert_eq!( - drive_entry(drive).as_deref(), + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), Some(format!(r"{drive}:\").as_str()), "resolving created the entry on a host that had none" ); - set_drive_entry(drive, restore.as_deref()); + set_drive_entry_units(drive, restore.as_ref()); if probe_dir.created { let _ = std::fs::remove_dir(&probe_dir.path); } @@ -693,14 +718,41 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { "{spelling:?} names an existing directory but is rejected on shape" ); assert_eq!( - drive_entry(drive).as_deref(), + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), Some(format!(r"{drive}:\").as_str()), "and the rejected entry is written back as the drive root" ); } - set_drive_entry(drive, restore.as_deref()); + set_drive_entry_units(drive, restore.as_ref()); if probe_dir.created { let _ = std::fs::remove_dir(&probe_dir.path); } } + +#[test] +fn a_long_drive_entry_round_trips_through_the_reader() { + // The reader grows its buffer, and this is what proves it. Windows reports + // an undersized buffer by returning the REQUIRED capacity rather than the + // units written, so a reader that treats the two alike slices past its own + // buffer and panics -- while preserving a legitimate entry, which is the + // worst moment for it, because the entry is then never restored. + // + // 1200 units is comfortably past the 256 the reader starts with and past + // the 1024 an earlier fixed-size version used, and is a legitimate value: a + // per-drive entry is a path, and long paths reach far beyond this. + let drive = probe_drive('R', 'S'); + let restore = drive_entry(drive); + + let long = format!(r"C:\{}", "a".repeat(1200)); + set_drive_entry(drive, Some(&long)); + + let read_back = drive_entry(drive).expect("the entry was just set"); + assert_eq!( + read_back.to_string_lossy(), + long, + "a long entry survives the read, so the buffer grew instead of truncating" + ); + + set_drive_entry_units(drive, restore.as_ref()); +} diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index a9f94b6ed..54bd6995a 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -301,32 +301,34 @@ fn render(out: &mut dyn std::fmt::Write) { // two successive attempts to name one were each wrong in the same way. let _ = writeln!( out, - " `prepare` calls GetFullPathNameW to resolve the path against the" + " `prepare` calls GetFullPathNameW, which resolves a path that is not fully" ); let _ = writeln!( out, - " process working directory, because the CWD is mutable by any thread" + " qualified against process state -- the CWD is mutable by any thread, so" ); let _ = writeln!( out, - " and resolving later would be racy. That reads process state and" + " resolving later would be racy. BOTH SAMPLES HERE ARE FULLY QUALIFIED, so" ); let _ = writeln!( out, - " verifies nothing it produces. The gap between building and cloning" + " that rooting is the motivation for resolving at submission and is not" ); + let _ = writeln!(out, " what these numbers measure."); let _ = writeln!( out, - " bounds that resolution from above; it is not the call's own cost," + " The gap between building and cloning bounds the resolution step from" ); let _ = writeln!( out, - " because it also spans this crate's allocations and the builder chain." + " above. It is not the call's own cost: it also spans this crate's two" ); let _ = writeln!( out, - " Whether any of it enters the kernel is not something this run measured." + " allocations and the builder chain. Whether any of it enters the kernel is" ); + let _ = writeln!(out, " not something this run measured."); let _ = writeln!( out, " Two different schemes recover two different things, and this said" @@ -337,16 +339,16 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " of {build:.0} ns, so it recovers {:.0} ns -- but that saving is the Win32", + " of {build:.0} ns, so it recovers {:.0} ns -- but that saving is the", build - clone ); let _ = writeln!( out, - " resolution, not an allocation, and only a caller that can reuse a" + " RESOLUTION STEP, allocations included, and only a caller that can" ); let _ = writeln!( out, - " resolved path gets it. INLINE STORAGE removes the allocation and" + " reuse a resolved path gets it. INLINE STORAGE removes the allocation and" ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 5bcfd92f7..2c1369342 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -63,17 +63,25 @@ //! path is resolved at submission -- the process CWD is mutable by any thread, //! so even perfect remoting would be racy. //! -//! That work reads **process state**: it resolves against the current -//! directory, and for a drive-relative path against the per-drive current -//! directory held in the `=C:` environment variables. So the measured remainder -//! is path resolution, not allocation -- and naming a *mechanism* for it has -//! now been got wrong twice. Calling it a *syscall cost* claimed a kernel -//! transition a timing loop cannot establish; calling it *lexical*, which -//! replaced it, claimed pure string work it equally is not. A genuinely lexical -//! canonicalizer is a different call (`PathCchCanonicalizeEx`) and is +//! That call reads **process state** when it has to root a path -- the current +//! directory, or for a drive-relative path the entry recorded for that drive in +//! the `=C:` environment variables. **Neither sample here is rooted**: both are +//! fully qualified, so the rooting is why resolution happens at submission and +//! is not what these timings contain. +//! +//! So the measured remainder is the **resolution step**, which is an upper +//! bound on the call and not the call itself: it also spans this crate's two +//! allocations and the builder chain. Saying the remainder *is* the resolution, +//! as an earlier revision did, hands the call credit for the allocator work the +//! same sentence sets out to exclude. +//! +//! Naming a *mechanism* for it has now been got wrong repeatedly. A *syscall +//! cost* claimed a kernel transition a timing loop cannot establish; *lexical*, +//! which replaced it, claimed pure string work it equally is not. A genuinely +//! lexical canonicalizer is a different call (`PathCchCanonicalizeEx`) and is //! deliberately not the one wanted here, because resolving against the CWD at //! submission is the property being bought. What survives either way is the -//! part that matters: an allocator cannot remove it. +//! part that matters: an allocator cannot remove all of it. //! //! The owning crate now settles both halves rather than leaving them to be //! re-derived from a probe: see `windows-namespace-request-sys`' From fef59ae0c74ed98c0cdea616301a156ae38be76f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 11:00:32 -0400 Subject: [PATCH 21/36] fix(probes): the gap spans one NET allocation, not two Twelfth review round, one finding, and it is arithmetic rather than prose. `092bda8` sharpened "this crate's allocations" into "this crate's TWO allocations" in the module doc and the emitted report. The quantity described is `build_open_request - clone_prepared_units`. `prepare` allocates twice -- an input copy and a MAX_PATH output buffer -- and the clone allocates once, so the subtraction CANCELS one. What survives it is one net allocation, not two. The magnitude matters in the one sentence whose whole job is to bound what the number contains: the clone measures ~42 ns against a ~158 ns gap, so "two allocations" charges the gap with roughly twice the allocator work it holds. The PR states it correctly elsewhere -- full_path.rs says "two allocations against the clone's one" -- and it is right there precisely because it names the difference. Dropping the second half inverted it. The over-count also broke the paragraph that follows. With "two allocations" in the gap and "allocations included" on the recycling line, recycling and inline storage were credited with overlapping work while the same paragraph insisted they recover different things. Recycling recovers the resolution plus that one net allocation; inline storage recovers the clone's allocation and copy; only recycling reaches the resolution. That is the distinction worth keeping, and it is now what the report says. Same failure as the round it corrects: making a claim more precise without re-deriving it, so the sharpening carried an error the vaguer wording did not. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/bin/request_cost.rs | 17 ++++++++++++----- .../windows-platform-probes/src/request_cost.rs | 9 +++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index 54bd6995a..20d2555fd 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -322,16 +322,23 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " above. It is not the call's own cost: it also spans this crate's two" + " above. It is not the call's own cost: it also spans ONE NET allocation" ); let _ = writeln!( out, - " allocations and the builder chain. Whether any of it enters the kernel is" + " of this crate's own -- prepare allocates twice against the clone's once," ); - let _ = writeln!(out, " not something this run measured."); let _ = writeln!( out, - " Two different schemes recover two different things, and this said" + " so the subtraction cancels one -- and the builder chain." + ); + let _ = writeln!( + out, + " Whether any of it enters the kernel is not something this run measured." + ); + let _ = writeln!( + out, + " Two different schemes recover different things, and this said" ); let _ = writeln!( out, @@ -344,7 +351,7 @@ fn render(out: &mut dyn std::fmt::Write) { ); let _ = writeln!( out, - " RESOLUTION STEP, allocations included, and only a caller that can" + " RESOLUTION STEP plus that net allocation, and only a caller that can" ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 2c1369342..8454773d5 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -70,8 +70,13 @@ //! is not what these timings contain. //! //! So the measured remainder is the **resolution step**, which is an upper -//! bound on the call and not the call itself: it also spans this crate's two -//! allocations and the builder chain. Saying the remainder *is* the resolution, +//! bound on the call and not the call itself: it also spans one net allocation +//! of this crate's own and the builder chain. **Net**, because the subtraction +//! cancels one -- `prepare` allocates twice, an input copy and a `MAX_PATH` +//! output buffer, against the clone's one, so what survives the subtraction is +//! the difference and not both. Calling it "two allocations", as a draft did, +//! charges the gap with allocator work the subtraction has already removed. +//! Saying the remainder *is* the resolution, //! as an earlier revision did, hands the call credit for the allocator work the //! same sentence sets out to exclude. //! From 83954cad78d9c285a7ce67c82626a9d877b9a1cf Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 11:47:21 -0400 Subject: [PATCH 22/36] docs(probes): let the design-session link render Thirteenth review round found nothing substantive in the branch. This is the one thing it flagged and deliberately did NOT report as a defect, because it is pre-existing on main: a link-reference definition abutting the paragraph above it, which CommonMark folds into that paragraph. The published rustdoc therefore showed the raw `[the namespace session]: ../../..` line as body text, and the reference above it was not a link at all. Fixed here rather than queued because it sits inside the module doc this PR rewrites, and the fix is one blank line. Worth recording how nearly it was missed: my first check searched an old `target/doc` and reported zero literal occurrences, which looked like the reviewer being wrong. Deleting the directory and rebuilding showed the opposite -- literal text present, no `` element emitted. Verified after the fix by the same route: literal gone, link rendered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/src/request_cost.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 8454773d5..ea03e175b 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -105,6 +105,12 @@ //! place of the whole build, so it recovers the difference between them. Naming //! one figure for both -- as this did -- credits an allocator with work it //! cannot remove. Knowing which half is which is the point of measuring both. +//! +//! A link-reference definition has to be its own block: abutting the paragraph +//! above, CommonMark folds it in, so rustdoc rendered this line as literal text +//! and left the reference to it unlinked. Pre-existing, and fixed here because +//! this module doc is being rewritten around it. +//! //! [the namespace session]: ../../../design-sessions/DESIGN-SESSION-2026-08-27-pseudo-async-namespace-operations.md //! //! Each timing is reported per operation. Absolute values are host-specific; From 13882df5fe12cb63c386ad5e050474c0fc4813f6 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 12:00:23 -0400 Subject: [PATCH 23/36] test(namespace-request): pick the probe drive against the probe directory, and clean up on unwind Three more GitHub Copilot reviews. Five live findings; a sixth was already fixed by fef59ae, which the 14:44 review predates. THE CROSS-DRIVE PROPERTY COULD GO UNEXERCISED. `probe_drive` excluded only the process current drive, but the probe directory comes from %TEMP%, which need not be on the same drive. With temp on W:, the entry and the directory it names land on the SAME drive: the entry is still honoured, the test still passes, and "honoured verbatim ACROSS drives" -- the property the test exists to pin -- is never demonstrated. A silent loss of coverage, which is worse than a failure. The directory is now created first and the letter chosen to avoid its drive as well as the current one, with an assertion that the two differ. Verified by reverting to the non-avoiding selection with TMP/TEMP=W:\ and a subst'd W:, where the guard fires as designed. THE REJECTED-ENTRY PATH WAS HARD-CODED. `C:\no-such-directory-for-this-test` is only missing until some host has it, and then the test asserts the rejected case against an accepted one. It now derives a child of the directory this test just created and asserts the precondition. CLEANUP DID NOT RUN ON UNWIND. `ProbeDir` recorded whether it created a directory but had no `Drop`, so a failing assertion left it behind. My note defended that as a deliberate trade -- a guard writing during unwinding can panic and abort -- which is true of the `=X:` restore beside it and NOT of removing a directory. This crate's own `Fixture` in handle/tests.rs already uses `Drop` for exactly this. Conflating the two cost the cleanup. Two overstatements, both mine: - D-18 said three rooting forms "read three different pieces" of process state. Two of them, and the current-drive case of the third, derive from the same process current directory. Three uses, not three sources. - A test comment still said the call writes the entry "on every drive-relative resolution". It writes when the entry is absent or rejected; an accepted entry is left alone and the current-drive form touches nothing. The isolation argument now rests where it actually holds: the disjoint drive letters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 4 +- .../src/full_path/tests.rs | 133 +++++++++++++----- 2 files changed, 102 insertions(+), 35 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index d5090c68d..bfbddd342 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -543,7 +543,9 @@ the call is better described as doing two separable things. It collapses `.`/`..` and normalizes separators, which *is* lexical -- `C:\a\..\b` becomes `C:\b` whatever the current directory is, and whether or not `C:\a` exists. It *also* **roots** most paths that are not fully qualified, and that reads mutable -process state. Three forms read three different pieces of it: a relative path +process state. Three forms use it three different ways -- though not from three +different sources, since the first two both derive from the process current +directory and so does the current-drive case of the third: a relative path takes the current directory; a root-relative path like `\foo` takes only that directory's *root*, which is `\\server\share\` when the current directory is a UNC path and so is not a drive at all; and a drive-relative path such as diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 3cfaa6b4e..5a688e9fd 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -374,11 +374,11 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // on `Q:` changes nothing. // // **This test does not mutate `=X:`, but the call it exercises may.** - // Measured: resolving `X:foo` for a non-current drive validates that - // drive's entry against the filesystem and WRITES it to `X:\` when it does - // not name an existing directory -- creating it when absent, so merely - // running this test changes the process environment on ANY host, not just - // one carrying a stale entry. That is + // Measured: resolving `X:foo` for a non-current drive checks that drive's + // entry and WRITES it to `X:\` when the entry is absent or rejected. An + // accepted entry is left alone, and the current-drive form touches nothing + // -- so this is not "every resolution", but it does mean an ordinary host + // with no entry has one written merely by running this test. That is // a property of the call, documented in the module doc; it is noted here so // the next reader does not take "reads process state" at face value, as // four revisions of that doc did. @@ -490,12 +490,44 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { /// So the temp directory is used only when it is drive-rooted, and otherwise /// the fallback is `%SystemRoot%`, which is guaranteed to exist, to be /// canonical, and not to be a drive root. Nothing is created in the fallback -/// case, so [`ProbeDir::created`] tells the caller whether to clean up. +/// case, so `created` records whether there is anything to remove. +/// +/// Removal is a [`Drop`], matching this crate's own `Fixture` in +/// [`crate::handle`]'s tests. An earlier version cleaned up with a statement at +/// the end of each test and argued that a guard writing during unwinding could +/// panic and abort -- which is true of the `=X:` entry restore beside it, and +/// not of removing a directory. Conflating the two left a directory behind +/// after every failing assertion. struct ProbeDir { path: std::path::PathBuf, created: bool, } +impl ProbeDir { + /// The drive letter this probe lives on, if it has one. + /// + /// The caller needs it to pick a probe drive that is *not* this one: + /// resolving `W:foo` against an entry naming a directory that is itself on + /// `W:` cannot demonstrate that an accepted entry is used verbatim across + /// drives, which is the property being pinned. + fn drive(&self) -> Option { + self.path + .as_os_str() + .to_string_lossy() + .chars() + .next() + .filter(char::is_ascii_alphabetic) + } +} + +impl Drop for ProbeDir { + fn drop(&mut self) { + if self.created { + let _ = std::fs::remove_dir(&self.path); + } + } +} + fn probe_directory(tag: &str) -> ProbeDir { let drive_rooted = |p: &std::path::Path| { let s = p.as_os_str().to_string_lossy().into_owned(); @@ -543,11 +575,30 @@ fn probe_directory(tag: &str) -> ProbeDir { /// Each caller passes a disjoint pair, so two tests can never land on the same /// letter and race under libtest's thread-per-test model. fn probe_drive(preferred: char, fallback: char) -> char { + probe_drive_avoiding(preferred, fallback, None) +} + +/// [`probe_drive`], also avoiding the drive some other directory sits on. +/// +/// **Excluding only the current drive is not enough for a test that asserts an +/// entry is honoured *across* drives.** `%TEMP%` need not be on the same drive +/// as the process, so a host with temp on `W:` would have the probe directory +/// and the probe drive coincide: the entry would still be honoured, the test +/// would still pass, and the cross-drive property it exists to pin would go +/// unexercised. That is a silent loss of coverage rather than a failure, which +/// is the worse of the two. +fn probe_drive_avoiding(preferred: char, fallback: char, avoid: Option) -> char { let cwd = current_directory(); - match cwd.chars().next().filter(char::is_ascii_alphabetic) { - Some(current) if current.eq_ignore_ascii_case(&preferred) => fallback, - // A UNC current directory has no drive letter, so nothing collides. - _ => preferred, + // A UNC current directory has no drive letter, so nothing collides there. + let current = cwd.chars().next().filter(char::is_ascii_alphabetic); + let taken = |c: char| { + current.is_some_and(|d| d.eq_ignore_ascii_case(&c)) + || avoid.is_some_and(|d| d.eq_ignore_ascii_case(&c)) + }; + if taken(preferred) { + fallback + } else { + preferred } } @@ -619,23 +670,29 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // assertion there, because a host with no `=X:` entry cannot tell the two // rules apart. // - // **Controlling it is not the hazard it looks like**, and that is what - // unblocked this test. The objection was that `=X:` is process-global while - // these tests share a process -- but `GetFullPathNameW` *itself* writes the - // entry on every drive-relative resolution, creating it when absent. The - // code under test already mutates this state, so a test that sets it first - // introduces no hazard that resolving alone did not. + // **Controlling the entry is not the hazard it looks like**, and that is + // what unblocked this test. The objection was that `=X:` is process-global + // while these tests share a process. But the call under test writes that + // entry itself whenever it is absent or rejected, so this state is already + // mutated by the code being exercised. What keeps the tests from + // interfering is not that -- it is that each takes a drive letter no other + // one can choose. // - // `W` (or `U` when the suite runs from `W:`) keeps this clear of the - // sibling tests' `X`/`Y` and `V`/`T`, so none of them can race. - let drive = probe_drive('W', 'U'); - let restore = drive_entry(drive); - - // A real directory that is certainly NOT on the probe drive, which is what - // makes it the right probe: if the entry is honoured verbatim, a - // drive-relative path resolves onto another drive entirely. + // The probe directory is created FIRST so the letter can avoid its drive as + // well as the current one: an entry naming a directory on the same drive it + // is recorded for would be honoured, the test would pass, and the + // cross-drive property below would go unexercised. let probe_dir = probe_directory("verbatim"); let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let drive = probe_drive_avoiding('W', 'U', probe_dir.drive()); + let restore = drive_entry(drive); + + assert_ne!( + Some(drive.to_ascii_uppercase()), + probe_dir.drive().map(|d| d.to_ascii_uppercase()), + "the probe directory must be on a different drive, or the assertion \ + below cannot show the entry is honoured ACROSS drives" + ); set_drive_entry(drive, Some(probe)); assert_eq!( @@ -648,7 +705,22 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // An entry that does not name an existing directory is rejected, and the // call rewrites it to the drive root rather than leaving it stale. - set_drive_entry(drive, Some(r"C:\no-such-directory-for-this-test")); + // + // Derived from the probe directory rather than hard-coded: a literal like + // `C:\no-such-directory-for-this-test` is only missing until some host + // happens to have it, and the test would then assert the rejected case + // against an accepted one. A child of a directory this test just created + // cannot exist unless something else creates it in between. + let missing = probe_dir.path.join("no-such-child"); + assert!( + !missing.exists(), + "precondition: the rejected entry must name nothing: {}", + missing.display() + ); + set_drive_entry( + drive, + Some(missing.to_str().expect("the probe path is UTF-8")), + ); assert_eq!( resolve(&format!("{drive}:foo")), format!(r"{drive}:\foo"), @@ -672,9 +744,6 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( ); set_drive_entry_units(drive, restore.as_ref()); - if probe_dir.created { - let _ = std::fs::remove_dir(&probe_dir.path); - } } #[test] @@ -685,11 +754,10 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { // existing directory, so anything rejected here is rejected on shape alone. // // Pinned because the distinction is not guessable and the doc asserts it. - let drive = probe_drive('V', 'T'); - let restore = drive_entry(drive); - let probe_dir = probe_directory("shape"); let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let drive = probe_drive_avoiding('V', 'T', probe_dir.drive()); + let restore = drive_entry(drive); // The control: this exact directory IS accepted in canonical form, so the // rejections below cannot be blamed on the directory itself. @@ -725,9 +793,6 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { } set_drive_entry_units(drive, restore.as_ref()); - if probe_dir.created { - let _ = std::fs::remove_dir(&probe_dir.path); - } } #[test] From df5ffd2b89ecad306e120ca6714b294995f7ef87 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 12:25:47 -0400 Subject: [PATCH 24/36] test(namespace-request): derive the nonexistent path, and correct four records One more Copilot review, against 83954ca. Six findings; all still live. THE EXISTENCE TEST COULD DEMONSTRATE THE OPPOSITE. It resolved a hard-coded `C:\no-such-directory\..\nothing-here.txt`, which is only missing until some host has it -- and then the test would be showing that an EXISTING path resolves, which every other test here already covers. It now derives a process-specific path under %TEMP% and asserts the absence first, the same shape the drive-entry test already uses. FOUR RECORDS CORRECTED: - The archive said the verbatim test "uses drive W". It draws from the disjoint W/U pair and takes U when W is the current or probe drive. - Two completion stamps did not match the repository format: one carried only a date, the other spelled the offset without `UTC`. - DESIGN-RATIONALE said the mistake "recurred five times" directly above a list enumerating nine drafts as instances of it. ONE FINDING DELIBERATELY NOT ACTED ON, with the measurement that decides it. The review asked for `drive_entry` to distinguish an absent entry from an empty one by clearing the last error and returning None only for ERROR_ENVVAR_NOT_FOUND, since cleanup would otherwise delete an originally empty entry rather than restore it. Measured: the two states are not distinguishable because the second does not exist. `SetEnvironmentVariableW(name, "")` reports success and a subsequent read returns zero with an empty buffer -- byte for byte what a name that was never set returns. Setting empty IS deletion. So restoring "absent" cannot lose an empty value, and an error check keyed on ERROR_ENVVAR_NOT_FOUND would be guarding a state Windows will not produce. The reasoning is now recorded at the return site so the next reader does not re-derive it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 6 +++-- .../DESIGN-RATIONALE.md | 2 +- .../src/full_path/tests.rs | 23 ++++++++++++++++--- .../COMPLETED-CHECKLIST.md | 2 +- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md index 792c9a58c..42ea28e16 100644 --- a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -4,7 +4,7 @@ Append-only. Newest groups at the bottom. ## Moved 2026-09-10 -- NR-1: the per-drive current-directory arm, pinned -### NR-1.1 -- Pin the `=X:` arm of drive-relative rooting. *(completed 2026-09-10)* +### NR-1.1 -- Pin the `=X:` arm of drive-relative rooting. *(completed 2026-09-10 12:24:46 UTC-04:00)* - [x] **NR-1.1** -- Decide how this crate's tests may control process-global state, then pin the `=X:` arm of drive-relative rooting. @@ -32,7 +32,9 @@ Append-only. Newest groups at the bottom. which is what makes "that drive's own current directory" a convention rather than a guarantee); an entry naming nothing is rejected in favour of the drive root; and the call writes the entry back, creating it on a host that had none. - It uses drive `W` so it cannot race the sibling test's `X`/`Y` under libtest's + It draws from the disjoint `W`/`U` pair -- taking `U` when the current drive or +the probe directory's drive is `W` -- so it cannot race the sibling tests' +`X`/`Y`, `V`/`T` or `R`/`S` under libtest's thread-per-test model. The sibling test diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 3f9ad4cf3..7c7e67145 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -11,7 +11,7 @@ Cross-referenced by decision ID. The decision is in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> `D-18`. What follows is the record of getting there, which is unusually worth keeping because the same -mistake recurred five times in five different wordings. +mistake recurred in nine successive wordings, enumerated below. ### The shape of the error, which never changed diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 5a688e9fd..5dbabe682 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -48,10 +48,20 @@ fn a_path_that_does_not_exist_resolves_perfectly_happily() { // Resolution does not verify the result: no error, and no check that any // component exists. A consumer wanting a verified path wants an open plus // GetFinalPathNameByHandleW. - assert_eq!( - resolve(r"C:\no-such-directory\..\nothing-here.txt"), - r"C:\nothing-here.txt" + // + // The components are process-specific and their absence is asserted first. + // A hard-coded literal is only missing until some host happens to have it, + // and this test would then be demonstrating that an EXISTING path resolves + // -- which every other test here already covers. + let missing = std::env::temp_dir().join(format!("wnrs-{}-absent", std::process::id())); + let missing = missing.to_str().expect("the temp path is UTF-8"); + assert!( + !std::path::Path::new(missing).exists(), + "precondition: {missing} must not exist" ); + + let doubled = format!(r"{missing}\also-absent\..\leaf.txt"); + assert_eq!(resolve(&doubled), format!(r"{missing}\leaf.txt")); } #[test] @@ -627,6 +637,13 @@ fn drive_entry(drive: char) -> Option { }; let written = written as usize; if written == 0 { + // Zero means absent. It is also what an *empty* value would report, + // and this deliberately does not try to tell the two apart -- + // measured, they are the same state: `SetEnvironmentVariableW(name, + // "")` returns success and a subsequent read reports zero with an + // empty buffer, exactly as for a name that was never set. Windows + // has no environment variable with an empty value for this API to + // return, so restoring "absent" cannot lose one. return None; } if written < buffer.len() { diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 6835b8751..48c3f06d6 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -96,7 +96,7 @@ piece of work rather than a correction to that one. ## Moved 2026-09-09 22:54:01 -04:00 -- M2.6: what `GetFullPathNameW` does, and whether it stays -### M2.6 -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. *(completed 2026-09-09 22:54:01 -04:00)* +### M2.6 -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. *(completed 2026-09-09 22:54:01 UTC-04:00)* **Resolved.** The correction and the decision both landed in the owning crate as `D-18` in [../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md): From 5903b4cd101d6b836aecd7a474280abbd771adfb Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 12:58:04 -0400 Subject: [PATCH 25/36] test(namespace-request): validate every probe-drive candidate, not just the first A broad local review, with no narrow focus, found what the focused rounds missed: `probe_drive_avoiding` tested only its PREFERRED letter and returned the fallback unchecked. Its own doc promised "certainly not the current drive" -- a guarantee it never enforced -- and both drive-entry tests relied on it. Reproduced with subst drives rather than argued. With the process on `U:` and %TEMP% on `W:`, the preferred `W` is excluded as the probe drive, the unchecked fallback `U` is returned, and `U` is the current drive: the test then asserts the OTHER-drive contract while exercising the CURRENT-drive arm, which is the one case where the entry is ignored. cwd=U:\ TEMP=W:\probe-temp -> left "U:\foo" right "W:\probe-temp\...\foo" cwd=W:\ TEMP=U:\probe-temp2 -> probe directory and probe drive both U That is the third distinct shape of the same defect in this helper, and the first where the code contradicted its own stated guarantee rather than merely being too narrow. `probe_drive_from` now searches a candidate list and rejects any letter matching the current drive or the avoided one, panicking if the list is exhausted. Three candidates against at most two exclusions, so one always survives; the assertion covers the part this function cannot check, which is the caller's list. The lists stay disjoint -- X/Y/P, W/U/N, V/T/M, R/S/K -- so no two tests can select the same letter. The sibling test's inline selection moved onto the same helper. Verified: both reproduced configurations now pass, and re-introducing the "return the second candidate unchecked" behaviour makes case 1 fail again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path/tests.rs | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 5dbabe682..d1df5ce9a 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -405,10 +405,7 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // The other-drive arm needs no drive letter from the current directory -- // under a UNC current directory every letter is "other" -- so it runs // unconditionally and this test never degenerates to a silent skip. - let other = match cwd_drive { - Some(d) if d.eq_ignore_ascii_case(&'X') => 'Y', - _ => 'X', - }; + let other = probe_drive_from(&['X', 'Y', 'P'], None); let resolved = resolve(&format!("{other}:foo")); // Compared case-insensitively, because the case is not this test's to @@ -572,32 +569,26 @@ fn probe_directory(tag: &str) -> ProbeDir { created: false, } } -/// A drive letter to probe with, which is certainly not the current drive. +/// A drive letter to probe with, drawn from `candidates` and guaranteed to be +/// neither the current drive nor `avoid`. /// -/// **The letter cannot be a constant, for the reason these tests exist.** An -/// entry is only consulted for a drive *other* than the current one -- on the -/// current drive it is ignored and the process directory wins -- so a test that -/// hard-codes `W` asserts something false when run from `W:`. Measured: with -/// `subst W:` and the suite launched from `W:\`, the verbatim test failed. -/// `W` and `V` are exactly the letters a mapped network drive or a `subst` -/// tends to take. +/// **Every candidate is checked, which an earlier version did not do.** It took +/// a preferred letter and a fallback, tested only the preferred one, and +/// returned the fallback unvalidated -- so when the preferred letter was +/// excluded the caller could still be handed the current drive. Measured: with +/// the process on `U:` and `%TEMP%` on a `subst`-ed `W:`, the verbatim test +/// selected `U` and then asserted the *other-drive* contract while exercising +/// the *current-drive* arm, which is the one case where the entry is ignored. +/// It failed, but the mode is worse than a failure: the helper's own doc +/// promised a guarantee it never enforced. /// -/// Each caller passes a disjoint pair, so two tests can never land on the same -/// letter and race under libtest's thread-per-test model. -fn probe_drive(preferred: char, fallback: char) -> char { - probe_drive_avoiding(preferred, fallback, None) -} - -/// [`probe_drive`], also avoiding the drive some other directory sits on. +/// Three candidates against at most two exclusions, so one always survives; the +/// assertion is there because that argument is about the caller's list and +/// nothing here can check it. /// -/// **Excluding only the current drive is not enough for a test that asserts an -/// entry is honoured *across* drives.** `%TEMP%` need not be on the same drive -/// as the process, so a host with temp on `W:` would have the probe directory -/// and the probe drive coincide: the entry would still be honoured, the test -/// would still pass, and the cross-drive property it exists to pin would go -/// unexercised. That is a silent loss of coverage rather than a failure, which -/// is the worse of the two. -fn probe_drive_avoiding(preferred: char, fallback: char, avoid: Option) -> char { +/// Callers pass disjoint lists, so no two tests can select the same letter and +/// race under libtest's thread-per-test model. +fn probe_drive_from(candidates: &[char], avoid: Option) -> char { let cwd = current_directory(); // A UNC current directory has no drive letter, so nothing collides there. let current = cwd.chars().next().filter(char::is_ascii_alphabetic); @@ -605,13 +596,14 @@ fn probe_drive_avoiding(preferred: char, fallback: char, avoid: Option) -> current.is_some_and(|d| d.eq_ignore_ascii_case(&c)) || avoid.is_some_and(|d| d.eq_ignore_ascii_case(&c)) }; - if taken(preferred) { - fallback - } else { - preferred - } -} + *candidates.iter().find(|&&c| !taken(c)).unwrap_or_else(|| { + panic!( + "every candidate of {candidates:?} is excluded by the current \ + drive ({current:?}) or the probe drive ({avoid:?})" + ) + }) +} /// Reads one of the hidden `=X:` per-drive current-directory entries. /// /// Through Win32 rather than `std::env`, which rejects a key containing `=` @@ -701,7 +693,7 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // cross-drive property below would go unexercised. let probe_dir = probe_directory("verbatim"); let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_avoiding('W', 'U', probe_dir.drive()); + let drive = probe_drive_from(&['W', 'U', 'N'], probe_dir.drive()); let restore = drive_entry(drive); assert_ne!( @@ -773,7 +765,7 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { // Pinned because the distinction is not guessable and the doc asserts it. let probe_dir = probe_directory("shape"); let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_avoiding('V', 'T', probe_dir.drive()); + let drive = probe_drive_from(&['V', 'T', 'M'], probe_dir.drive()); let restore = drive_entry(drive); // The control: this exact directory IS accepted in canonical form, so the @@ -823,7 +815,7 @@ fn a_long_drive_entry_round_trips_through_the_reader() { // 1200 units is comfortably past the 256 the reader starts with and past // the 1024 an earlier fixed-size version used, and is a legitimate value: a // per-drive entry is a path, and long paths reach far beyond this. - let drive = probe_drive('R', 'S'); + let drive = probe_drive_from(&['R', 'S', 'K'], None); let restore = drive_entry(drive); let long = format!(r"C:\{}", "a".repeat(1200)); From 8f921da8118b89ffbffdab8c9a48386003854235 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 13:34:40 -0400 Subject: [PATCH 26/36] test(namespace-request): restore borrowed drive entries on unwind, and pin the current-drive arm Copilot's review of the pushed branch raised eight points. Three were real and are fixed here; one was false and is answered with a measurement; and the sweep those findings prompted turned up two more that no reviewer had named. **Restoring on the last line of a test is not restoring.** Three tests saved a process-global `=X:` entry and put it back after their final assertion, so any failure in between abandoned it. What a sibling inherits is not merely stale: it is a path to a probe directory that no longer exists, or the 1200-unit value one test installs on purpose. `cargo test` runs tests as threads in ONE process, so that leak crosses tests. The comment on the long-entry reader already named the hazard -- "the entry is then never restored" -- without defending against it. `BorrowedDriveEntry` is the defence: it restores from `Drop`, through a non-asserting setter, because a panic while unwinding would abort the process and destroy the report of the failure that started it. The guard is pinned by a test that panics on purpose, since a passing suite never takes the path the guard exists for. Removing the restore makes it fail. **The current-drive arm was documented and unpinned, and the test that looked like it pinned it could not.** `full_path.rs` says the entry is neither consulted nor rewritten there; the assertion that appeared to show this resolved `X:foo` WITHOUT controlling the entry and compared against the process directory -- and Windows keeps the current drive's entry equal to that directory, so it read the same either way. The new test installs an entry the non-current arm would honour verbatim and shows the process directory wins anyway, then installs one the non-current arm would REWRITE and shows it is left untouched. Both halves of the doc claim hold; both were sabotaged to confirm the assertions discriminate. An anti-vacuity precondition is now permanent rather than performed once by hand. **The mechanism claim swept in `D-18` had one site left.** `request_cost.rs` still said a fully qualified path is "normalized without consulting a device" while the test it cites correctly says a black-box success cannot establish that. Restated as the outcome that is actually measured: preparation succeeds with no volume behind the letter. **Declined, with the measurement rather than an argument.** The review held that a non-canonical `%TMP%` reaches `probe_directory` and yields a probe path containing `..`, making the shape test's control assertion environment-dependent. It does not: `std::env::temp_dir` goes through `GetTempPath2W`, which normalises. Measured, `C:/Users/.../Temp`, `...\Temp\.`, `...\Temp\..\Temp`, `...\Temp\\` and the drive-relative `C:Users\...` all return `C:\Users\...\Temp\`. The one spelling passed through verbatim is `\\?\C:\...`, which is not drive-rooted and already takes the fallback. The check is widened to the full canonical shape anyway, and now covers the fallback base too -- it costs nothing, and the point of this whole change is the difference between a precondition enforced and one argued. Found by sweeping rather than reported: `DESIGN-NOTES.md` counted seven drafts where Tier 2 enumerates nine, and the probes archive still asserted "Touches no filesystem is the claim that holds" -- the claim this PR measured false. Both corrected, the archive by an append rather than a rewrite. `PLANS.md` said the crate has no local checklist, which stopped being true when one was added; it now tracks both and says why the split exists. Verified: 233 lib + 37 doc tests (namespace-request), 158 probe tests, clippy --all-targets, cargo doc, cargo fmt, encoding check over 629 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 14 +- .../DESIGN-NOTES.md | 2 +- crates/windows-namespace-request-sys/PLANS.md | 11 +- .../src/full_path/tests.rs | 184 ++++++++++++++++-- .../COMPLETED-CHECKLIST.md | 7 + .../src/request_cost.rs | 16 +- 6 files changed, 206 insertions(+), 28 deletions(-) diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md index 42ea28e16..c38e271cc 100644 --- a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -32,10 +32,16 @@ Append-only. Newest groups at the bottom. which is what makes "that drive's own current directory" a convention rather than a guarantee); an entry naming nothing is rejected in favour of the drive root; and the call writes the entry back, creating it on a host that had none. - It draws from the disjoint `W`/`U` pair -- taking `U` when the current drive or -the probe directory's drive is `W` -- so it cannot race the sibling tests' -`X`/`Y`, `V`/`T` or `R`/`S` under libtest's - thread-per-test model. + It draws its drive letter from a list disjoint from every sibling test's, so + no two can select the same one and race under libtest's thread-per-test model. + + *(Later correction, recorded here because the archive is history and the + history was briefly wrong: this entry described the selection as a `W`/`U` + **pair**, matching the helper as written. That helper validated only its first + letter and returned the second unchecked, so the guarantee the pair implied did + not hold. The lists are now three letters each -- `X`/`Y`/`P`, `W`/`U`/`N`, + `V`/`T`/`M`, `R`/`S`/`K`, `G`/`H`/`J` -- and every candidate is checked against + both the current drive and the probe drive.)* The sibling test `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index bfbddd342..51515a597 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -631,7 +631,7 @@ on has to be the narrow one -- the call does not *verify* what it produces -- because the broad one is not merely unproven but false. **The constraint this decision carries, and not just its conclusion:** state -only what the evidence reaches. Seven drafts of this entry each named a +only what the evidence reaches. Nine drafts of this entry each named a mechanism it did not -- the call's nature, what a number measured, what the alternatives cost, whether any filesystem was touched. The wordings differ; the error does not. A reader taking only "it is not lexical" away from D-18 has the diff --git a/crates/windows-namespace-request-sys/PLANS.md b/crates/windows-namespace-request-sys/PLANS.md index de5fb4ac0..297c94909 100644 --- a/crates/windows-namespace-request-sys/PLANS.md +++ b/crates/windows-namespace-request-sys/PLANS.md @@ -2,12 +2,15 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). -This crate's work is planned in a feature-scoped checklist at the workspace root -rather than a local one, because it lands alongside a sibling crate and a set of -workspace-level corrections, and the workspace root is their lowest common -source-component. That file is not the workspace +This crate is planned in two files, and the split is deliberate. Its *creation* +lives in a feature-scoped checklist at the workspace root, because it lands +alongside a sibling crate and a set of workspace-level corrections whose lowest +common source-component is the workspace root; that file is deleted when its +feature completes. *Durable follow-up work* therefore cannot live there, and has +a local checklist instead. Neither is the workspace [CHECKLIST.md](../../CHECKLIST.md), which holds unrelated deferred work. | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| +| [CHECKLIST.md](CHECKLIST.md) | not started | This crate's durable follow-up queue, which outlives any one feature. No open milestones; completed items are in [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). | [DESIGN-NOTES.md](DESIGN-NOTES.md) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | **This crate's part (M24-M26) is complete**: the foundations (owned handle duplication, security attributes, path preparation, the faithful-execution contract), the four handle-producing entries, the five query entries, a test seam, and an acceptance pass over both operation and scenario coverage. The checklist itself stays open for M27 (`windows-platform-probes`) and the `M26+` items gated on this branch merging with `main` -- including `M26+.3`, the merge-or-delete decision on this crate's duplicated path preparation. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index d1df5ce9a..252f1e1da 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -436,6 +436,69 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() } } +#[test] +fn the_current_drives_entry_is_neither_consulted_nor_rewritten() { + // The module doc states both halves of the current-drive arm as fact. Until + // now nothing pinned either, and the assertion just above -- which looks + // like it does -- cannot: it resolves `X:foo` WITHOUT controlling the entry + // and compares against the process directory, and Windows keeps the current + // drive's entry equal to that directory. So it reads the same whether the + // entry is consulted or ignored. Vacuous in precisely the way this crate + // keeps rediscovering, and the reason the two arms need opposite fixtures: + // the sibling tests must AVOID the current drive, and this one must be on + // it. + let cwd = current_directory(); + let Some(drive) = cwd.chars().next().filter(char::is_ascii_alphabetic) else { + // A UNC current directory has no drive letter, so there is no + // current-drive arm to exercise. Not a skip of something testable. + return; + }; + let process_directory = format!(r"{}\foo", cwd.trim_end_matches('\\')); + + let probe_dir = probe_directory("current-drive"); + let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let _restore = BorrowedDriveEntry::take(drive); + + // The anti-vacuity check, made permanent rather than performed once by + // hand: unless the two arms would give DIFFERENT answers, every assertion + // below passes without distinguishing them, which is the failure this test + // was written to correct. + assert_ne!( + process_directory, + format!(r"{probe}\foo"), + "precondition: the entry must name somewhere other than the process \ + directory, or consulting it and ignoring it look identical" + ); + + // Not consulted. The entry is one the OTHER arm would honour verbatim -- an + // existing directory in canonical form -- and it names somewhere the + // process directory cannot be, because this test just created it under a + // process-unique name. If the entry were read, the result would be under + // `probe`. + set_drive_entry(drive, Some(probe)); + assert_eq!( + resolve(&format!("{drive}:foo")), + process_directory, + "the current drive's entry was set to {probe}, an entry the non-current \ + arm honours verbatim, and the process directory won anyway" + ); + + // Not rewritten, which needs a REJECTED entry to be visible: an accepted one + // is left alone on both arms, so leaving it alone shows nothing. A child of + // the probe directory cannot exist, and on the non-current arm that is + // replaced by the drive root. + let missing = probe_dir.path.join("no-such-child"); + let missing = missing.to_str().expect("the probe path is UTF-8"); + set_drive_entry(drive, Some(missing)); + let _ = resolve(&format!("{drive}:foo")); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(missing), + "an entry the non-current arm would have replaced with {drive}:\\ is \ + left untouched on the current drive" + ); +} + #[test] fn trailing_dots_and_spaces_are_trimmed_from_ordinary_components() { // The module doc says the rewrite trims trailing dots and spaces. Until now @@ -536,17 +599,38 @@ impl Drop for ProbeDir { } fn probe_directory(tag: &str) -> ProbeDir { - let drive_rooted = |p: &std::path::Path| { + // The full shape an accepted `=X:` entry must have, not just its first + // three characters: rooted at `X:\`, with no `.` or `..` component and no + // forward slash. `a_rejected_drive_entry_is_replaced_by_the_drive_root` + // shows each of those spellings is rejected while naming the same existing + // directory, so a base carrying one would turn that test's CONTROL + // assertion -- "the same directory in canonical form is accepted" -- into a + // rejection, and it would fail for a reason unrelated to what it pins. + // + // **A review read that as reachable through a non-canonical `%TMP%`. It is + // not, and the measurement is here so the next reader need not repeat it.** + // `std::env::temp_dir` goes through `GetTempPath2W`, which normalises what + // it finds: `C:/Users/.../Temp`, `...\Temp\.`, `...\Temp\..\Temp`, + // `...\Temp\\` and even the drive-relative `C:Users\...` all came back as + // `C:\Users\...\Temp\`. The one spelling passed through verbatim is + // `\\?\C:\...`, which is not drive-rooted and so takes the fallback below. + // + // The check is widened anyway. It costs nothing, it covers the fallback + // base too, and it is the difference between a precondition that is + // enforced and one that is argued -- which is the distinction this whole + // change exists to hold. + let canonical_drive_rooted = |p: &std::path::Path| { let s = p.as_os_str().to_string_lossy().into_owned(); let mut chars = s.chars(); matches!( (chars.next(), chars.next(), chars.next()), (Some(d), Some(':'), Some('\\')) if d.is_ascii_alphabetic() - ) + ) && !s.contains('/') + && !s.split('\\').any(|c| c == "." || c == "..") }; let temp = std::env::temp_dir(); - if drive_rooted(&temp) { + if canonical_drive_rooted(&temp) { let path = temp.join(format!("wnrs-{}-{tag}", std::process::id())); std::fs::create_dir_all(&path).expect("create the probe directory"); return ProbeDir { @@ -560,8 +644,8 @@ fn probe_directory(tag: &str) -> ProbeDir { let system_root = std::env::var("SystemRoot").expect("SystemRoot is always set on Windows"); let path = std::path::PathBuf::from(system_root); assert!( - drive_rooted(&path), - "the fallback probe must be drive-rooted: {}", + canonical_drive_rooted(&path), + "the fallback probe must be canonical and drive-rooted: {}", path.display() ); ProbeDir { @@ -658,6 +742,18 @@ fn set_drive_entry(drive: char, value: Option<&str>) { /// Restoration goes through this rather than through `&str`, so an entry /// containing an unpaired surrogate is put back byte for byte. fn set_drive_entry_units(drive: char, value: Option<&Wtf16String>) { + assert!( + try_set_drive_entry_units(drive, value), + "set ={drive}: entry" + ); +} + +/// [`set_drive_entry_units`] without the assertion, reporting success instead. +/// +/// Separate because the restoration in [`BorrowedDriveEntry`] runs during +/// unwinding, where a panic would abort the process and destroy the report of +/// the failure that started the unwind. +fn try_set_drive_entry_units(drive: char, value: Option<&Wtf16String>) -> bool { let name = Wtf16String::from(format!("={drive}:").as_str()); let value_ptr = value .as_ref() @@ -669,7 +765,42 @@ fn set_drive_entry_units(drive: char, value: Option<&Wtf16String>) { value_ptr, ) }; - assert!(ok != 0, "set ={drive}: entry"); + ok != 0 +} + +/// Borrows one drive's `=X:` entry and puts it back when the test ends, +/// **whether or not the test panicked**. +/// +/// Restoring on the last line of the test is not enough, and the hazard is not +/// theoretical: `=X:` is process-global, `cargo test` runs tests as threads in +/// ONE process, and every assertion between the save and the restore is a place +/// the entry can be abandoned. What a sibling test would then inherit is not +/// merely a stale value but one no host would produce -- a path to a directory +/// that no longer exists once the probe directory is removed, or the 1200-unit +/// value that `a_long_drive_entry_round_trips_through_the_reader` installs on +/// purpose. The reader above already names this ("the entry is then never +/// restored") without defending against it; this is the defence. +/// +/// A failed restore is dropped rather than asserted, for the reason given on +/// [`try_set_drive_entry_units`]. +struct BorrowedDriveEntry { + drive: char, + saved: Option, +} + +impl BorrowedDriveEntry { + fn take(drive: char) -> Self { + Self { + drive, + saved: drive_entry(drive), + } + } +} + +impl Drop for BorrowedDriveEntry { + fn drop(&mut self) { + let _ = try_set_drive_entry_units(self.drive, self.saved.as_ref()); + } } #[test] @@ -694,7 +825,7 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( let probe_dir = probe_directory("verbatim"); let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); let drive = probe_drive_from(&['W', 'U', 'N'], probe_dir.drive()); - let restore = drive_entry(drive); + let _restore = BorrowedDriveEntry::take(drive); assert_ne!( Some(drive.to_ascii_uppercase()), @@ -751,8 +882,6 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( Some(format!(r"{drive}:\").as_str()), "resolving created the entry on a host that had none" ); - - set_drive_entry_units(drive, restore.as_ref()); } #[test] @@ -766,7 +895,7 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { let probe_dir = probe_directory("shape"); let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); let drive = probe_drive_from(&['V', 'T', 'M'], probe_dir.drive()); - let restore = drive_entry(drive); + let _restore = BorrowedDriveEntry::take(drive); // The control: this exact directory IS accepted in canonical form, so the // rejections below cannot be blamed on the directory itself. @@ -800,8 +929,6 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { "and the rejected entry is written back as the drive root" ); } - - set_drive_entry_units(drive, restore.as_ref()); } #[test] @@ -816,7 +943,7 @@ fn a_long_drive_entry_round_trips_through_the_reader() { // the 1024 an earlier fixed-size version used, and is a legitimate value: a // per-drive entry is a path, and long paths reach far beyond this. let drive = probe_drive_from(&['R', 'S', 'K'], None); - let restore = drive_entry(drive); + let _restore = BorrowedDriveEntry::take(drive); let long = format!(r"C:\{}", "a".repeat(1200)); set_drive_entry(drive, Some(&long)); @@ -827,6 +954,35 @@ fn a_long_drive_entry_round_trips_through_the_reader() { long, "a long entry survives the read, so the buffer grew instead of truncating" ); +} - set_drive_entry_units(drive, restore.as_ref()); +#[test] +fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { + // The guard exists for the unwinding path, and a suite that passes never + // takes it -- so trusting it would mean shipping an untested defence + // against the exact failure it is there for. This takes the path on + // purpose. + let drive = probe_drive_from(&['G', 'H', 'J'], None); + let sentinel = format!(r"C:\borrowed-entry-{}", std::process::id()); + set_drive_entry(drive, Some(&sentinel)); + + let outcome = std::panic::catch_unwind(|| { + let _restore = BorrowedDriveEntry::take(drive); + set_drive_entry(drive, Some(r"C:\the-borrowed-value")); + panic!("expected: this panic exercises the restore-on-unwind path"); + }); + assert!( + outcome.is_err(), + "precondition: the borrower must actually panic, or the unwinding path \ + is not the thing being measured" + ); + + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(sentinel.as_str()), + "the guard put the entry back while unwinding, where an end-of-test \ + restore would have been skipped" + ); + + set_drive_entry(drive, None); } diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 48c3f06d6..cb7807204 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -123,6 +123,13 @@ request as it was written, and quotes the module doc as it read before the corre for a drive-relative path (`C:foo`) it also reads the per-drive current directory held in the `=C:` environment variables. "Touches no filesystem" is the claim that holds; "lexical" is not. + *(Later correction: the second half stood, the first did not. "Touches no filesystem" was measured + false while carrying out this item -- resolving `X:foo` for a non-current drive distinguishes an + existing directory from an existing file from a missing one, and rewrites the `=X:` entry. What + Microsoft documents is only that the call does not VERIFY its result. See + [../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md) + -> `D-18`.)* + **The mono-repo rule says fix the layer, so the correction belongs in `windows-namespace-request-sys`, not in the probe that consumes it.** It is queued rather than taken because that crate is outside this peel and is release-managed, so a docs change there is its diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index ea03e175b..1696643f3 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -225,11 +225,17 @@ pub fn measure() -> Observation { // length that varied with the local system directory would make the figure // incomparable between the machines the report asks a reader to compare. // - // The `C:` is safe for the same reason the probe's own conclusion is: a - // fully-qualified path is normalized without consulting a device, so no - // volume is needed behind the letter. Two review passes read this as the - // portability bug fixed above, so it is now measured rather than argued -- - // see `preparing_a_path_needs_no_volume_behind_its_drive_letter`. + // The `C:` is safe, and what makes it safe is an OUTCOME, not a mechanism: + // preparation succeeds with no volume behind the drive letter. Whether a + // device is consulted is neither established here nor needed. A black-box + // success is equally compatible with a consultation whose failure is + // ignored, so claiming the letter is "normalized without consulting a + // device" -- as this comment did -- reaches past its evidence, which is the + // one overreach the owning crate's `D-18` exists to remove. + // + // Two review passes read this as the portability bug fixed above, so the + // outcome is measured rather than argued -- see + // `preparing_a_path_needs_no_volume_behind_its_drive_letter`. let long_text = format!(r"C:\{}\file.txt", vec!["directory"; 24].join("\\")); let long = Wtf16String::from(long_text.as_str()); From 016f0320ff6a46ebd01a4f1e88818f3908662c74 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 13:50:33 -0400 Subject: [PATCH 27/36] fix(namespace-request): tell an empty per-drive entry from an absent one Copilot raised this twice and was right both times; the answer given the first time cited a measurement, and the measurement was wrong. `drive_entry` folded "empty value" and "absent name" into `None`, on a comment that called the equivalence measured: `SetEnvironmentVariableW(name, "")` was reported to succeed and then read back exactly as a name never set. It does not. `GetEnvironmentVariableW` returns 0 for both, and the last error is the ONLY thing separating them -- so a measurement that never cleared the last error first could read nothing but what an earlier call left behind. Cleared and re-measured, for an ordinary name and an `=X:` name alike: set to "" -> returns 0, last error ERROR_SUCCESS deleted -> returns 0, last error ERROR_ENVVAR_NOT_FOUND The companion note that ERROR_ENVVAR_NOT_FOUND "never surfaced even for genuinely absent variables" had the same cause plus one more: the deletion under test never happened, because the null that deletes an entry was marshalled as an empty string. The consequence was live. `BorrowedDriveEntry` restores on unwind so a panicking test cannot leak process state; with the answers collapsed, restoring an inherited EMPTY entry deleted it -- the guard destroying the state it exists to preserve, in exactly the one case it could. The reader now clears the error, returns an empty `Wtf16String` on ERROR_SUCCESS, `None` only on ERROR_ENVVAR_NOT_FOUND, and panics on anything else rather than folding a transient failure into "absent". Pinned, and verified by re-introducing the collapse, which makes the new test fail. The rest of the round, all from the same review: * The guard test installed its sentinel BEFORE any guard saved the entry, so the test for not losing borrowed state lost some. An outer guard now covers its own borrow while the inner one still takes the unwinding path. * `probe_directory`'s `%SystemRoot%` fallback did not uphold `ProbeDir`'s stated invariant that it differs from the process current directory -- reachable with a UNC temp directory and cargo launched from `%SystemRoot%`, where the new current-drive test would fail its anti-vacuity check before reaching the API. It now selects between `%SystemRoot%` and its `System32` child, for the same reason `probe_drive_from` takes a list rather than one usually-right value. * Two documented observations had no test: an entry naming an existing FILE is rejected (the type check, distinct from both the shape check and the existence check), and an accepted entry ending in a separator yields a DOUBLED one. Both measured and pinned. The file case uses `kernel32.dll` rather than one the test creates, because the probe directory may be the read-only fallback. * `PLANS.md` listed the local checklist as "not started" while `COMPLETED-PLANS.md` records the same path as completed -- two incompatible states for one path, introduced by the previous commit. The row is gone; the prose says why. Verified: 234 lib + 37 doc tests, 20 + 11 acceptance, clippy --all-targets, cargo fmt, encoding check over 629 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../windows-namespace-request-sys/Cargo.toml | 6 +- .../DESIGN-NOTES.md | 9 + .../DESIGN-RATIONALE.md | 41 ++++ crates/windows-namespace-request-sys/PLANS.md | 7 +- .../src/full_path/tests.rs | 175 ++++++++++++++++-- 5 files changed, 221 insertions(+), 17 deletions(-) diff --git a/crates/windows-namespace-request-sys/Cargo.toml b/crates/windows-namespace-request-sys/Cargo.toml index ade033ea0..7cf955800 100644 --- a/crates/windows-namespace-request-sys/Cargo.toml +++ b/crates/windows-namespace-request-sys/Cargo.toml @@ -48,7 +48,11 @@ windows-thread-ambient-sys = { version = "0.2.0", path = "../windows-thread-ambi [dev-dependencies.windows-sys] version = "0.61.2" default-features = false -features = ["Win32_System_Environment"] +# `Win32_Foundation` is named here rather than inherited from the dependency +# below: reading an entry has to clear and re-read the last error to tell an +# empty value from an absent one, and a test target must not depend on a +# feature staying enabled for a reason unrelated to it. +features = ["Win32_Foundation", "Win32_System_Environment"] [dependencies.windows-sys] version = "0.61.2" diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 51515a597..99550b9c7 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -637,6 +637,15 @@ alternatives cost, whether any filesystem was touched. The wordings differ; the error does not. A reader taking only "it is not lexical" away from D-18 has the answer without the thing that kept producing wrong ones. +The constraint reaches further than prose, and the sharpest case was not about +this call at all: a test helper collapsed an *empty* `=X:` entry into an absent +one on a comment that called the equivalence measured, when the measurement had +never cleared the last error that distinguishes them. So **"measured" is itself +a claim, and a procedure can be wrong in ways its result never shows.** The +practical consequence was that restoring a borrowed empty entry deleted it. See +[DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) -> "The measurement that was itself +unmeasured". + The drafts themselves, and why each failed, are Tier 2: [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md) -> `D-18`. diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 7c7e67145..1d631c190 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -118,6 +118,47 @@ were *enumerations* -- which is the form this kind of error likes. it. The tests in [tests.rs](src/full_path/tests.rs) now pin every documented spelling so the next omission fails CI instead of a review. +### The measurement that was itself unmeasured + +The sharpest instance in this whole sequence is not about `GetFullPathNameW` at +all. The test helper that reads a `=X:` entry folded "empty value" and "absent +name" into one answer, and said so in a comment that called the equivalence +*measured*: `SetEnvironmentVariableW(name, "")` was reported to succeed and then +read back exactly as a name that was never set. + +It does not. `GetEnvironmentVariableW` returns `0` for both, and the last error +is the only thing that separates them -- so a measurement that never cleared the +last error first could read nothing but whatever an earlier call had left there. +Cleared and re-measured, the two are distinct, for an ordinary name and an `=X:` +name alike: + +| state | return | last error | +|---|---|---| +| set to `""` | `0` | `ERROR_SUCCESS` | +| deleted | `0` | `ERROR_ENVVAR_NOT_FOUND` | + +An earlier note also recorded that `ERROR_ENVVAR_NOT_FOUND` "never surfaced even +for genuinely absent variables", which has the same cause and one more: the +deletion under test had not happened, because the null that deletes an entry had +been marshalled as an empty string instead. Two layers of the harness agreeing +with each other is not a measurement. + +The consequence was live rather than cosmetic. `BorrowedDriveEntry` restores a +borrowed entry on unwind precisely so a panicking test cannot leak process +state; with the two answers collapsed, restoring an inherited *empty* entry +**deleted** it. The guard destroyed the state it existed to preserve, in exactly +one case, and only that case. + +Pinned by `an_empty_drive_entry_is_distinguished_from_an_absent_one` in +[tests.rs](src/full_path/tests.rs), and verified by re-introducing the collapse, +which makes it fail. + +The general form is worth keeping separately from the specific fact: **a +comment that says "measured" is a claim about a procedure, and the procedure can +be wrong in ways the result never reveals.** Every other entry in this file is an +assertion that outran its evidence. This one had evidence, and the evidence was +of something else. + ### The sweep, and its arithmetic The wrong word had spread well beyond where it was reported. The consuming diff --git a/crates/windows-namespace-request-sys/PLANS.md b/crates/windows-namespace-request-sys/PLANS.md index 297c94909..f1b36375e 100644 --- a/crates/windows-namespace-request-sys/PLANS.md +++ b/crates/windows-namespace-request-sys/PLANS.md @@ -10,7 +10,12 @@ feature completes. *Durable follow-up work* therefore cannot live there, and has a local checklist instead. Neither is the workspace [CHECKLIST.md](../../CHECKLIST.md), which holds unrelated deferred work. +The local [CHECKLIST.md](CHECKLIST.md) has no open milestone, so it has no row +below: its one plan so far is finished and recorded in +[COMPLETED-PLANS.md](COMPLETED-PLANS.md). The file stays because the queue is +durable even when it is empty, and a row here would give the same path two +incompatible states across the two indexes. + | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | not started | This crate's durable follow-up queue, which outlives any one feature. No open milestones; completed items are in [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). | [DESIGN-NOTES.md](DESIGN-NOTES.md) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | **This crate's part (M24-M26) is complete**: the foundations (owned handle duplication, security attributes, path preparation, the faithful-execution contract), the four handle-producing entries, the five query entries, a test seam, and an acceptance pass over both operation and scenario coverage. The checklist itself stays open for M27 (`windows-platform-probes`) and the `M26+` items gated on this branch merging with `main` -- including `M26+.3`, the merge-or-delete decision on this crate's duplicated path preparation. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 252f1e1da..14d185c1f 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -6,7 +6,9 @@ //! what it produces, and a suite that only ever resolved existing paths would //! leave a reader believing it does. -use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; +use windows_sys::Win32::Foundation::{ + ERROR_ENVVAR_NOT_FOUND, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, +}; use wtf_string::Wtf16String; use super::{FullPathError, ResolveFullPath}; @@ -641,13 +643,37 @@ fn probe_directory(tag: &str) -> ProbeDir { // Not creating anything here, so no write permission is needed on a host // whose temp directory is redirected off a drive letter. - let system_root = std::env::var("SystemRoot").expect("SystemRoot is always set on Windows"); - let path = std::path::PathBuf::from(system_root); - assert!( - canonical_drive_rooted(&path), - "the fallback probe must be canonical and drive-rooted: {}", - path.display() + // + // **It must also differ from the process current directory**, which the + // temp branch gets for free -- it creates a uniquely named child -- and this + // branch does not. Cargo launched from `%SystemRoot%` on a host with a UNC + // temp directory would otherwise hand back the current directory itself, + // and a probe indistinguishable from the current directory cannot separate + // "the entry was honoured" from "the entry was ignored". `System32` is the + // second candidate for the same reason `probe_drive_from` takes a list: + // one value that is usually right is not a guarantee. + let system_root = std::path::PathBuf::from( + std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), ); + let cwd = current_directory(); + let cwd = cwd.trim_end_matches('\\'); + let distinct = |p: &std::path::Path| { + !p.as_os_str() + .to_string_lossy() + .trim_end_matches('\\') + .eq_ignore_ascii_case(cwd) + }; + + let path = [system_root.clone(), system_root.join("System32")] + .into_iter() + .find(|p| canonical_drive_rooted(p) && distinct(p)) + .unwrap_or_else(|| { + panic!( + "no fallback probe directory is both canonical and distinct \ + from the current directory {cwd}" + ) + }); + ProbeDir { path, created: false, @@ -702,6 +728,13 @@ fn drive_entry(drive: char) -> Option { // would land before the test could restore the process state it borrowed. let mut buffer = vec![0u16; 256]; loop { + // Zero is TWO different answers, and the last error is the only thing + // that separates them -- so it is cleared first, because the value left + // by some earlier call would otherwise be read as this one's. + // + // SAFETY: no preconditions. + unsafe { windows_sys::Win32::Foundation::SetLastError(ERROR_SUCCESS) }; + // SAFETY: the name is NUL-terminated and the buffer is writable for // the length passed. let written = unsafe { @@ -713,14 +746,31 @@ fn drive_entry(drive: char) -> Option { }; let written = written as usize; if written == 0 { - // Zero means absent. It is also what an *empty* value would report, - // and this deliberately does not try to tell the two apart -- - // measured, they are the same state: `SetEnvironmentVariableW(name, - // "")` returns success and a subsequent read reports zero with an - // empty buffer, exactly as for a name that was never set. Windows - // has no environment variable with an empty value for this API to - // return, so restoring "absent" cannot lose one. - return None; + // **An earlier version of this comment claimed, as measured, that + // an empty value and an absent name are the same state and cannot + // be told apart. That was wrong, and wrong in this crate's + // signature way: the measurement behind it never cleared the last + // error, so it could only ever have seen whatever was already + // there.** Cleared first and re-measured, the two are distinct, for + // an ordinary name and an `=X:` name alike: + // + // set to "" -> returns 0, last error ERROR_SUCCESS + // deleted -> returns 0, last error ERROR_ENVVAR_NOT_FOUND + // + // The difference is not academic here. Collapsing both to `None` + // makes the restoration in `BorrowedDriveEntry` DELETE an inherited + // empty entry rather than put it back -- losing exactly the process + // state the guard exists to preserve. + // + // SAFETY: no preconditions. + let last = unsafe { windows_sys::Win32::Foundation::GetLastError() }; + return match last { + ERROR_SUCCESS => Some(Wtf16String::from_units(&[])), + ERROR_ENVVAR_NOT_FOUND => None, + // Anything else is neither answer, and folding it into "absent" + // would make the guard delete an entry over a transient error. + other => panic!("reading ={drive}: failed with error {other}"), + }; } if written < buffer.len() { // Kept as WTF-16 units rather than going through String: a lossy @@ -843,6 +893,19 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( convention the entry usually holds, not a guarantee about the result" ); + // Verbatim means verbatim, including the join. The module doc records that + // an accepted entry ending in a separator yields a DOUBLED one, and nothing + // pinned it -- so the observation could have stopped being true without CI + // noticing, which is the drift this change exists to close rather than + // commit again. + set_drive_entry(drive, Some(&format!(r"{probe}\"))); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{probe}\\foo"), + "an entry is accepted with a trailing separator and concatenated \ + without normalising the join" + ); + // An entry that does not name an existing directory is rejected, and the // call rewrites it to the drive root rather than leaving it stale. // @@ -929,6 +992,41 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { "and the rejected entry is written back as the drive root" ); } + + // The type check, which is a separate necessary condition from both the + // shape above and the existence check in the sibling test. The module doc + // has listed an existing FILE among the rejections since the drive-entry + // work, and nothing pinned it -- so the one observation distinguishing + // "names a directory" from "names something" lived only in prose. + // + // Not a file this test creates: `ProbeDir` may be the read-only + // `%SystemRoot%` fallback, where creating one needs privileges the suite + // must not assume. This one is present on every Windows host by + // construction, and setting an entry to a file does not touch the file. + let system_file = std::path::PathBuf::from( + std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), + ) + .join("System32") + .join("kernel32.dll"); + assert!( + system_file.is_file(), + "precondition: the rejected entry must name an existing FILE: {}", + system_file.display() + ); + let system_file = system_file.to_str().expect("the system path is UTF-8"); + + set_drive_entry(drive, Some(system_file)); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), + "{system_file} exists and is canonical, and is rejected anyway because \ + it is not a directory -- so existence alone is not the gate" + ); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(format!(r"{drive}:\").as_str()), + "and an entry naming a file is written back as the drive root too" + ); } #[test] @@ -963,6 +1061,13 @@ fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { // against the exact failure it is there for. This takes the path on // purpose. let drive = probe_drive_from(&['G', 'H', 'J'], None); + + // The outer guard is not ceremony. This test installs a sentinel to watch + // the inner guard put back, and without it that install would destroy + // whatever the process inherited -- so the test for not losing borrowed + // state would itself lose some. The inner guard still takes the unwinding + // path; the outer one covers this test's own borrow. + let _outer = BorrowedDriveEntry::take(drive); let sentinel = format!(r"C:\borrowed-entry-{}", std::process::id()); set_drive_entry(drive, Some(&sentinel)); @@ -983,6 +1088,46 @@ fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { "the guard put the entry back while unwinding, where an end-of-test \ restore would have been skipped" ); +} + +#[test] +fn an_empty_drive_entry_is_distinguished_from_an_absent_one() { + // **This pins a correction, not a discovery.** The reader used to fold both + // into `None`, on a recorded measurement that an empty value and an absent + // name are the same state. They are not; the measurement behind that claim + // never cleared the last error, so it could only have read whatever an + // earlier call left behind -- the same "stated more precisely than the + // evidence reaches" failure this crate keeps meeting, committed inside the + // comment that called itself measured. + // + // The consequence is what makes it worth a test rather than a fix: with the + // two collapsed, restoring an inherited EMPTY entry deletes it, so the guard + // written to preserve process state destroys it in exactly one case. + let drive = probe_drive_from(&['E', 'F', 'B'], None); + let _outer = BorrowedDriveEntry::take(drive); + + set_drive_entry(drive, Some("")); + let empty = drive_entry(drive); + assert_eq!( + empty.as_ref().map(|v| v.to_string_lossy()), + Some(String::new()), + "an entry set to the empty string reads back as PRESENT and empty" + ); set_drive_entry(drive, None); + assert_eq!( + drive_entry(drive), + None, + "and a deleted entry reads back as absent, which is the answer the \ + empty one must not be confused with" + ); + + // The two are distinct in the round trip as well as in the read, which is + // the property restoration actually depends on. + set_drive_entry_units(drive, empty.as_ref()); + assert_eq!( + drive_entry(drive).as_ref().map(|v| v.to_string_lossy()), + Some(String::new()), + "restoring an empty entry puts back an empty entry, not an absent one" + ); } From fe3b6b4f8d2cec687ef9fd7672bfb5856bcfee6e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 14:06:29 -0400 Subject: [PATCH 28/36] docs(probes): stop the report contradicting its own decomposition Three of these are the same defect wearing different clothes: a heading, a line of shipped output, and an assertion message each kept a claim that the text around them had already withdrawn. All were found by Copilot, and the third by a sweep I had run and mis-scoped -- I grepped the comment that disclaims the mechanism and not the failure message forty lines below it that still asserts it. * The module heading said preparation "is not an allocation" while the paragraph below charges the measured gap with one net allocation. "Not ONLY that", with the reason stated, so the heading and the decomposition agree. * The emitted report said the same thing at "it is not the allocator", and said GetFullPathNameW "resolves a path that is not fully qualified" -- which omits the legacy-device short-circuit that D-18 and the module doc both record. A reader running the probe got a flatter rule than the documentation gives. * `preparing_a_path_needs_no_volume_behind_its_drive_letter` explains at length that a black-box success cannot establish whether a device was consulted, then asserted with a message saying the path is "not resolved against a device". The comment was right and the message it would actually print was the claim the comment rejects. And the coverage gap, which is the substantive one: `a_root_relative_path_takes_the_root_and_not_the_whole_directory` computes a UNC expectation but runs under whatever directory launched the suite, so on ordinary drive-rooted CI it only ever exercises the `C:\foo` half. The UNC branch of a documented public guarantee -- that `\foo` takes the ROOT of the current directory, which under a UNC one is the share root and not a drive -- was therefore unpinned, which is exactly the "current drive has no referent here" correction this PR made. The current directory is process-wide, so nothing in a thread-per-test suite can reach it. `tests/unc_current_directory.rs` re-executes the test binary with `\\localhost\C$` as its working directory and a filter naming the one test, and the child asserts both that it really holds a UNC current directory and what the call produces. Measured: `\foo` resolves to `\\localhost\C$\foo`. Verified by sabotage -- expecting `C:\foo` fails in the child and propagates through the parent. The precondition is asserted, not skipped. A silent skip would leave the branch unpinned while reporting that it is pinned, which is the vacuity this crate keeps finding. If a host cannot reach the administrative share the test says so in those terms. Verified: 234 lib + 37 doc + 32 integration tests, 158 probe tests, clippy --all-targets, cargo fmt, encoding check over 629 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/unc_current_directory.rs | 99 +++++++++++++++++++ .../src/bin/request_cost.rs | 10 +- .../src/request_cost.rs | 5 +- crates/windows-platform-probes/src/tests.rs | 8 +- 4 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 crates/windows-namespace-request-sys/tests/unc_current_directory.rs diff --git a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs new file mode 100644 index 000000000..315d43dc4 --- /dev/null +++ b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs @@ -0,0 +1,99 @@ +// Copyright (c) Mike Grier. + +//! Root-relative rooting under a UNC current directory. +//! +//! An integration test, and a two-process one, because the property is not +//! expressible any other way. The current directory is **process-wide**, so a +//! test that wants to observe what `GetFullPathNameW` does under a UNC one has +//! to *be* a process that has a UNC one -- and it must not impose that on the +//! rest of the suite, which runs as threads in the same process. +//! +//! What it pins is the branch the module doc calls out and nothing else +//! reached: a root-relative path (`\foo`) is rooted at the **root of the +//! current directory**, which under a UNC current directory is the share root +//! `\\server\share\` and not a drive. The unit test for the same rule runs +//! under whatever directory launched the suite, so on ordinary drive-rooted CI +//! it only ever exercises the `C:\foo` half; it computes the UNC expectation +//! but never puts the call in a position to produce one. +//! +//! The child is this same test binary, re-executed with a marker in its +//! environment and a filter naming the one test to run. That keeps the +//! assertion beside the thing it asserts rather than in a fixture binary whose +//! drift nothing would catch. + +use windows_namespace_request_sys::ResolveFullPath; +use wtf_string::Wtf16String; + +/// Set on the child, absent on the parent. Presence, not value, is the signal. +const CHILD_MARKER: &str = "WNRS_UNC_CURRENT_DIRECTORY_CHILD"; + +/// The administrative share, which is the only UNC path a test can rely on +/// existing without creating one -- and creating one needs privileges the suite +/// must not assume it has. +const UNC_ROOT: &str = r"\\localhost\C$"; + +const TEST_NAME: &str = "a_root_relative_path_takes_the_share_root_under_a_unc_current_directory"; + +#[test] +fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { + if std::env::var_os(CHILD_MARKER).is_some() { + assert_root_relative_takes_the_share_root(); + return; + } + + // The precondition is asserted rather than silently skipped. A test that + // quietly passes when it could not run is the vacuity this crate keeps + // finding, and it is worse here than elsewhere: this is the only coverage + // of the branch, so a silent skip would leave the doc unpinned while + // reporting that it is pinned. + assert!( + std::path::Path::new(UNC_ROOT).is_dir(), + "{UNC_ROOT} is not reachable, so no UNC current directory can be \ + established -- an environment limitation, not a failure of the \ + behaviour under test" + ); + + let exe = std::env::current_exe().expect("the test binary knows its own path"); + let status = std::process::Command::new(exe) + .arg("--exact") + .arg(TEST_NAME) + .arg("--nocapture") + .env(CHILD_MARKER, "1") + .current_dir(UNC_ROOT) + .status() + .expect("re-execute this test binary with a UNC current directory"); + + assert!( + status.success(), + "the child, running with {UNC_ROOT} as its current directory, did not \ + pass: {status}" + ); +} + +fn assert_root_relative_takes_the_share_root() { + let cwd = std::env::current_dir().expect("the child has a current directory"); + let cwd = cwd.to_str().expect("the current directory is UTF-8"); + + // Proving the child is where the parent put it. Without this the test could + // pass having silently inherited an ordinary drive-rooted directory -- + // `cmd.exe`, for one, refuses a UNC current directory and falls back to the + // Windows directory, so a child that is not a plain Win32 process can lose + // it without saying so. + assert!( + cwd.starts_with(r"\\"), + "precondition: the child must actually hold a UNC current directory, \ + not a drive-rooted one: {cwd}" + ); + + let resolved = ResolveFullPath::new(Wtf16String::from(r"\foo")) + .perform() + .expect("a root-relative path resolves"); + + assert_eq!( + resolved.to_string_lossy(), + format!(r"{UNC_ROOT}\foo"), + "a root-relative path takes the ROOT of the current directory, which \ + under a UNC current directory is the share root -- so \"the current \ + drive\", as a draft of the module doc said, has no referent here" + ); +} diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index 20d2555fd..1e52585ee 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -282,7 +282,7 @@ fn render(out: &mut dyn std::fmt::Write) { { let _ = writeln!( out, - "\n WHERE THE TIME ACTUALLY GOES, and it is not the allocator:" + "\n WHERE THE TIME ACTUALLY GOES, and it is not only the allocator:" ); // "resolves against process state" -- not "a syscall", and not // "lexical" either. @@ -301,11 +301,15 @@ fn render(out: &mut dyn std::fmt::Write) { // two successive attempts to name one were each wrong in the same way. let _ = writeln!( out, - " `prepare` calls GetFullPathNameW, which resolves a path that is not fully" + " `prepare` calls GetFullPathNameW, which roots MOST paths that are not fully" ); let _ = writeln!( out, - " qualified against process state -- the CWD is mutable by any thread, so" + " qualified against process state -- most, because a legacy device name such" + ); + let _ = writeln!( + out, + " as CON short-circuits rooting entirely. The CWD is mutable by any thread, so" ); let _ = writeln!( out, diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 1696643f3..45968fd46 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -58,7 +58,10 @@ //! # Preparing a path is a Win32 call, not an allocation //! //! This probe was written expecting `prepare` to be an allocation and a copy. -//! It is not: it calls **`GetFullPathNameW`** to resolve the path against the +//! It is not *only* that -- the qualifier matters, because the decomposition +//! below charges the measured gap with one net allocation, and a heading saying +//! flatly "not an allocation" would contradict it: it calls +//! **`GetFullPathNameW`** to resolve the path against the //! process working directory, because [the namespace session] settled that the //! path is resolved at submission -- the process CWD is mutable by any thread, //! so even perfect remoting would be racy. diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index ec05116c9..a53d9a232 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4442,9 +4442,9 @@ fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { assert!( windows_namespace_request_sys::prepare(&path).is_ok(), - "preparing {text} must succeed with no {absent}: volume mounted -- \ - a fully-qualified path is normalized, not resolved against a device, \ - and `request_cost` depends on that both for its long-path sample and \ - for its claim about where the measured time goes" + "preparing {text} must succeed with no {absent}: volume mounted. That \ + outcome is the whole claim -- this says nothing about whether a device \ + is consulted, because a black-box success cannot -- and it is what \ + `request_cost` depends on for its hard-coded long-path sample" ); } From e13aac78d4789c88d2ff2cfc88a98307ec55858b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 14:21:37 -0400 Subject: [PATCH 29/36] fix(namespace-request): state the current-drive arm at the boundary observation reaches The headline finding is one I committed while fixing the same defect two commits earlier. `the_current_drives_entry_is_neither_consulted_nor_rewritten` -- a test written to correct an overreaching claim -- overreached in its own NAME. Setting the entry and watching the outcome cannot separate "not read" from "read and ignored", which is exactly the argument that removed "normalized without consulting a device" from the probe in the commit before it. So the guarantee is restated everywhere it appears -- module doc, `D-18`, the test name and its commentary -- as the two effects that were actually measured: **the current drive's entry makes no difference to the result, and is not rewritten.** Whether Windows reads it internally is not established and is no longer implied. Two more, both real, both about the guard added in this branch: * `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` controls no entry, so it looked like a non-mutating test. The CALL mutates: resolving for a non-current drive writes `=X:` whenever the recorded entry is absent or rejected, and a letter chosen for being unused usually has no entry. It was the one mutating case here without a guard, and now borrows. * `BorrowedDriveEntry::drop` swallowed a failed restore unconditionally. The silence is only worth buying while unwinding, where a panic would abort the process and destroy the report of the failure that started it. On the ordinary path it let the suite continue with corrupted process-global state and fail somewhere unrelated, so it now asserts unless already panicking. The UNC test is ignored by default and run explicitly in CI. `C$` is an ADMINISTRATIVE share: reachable for an administrator, not for an ordinary user, and switchable off -- so running by default turned `cargo test` red on a perfectly good host, reporting an environment limitation as a defect. Provisioning our own share is not an escape, because that needs the same privileges. `#[ignore]` is this repository's existing pattern for exactly this (see the probes ignored tier) and keeps the gap VISIBLE: an ignored test is named in the output, where a skip inside a passing test is not. That mattered here more than usual, since this is the only coverage of the UNC branch. The child re-exec now passes --include-ignored as well. Without it the child would have run zero tests, exited 0, and the parent would have reported success having measured nothing -- the same vacuity in a new place, and one this round introduced rather than found. Verified both directions: ignored by default, and parent plus child both pass under --include-ignored. 234 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, workflow references, encoding check over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 17 ++++++ .../DESIGN-NOTES.md | 5 +- .../src/full_path.rs | 11 +++- .../src/full_path/tests.rs | 56 ++++++++++++++----- .../tests/unc_current_directory.rs | 20 +++++++ 5 files changed, 93 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f6a149a..117c87126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,6 +165,23 @@ jobs: RUST_BACKTRACE: 1 RUST_LIB_BACKTRACE: 1 run: cargo test --workspace --locked --no-fail-fast + # The UNC current-directory test is ignored by default because it needs a + # reachable administrative share (`\\localhost\C$`), which an ordinary + # developer account does not have -- running it by default would report an + # environment limitation as a defect. The runner is an administrator, so + # the coverage is deterministic HERE, which is where it has to be: this is + # the only test that puts `GetFullPathNameW` under a UNC current directory, + # and the guarantee it pins ("a root-relative path takes the ROOT of the + # current directory, which is a share root and not a drive") is public. + # + # A FAILURE HERE IS A RESULT. If the share stops being reachable on the + # runner the test says so in those terms, and the answer is to provision a + # UNC fixture rather than to delete the assertion. + - name: cargo test (namespace-request, UNC current directory) + env: + RUST_BACKTRACE: 1 + RUST_LIB_BACKTRACE: 1 + run: cargo test -p windows-namespace-request-sys --locked --test unc_current_directory -- --include-ignored # windows-file-watcher-example-test-harness is a normal workspace member # that depends on windows-file-watcher with the test-util feature, so # the two `--workspace` steps above unify that feature onto diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 99550b9c7..7682726cf 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -620,7 +620,10 @@ directory that `C:\Windows\System32` was accepted for. So the draft calling this "a filesystem query rather than a syntax test" named a mechanism the evidence contradicts: it is both, and the list is observation rather than specification. The rewrite mutates the process environment block as a side effect of what reads like a pure query. For the current drive -the entry is neither consulted nor rewritten. +the entry makes no difference to the result and is not rewritten -- stated as +those two measured effects rather than as "not consulted", because installing an +entry and observing the outcome cannot separate "not read" from "read and +ignored", and a draft of this very paragraph said "not consulted" anyway. Earlier drafts concluded the opposite by reasoning that the current directory lives in the PEB and the `=X:` variables in the environment block, so both are diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index e52430826..d685f194f 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -142,8 +142,15 @@ //! of this doc did, states a mechanism the evidence contradicts. It is both, and //! this list is a set of observations rather than a specification. //! -//! For the current drive neither happens: the entry is not consulted and not -//! rewritten. +//! For the current drive neither happens, and the guarantee is stated at the +//! boundary observation can actually reach: **the entry makes no difference to +//! the result, and is not rewritten.** Both halves are measured -- an entry the +//! non-current arm would honour verbatim is installed and the process directory +//! wins anyway, and an entry the non-current arm would replace is left +//! untouched. Whether Windows *reads* it internally is not established, because +//! setting a value and observing the result cannot separate "not read" from +//! "read and ignored". An earlier revision said "not consulted", which is the +//! same overreach this section corrects two paragraphs above. //! //! This is why the "does not verify what it produces" guarantee above is worth //! stating narrowly. The broad reading -- that the call touches no filesystem -- diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 14d185c1f..f250f9ef8 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -408,6 +408,14 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // under a UNC current directory every letter is "other" -- so it runs // unconditionally and this test never degenerates to a silent skip. let other = probe_drive_from(&['X', 'Y', 'P'], None); + + // This test controls no entry, but the CALL does: resolving for a + // non-current drive writes `=X:` whenever the recorded entry is absent or + // rejected, and on most hosts a letter chosen for being unused has no entry + // at all. So merely observing the arm mutates process-global state, and + // this was the one mutating case here without a guard -- the borrow is + // needed exactly because the mutation is not the test's own doing. + let _restore = BorrowedDriveEntry::take(other); let resolved = resolve(&format!("{other}:foo")); // Compared case-insensitively, because the case is not this test's to @@ -439,16 +447,24 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() } #[test] -fn the_current_drives_entry_is_neither_consulted_nor_rewritten() { +fn the_current_drives_entry_does_not_affect_resolution_and_is_not_rewritten() { // The module doc states both halves of the current-drive arm as fact. Until // now nothing pinned either, and the assertion just above -- which looks // like it does -- cannot: it resolves `X:foo` WITHOUT controlling the entry // and compares against the process directory, and Windows keeps the current - // drive's entry equal to that directory. So it reads the same whether the - // entry is consulted or ignored. Vacuous in precisely the way this crate - // keeps rediscovering, and the reason the two arms need opposite fixtures: - // the sibling tests must AVOID the current drive, and this one must be on - // it. + // drive's entry equal to that directory. So it reads the same either way. + // Vacuous in precisely the way this crate keeps rediscovering, and the + // reason the two arms need opposite fixtures: the sibling tests must AVOID + // the current drive, and this one must be on it. + // + // **The name says what is observable, and an earlier one did not.** This + // was `..._is_neither_consulted_nor_rewritten`, which claims the entry is + // not READ -- and installing a value and watching the outcome cannot + // separate "not read" from "read and ignored". That is the same overreach + // this branch removed from the probe's "without consulting a device", and + // the test correcting it committed it in its own name. What the two + // assertions below reach is the pair of observable effects: the entry makes + // no difference to the result, and it is not written back. let cwd = current_directory(); let Some(drive) = cwd.chars().next().filter(char::is_ascii_alphabetic) else { // A UNC current directory has no drive letter, so there is no @@ -469,14 +485,14 @@ fn the_current_drives_entry_is_neither_consulted_nor_rewritten() { process_directory, format!(r"{probe}\foo"), "precondition: the entry must name somewhere other than the process \ - directory, or consulting it and ignoring it look identical" + directory, or honouring it and ignoring it look identical" ); - // Not consulted. The entry is one the OTHER arm would honour verbatim -- an - // existing directory in canonical form -- and it names somewhere the - // process directory cannot be, because this test just created it under a - // process-unique name. If the entry were read, the result would be under - // `probe`. + // No difference to the result. The entry is one the OTHER arm would honour + // verbatim -- an existing directory in canonical form -- and it names + // somewhere the process directory cannot be, because this test just created + // it under a process-unique name. If the entry were HONOURED, the result + // would be under `probe`. set_drive_entry(drive, Some(probe)); assert_eq!( resolve(&format!("{drive}:foo")), @@ -849,7 +865,21 @@ impl BorrowedDriveEntry { impl Drop for BorrowedDriveEntry { fn drop(&mut self) { - let _ = try_set_drive_entry_units(self.drive, self.saved.as_ref()); + let restored = try_set_drive_entry_units(self.drive, self.saved.as_ref()); + + // Silence is bought only where it buys something. While unwinding, a + // panic here aborts the process and destroys the report of the failure + // that started the unwind, so a failed restore is worth less than the + // diagnosis it would replace. On the ordinary path there is no such + // trade: staying quiet would let the suite carry on with corrupted + // process-global state and fail somewhere unrelated, which is the + // hardest kind of failure to read. + assert!( + restored || std::thread::panicking(), + "restoring ={}: failed, leaving process-global state corrupted for \ + every test that follows", + self.drive + ); } } diff --git a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs index 315d43dc4..6570ee505 100644 --- a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs +++ b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs @@ -34,7 +34,23 @@ const UNC_ROOT: &str = r"\\localhost\C$"; const TEST_NAME: &str = "a_root_relative_path_takes_the_share_root_under_a_unc_current_directory"; +// Ignored by default and run explicitly in CI, which is this repository's +// existing "ignored tier" pattern rather than a new one. +// +// `C$` is an ADMINISTRATIVE share. It is reachable for a member of +// Administrators and not for an ordinary user, and it can be switched off +// entirely -- so running by default would turn `cargo test` red for a +// non-administrator developer on a perfectly good host, reporting an +// environment limitation as a defect in the crate. Provisioning a share of our +// own is not an escape: creating one needs the same privileges. +// +// The alternative -- skipping quietly when the share is missing -- is the one +// thing that must not happen, because this is the ONLY coverage of the UNC +// branch and a silent skip would leave it unpinned while reporting otherwise. +// `#[ignore]` keeps that visible: an ignored test is counted and named in the +// output, where a skip inside a passing test is not. #[test] +#[ignore = "needs a reachable administrative share for a UNC current directory; run in CI with --include-ignored"] fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { if std::env::var_os(CHILD_MARKER).is_some() { assert_root_relative_takes_the_share_root(); @@ -58,6 +74,10 @@ fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { .arg("--exact") .arg(TEST_NAME) .arg("--nocapture") + // Without this the child runs zero tests and exits 0 -- the parent + // would report success having measured nothing at all. The test it is + // told to run is the ignored one, so the filter alone is not enough. + .arg("--include-ignored") .env(CHILD_MARKER, "1") .current_dir(UNC_ROOT) .status() From 9a6e1563d915f5cee5b0fe43a56ea1c7632b126c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 14:37:04 -0400 Subject: [PATCH 30/36] docs: sweep the current-drive arm into every restatement of the rooting rule The rule "a drive-relative path takes the entry recorded for that drive" is stated in six places. The current-drive exception -- that the entry makes no difference there and the process directory wins -- had been added to exactly one of them, the owning crate's D-18, where it was discovered. The other five kept the broad form, so the contract read differently depending on which file a reader opened. That is this repository's blast-radius rule failing on the change that introduced the fact, not on inherited text. The measurement and the sweep landed in different commits, which is the gap the rule exists to close. Swept: the root DESIGN-NOTES corollary, CHECKLIST-thread-ambient M26.5, the cost probe's decomposition, and the Tier 2 enumeration of the rooting forms -- which also now records that "three forms" was itself one of the things stated more confidently than measured, the current-drive arm being a fourth correction to the same list. `path.rs` already had the narrow form and is unchanged. The owning crate's own wording moves too, from "the entry is ignored" to "makes no difference to the result", for the same reason the probe stopped saying "without consulting a device": an entry read and discarded is indistinguishable from one never read, and leaving one file saying "ignored" while another says "makes no difference" is the drift this sweep is meant to remove. Separately, `.\CON` was excluded from the exact-output device test on the grounds that the `.` collapses and it would need a special case. It needed one, so it has one. With the case left to the device-NEGATIVE test alone, the only claim made about `.\CON` was that it does not begin `\\.\` -- which an implementation returning it unchanged satisfies, and that too-weak assertion is the exact thing the exact-output test was written to strengthen. Measured: `.\CON` resolves to `\CON`. Verified: 234 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, encoding check over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST-thread-ambient.md | 3 ++- DESIGN-NOTES.md | 6 ++++-- .../DESIGN-NOTES.md | 7 +++++-- .../DESIGN-RATIONALE.md | 8 ++++++-- .../src/full_path/tests.rs | 18 +++++++++++++++--- .../src/request_cost.rs | 6 ++++-- 6 files changed, 36 insertions(+), 12 deletions(-) diff --git a/CHECKLIST-thread-ambient.md b/CHECKLIST-thread-ambient.md index 418c81008..ecdddab31 100644 --- a/CHECKLIST-thread-ambient.md +++ b/CHECKLIST-thread-ambient.md @@ -419,7 +419,8 @@ Entries 5-9 of the audited list. All but the last take a handle, so all but the - [x] **M26.5** -- The `GetFullPathNameW` entry. Does not verify its result: it collapses `.`/`..` lexically and roots most paths that are not fully qualified against process state -- the current - directory, or for a drive-relative path the entry recorded for that drive -- and never expands a + directory, or for a drive-relative path naming another drive the entry recorded for that drive, + while on the current drive that entry makes no difference -- and never expands a drive letter, so it does **not** close the session-relative hazard from M20.1, and its documentation must say which problem it solves and which it leaves standing. diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md index 014c3aad3..2ce928d50 100644 --- a/DESIGN-NOTES.md +++ b/DESIGN-NOTES.md @@ -1463,8 +1463,10 @@ Two corollaries that decide the design: still carries a session-relative reference. (It is not *lexical* as a whole either, which matters elsewhere but not here: it collapses `.`/`..` lexically but roots most paths that are not fully qualified against process state -- the - current directory, or for a drive-relative path the entry recorded for that - drive, which moves independently of it -- and that rooting is the property + current directory, or for a drive-relative path naming a drive OTHER than the + current one the entry recorded for that drive, which moves independently of + it; on the current drive that entry makes no difference and the process + current directory wins -- and that rooting is the property submission-time resolution buys. See `windows-namespace-request-sys`' [DESIGN-NOTES.md](crates/windows-namespace-request-sys/DESIGN-NOTES.md) -> diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 7682726cf..90231d03e 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -554,8 +554,11 @@ current directory, though the entry is used verbatim and an accepted one may name a directory on another drive entirely. That rule has two arms: for a drive other than the current one Windows reads the hidden `=C:` entry recorded for it, which moves independently of the process current directory; -for the *current* drive the entry is ignored and the process current directory -wins. Measured -- setting `=Q:` while the process is on `Q:` changes nothing. So the call is not lexical *as a whole*, and the claim that +for the *current* drive the entry makes no difference to the result and the +process current directory wins. Measured -- setting `=Q:` while the process is +on `Q:` changes nothing. ("Makes no difference" rather than "is ignored" for the +same reason the probe no longer says "without consulting a device": an entry +that is read and then discarded is indistinguishable from one never read.) So the call is not lexical *as a whole*, and the claim that holds unqualified is that it **does not verify what it produces** -- the documented guarantee, and narrower than the "touches no filesystem" an earlier draft claimed. A black-box success cannot establish that broader claim, and it diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 1d631c190..d2cc84698 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -106,8 +106,12 @@ were *enumerations* -- which is the form this kind of error likes. two, and finally three: a relative path takes the current directory, a root-relative path takes only that directory's *root* (which is `\\server\share\` under a UNC current directory, so "current drive" was - wrong), and a drive-relative path takes the entry recorded for that drive - from `=C:`. + wrong), and a drive-relative path naming a drive OTHER than the current one + takes the entry recorded for that drive from `=C:`, while on the current drive + that entry makes no difference and the process directory wins. The + current-drive arm is a fourth correction to the same enumeration, found the + same way as the first three and after them: the count "three forms" was itself + one of the things stated more confidently than measured. - **The device set.** The short-circuit was first described as "exact-match only", which `CON:` disproves; then enumerated as `CON`/`NUL`/`PRN`/`AUX`/ diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index f250f9ef8..2a8445203 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -547,9 +547,6 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { // would satisfy it while rooting nothing. These assert the full resolved // path, so the rooting guarantee is actually covered. // - // `.\CON` is excluded deliberately -- the `.` component collapses, so its - // expected form is the bare name, which the loop below would have to - // special-case. It is covered by the device-negative test instead. let base = current_directory(); let base = base.trim_end_matches('\\'); for name in ["CON.txt", "CONIN", "COM0", "COM10", r"a\CON"] { @@ -560,6 +557,21 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { directory rather than merely avoiding the device namespace" ); } + + // `.\CON` is asserted separately rather than excluded from the loop, which + // is what an earlier version did on the grounds that the `.` collapses and + // the expected form would need a special case. It needs one, so it gets + // one: with the case left to the device-NEGATIVE test alone, the only claim + // made about `.\CON` was that it does not start with `\\.\` -- and an + // implementation returning it unchanged satisfies that, which is precisely + // the too-weak assertion this test exists to strengthen. + assert_eq!( + resolve(r".\CON"), + format!(r"{base}\CON"), + "the `.` collapses and the result roots under the current directory, so \ + a leading `.\\` is enough to take the name out of the device \ + short-circuit without taking it out of ordinary rooting" + ); } /// A directory that exists, is in canonical `X:\...` form, and is neither a diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 45968fd46..3cb4d2f11 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -67,8 +67,10 @@ //! so even perfect remoting would be racy. //! //! That call reads **process state** when it has to root a path -- the current -//! directory, or for a drive-relative path the entry recorded for that drive in -//! the `=C:` environment variables. **Neither sample here is rooted**: both are +//! directory, or for a drive-relative path naming a drive OTHER than the +//! current one the entry recorded for that drive in the `=C:` environment +//! variables -- on the current drive that entry makes no difference and the +//! process directory wins. **Neither sample here is rooted**: both are //! fully qualified, so the rooting is why resolution happens at submission and //! is not what these timings contain. //! From 137b544b60fd68c0befc324fd7505bfa19d23b1d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 16:39:28 -0400 Subject: [PATCH 31/36] fix(namespace-request): close five rounds of vacuous tests and unswept claims Five local review rounds, one per model, squashed. Each reviewer found something the others could not see, and every finding is the branch's own defect class: a claim stated more precisely than the evidence supports. **A fixture that deleted directories it did not create.** `probe_directory` called `create_dir_all` and set `created: true` unconditionally; `Drop` removes the path on that flag. `create_dir_all` succeeds on an existing directory, so the flag recorded a claim nothing checked -- and the name `%TEMP%\wnrs--` is one an interrupted run leaves behind and Windows can hand to a later process on PID reuse. Measured: `create_dir_all` on an existing directory returns `Ok(())` and establishes nothing, while `create_dir` returns `AlreadyExists`. Ownership is now recorded only on a real creation. **Two tests that could not observe what they claimed.** The UNC fixture ran AT `\\localhost\C$`, where rooting `\foo` at the ROOT of the current directory and at the WHOLE current directory give the same answer -- so the only coverage of the UNC branch could not see the branch, for six commits. The child now runs one level down, asserts it is below the share root, and pins the contrasting rule alongside. And `CON:x` was covered only by `!starts_with("\\.\\")`, a predicate the unrooted literal satisfies; it is now asserted against its full rooted result, `\CON:x`. **A generalisation that measurement contradicts.** "Trimming applies per component" was extrapolated from one case. Measured, position decides: the final component loses any run of trailing dots and spaces, an intermediate component loses a single trailing dot and nothing else -- `C:\a...\b` and `C:\a \b` come back unchanged. The preservation cases were entirely absent, so an implementation trimming every component passed the whole test. **Three location kinds a sweep kept missing.** Round 17 corrected "not consulted" by grepping `consulted`, which cannot match the same claim spelled `ignored`: six sites survived, one an assertion failure MESSAGE. A later round fixed a paragraph explaining that a heading reading "not an allocation" would be contradictory, and left that exact HEADING one line above it. A third left the per-drive qualifier out of an INLINE COMMENT sitting between a module doc and emitted output that both had it. All swept, this time across every changed file. **Two properties restated in prose and checked nowhere.** The per-test drive letter lists were literals at six call sites, with disjointness and minimum length asserted only in comments -- and an archived note enumerated five lists after a sixth existed. They are now one table with a test enforcing both properties; the note points at the table instead of listing letters. **A harness that trusted its own marker.** The UNC test treated the presence of an environment variable as proof it was the child it spawned, so a stray value made the parent run the child's assertions in cargo's directory and blame the crate. The marker now carries the directory the parent chose. Also: a test name claiming two things its `ends_with` assertion cannot reach, renamed; `PLANS.md` giving one checklist two incompatible states, corrected; and `request_cost`'s cross-host ratio claim -- real, pre-existing on `main`, outside this diff -- queued as `M2.9` rather than folded in. Every fix verified by sabotage: the instrument was made to fail on purpose, then restored. Every Windows claim measured by P/Invoke rather than argued. Verified: 235 lib + 37 doc + 32 integration tests, the UNC test under --include-ignored, clippy --all-targets, cargo fmt, workflow references, and the encoding gate over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../COMPLETED-CHECKLIST.md | 28 ++- .../DESIGN-RATIONALE.md | 13 +- .../src/full_path.rs | 17 +- .../src/full_path/tests.rs | 206 +++++++++++++++--- .../windows-namespace-request-sys/src/path.rs | 2 +- .../tests/acceptance/operations.rs | 8 +- .../tests/unc_current_directory.rs | 109 +++++++-- crates/windows-platform-probes/CHECKLIST.md | 23 ++ .../COMPLETED-CHECKLIST.md | 5 +- .../src/bin/request_cost.rs | 10 +- .../src/request_cost.rs | 9 +- 11 files changed, 358 insertions(+), 72 deletions(-) diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md index c38e271cc..738624fd5 100644 --- a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -20,7 +20,7 @@ Append-only. Newest groups at the bottom. `GetFullPathNameW` **itself** writes the `=X:` entry -- when resolving for a drive other than the current one, and when the recorded entry is absent or rejected, in which case it is written as the drive root. (An accepted entry is - left alone, and the current-drive form touches nothing.) The code under test + left alone, and the current-drive form writes nothing.) The code under test therefore already mutates that state on the very path these tests exercise, so a test that sets it first introduces no hazard that resolving alone did not, and there was nothing left to decide. Isolation across the tests comes from @@ -39,11 +39,21 @@ Append-only. Newest groups at the bottom. history was briefly wrong: this entry described the selection as a `W`/`U` **pair**, matching the helper as written. That helper validated only its first letter and returned the second unchecked, so the guarantee the pair implied did - not hold. The lists are now three letters each -- `X`/`Y`/`P`, `W`/`U`/`N`, - `V`/`T`/`M`, `R`/`S`/`K`, `G`/`H`/`J` -- and every candidate is checked against - both the current drive and the probe drive.)* - - The sibling test - `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory` - keeps its weaker form and now says so: without controlling the entry it can - only bound the arm. + not hold. The lists are now three letters each, every candidate is checked + against both the current drive and the probe drive, and the lists themselves + live in one table in [tests.rs](src/full_path/tests.rs) with a test enforcing + that they stay disjoint and long enough. This note deliberately does NOT + enumerate them: an earlier version did, and named five lists after a sixth had + been added -- so a reader picking letters for a seventh would have consulted + an inventory missing three of the letters already in use. The table is the + inventory.)* + + The sibling test keeps its weaker form and now says so: without controlling + the entry it can only bound the arm. + + *(Later correction: that sibling was named + `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory`, + and the name claimed two things its assertions do not reach -- `ends_with` + accepts any base, including the process directory, and an accepted entry is + used verbatim so the result need not be on that drive at all. It is now + `a_drive_relative_path_carries_its_component_and_the_current_drive_uses_the_process_directory`.)* diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index d2cc84698..f636d36f7 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -119,8 +119,17 @@ were *enumerations* -- which is the form this kind of error likes. spellings `COM\u{00b9}`, `COM\u{00b2}`, `COM\u{00b3}` and their `LPT` equivalents. Those are exactly the members a hand-written denylist misses, and the documentation asserted a closed list without them until a review measured - it. The tests in [tests.rs](src/full_path/tests.rs) now pin every documented spelling so the - next omission fails CI instead of a review. + it. The tests in [tests.rs](src/full_path/tests.rs) now pin every documented + spelling so the next omission fails CI instead of a review -- each positive + spelling against the device path it produces, and each negative one against + its full rooted result. + + That last distinction was itself a later correction, and it is the reason the + sentence now says which KIND of pinning each case gets. The claim "pins every + documented spelling" was written while one negative spelling, `CON:x`, was + covered only by `!starts_with("\\.\\")` -- a predicate the unrooted literal + satisfies. So the claim was true of the list and false of the strength, which + is the same shape as the trimming generalisation two entries below. ### The measurement that was itself unmeasured diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index d685f194f..ed8882b04 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -28,10 +28,15 @@ //! entry exists: //! //! 1. It rewrites the string. `.` and `..` are collapsed, `/` becomes `\`, and -//! trailing dots and spaces are trimmed. This part *is* lexical -- pure -//! string work over the input, reading no process state. `C:\a\..\b` becomes -//! `C:\b` whatever the current directory happens to be, and whether or not -//! `C:\a` exists. +//! trailing dots and spaces are trimmed -- but **not uniformly across +//! components**, and an earlier revision of this list said so without +//! qualification. Measured: the *final* component loses any run of trailing +//! dots and spaces (`C:\name...` and `C:\name ` both become `C:\name`), +//! while an *intermediate* component loses a single trailing dot and nothing +//! else -- `C:\a.\b` becomes `C:\a\b`, but `C:\a...\b` and `C:\a \b` are +//! returned unchanged. This part *is* lexical -- pure string work over the +//! input, reading no process state. `C:\a\..\b` becomes `C:\b` whatever the +//! current directory happens to be, and whether or not `C:\a` exists. //! 2. It **roots** a path that is not fully qualified, using mutable process //! state -- and on one form it also *changes* that state. There are three //! such forms: @@ -44,8 +49,8 @@ //! is why this says root and not drive. //! * A drive-relative path like `C:foo` is rooted at the entry Windows //! keeps for that drive in the hidden `=C:` environment variables. For -//! the *current* drive that entry is ignored and the process current -//! directory wins. +//! the *current* drive that entry makes no difference to the result and +//! the process current directory wins. //! //! **A whole class of input short-circuits both.** When the input names a //! legacy device and nothing else, it resolves into the device namespace and is diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 2a8445203..85bb551f3 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -374,21 +374,34 @@ fn a_fully_qualified_path_is_unaffected_by_the_current_directory() { } #[test] -fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() { - // The third rooting form. A drive-relative path is rooted at *that drive's* - // current directory -- and the rule has two arms, which is the part that - // gets missed: +fn a_drive_relative_path_carries_its_component_and_the_current_drive_uses_the_process_directory() { + // **The name says what the two assertions reach, and an earlier one did + // not.** This was + // `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory`, + // which claims more than anything here shows, in two separate ways. The + // other-drive assertion is `ends_with("\\foo")`, which accepts ANY base + // including the process directory -- so it cannot say "not the process + // directory". And "rooted at that drive" is not even true in general: an + // accepted entry is used verbatim and may name a directory on a different + // drive entirely, which is the sibling test's whole point. + // + // The body already said it only bounds the arm. The name did not, and the + // name is what a reader takes away -- the same defect as the test called + // `..._is_neither_consulted_nor_rewritten` before it was renamed. + // + // The third rooting form, with two arms, which is the part that gets + // missed: // // * For a drive OTHER than the current one, Windows reads the hidden // `=X:` entry recorded for it. - // * For the CURRENT drive the entry is ignored entirely and the process - // current directory wins. Measured: setting `=Q:` while the process is - // on `Q:` changes nothing. + // * For the CURRENT drive the entry makes no difference to the result and + // the process current directory wins. Measured: setting `=Q:` while the + // process is on `Q:` changes nothing. // // **This test does not mutate `=X:`, but the call it exercises may.** // Measured: resolving `X:foo` for a non-current drive checks that drive's // entry and WRITES it to `X:\` when the entry is absent or rejected. An - // accepted entry is left alone, and the current-drive form touches nothing + // accepted entry is left alone, and the current-drive form writes nothing // -- so this is not "every resolution", but it does mean an ordinary host // with no entry has one written merely by running this test. That is // a property of the call, documented in the module doc; it is noted here so @@ -407,7 +420,7 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() // The other-drive arm needs no drive letter from the current directory -- // under a UNC current directory every letter is "other" -- so it runs // unconditionally and this test never degenerates to a silent skip. - let other = probe_drive_from(&['X', 'Y', 'P'], None); + let other = probe_drive_from(probe_drives::ROOTED_AT_THAT_DRIVE, None); // This test controls no entry, but the CALL does: resolving for a // non-current drive writes `=X:` whenever the recorded entry is absent or @@ -418,11 +431,6 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() let _restore = BorrowedDriveEntry::take(other); let resolved = resolve(&format!("{other}:foo")); - // Compared case-insensitively, because the case is not this test's to - // choose: the letter comes back as Windows recorded it, not as it was - // typed. This host returns `q:\...` for an uppercase `Q:` input, because the - // shell was started with a lowercase `cd`. An earlier version compared bytes - // and would have failed on a drive visited in lowercase. // Only what is invariant without controlling the entry. An earlier version // required the result to start with `X:\`, which the verbatim rule breaks; // its replacement compared against the current directory, which `other` @@ -440,8 +448,8 @@ fn a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory() assert_eq!( resolve(&format!("{drive}:foo")), format!(r"{}\foo", cwd.trim_end_matches('\\')), - "on the current drive, the per-drive entry is ignored and the \ - process directory is used" + "on the current drive, the per-drive entry makes no difference to \ + the result and the process directory is used" ); } } @@ -518,23 +526,53 @@ fn the_current_drives_entry_does_not_affect_resolution_and_is_not_rewritten() { } #[test] -fn trailing_dots_and_spaces_are_trimmed_from_ordinary_components() { +fn trailing_dot_and_space_trimming_differs_between_final_and_intermediate_components() { // The module doc says the rewrite trims trailing dots and spaces. Until now // that was only exercised through a final `.` component (which is the // separate `.`-collapsing rule) and through device spellings (which take // the short-circuit and never reach the ordinary path). Neither pins this. // - // Every case measured before being written down. + // **An earlier version of this test generalised the rule, and the + // generalisation is false.** It carried one intermediate case, `C:\a.\b` -> + // `C:\a\b`, under the comment "trimming applies per component, not only at + // the end of the path". Measured, a component's position decides what it + // loses: + // + // final: C:\name... -> C:\name (a RUN of dots goes) + // C:\name -> C:\name (trailing spaces go) + // intermediate: C:\a.\b -> C:\a\b (ONE trailing dot goes) + // C:\a.b.\c -> C:\a.b\c (inner dots are not special) + // C:\a...\b -> unchanged (a run does NOT go) + // C:\a \b -> unchanged (a space does NOT go) + // C:\a. \b -> unchanged + // + // **The preservation cases are the load-bearing half**, and their absence + // is what let the wrong generalisation stand: with only the transforming + // inputs, an implementation trimming every component's trailing dots and + // spaces satisfies the entire test while being wrong about three of the + // five intermediate spellings. A reviewer demonstrated exactly that against + // the previous seven assertions. + // + // Every case measured before being written down -- which was also claimed + // last time, and was true of the cases present. What was not measured was + // the sentence generalising them. for (input, expected) in [ + // The final component loses any run of trailing dots and spaces. (r"C:\name.", r"C:\name"), (r"C:\name ", r"C:\name"), (r"C:\name...", r"C:\name"), (r"C:\name ", r"C:\name"), (r"C:\name. ", r"C:\name"), - // Trimming applies per component, not only at the end of the path. - (r"C:\a.\b", r"C:\a\b"), // An extension is not special: the trailing dot goes, the rest stays. (r"C:\name.txt.", r"C:\name.txt"), + // An intermediate component loses a single trailing dot ... + (r"C:\a.\b", r"C:\a\b"), + (r"C:\a.b.\c", r"C:\a.b\c"), + // ... and nothing else. These are the cases that fail the broad rule. + (r"C:\a...\b", r"C:\a...\b"), + (r"C:\a \b", r"C:\a \b"), + (r"C:\a. \b", r"C:\a. \b"), + (r"C:\a b \c", r"C:\a b \c"), ] { assert_eq!(resolve(input), expected, "trimming {input:?}"); } @@ -549,7 +587,13 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { // let base = current_directory(); let base = base.trim_end_matches('\\'); - for name in ["CON.txt", "CONIN", "COM0", "COM10", r"a\CON"] { + // `CON:x` is here because it was NOT, and the negative test alone cannot + // carry it: that test asserts only `!starts_with("\\\\.\\")`, which the + // unrooted literal `CON:x` satisfies. So the one spelling whose rooting is + // least obvious -- a device word followed by a colon, where `CON:` itself + // IS a device -- was the one nothing pinned. Measured: `\CON:x`, with + // the colon carried through untouched. + for name in ["CON.txt", "CONIN", "COM0", "COM10", r"a\CON", "CON:x"] { assert_eq!( resolve(name), format!(r"{base}\{name}"), @@ -662,11 +706,28 @@ fn probe_directory(tag: &str) -> ProbeDir { let temp = std::env::temp_dir(); if canonical_drive_rooted(&temp) { let path = temp.join(format!("wnrs-{}-{tag}", std::process::id())); - std::fs::create_dir_all(&path).expect("create the probe directory"); - return ProbeDir { - path, - created: true, + + // `create_dir`, not `create_dir_all`, and the difference is ownership + // rather than parents. `create_dir_all` SUCCEEDS on a directory that + // already exists, so setting `created` after it recorded a claim this + // fixture had not established -- and `Drop` then removes the path on + // the strength of that claim. The name is `%TEMP%\wnrs--`, + // which an interrupted earlier run leaves behind and which Windows can + // hand back to a later process when it reuses the PID. The blast radius + // is small, because `remove_dir` refuses a non-empty directory -- but + // "small" is not the point. Deleting something on an ownership claim + // nothing checked is the same defect as asserting a mechanism nothing + // measured, and this file exists to stop doing that. + // + // An existing directory is still perfectly usable as a probe; it is + // just not ours to remove. + let created = match std::fs::create_dir(&path) { + Ok(()) => true, + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => false, + Err(e) => panic!("create the probe directory {}: {e}", path.display()), }; + + return ProbeDir { path, created }; } // Not creating anything here, so no write permission is needed on a host @@ -707,6 +768,80 @@ fn probe_directory(tag: &str) -> ProbeDir { created: false, } } +/// The candidate drive letters, one list per test that mutates a `=X:` entry. +/// +/// **Centralised so the properties these tests depend on are CHECKED rather +/// than restated.** Both were previously prose -- a doc comment saying the +/// lists are disjoint, and an archived note enumerating them -- and prose +/// drifted: the archive named five lists after the sixth had been added, so a +/// reader picking letters for a seventh would have consulted an inventory +/// missing three of the eighteen letters already in use. Nothing checked +/// either claim, because nothing could: the lists were literals at six call +/// sites with no table to read. +/// +/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] now reads +/// this table, so adding a list that collides -- or one too short for +/// [`probe_drive_from`]'s guarantee -- fails a test instead of a review. +mod probe_drives { + pub const ROOTED_AT_THAT_DRIVE: &[char] = &['X', 'Y', 'P']; + pub const VERBATIM_ENTRY: &[char] = &['W', 'U', 'N']; + pub const REJECTED_ENTRY: &[char] = &['V', 'T', 'M']; + pub const LONG_ENTRY: &[char] = &['R', 'S', 'K']; + pub const BORROW_GUARD: &[char] = &['G', 'H', 'J']; + pub const EMPTY_VS_ABSENT: &[char] = &['E', 'F', 'B']; + + /// Every list above. A new list that is not added here is not covered by + /// the disjointness test, so keep them together. + pub const ALL: &[(&str, &[char])] = &[ + ("ROOTED_AT_THAT_DRIVE", ROOTED_AT_THAT_DRIVE), + ("VERBATIM_ENTRY", VERBATIM_ENTRY), + ("REJECTED_ENTRY", REJECTED_ENTRY), + ("LONG_ENTRY", LONG_ENTRY), + ("BORROW_GUARD", BORROW_GUARD), + ("EMPTY_VS_ABSENT", EMPTY_VS_ABSENT), + ]; +} + +#[test] +fn the_probe_drive_candidate_lists_are_disjoint_and_large_enough() { + for (name, list) in probe_drives::ALL { + // `probe_drive_from` excludes at most two letters -- the current drive + // and the probe directory's drive -- so three candidates guarantee a + // survivor. This is the premise of the panic in that function, checked + // here rather than left to the caller as the doc comment used to. + assert!( + list.len() >= 3, + "{name} has {} candidates, and at most two can be excluded, so \ + fewer than three cannot guarantee a survivor", + list.len() + ); + + let mut seen = list.to_vec(); + seen.sort_unstable(); + seen.dedup(); + assert_eq!(seen.len(), list.len(), "{name} repeats a letter"); + } + + for (a_name, a) in probe_drives::ALL { + for (b_name, b) in probe_drives::ALL { + if a_name == b_name { + continue; + } + let shared: Vec = a + .iter() + .copied() + .filter(|c| b.iter().any(|d| d.eq_ignore_ascii_case(c))) + .collect(); + assert!( + shared.is_empty(), + "{a_name} and {b_name} share {shared:?}, so the two tests can \ + select the same drive and race under libtest's \ + thread-per-test model" + ); + } + } +} + /// A drive letter to probe with, drawn from `candidates` and guaranteed to be /// neither the current drive nor `avoid`. /// @@ -716,13 +851,16 @@ fn probe_directory(tag: &str) -> ProbeDir { /// excluded the caller could still be handed the current drive. Measured: with /// the process on `U:` and `%TEMP%` on a `subst`-ed `W:`, the verbatim test /// selected `U` and then asserted the *other-drive* contract while exercising -/// the *current-drive* arm, which is the one case where the entry is ignored. +/// the *current-drive* arm, which is the one case where the entry makes no +/// difference to the result. /// It failed, but the mode is worse than a failure: the helper's own doc /// promised a guarantee it never enforced. /// -/// Three candidates against at most two exclusions, so one always survives; the -/// assertion is there because that argument is about the caller's list and -/// nothing here can check it. +/// Three candidates against at most two exclusions, so one always survives. The +/// panic remains because that argument is about the caller's list, which this +/// function cannot see -- but the argument is no longer only an argument: +/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] checks it +/// against every list in [`probe_drives`]. /// /// Callers pass disjoint lists, so no two tests can select the same letter and /// race under libtest's thread-per-test model. @@ -916,7 +1054,7 @@ fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one( // cross-drive property below would go unexercised. let probe_dir = probe_directory("verbatim"); let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_from(&['W', 'U', 'N'], probe_dir.drive()); + let drive = probe_drive_from(probe_drives::VERBATIM_ENTRY, probe_dir.drive()); let _restore = BorrowedDriveEntry::take(drive); assert_ne!( @@ -999,7 +1137,7 @@ fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { // Pinned because the distinction is not guessable and the doc asserts it. let probe_dir = probe_directory("shape"); let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_from(&['V', 'T', 'M'], probe_dir.drive()); + let drive = probe_drive_from(probe_drives::REJECTED_ENTRY, probe_dir.drive()); let _restore = BorrowedDriveEntry::take(drive); // The control: this exact directory IS accepted in canonical form, so the @@ -1082,7 +1220,7 @@ fn a_long_drive_entry_round_trips_through_the_reader() { // 1200 units is comfortably past the 256 the reader starts with and past // the 1024 an earlier fixed-size version used, and is a legitimate value: a // per-drive entry is a path, and long paths reach far beyond this. - let drive = probe_drive_from(&['R', 'S', 'K'], None); + let drive = probe_drive_from(probe_drives::LONG_ENTRY, None); let _restore = BorrowedDriveEntry::take(drive); let long = format!(r"C:\{}", "a".repeat(1200)); @@ -1102,7 +1240,7 @@ fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { // takes it -- so trusting it would mean shipping an untested defence // against the exact failure it is there for. This takes the path on // purpose. - let drive = probe_drive_from(&['G', 'H', 'J'], None); + let drive = probe_drive_from(probe_drives::BORROW_GUARD, None); // The outer guard is not ceremony. This test installs a sentinel to watch // the inner guard put back, and without it that install would destroy @@ -1145,7 +1283,7 @@ fn an_empty_drive_entry_is_distinguished_from_an_absent_one() { // The consequence is what makes it worth a test rather than a fix: with the // two collapsed, restoring an inherited EMPTY entry deletes it, so the guard // written to preserve process state destroys it in exactly one case. - let drive = probe_drive_from(&['E', 'F', 'B'], None); + let drive = probe_drive_from(probe_drives::EMPTY_VS_ABSENT, None); let _outer = BorrowedDriveEntry::take(drive); set_drive_entry(drive, Some("")); diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index ff2d62634..60ee5a12c 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -25,7 +25,7 @@ //! naming another drive the entry recorded for it in the `=C:` environment //! variables -- used verbatim when accepted, so it need not even be on that //! drive, and replaced by the drive root when not (for the current drive the -//! process directory is used and the entry is ignored). It is +//! process directory is used and the entry makes no difference). It is //! therefore not a lexical call as a whole, which is what makes resolving on //! the submitting thread meaningful. //! diff --git a/crates/windows-namespace-request-sys/tests/acceptance/operations.rs b/crates/windows-namespace-request-sys/tests/acceptance/operations.rs index 513c15c2c..3ff154fc5 100644 --- a/crates/windows-namespace-request-sys/tests/acceptance/operations.rs +++ b/crates/windows-namespace-request-sys/tests/acceptance/operations.rs @@ -275,7 +275,13 @@ fn watcher_getvolumeinformationbyhandle_shape_is_reachable() { #[test] fn enumeration_getfullpathname_shape_is_reachable() { - // path.rs:149 -- resolution with a null file-part out-param. + // enumeration path.rs:149 -- resolution with a null file-part out-param. + // Qualified by crate because this is the one consumer filename that also + // exists in THIS crate, and a reviewer resolved it against the local + // `src/path.rs` -- where line 149 is unrelated -- and reported the citation + // as stale. It is not. The neighbouring "watcher directory.rs" / + // "enumeration native.rs" citations already use this form for the same + // reason. let resolved = ResolveFullPath::new(Wtf16String::from(r"C:\Windows\System32\..\.\Temp")) .perform() .expect("the enumeration crate's resolution shape") diff --git a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs index 6570ee505..6d72a4939 100644 --- a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs +++ b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs @@ -24,7 +24,19 @@ use windows_namespace_request_sys::ResolveFullPath; use wtf_string::Wtf16String; -/// Set on the child, absent on the parent. Presence, not value, is the signal. +/// Set on the child, absent on the parent, and carrying the directory the +/// parent placed it in. +/// +/// **The value is load-bearing, and an earlier version ignored it.** Presence +/// alone was the signal, so a variable of this name already in the environment +/// -- a leftover from debugging the child by hand, say -- made the PARENT take +/// the child branch, in the ordinary drive-rooted directory cargo starts it in. +/// The test then failed on the UNC precondition, blaming the crate for an +/// environment collision. +/// +/// Carrying the intended directory fixes that and buys a real check besides: +/// the child now confirms it is where the parent put it, rather than inferring +/// it from the shape of whatever directory it happens to hold. const CHILD_MARKER: &str = "WNRS_UNC_CURRENT_DIRECTORY_CHILD"; /// The administrative share, which is the only UNC path a test can rely on @@ -32,6 +44,22 @@ const CHILD_MARKER: &str = "WNRS_UNC_CURRENT_DIRECTORY_CHILD"; /// must not assume it has. const UNC_ROOT: &str = r"\\localhost\C$"; +/// The child runs in a SUBDIRECTORY of the share, not at its root, and the +/// distinction is the whole test. +/// +/// **At the share root the two rooting rules coincide**, so the first version of +/// this test could not tell them apart. Rooting `\foo` at the *root of* the +/// current directory and rooting it at the *whole* current directory both give +/// `\\localhost\C$\foo` when the current directory IS the root -- which is the +/// exact distinction the module doc cites this test as pinning. Measured one +/// level down, they separate: `\foo` gives `\\localhost\C$\foo` while `foo` +/// gives `\\localhost\C$\Windows\foo`. +/// +/// Candidates rather than one name, because the share is `C:` and Windows need +/// not be installed there. Any enterable subdirectory works; these three are +/// the ones a Windows system drive has. +const UNC_SUBDIRECTORY_CANDIDATES: &[&str] = &["Windows", "Users", "ProgramData"]; + const TEST_NAME: &str = "a_root_relative_path_takes_the_share_root_under_a_unc_current_directory"; // Ignored by default and run explicitly in CI, which is this repository's @@ -52,8 +80,12 @@ const TEST_NAME: &str = "a_root_relative_path_takes_the_share_root_under_a_unc_c #[test] #[ignore = "needs a reachable administrative share for a UNC current directory; run in CI with --include-ignored"] fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { - if std::env::var_os(CHILD_MARKER).is_some() { - assert_root_relative_takes_the_share_root(); + if let Some(placed_at) = std::env::var_os(CHILD_MARKER) { + let placed_at = placed_at + .to_str() + .expect("the parent passes a UTF-8 directory") + .to_owned(); + assert_root_relative_takes_the_share_root(&placed_at); return; } @@ -69,6 +101,19 @@ fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { behaviour under test" ); + let working_directory = UNC_SUBDIRECTORY_CANDIDATES + .iter() + .map(|name| format!(r"{UNC_ROOT}\{name}")) + .find(|path| std::path::Path::new(path).is_dir()) + .unwrap_or_else(|| { + panic!( + "none of {UNC_SUBDIRECTORY_CANDIDATES:?} exists under {UNC_ROOT}, \ + so the child cannot be placed BELOW the share root -- and at the \ + root the two rooting rules give the same answer, which is what \ + this test exists to separate" + ) + }); + let exe = std::env::current_exe().expect("the test binary knows its own path"); let status = std::process::Command::new(exe) .arg("--exact") @@ -78,33 +123,54 @@ fn a_root_relative_path_takes_the_share_root_under_a_unc_current_directory() { // would report success having measured nothing at all. The test it is // told to run is the ignored one, so the filter alone is not enough. .arg("--include-ignored") - .env(CHILD_MARKER, "1") - .current_dir(UNC_ROOT) + .env(CHILD_MARKER, &working_directory) + .current_dir(&working_directory) .status() .expect("re-execute this test binary with a UNC current directory"); assert!( status.success(), - "the child, running with {UNC_ROOT} as its current directory, did not \ - pass: {status}" + "the child, running with {working_directory} as its current directory, \ + did not pass: {status}" ); } -fn assert_root_relative_takes_the_share_root() { +fn assert_root_relative_takes_the_share_root(placed_at: &str) { let cwd = std::env::current_dir().expect("the child has a current directory"); let cwd = cwd.to_str().expect("the current directory is UTF-8"); - // Proving the child is where the parent put it. Without this the test could - // pass having silently inherited an ordinary drive-rooted directory -- - // `cmd.exe`, for one, refuses a UNC current directory and falls back to the - // Windows directory, so a child that is not a plain Win32 process can lose - // it without saying so. + // Proving the child is where the parent put it, against the parent's own + // record rather than against the shape of the directory. Two things can go + // wrong and this separates them: a process can LOSE a UNC current directory + // (`cmd.exe` refuses one and falls back to the Windows directory), and this + // branch can be entered by something that is not the parent's child at all, + // when `CHILD_MARKER` is already set in the environment. + assert!( + cwd.eq_ignore_ascii_case(placed_at.trim_end_matches('\\')), + "the child is at {cwd}, not the {placed_at} the parent chose. If no \ + parent placed it there, {CHILD_MARKER} is set in this environment and \ + should be unset -- that is a collision, not a defect in the crate" + ); + assert!( cwd.starts_with(r"\\"), "precondition: the child must actually hold a UNC current directory, \ not a drive-rooted one: {cwd}" ); + // **And below the share root**, which the first version of this test did + // not check because it did not need to -- it ran AT the root, where the two + // rules being separated give the same answer. This assertion is the one + // that keeps the test from going vacuous again if the parent's directory + // choice ever regresses. + assert_ne!( + cwd.trim_end_matches('\\'), + UNC_ROOT, + "precondition: the child must be BELOW the share root. At the root, \ + rooting at the root of the current directory and rooting at the whole \ + current directory agree, so neither assertion below can tell them apart" + ); + let resolved = ResolveFullPath::new(Wtf16String::from(r"\foo")) .perform() .expect("a root-relative path resolves"); @@ -116,4 +182,21 @@ fn assert_root_relative_takes_the_share_root() { under a UNC current directory is the share root -- so \"the current \ drive\", as a draft of the module doc said, has no referent here" ); + + // The contrast, and the reason the assertion above means anything. An + // ordinary relative path takes the WHOLE current directory, so the two + // resolutions differ by everything below the share root. Without this, a + // resolver that ignored the leading separator entirely would still satisfy + // the equality above whenever the child happened to sit at the root. + let relative = ResolveFullPath::new(Wtf16String::from("foo")) + .perform() + .expect("a relative path resolves"); + + assert_eq!( + relative.to_string_lossy(), + format!(r"{cwd}\foo"), + "an ordinary relative path takes the whole current directory, not its \ + root -- the two rules are distinguishable here and identical at the \ + share root" + ); } diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 9177d2033..6aebfbfc6 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -126,3 +126,26 @@ speculative list to extend by imagination -- a fourth is added when a fourth con operation completed successfully" under a message saying something failed. And attach it only to a condition that is genuinely an OS failure: a call that returned a size rather than an error should not carry one, since `GetLastError` says nothing about it. + +- [ ] **M2.9** -- Stop `request_cost` calling a cross-host ratio "the finding". + + [src/request_cost.rs](src/request_cost.rs) ends its module doc with "Absolute values are + host-specific; the **ratios against the doorbell and the atomic** are the finding." The ratios that + [src/bin/request_cost.rs](src/bin/request_cost.rs) actually prints divide THIS host's measurement by + `DOORBELL_NS_REFERENCE` / `ATOMIC_NS_REFERENCE`, which are constants measured on the Snapdragon X2 + development machine. A ratio with this host's numerator and another host's denominator is neither a + same-host ratio nor a portable finding, and the emitted report says as much two lines later: + "re-read that probe on this host before trusting them". So the module doc promotes to "the finding" + exactly the number its own output tells the reader not to trust. + + **Pre-existing, and deliberately not fixed in the `GetFullPathNameW` peel (PR #86) that found it.** + It arrived in `ae1e39f`, is already on `main`, and is outside that branch's diff; folding it in + would have put an unrelated behavioural change into a documentation peel that had already run to + nineteen review rounds. + + The fix is a decision, not a sweep, which is why this is queued rather than taken: either compute + both figures on the same host and run (the probe would have to measure the doorbell itself, or read + a companion artifact), or keep the fixed references and demote them in the prose from "the finding" + to a labelled cross-host comparison. The first is more useful and more work; the second is honest + and cheap. Same defect class as PR #86's subject -- a claim stated more strongly than the evidence + supports -- so whichever is chosen, the wording has to end up matching what the numbers can carry. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index cb7807204..98c8e638f 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -126,7 +126,10 @@ request as it was written, and quotes the module doc as it read before the corre *(Later correction: the second half stood, the first did not. "Touches no filesystem" was measured false while carrying out this item -- resolving `X:foo` for a non-current drive distinguishes an existing directory from an existing file from a missing one, and rewrites the `=X:` entry. What - Microsoft documents is only that the call does not VERIFY its result. See + Microsoft documents is only that the call does not VERIFY its result. Two smaller things in the + paragraph above also turned out to be stated too broadly: the per-drive entry is consulted for a + drive OTHER than the current one, and on the current drive it makes no difference to the result -- + and "reads" is a mechanism word that observation cannot reach either way. See [../windows-namespace-request-sys/DESIGN-NOTES.md](../windows-namespace-request-sys/DESIGN-NOTES.md) -> `D-18`.)* diff --git a/crates/windows-platform-probes/src/bin/request_cost.rs b/crates/windows-platform-probes/src/bin/request_cost.rs index 1e52585ee..58a74b74f 100644 --- a/crates/windows-platform-probes/src/bin/request_cost.rs +++ b/crates/windows-platform-probes/src/bin/request_cost.rs @@ -290,8 +290,14 @@ fn render(out: &mut dyn std::fmt::Write) { // The first was wrong because a timing loop cannot establish a kernel // transition. The second, which replaced it, is wrong for a symmetric // reason: `GetFullPathNameW` consults the process current directory, - // and for a drive-relative path the per-drive current directory held in - // the `=C:` environment variables, so it is not pure string work. A + // and for a drive-relative path naming a drive OTHER than the current + // one the per-drive current directory held in the `=C:` environment + // variables, so it is not pure string work. (On the current drive that + // entry makes no difference to the result; whether it is read is not + // observable and is not claimed. The qualifier was missing here for two + // rounds after the module doc above gained it -- the sweep reached the + // module doc and the emitted report and stopped short of this inline + // comment.) A // genuinely lexical canonicalizer is a different call // (`PathCchCanonicalizeEx`), and it is deliberately NOT the one // `prepare` wants -- resolving against the CWD at submission is the diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 3cb4d2f11..2e33dd414 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -55,12 +55,15 @@ //! copy. So "what does a request cost" is not only an allocation question, and //! measuring only the path would understate it. //! -//! # Preparing a path is a Win32 call, not an allocation +//! # Preparing a path is a Win32 call as well as an allocation //! //! This probe was written expecting `prepare` to be an allocation and a copy. //! It is not *only* that -- the qualifier matters, because the decomposition -//! below charges the measured gap with one net allocation, and a heading saying -//! flatly "not an allocation" would contradict it: it calls +//! below charges the measured gap with one net allocation, so a heading saying +//! flatly "not an allocation" contradicts it. This heading said exactly that +//! for two rounds after the sentence you are reading was written to explain why +//! it must not: the correction went into the prose and stopped one line short +//! of the heading above it. What `prepare` additionally does is call //! **`GetFullPathNameW`** to resolve the path against the //! process working directory, because [the namespace session] settled that the //! path is resolved at submission -- the process CWD is mutable by any thread, From 41988625eba4ace6ef31b9a9143d985753e97a88 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 16:55:48 -0400 Subject: [PATCH 32/36] fix(namespace-request): NUL is the one device word a path in front of it does not save Copilot asked for the documented `\CON` case to be pinned, since the module doc states it and no test covered it. Writing that assertion as the general rule the request was phrased in -- "a root-relative device word roots at the current directory's root" -- would have pinned a FALSE claim. Measured across all eight accepted device names, `NUL` alone short-circuits as the final component of any path, however much path precedes it: input CON NUL X \\.\CON \\.\NUL \X Q:\CON \\.\NUL .\X Q:\...\CON \\.\NUL a\X Q:\...\a\CON \\.\NUL C:\X C:\CON \\.\NUL X.txt rooted rooted X:x rooted rooted `PRN`, `AUX`, `CONIN$`, `CONOUT$`, `COM1` and `LPT1` all behave as `CON` does. So a FULLY QUALIFIED path can still resolve to a device -- `prepare(r"C:\NUL")` is `\\.\NUL` -- and a caller treating a rooted path as evidence that a name refers to a file on that volume is wrong for this one name. Only a suffix takes it out. **Why this was invisible for twenty-two rounds.** Every earlier finding in this branch was a claim generalised from too few observations. This one was generalised from one MEMBER to a set: the doc enumerates the device set, then states the negative forms in terms of `CON`. The membership of that set was audited twice -- it is where the superscript `COM` spellings were found -- and the assumption that its members behave alike was never checked at all. Pinned by `nul_is_the_one_device_word_a_path_around_it_does_not_save`, which asserts the seven conforming names in both distinguishing forms and the four `NUL` spellings that reach the device. `\CON` is now asserted against its full rooted result, which is what was asked for. Verified by sabotage: replacing the NUL expectation with the general rule -- the exact rule the request implied -- fails on `\NUL`, `\\.\NUL` against `q:\NUL`. Swept into every restatement rather than the one file the finding named: `full_path.rs` (with the table), `path.rs`'s public summary, `D-18`, and the Tier 2 device-set entry. **Declined, with the measurement.** The second comment read the UNC assertion's case-sensitive comparison as a portability bug -- a host recording `LOCALHOST` would fail against the lowercase literal. It cannot: both sides descend from `UNC_ROOT`, since the parent builds the child's working directory from it. Measured -- a process started with `\\LOCALHOST\C$\Windows` reports exactly that from `current_dir()`, and likewise for `\\localhost\c$\Windows`, so Windows preserves the spelling it is given rather than canonicalising it. Recorded in place, with why the neighbouring precondition is case-insensitive for a different reason. Verified: 236 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, encoding over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-NOTES.md | 10 +++ .../DESIGN-RATIONALE.md | 20 +++++- .../src/full_path.rs | 21 +++++++ .../src/full_path/tests.rs | 61 +++++++++++++++++++ .../windows-namespace-request-sys/src/path.rs | 5 +- .../tests/unc_current_directory.rs | 14 +++++ 6 files changed, 128 insertions(+), 3 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-NOTES.md b/crates/windows-namespace-request-sys/DESIGN-NOTES.md index 90231d03e..6353f89c5 100644 --- a/crates/windows-namespace-request-sys/DESIGN-NOTES.md +++ b/crates/windows-namespace-request-sys/DESIGN-NOTES.md @@ -573,6 +573,16 @@ clause cannot be stated unconditionally. The form is looser than exact match: `CON:x` root normally. This matters to a crate that prepares paths on a caller's behalf: `prepare("CON")` returns a device. +**`NUL` is the one member that clause does not describe**, and the clause was +written from `CON` and stated of the whole set. Measured across all eight +accepted names: seven root normally once anything precedes them, and `NUL` +short-circuits as the final component of any path -- `\NUL`, `.\NUL`, `a\NUL` +and `C:\NUL` all reach `\\.\NUL`, where the `CON` spellings root. Only a suffix +(`NUL.txt`, `NUL:x`) takes it out. So a **fully qualified** path can still +resolve to a device, which is the part a caller needs: a rooted result is not by +itself evidence that a name refers to a file on that volume. Pinned by +`nul_is_the_one_device_word_a_path_around_it_does_not_save`. + Keeping the two halves apart matters, because the decision below turns on the rooting half alone. Saying the call resolves `.`/`..` "against the current directory" -- as a first draft of this correction did -- attributes process-state diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index f636d36f7..5754f8b7e 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -124,8 +124,24 @@ were *enumerations* -- which is the form this kind of error likes. spelling against the device path it produces, and each negative one against its full rooted result. - That last distinction was itself a later correction, and it is the reason the - sentence now says which KIND of pinning each case gets. The claim "pins every + **A third correction, and the one that shows why "the device set" was the + wrong unit all along.** A review asked for the documented `\CON` case to be + pinned, since the doc stated it and no test covered it. Writing that + assertion as the general rule the request was phrased in -- "a root-relative + device word roots at the current directory's root" -- would have pinned a + false claim. Measured across all eight accepted names, `NUL` alone + short-circuits as the final component of ANY path: `\NUL`, `.\NUL`, `a\NUL` + and `C:\NUL` all reach `\\.\NUL`, while every `CON`, `PRN`, `AUX`, `CONIN$`, + `CONOUT$`, `COM1` and `LPT1` spelling roots. So a fully qualified path can + still name a device. + + Every earlier entry here is a claim generalised from too few observations. + This one was generalised from one MEMBER to a set, which is the same error in + a dimension nobody had checked -- the enumeration of the set was audited + twice, and the assumption that its members behave alike never was. + + That distinction between kinds of pinning was itself a later correction, and + it is the reason the sentence now says which KIND of pinning each case gets. The claim "pins every documented spelling" was written while one negative spelling, `CON:x`, was covered only by `!starts_with("\\.\\")` -- a predicate the unrooted literal satisfies. So the claim was true of the list and false of the strength, which diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index ed8882b04..77291249d 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -64,6 +64,27 @@ //! around it (`CON.txt`, `a\CON`, `.\CON`, `CON:x`), and `\CON`, which //! becomes `Q:\CON` for a current directory on `Q:`. //! +//! **`NUL` does not follow that second list, and it is the only member that +//! does not.** The paragraph above was written from `CON` and stated of the +//! whole set; measured across all eight accepted names, seven behave as it +//! says and `NUL` short-circuits as the *final component of any path*, +//! however much path is in front of it: +//! +//! | input | `CON` | `NUL` | +//! |---|---|---| +//! | `X` | `\\.\CON` | `\\.\NUL` | +//! | `\X` | `Q:\CON` | `\\.\NUL` | +//! | `.\X` | `Q:\...\CON` | `\\.\NUL` | +//! | `a\X` | `Q:\...\a\CON` | `\\.\NUL` | +//! | `C:\X` | `C:\CON` | `\\.\NUL` | +//! | `X.txt` | rooted | rooted | +//! | `X:x` | rooted | rooted | +//! +//! So a *fully qualified* path can still resolve to a device, which is the +//! part worth knowing: `prepare(r"C:\NUL")` yields `\\.\NUL`, and a caller +//! treating a rooted path as proof it names a file on that volume is wrong for +//! this one name. Only a suffix (`NUL.txt`, `NUL:x`) takes it out. +//! //! **Do not build a name filter from the list below.** The accepted names are //! `CON`, `NUL`, `PRN`, `AUX`, `CONIN$`, `CONOUT$`, and `COM`/`LPT` //! followed by a single digit -- where "digit" includes the *superscripts* diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 85bb551f3..bb000b838 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -616,6 +616,67 @@ fn a_name_containing_a_device_word_is_rooted_under_the_current_directory() { a leading `.\\` is enough to take the name out of the device \ short-circuit without taking it out of ordinary rooting" ); + + // The root-relative form, which the module doc states and nothing pinned. + // Its expectation is the current directory's ROOT rather than `base`, so it + // cannot join the loop above. + let root = root_of(¤t_directory()); + assert_eq!( + resolve(r"\CON"), + format!("{root}CON"), + "a leading separator roots the device word at the current directory's \ + root instead of reaching the device" + ); +} + +#[test] +fn nul_is_the_one_device_word_a_path_around_it_does_not_save() { + // **The exception to the test above, and it was found by trying to write + // the general rule.** A review asked for the `\CON` case on the grounds + // that the doc states it; the doc stated it of the whole device SET, having + // been written from `CON` alone. Measured across all eight accepted names, + // seven root normally once anything precedes them and `NUL` does not -- + // `NUL` short-circuits as the final component of any path at all. + // + // Had the requested assertion been written as the general rule it was + // phrased as ("a root-relative device word roots at the root"), it would + // have pinned a false claim, which is the failure this branch exists to + // stop rather than repeat. + let base = current_directory(); + let base = base.trim_end_matches('\\'); + let root = root_of(¤t_directory()); + + // The seven that behave as the doc says, in the form that separates them. + for name in ["CON", "PRN", "AUX", "CONIN$", "CONOUT$", "COM1", "LPT1"] { + assert_eq!( + resolve(&format!(r".\{name}")), + format!(r"{base}\{name}"), + "{name:?} stops being a device once a path precedes it" + ); + assert_eq!( + resolve(&format!(r"C:\{name}")), + format!(r"C:\{name}"), + "{name:?} is an ordinary component of a fully-qualified path" + ); + } + + // And `NUL`, which does not -- INCLUDING from a fully-qualified path, so a + // rooted result is not by itself evidence that a path names a file. + for input in [r"\NUL", r".\NUL", r"a\NUL", r"C:\NUL"] { + assert_eq!( + resolve(input), + r"\\.\NUL", + "{input:?} still reaches the device: NUL is not saved by a path in \ + front of it, where every other device word is" + ); + } + + // A suffix is what takes it out, which is the boundary of the exception. + assert_eq!( + resolve(r"\NUL.txt"), + format!("{root}NUL.txt"), + "an extension takes even NUL out of the device namespace" + ); } /// A directory that exists, is in canonical `X:\...` form, and is neither a diff --git a/crates/windows-namespace-request-sys/src/path.rs b/crates/windows-namespace-request-sys/src/path.rs index 60ee5a12c..e211783a2 100644 --- a/crates/windows-namespace-request-sys/src/path.rs +++ b/crates/windows-namespace-request-sys/src/path.rs @@ -35,7 +35,10 @@ //! not a file under the current directory -- and the same holds for `CON:`, //! `NUL`, `LPT1:` and the rest of the legacy set. A caller passing through an //! untrusted name should know that. Anything with more after it (`CON.txt`, -//! `a\CON`) roots normally. The full shape is in +//! `a\CON`) roots normally -- **except for `NUL`, the one member a path in +//! front of it does not save**: `prepare(r"C:\NUL")` is `\\.\NUL`, so a fully +//! qualified path is not by itself evidence that a name refers to a file on +//! that volume. Only a suffix (`NUL.txt`) takes it out. The full shape is in //! [`crate::full_path`], which documents the call itself. //! //! What it never does is expand diff --git a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs index 6d72a4939..aaa999677 100644 --- a/crates/windows-namespace-request-sys/tests/unc_current_directory.rs +++ b/crates/windows-namespace-request-sys/tests/unc_current_directory.rs @@ -175,6 +175,20 @@ fn assert_root_relative_takes_the_share_root(placed_at: &str) { .perform() .expect("a root-relative path resolves"); + // **Compared case-sensitively on purpose, and a review read that as a + // portability bug.** The concern was that a host recording the share as + // `LOCALHOST` would fail this equality against the lowercase literal. It + // cannot, because both sides descend from `UNC_ROOT`: the parent builds the + // child's working directory from it, and the root this call returns is the + // root of that directory. Measured -- a process started with + // `\\LOCALHOST\C$\Windows` reports exactly that back from + // `current_dir()`, and likewise for `\\localhost\c$\Windows`, so Windows + // preserves the spelling it was given rather than canonicalising it. + // + // The precondition above is case-INSENSITIVE for a different reason: it + // compares against a value that may have crossed a process boundary by a + // route this test does not control, so it accepts what a Windows path + // comparison would. assert_eq!( resolved.to_string_lossy(), format!(r"{UNC_ROOT}\foo"), From c20ef91c534ac4c24101165c49344584d6470c4e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 17:05:15 -0400 Subject: [PATCH 33/36] fix(namespace-request): the lexical half claimed a mechanism too, for twenty-three rounds Copilot found the overreach in the half nobody was auditing. Every correction in this branch has been aimed at ROOTING -- "not consulted", "is ignored", "without consulting a device". The same error was sitting in the sentence doing the correcting: the LEXICAL half said collapsing `.`/`..` is "pure string work, reading no process state". Varying the current directory and getting the same answer shows the output does not DEPEND on that state. It does not show nothing was read -- which is exactly the argument this branch used to strike "not consulted" from the current-drive entry, applied to the positive claim instead of the negative one. Seven reviewers read that paragraph; it was invisible because it was the standard the other corrections were measured against. Both statements now say what the evidence reaches: the output is a function of the input alone. That is also all a caller needs, since it is what lets this half be reasoned about without knowing the process's state. **And the NUL escape boundary, which the doc named and the test half-covered.** The contract names `NUL.txt` and `NUL:x` as the spellings that take NUL out of the device namespace; the new test asserted only the root-relative `\NUL.txt`. Bare `NUL.txt` and `NUL:x` are now asserted, and so is the other side of the boundary -- `NUL::` and `NUL ` still reach the device, so the rule reads as "a suffix escapes" rather than "anything after NUL escapes". Measured: `NUL.txt` and `NUL:x` root under the current directory, `NUL::` and `NUL ` give `\\.\NUL`. Not taken here, and raised instead: this file is now 60.5 KiB, past the repository's 32 KiB "large" threshold and close to the 64 KiB one where a split becomes the default. The drive-entry fixture and its tests are a clean fracture point. A split is a pure relocation with its own commit and provenance trailers, so it does not belong inside a content change. Verified: 236 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, encoding over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-RATIONALE.md | 18 +++++++++---- .../src/full_path.rs | 15 ++++++++--- .../src/full_path/tests.rs | 25 ++++++++++++++++++- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 5754f8b7e..15aeec950 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -30,11 +30,19 @@ producing wrong answers. process state. 2. **"It resolves relative components and `.`/`..` against the process current - directory."** The first correction, which overshot. Collapsing `.`/`..` is - pure string work and reads no process state at all -- `C:\a\..\b` becomes - `C:\b` under any current directory, and whether or not `C:\a` exists. Only - *rooting* reads process state. Measured under two different current - directories. + directory."** The first correction, which overshot. Collapsing `.`/`..` has + an output that is a function of the input alone -- `C:\a\..\b` becomes + `C:\b` under any current directory, and whether or not `C:\a` exists. + Measured under two different current directories. + + **This entry itself carried the error it describes, for twenty-three + rounds.** It read "pure string work and reads no process state at all", and + two different current directories giving the same answer shows the output + does not depend on that state, not that nothing was read. Every correction in + this file was aimed at the *rooting* half; the overreach in the **lexical** + half was the sentence doing the correcting, which is why nobody looked at it. + Whether rooting reads process state is a separate question, and it is + answered observably: changing the current directory changes the result. 3. **"The probe measures roughly 212 ns per resolution."** It does not. The probe reports a construct-and-drop cycle whose total contains an allocation diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 77291249d..0f5c7b07c 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -34,9 +34,18 @@ //! dots and spaces (`C:\name...` and `C:\name ` both become `C:\name`), //! while an *intermediate* component loses a single trailing dot and nothing //! else -- `C:\a.\b` becomes `C:\a\b`, but `C:\a...\b` and `C:\a \b` are -//! returned unchanged. This part *is* lexical -- pure string work over the -//! input, reading no process state. `C:\a\..\b` becomes `C:\b` whatever the -//! current directory happens to be, and whether or not `C:\a` exists. +//! returned unchanged. This part's **output is a function of the input +//! alone**: `C:\a\..\b` becomes `C:\b` whatever the current directory +//! happens to be, and whether or not `C:\a` exists. +//! +//! Stated that way deliberately. Earlier revisions said it "reads no process +//! state", which the evidence does not reach: varying the current directory +//! and getting the same answer shows the output does not DEPEND on it, not +//! that nothing was read. That is the same overreach this doc removes from +//! the current-drive entry below, and it sat here in the positive half while +//! seven reviews corrected the negative one. Invariance is the whole claim, +//! and it is also all a caller needs: this half can be reasoned about +//! without knowing the process's state. //! 2. It **roots** a path that is not fully qualified, using mutable process //! state -- and on one form it also *changes* that state. There are three //! such forms: diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index bb000b838..b3fdb5aad 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -671,12 +671,35 @@ fn nul_is_the_one_device_word_a_path_around_it_does_not_save() { ); } - // A suffix is what takes it out, which is the boundary of the exception. + // A suffix is what takes it out, which is the boundary of the exception -- + // and the doc names both spellings, so both are asserted. The root-relative + // form alone would leave the bare ones uncovered, which is how `CON:x` came + // to be pinned by a predicate the wrong answer satisfied. assert_eq!( resolve(r"\NUL.txt"), format!("{root}NUL.txt"), "an extension takes even NUL out of the device namespace" ); + for (input, expected) in [("NUL.txt", "NUL.txt"), ("NUL:x", "NUL:x")] { + assert_eq!( + resolve(input), + format!(r"{base}\{expected}"), + "{input:?} is a suffixed name, so it roots under the current \ + directory like any other" + ); + } + + // And the boundary is a suffix, not merely "more characters": a trailing + // colon or space still reaches the device, exactly as for `CON`. Without + // these the rule above would read as "anything after NUL saves it". + for input in ["NUL::", "NUL "] { + assert_eq!( + resolve(input), + r"\\.\NUL", + "{input:?} is still the device: trailing colons and spaces are part \ + of the device form, not a suffix that escapes it" + ); + } } /// A directory that exists, is in canonical `X:\...` form, and is neither a From 3e48630b1cfd47a2037f2393fbd05f50334099ed Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 17:12:22 -0400 Subject: [PATCH 34/36] fix(namespace-request): take the handle-allocation guard, and say which layer the device rules describe Opening an older review thread surfaced that Copilot's review BODIES only list a subset of what it files -- twelve threads were open, several never seen here. Four were live. Two are substantive. **The probe fixture allocated handles outside the suite's serialisation.** `handle_allocation()`'s read guard means "I may open handles"; its write guard means "no other test may, while I reason about a specific handle value". Eleven tests across four modules take the write guard for exactly that. `probe_directory` called `create_dir`, `remove_dir`, `Path::exists` and `is_file` without the read guard -- and `std`'s Windows metadata goes through `CreateFileW` -- so those tests could run beside a write-guard holder and make its assertions fail for a reason unrelated to what it pins. The guard is now held by `ProbeDir` for its whole lifetime, which covers the fixture's own calls and the preconditions its tests run while it is alive. One older test doing a bare `Path::exists` takes it directly; it was the single filesystem call in the module the fixture does not cover. **A doc claim about the wrong layer, checked at both.** The review held that saying `CON.txt` and `a\CON` "do not reach a device" gives callers a false safety boundary, since Win32 reserves device basenames at open time. Measured at the open rather than the resolver: creating `\NUL` returns a `FILE_TYPE_CHAR` handle and leaves nothing on disk, while `\CON`, `\CON.txt` and `\NUL.txt` each create an ordinary `FILE_TYPE_DISK` file -- a real file named `CON` included. So the premise is false on this build: the reservation lives in ROOTING, the two layers agree, and `NUL` reaches the device at both. The valid half is kept anyway, because agreement measured on one build is not a guarantee this crate makes: the doc now says explicitly that those paragraphs describe the RESOLVER's output, and that a caller sanitising untrusted names should decide against what it will do with the result rather than infer open-time safety from a resolved spelling. Two stale restatements, both from earlier rounds of this branch: a comment still calling the drive-letter candidates a "disjoint pair" after they became three-element lists, and the probe's summary of the write-back stated unconditionally when an accepted entry is left alone. The PR description carried the same defect and is corrected in the same pass: it still said the current drive's entry is "neither consulted nor rewritten", the exact phrase the code moved away from, and now states the measured boundary and the NUL exception. Still open and not taken here: `full_path/tests.rs` is 62 KiB, past the 32 KiB "large" threshold and near the 64 KiB one. The drive-entry cluster is a clean fracture point, but a split is a pure relocation with its own commit and provenance trailers and does not belong inside a content change. Verified: 236 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, encoding over 630 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/full_path.rs | 15 +++++++ .../src/full_path/tests.rs | 45 +++++++++++++++++-- .../src/request_cost.rs | 4 +- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/crates/windows-namespace-request-sys/src/full_path.rs b/crates/windows-namespace-request-sys/src/full_path.rs index 0f5c7b07c..34222dc89 100644 --- a/crates/windows-namespace-request-sys/src/full_path.rs +++ b/crates/windows-namespace-request-sys/src/full_path.rs @@ -94,6 +94,21 @@ //! treating a rooted path as proof it names a file on that volume is wrong for //! this one name. Only a suffix (`NUL.txt`, `NUL:x`) takes it out. //! +//! **Everything above describes what this call returns, and a review asked +//! whether that is a safe boundary for what a later `CreateFileW` does.** +//! Measured on this build, at the open rather than the resolver: creating +//! `\NUL` returns a `FILE_TYPE_CHAR` handle and leaves nothing on disk, +//! while `\CON`, `\CON.txt` and `\NUL.txt` each create an +//! ordinary `FILE_TYPE_DISK` file. The two layers agree -- the reservation +//! lives in *rooting*, so a name that roots normally opens normally, and `NUL` +//! reaches the device at both layers. +//! +//! That agreement is a measurement of one build, not a guarantee this crate +//! makes. The durable statement is the narrower one: these paragraphs describe +//! the RESOLVER's output. A caller sanitising untrusted names should decide +//! against what it will do with the result, not infer open-time safety from a +//! resolved spelling. +//! //! **Do not build a name filter from the list below.** The accepted names are //! `CON`, `NUL`, `PRN`, `AUX`, `CONIN$`, `CONOUT$`, and `COM`/`LPT` //! followed by a single digit -- where "digit" includes the *superscripts* diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index b3fdb5aad..dc3993a7c 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -55,6 +55,14 @@ fn a_path_that_does_not_exist_resolves_perfectly_happily() { // A hard-coded literal is only missing until some host happens to have it, // and this test would then be demonstrating that an EXISTING path resolves // -- which every other test here already covers. + // `Path::exists` goes through `CreateFileW` on Windows, so it allocates a + // handle and this suite serialises that -- see `ProbeDir::_allocating`. + // This test predates the fixture and was the one filesystem call in the + // module not covered by it. + let _allocating = handle_allocation() + .read() + .expect("the lock is not poisoned"); + let missing = std::env::temp_dir().join(format!("wnrs-{}-absent", std::process::id())); let missing = missing.to_str().expect("the temp path is UTF-8"); assert!( @@ -412,8 +420,8 @@ fn a_drive_relative_path_carries_its_component_and_the_current_drive_uses_the_pr // the entry: with no `=X:` set, an implementation that always used the // drive root would satisfy everything here. The arm is pinned properly by // `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one`, - // which sets the entry and draws its letter from a disjoint pair, so the - // two cannot race. + // which sets the entry and draws its letter from a candidate list disjoint + // from this one's, so the two cannot race. let cwd = current_directory(); let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); @@ -729,6 +737,26 @@ fn nul_is_the_one_device_word_a_path_around_it_does_not_save() { struct ProbeDir { path: std::path::PathBuf, created: bool, + + /// Held for the fixture's whole lifetime, because this fixture opens + /// handles and this suite serialises that. + /// + /// `handle_allocation()`'s read guard means "I may open handles"; its write + /// guard means "no other test may, while I reason about a specific handle + /// value". Eleven tests across four modules take the write guard to assert + /// things like a closed handle's value not being reused, and any test + /// allocating a handle beside them can make those assertions fail for a + /// reason that has nothing to do with what they pin. + /// + /// This fixture allocates: `create_dir` and `remove_dir` here, and the + /// `Path::exists` / `is_file` preconditions its tests run while it is + /// alive -- `std`'s Windows metadata goes through `CreateFileW`. Holding + /// the guard on the fixture covers all of them for the whole test, which a + /// guard taken at each call site would not. + /// + /// A test holding one of these must not take a second read guard: `std` + /// does not promise recursive read locking is deadlock-free. + _allocating: std::sync::RwLockReadGuard<'static, ()>, } impl ProbeDir { @@ -757,6 +785,12 @@ impl Drop for ProbeDir { } fn probe_directory(tag: &str) -> ProbeDir { + // Taken before the first filesystem call, and handed to the fixture so it + // outlives this function. See `ProbeDir::_allocating`. + let _allocating = handle_allocation() + .read() + .expect("the lock is not poisoned"); + // The full shape an accepted `=X:` entry must have, not just its first // three characters: rooted at `X:\`, with no `.` or `..` component and no // forward slash. `a_rejected_drive_entry_is_replaced_by_the_drive_root` @@ -811,7 +845,11 @@ fn probe_directory(tag: &str) -> ProbeDir { Err(e) => panic!("create the probe directory {}: {e}", path.display()), }; - return ProbeDir { path, created }; + return ProbeDir { + path, + created, + _allocating, + }; } // Not creating anything here, so no write permission is needed on a host @@ -850,6 +888,7 @@ fn probe_directory(tag: &str) -> ProbeDir { ProbeDir { path, created: false, + _allocating, } } /// The candidate drive letters, one list per test that mutates a `=X:` entry. diff --git a/crates/windows-platform-probes/src/request_cost.rs b/crates/windows-platform-probes/src/request_cost.rs index 2e33dd414..bcaa957a1 100644 --- a/crates/windows-platform-probes/src/request_cost.rs +++ b/crates/windows-platform-probes/src/request_cost.rs @@ -104,7 +104,9 @@ //! those alternatives, because nothing here measures them -- but it does settle //! the mechanism question this module once left open: resolving a //! drive-relative path for another drive checks that drive's recorded entry -//! against the filesystem, and writes the entry back. +//! against the filesystem, and writes the entry back **when that check rejects +//! it** -- an accepted entry is left alone, so the write is conditional rather +//! than part of every such resolution. //! //! The two schemes that might reduce it recover different halves. **Inline //! storage** removes the allocation and copy, which is what From 65d170679858710f1783a121943ae9f7199a37f1 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 17:19:21 -0400 Subject: [PATCH 35/36] refactor(namespace-request): split full_path/tests.rs, extract the drive-entry arm Pure relocation, no behaviour change: 236 lib tests before and after, and `cargo fmt --check` is clean on the moved blocks. full_path/tests.rs had reached 62.3 KiB -- past this repository's 32 KiB "large" threshold and closing on the 64 KiB one where splitting becomes the default. The per-drive current-directory (`=X:`) cluster is the fracture point: it is the only part with a fixture of its own -- a probe directory, a drive-letter allocator, a reader and writer for the hidden entries, and a guard that restores a borrowed one -- and nothing outside it uses any of that. The two drive-relative ROOTING tests moved with the fixture rather than staying beside the other rooting forms, and that is a privacy constraint rather than a preference. A child module can see its ancestors' private items; a parent cannot see its child's. Left in the parent, those two tests could not reach the helpers, so the cut would not have been viable as drawn. They are also the two tests about this arm, so the constraint and the responsibility agree. The mass moved rather than the periphery: 21.8 KiB stays in the facade against 41.5 KiB extracted, so the parent is back under "large" and the child is a single coherent responsibility well clear of XL. Only 18 lines are additions -- the provenance header, the module doc, `use super::*;` and the `mod drive_entry;` declaration. Everything else is relocation, which is what lets `git blame -w -C1 -C1` attribute the moved lines to the commits that wrote them rather than to this one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Split-Source: crates/windows-namespace-request-sys/src/full_path/tests.rs Split-Into: crates/windows-namespace-request-sys/src/full_path/tests/drive_entry.rs --- .../src/full_path/tests.rs | 879 +---------------- .../src/full_path/tests/drive_entry.rs | 897 ++++++++++++++++++ 2 files changed, 899 insertions(+), 877 deletions(-) create mode 100644 crates/windows-namespace-request-sys/src/full_path/tests/drive_entry.rs diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index dc3993a7c..222389eeb 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -15,6 +15,8 @@ use super::{FullPathError, ResolveFullPath}; use crate::handle::tests::handle_allocation; use crate::outcome::Win32Error; +mod drive_entry; + fn resolve(path: &str) -> String { ResolveFullPath::new(Wtf16String::from(path)) .perform() @@ -381,158 +383,6 @@ fn a_fully_qualified_path_is_unaffected_by_the_current_directory() { assert_eq!(resolve("C:/a/b//c"), r"C:\a\b\c"); } -#[test] -fn a_drive_relative_path_carries_its_component_and_the_current_drive_uses_the_process_directory() { - // **The name says what the two assertions reach, and an earlier one did - // not.** This was - // `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory`, - // which claims more than anything here shows, in two separate ways. The - // other-drive assertion is `ends_with("\\foo")`, which accepts ANY base - // including the process directory -- so it cannot say "not the process - // directory". And "rooted at that drive" is not even true in general: an - // accepted entry is used verbatim and may name a directory on a different - // drive entirely, which is the sibling test's whole point. - // - // The body already said it only bounds the arm. The name did not, and the - // name is what a reader takes away -- the same defect as the test called - // `..._is_neither_consulted_nor_rewritten` before it was renamed. - // - // The third rooting form, with two arms, which is the part that gets - // missed: - // - // * For a drive OTHER than the current one, Windows reads the hidden - // `=X:` entry recorded for it. - // * For the CURRENT drive the entry makes no difference to the result and - // the process current directory wins. Measured: setting `=Q:` while the - // process is on `Q:` changes nothing. - // - // **This test does not mutate `=X:`, but the call it exercises may.** - // Measured: resolving `X:foo` for a non-current drive checks that drive's - // entry and WRITES it to `X:\` when the entry is absent or rejected. An - // accepted entry is left alone, and the current-drive form writes nothing - // -- so this is not "every resolution", but it does mean an ordinary host - // with no entry has one written merely by running this test. That is - // a property of the call, documented in the module doc; it is noted here so - // the next reader does not take "reads process state" at face value, as - // four revisions of that doc did. - // - // **This test only BOUNDS the other-drive arm**, because it does not control - // the entry: with no `=X:` set, an implementation that always used the - // drive root would satisfy everything here. The arm is pinned properly by - // `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one`, - // which sets the entry and draws its letter from a candidate list disjoint - // from this one's, so the two cannot race. - let cwd = current_directory(); - let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); - - // The other-drive arm needs no drive letter from the current directory -- - // under a UNC current directory every letter is "other" -- so it runs - // unconditionally and this test never degenerates to a silent skip. - let other = probe_drive_from(probe_drives::ROOTED_AT_THAT_DRIVE, None); - - // This test controls no entry, but the CALL does: resolving for a - // non-current drive writes `=X:` whenever the recorded entry is absent or - // rejected, and on most hosts a letter chosen for being unused has no entry - // at all. So merely observing the arm mutates process-global state, and - // this was the one mutating case here without a guard -- the borrow is - // needed exactly because the mutation is not the test's own doing. - let _restore = BorrowedDriveEntry::take(other); - let resolved = resolve(&format!("{other}:foo")); - - // Only what is invariant without controlling the entry. An earlier version - // required the result to start with `X:\`, which the verbatim rule breaks; - // its replacement compared against the current directory, which `other` - // differs from by construction, so it could fire only if the entry happened - // to equal the process directory exactly -- the same vacuity, respelled. - // What survives every entry value is that the component is carried through. - assert!( - resolved.ends_with(r"\foo"), - "the component is carried through whatever the entry holds: {resolved}" - ); - - // The current-drive arm, where the process directory wins over any `=X:`. - // Only expressible when the current directory has a drive letter at all. - if let Some(drive) = cwd_drive { - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{}\foo", cwd.trim_end_matches('\\')), - "on the current drive, the per-drive entry makes no difference to \ - the result and the process directory is used" - ); - } -} - -#[test] -fn the_current_drives_entry_does_not_affect_resolution_and_is_not_rewritten() { - // The module doc states both halves of the current-drive arm as fact. Until - // now nothing pinned either, and the assertion just above -- which looks - // like it does -- cannot: it resolves `X:foo` WITHOUT controlling the entry - // and compares against the process directory, and Windows keeps the current - // drive's entry equal to that directory. So it reads the same either way. - // Vacuous in precisely the way this crate keeps rediscovering, and the - // reason the two arms need opposite fixtures: the sibling tests must AVOID - // the current drive, and this one must be on it. - // - // **The name says what is observable, and an earlier one did not.** This - // was `..._is_neither_consulted_nor_rewritten`, which claims the entry is - // not READ -- and installing a value and watching the outcome cannot - // separate "not read" from "read and ignored". That is the same overreach - // this branch removed from the probe's "without consulting a device", and - // the test correcting it committed it in its own name. What the two - // assertions below reach is the pair of observable effects: the entry makes - // no difference to the result, and it is not written back. - let cwd = current_directory(); - let Some(drive) = cwd.chars().next().filter(char::is_ascii_alphabetic) else { - // A UNC current directory has no drive letter, so there is no - // current-drive arm to exercise. Not a skip of something testable. - return; - }; - let process_directory = format!(r"{}\foo", cwd.trim_end_matches('\\')); - - let probe_dir = probe_directory("current-drive"); - let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let _restore = BorrowedDriveEntry::take(drive); - - // The anti-vacuity check, made permanent rather than performed once by - // hand: unless the two arms would give DIFFERENT answers, every assertion - // below passes without distinguishing them, which is the failure this test - // was written to correct. - assert_ne!( - process_directory, - format!(r"{probe}\foo"), - "precondition: the entry must name somewhere other than the process \ - directory, or honouring it and ignoring it look identical" - ); - - // No difference to the result. The entry is one the OTHER arm would honour - // verbatim -- an existing directory in canonical form -- and it names - // somewhere the process directory cannot be, because this test just created - // it under a process-unique name. If the entry were HONOURED, the result - // would be under `probe`. - set_drive_entry(drive, Some(probe)); - assert_eq!( - resolve(&format!("{drive}:foo")), - process_directory, - "the current drive's entry was set to {probe}, an entry the non-current \ - arm honours verbatim, and the process directory won anyway" - ); - - // Not rewritten, which needs a REJECTED entry to be visible: an accepted one - // is left alone on both arms, so leaving it alone shows nothing. A child of - // the probe directory cannot exist, and on the non-current arm that is - // replaced by the drive root. - let missing = probe_dir.path.join("no-such-child"); - let missing = missing.to_str().expect("the probe path is UTF-8"); - set_drive_entry(drive, Some(missing)); - let _ = resolve(&format!("{drive}:foo")); - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(missing), - "an entry the non-current arm would have replaced with {drive}:\\ is \ - left untouched on the current drive" - ); -} - #[test] fn trailing_dot_and_space_trimming_differs_between_final_and_intermediate_components() { // The module doc says the rewrite trims trailing dots and spaces. Until now @@ -709,728 +559,3 @@ fn nul_is_the_one_device_word_a_path_around_it_does_not_save() { ); } } - -/// A directory that exists, is in canonical `X:\...` form, and is neither a -/// drive root nor the process current directory. -/// -/// **The form is enforced, not assumed, and that distinction has now cost two -/// rounds.** An earlier version derived the probe from `current_directory()`, -/// which yields `C:` at a drive root -- drive-relative, not a directory -- and -/// both tests failed there. Its replacement used `std::env::temp_dir()`, which -/// is `%TMP%`/`%TEMP%` verbatim and carries no guarantee of a drive letter: with -/// temp redirected to a share, the probe is a UNC path, which -/// `GetFullPathNameW` rejects as an entry *on shape* -- the very rule the -/// caller is trying to pin. Folder redirection makes that an ordinary -/// configuration, not a contrived one. -/// -/// So the temp directory is used only when it is drive-rooted, and otherwise -/// the fallback is `%SystemRoot%`, which is guaranteed to exist, to be -/// canonical, and not to be a drive root. Nothing is created in the fallback -/// case, so `created` records whether there is anything to remove. -/// -/// Removal is a [`Drop`], matching this crate's own `Fixture` in -/// [`crate::handle`]'s tests. An earlier version cleaned up with a statement at -/// the end of each test and argued that a guard writing during unwinding could -/// panic and abort -- which is true of the `=X:` entry restore beside it, and -/// not of removing a directory. Conflating the two left a directory behind -/// after every failing assertion. -struct ProbeDir { - path: std::path::PathBuf, - created: bool, - - /// Held for the fixture's whole lifetime, because this fixture opens - /// handles and this suite serialises that. - /// - /// `handle_allocation()`'s read guard means "I may open handles"; its write - /// guard means "no other test may, while I reason about a specific handle - /// value". Eleven tests across four modules take the write guard to assert - /// things like a closed handle's value not being reused, and any test - /// allocating a handle beside them can make those assertions fail for a - /// reason that has nothing to do with what they pin. - /// - /// This fixture allocates: `create_dir` and `remove_dir` here, and the - /// `Path::exists` / `is_file` preconditions its tests run while it is - /// alive -- `std`'s Windows metadata goes through `CreateFileW`. Holding - /// the guard on the fixture covers all of them for the whole test, which a - /// guard taken at each call site would not. - /// - /// A test holding one of these must not take a second read guard: `std` - /// does not promise recursive read locking is deadlock-free. - _allocating: std::sync::RwLockReadGuard<'static, ()>, -} - -impl ProbeDir { - /// The drive letter this probe lives on, if it has one. - /// - /// The caller needs it to pick a probe drive that is *not* this one: - /// resolving `W:foo` against an entry naming a directory that is itself on - /// `W:` cannot demonstrate that an accepted entry is used verbatim across - /// drives, which is the property being pinned. - fn drive(&self) -> Option { - self.path - .as_os_str() - .to_string_lossy() - .chars() - .next() - .filter(char::is_ascii_alphabetic) - } -} - -impl Drop for ProbeDir { - fn drop(&mut self) { - if self.created { - let _ = std::fs::remove_dir(&self.path); - } - } -} - -fn probe_directory(tag: &str) -> ProbeDir { - // Taken before the first filesystem call, and handed to the fixture so it - // outlives this function. See `ProbeDir::_allocating`. - let _allocating = handle_allocation() - .read() - .expect("the lock is not poisoned"); - - // The full shape an accepted `=X:` entry must have, not just its first - // three characters: rooted at `X:\`, with no `.` or `..` component and no - // forward slash. `a_rejected_drive_entry_is_replaced_by_the_drive_root` - // shows each of those spellings is rejected while naming the same existing - // directory, so a base carrying one would turn that test's CONTROL - // assertion -- "the same directory in canonical form is accepted" -- into a - // rejection, and it would fail for a reason unrelated to what it pins. - // - // **A review read that as reachable through a non-canonical `%TMP%`. It is - // not, and the measurement is here so the next reader need not repeat it.** - // `std::env::temp_dir` goes through `GetTempPath2W`, which normalises what - // it finds: `C:/Users/.../Temp`, `...\Temp\.`, `...\Temp\..\Temp`, - // `...\Temp\\` and even the drive-relative `C:Users\...` all came back as - // `C:\Users\...\Temp\`. The one spelling passed through verbatim is - // `\\?\C:\...`, which is not drive-rooted and so takes the fallback below. - // - // The check is widened anyway. It costs nothing, it covers the fallback - // base too, and it is the difference between a precondition that is - // enforced and one that is argued -- which is the distinction this whole - // change exists to hold. - let canonical_drive_rooted = |p: &std::path::Path| { - let s = p.as_os_str().to_string_lossy().into_owned(); - let mut chars = s.chars(); - matches!( - (chars.next(), chars.next(), chars.next()), - (Some(d), Some(':'), Some('\\')) if d.is_ascii_alphabetic() - ) && !s.contains('/') - && !s.split('\\').any(|c| c == "." || c == "..") - }; - - let temp = std::env::temp_dir(); - if canonical_drive_rooted(&temp) { - let path = temp.join(format!("wnrs-{}-{tag}", std::process::id())); - - // `create_dir`, not `create_dir_all`, and the difference is ownership - // rather than parents. `create_dir_all` SUCCEEDS on a directory that - // already exists, so setting `created` after it recorded a claim this - // fixture had not established -- and `Drop` then removes the path on - // the strength of that claim. The name is `%TEMP%\wnrs--`, - // which an interrupted earlier run leaves behind and which Windows can - // hand back to a later process when it reuses the PID. The blast radius - // is small, because `remove_dir` refuses a non-empty directory -- but - // "small" is not the point. Deleting something on an ownership claim - // nothing checked is the same defect as asserting a mechanism nothing - // measured, and this file exists to stop doing that. - // - // An existing directory is still perfectly usable as a probe; it is - // just not ours to remove. - let created = match std::fs::create_dir(&path) { - Ok(()) => true, - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => false, - Err(e) => panic!("create the probe directory {}: {e}", path.display()), - }; - - return ProbeDir { - path, - created, - _allocating, - }; - } - - // Not creating anything here, so no write permission is needed on a host - // whose temp directory is redirected off a drive letter. - // - // **It must also differ from the process current directory**, which the - // temp branch gets for free -- it creates a uniquely named child -- and this - // branch does not. Cargo launched from `%SystemRoot%` on a host with a UNC - // temp directory would otherwise hand back the current directory itself, - // and a probe indistinguishable from the current directory cannot separate - // "the entry was honoured" from "the entry was ignored". `System32` is the - // second candidate for the same reason `probe_drive_from` takes a list: - // one value that is usually right is not a guarantee. - let system_root = std::path::PathBuf::from( - std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), - ); - let cwd = current_directory(); - let cwd = cwd.trim_end_matches('\\'); - let distinct = |p: &std::path::Path| { - !p.as_os_str() - .to_string_lossy() - .trim_end_matches('\\') - .eq_ignore_ascii_case(cwd) - }; - - let path = [system_root.clone(), system_root.join("System32")] - .into_iter() - .find(|p| canonical_drive_rooted(p) && distinct(p)) - .unwrap_or_else(|| { - panic!( - "no fallback probe directory is both canonical and distinct \ - from the current directory {cwd}" - ) - }); - - ProbeDir { - path, - created: false, - _allocating, - } -} -/// The candidate drive letters, one list per test that mutates a `=X:` entry. -/// -/// **Centralised so the properties these tests depend on are CHECKED rather -/// than restated.** Both were previously prose -- a doc comment saying the -/// lists are disjoint, and an archived note enumerating them -- and prose -/// drifted: the archive named five lists after the sixth had been added, so a -/// reader picking letters for a seventh would have consulted an inventory -/// missing three of the eighteen letters already in use. Nothing checked -/// either claim, because nothing could: the lists were literals at six call -/// sites with no table to read. -/// -/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] now reads -/// this table, so adding a list that collides -- or one too short for -/// [`probe_drive_from`]'s guarantee -- fails a test instead of a review. -mod probe_drives { - pub const ROOTED_AT_THAT_DRIVE: &[char] = &['X', 'Y', 'P']; - pub const VERBATIM_ENTRY: &[char] = &['W', 'U', 'N']; - pub const REJECTED_ENTRY: &[char] = &['V', 'T', 'M']; - pub const LONG_ENTRY: &[char] = &['R', 'S', 'K']; - pub const BORROW_GUARD: &[char] = &['G', 'H', 'J']; - pub const EMPTY_VS_ABSENT: &[char] = &['E', 'F', 'B']; - - /// Every list above. A new list that is not added here is not covered by - /// the disjointness test, so keep them together. - pub const ALL: &[(&str, &[char])] = &[ - ("ROOTED_AT_THAT_DRIVE", ROOTED_AT_THAT_DRIVE), - ("VERBATIM_ENTRY", VERBATIM_ENTRY), - ("REJECTED_ENTRY", REJECTED_ENTRY), - ("LONG_ENTRY", LONG_ENTRY), - ("BORROW_GUARD", BORROW_GUARD), - ("EMPTY_VS_ABSENT", EMPTY_VS_ABSENT), - ]; -} - -#[test] -fn the_probe_drive_candidate_lists_are_disjoint_and_large_enough() { - for (name, list) in probe_drives::ALL { - // `probe_drive_from` excludes at most two letters -- the current drive - // and the probe directory's drive -- so three candidates guarantee a - // survivor. This is the premise of the panic in that function, checked - // here rather than left to the caller as the doc comment used to. - assert!( - list.len() >= 3, - "{name} has {} candidates, and at most two can be excluded, so \ - fewer than three cannot guarantee a survivor", - list.len() - ); - - let mut seen = list.to_vec(); - seen.sort_unstable(); - seen.dedup(); - assert_eq!(seen.len(), list.len(), "{name} repeats a letter"); - } - - for (a_name, a) in probe_drives::ALL { - for (b_name, b) in probe_drives::ALL { - if a_name == b_name { - continue; - } - let shared: Vec = a - .iter() - .copied() - .filter(|c| b.iter().any(|d| d.eq_ignore_ascii_case(c))) - .collect(); - assert!( - shared.is_empty(), - "{a_name} and {b_name} share {shared:?}, so the two tests can \ - select the same drive and race under libtest's \ - thread-per-test model" - ); - } - } -} - -/// A drive letter to probe with, drawn from `candidates` and guaranteed to be -/// neither the current drive nor `avoid`. -/// -/// **Every candidate is checked, which an earlier version did not do.** It took -/// a preferred letter and a fallback, tested only the preferred one, and -/// returned the fallback unvalidated -- so when the preferred letter was -/// excluded the caller could still be handed the current drive. Measured: with -/// the process on `U:` and `%TEMP%` on a `subst`-ed `W:`, the verbatim test -/// selected `U` and then asserted the *other-drive* contract while exercising -/// the *current-drive* arm, which is the one case where the entry makes no -/// difference to the result. -/// It failed, but the mode is worse than a failure: the helper's own doc -/// promised a guarantee it never enforced. -/// -/// Three candidates against at most two exclusions, so one always survives. The -/// panic remains because that argument is about the caller's list, which this -/// function cannot see -- but the argument is no longer only an argument: -/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] checks it -/// against every list in [`probe_drives`]. -/// -/// Callers pass disjoint lists, so no two tests can select the same letter and -/// race under libtest's thread-per-test model. -fn probe_drive_from(candidates: &[char], avoid: Option) -> char { - let cwd = current_directory(); - // A UNC current directory has no drive letter, so nothing collides there. - let current = cwd.chars().next().filter(char::is_ascii_alphabetic); - let taken = |c: char| { - current.is_some_and(|d| d.eq_ignore_ascii_case(&c)) - || avoid.is_some_and(|d| d.eq_ignore_ascii_case(&c)) - }; - - *candidates.iter().find(|&&c| !taken(c)).unwrap_or_else(|| { - panic!( - "every candidate of {candidates:?} is excluded by the current \ - drive ({current:?}) or the probe drive ({avoid:?})" - ) - }) -} -/// Reads one of the hidden `=X:` per-drive current-directory entries. -/// -/// Through Win32 rather than `std::env`, which rejects a key containing `=` -/// outright and so cannot address these at all. -fn drive_entry(drive: char) -> Option { - let name = Wtf16String::from(format!("={drive}:").as_str()); - // Start small and grow to whatever Windows asks for. The API's two return - // conventions differ: on success it reports the units written EXCLUDING the - // terminator, and on an undersized buffer it reports the capacity REQUIRED - // INCLUDING it. Treating the second as the first indexes past the buffer and - // panics -- while trying to preserve a legitimate long entry, so the failure - // would land before the test could restore the process state it borrowed. - let mut buffer = vec![0u16; 256]; - loop { - // Zero is TWO different answers, and the last error is the only thing - // that separates them -- so it is cleared first, because the value left - // by some earlier call would otherwise be read as this one's. - // - // SAFETY: no preconditions. - unsafe { windows_sys::Win32::Foundation::SetLastError(ERROR_SUCCESS) }; - - // SAFETY: the name is NUL-terminated and the buffer is writable for - // the length passed. - let written = unsafe { - windows_sys::Win32::System::Environment::GetEnvironmentVariableW( - name.as_terminated_ptr(), - buffer.as_mut_ptr(), - u32::try_from(buffer.len()).unwrap_or(u32::MAX), - ) - }; - let written = written as usize; - if written == 0 { - // **An earlier version of this comment claimed, as measured, that - // an empty value and an absent name are the same state and cannot - // be told apart. That was wrong, and wrong in this crate's - // signature way: the measurement behind it never cleared the last - // error, so it could only ever have seen whatever was already - // there.** Cleared first and re-measured, the two are distinct, for - // an ordinary name and an `=X:` name alike: - // - // set to "" -> returns 0, last error ERROR_SUCCESS - // deleted -> returns 0, last error ERROR_ENVVAR_NOT_FOUND - // - // The difference is not academic here. Collapsing both to `None` - // makes the restoration in `BorrowedDriveEntry` DELETE an inherited - // empty entry rather than put it back -- losing exactly the process - // state the guard exists to preserve. - // - // SAFETY: no preconditions. - let last = unsafe { windows_sys::Win32::Foundation::GetLastError() }; - return match last { - ERROR_SUCCESS => Some(Wtf16String::from_units(&[])), - ERROR_ENVVAR_NOT_FOUND => None, - // Anything else is neither answer, and folding it into "absent" - // would make the guard delete an entry over a transient error. - other => panic!("reading ={drive}: failed with error {other}"), - }; - } - if written < buffer.len() { - // Kept as WTF-16 units rather than going through String: a lossy - // conversion would replace an unpaired surrogate, so restoring the - // entry afterwards would write back something the process did not - // start with. - return Some(Wtf16String::from_units(&buffer[..written])); - } - buffer = vec![0u16; written]; - } -} -/// Sets or clears one of the hidden `=X:` entries. -fn set_drive_entry(drive: char, value: Option<&str>) { - set_drive_entry_units(drive, value.map(Wtf16String::from).as_ref()); -} - -/// [`set_drive_entry`], taking the exact units a [`drive_entry`] read returned. -/// -/// Restoration goes through this rather than through `&str`, so an entry -/// containing an unpaired surrogate is put back byte for byte. -fn set_drive_entry_units(drive: char, value: Option<&Wtf16String>) { - assert!( - try_set_drive_entry_units(drive, value), - "set ={drive}: entry" - ); -} - -/// [`set_drive_entry_units`] without the assertion, reporting success instead. -/// -/// Separate because the restoration in [`BorrowedDriveEntry`] runs during -/// unwinding, where a panic would abort the process and destroy the report of -/// the failure that started the unwind. -fn try_set_drive_entry_units(drive: char, value: Option<&Wtf16String>) -> bool { - let name = Wtf16String::from(format!("={drive}:").as_str()); - let value_ptr = value - .as_ref() - .map_or(core::ptr::null(), |v| v.as_terminated_ptr()); - // SAFETY: both pointers are NUL-terminated; a null value clears the entry. - let ok = unsafe { - windows_sys::Win32::System::Environment::SetEnvironmentVariableW( - name.as_terminated_ptr(), - value_ptr, - ) - }; - ok != 0 -} - -/// Borrows one drive's `=X:` entry and puts it back when the test ends, -/// **whether or not the test panicked**. -/// -/// Restoring on the last line of the test is not enough, and the hazard is not -/// theoretical: `=X:` is process-global, `cargo test` runs tests as threads in -/// ONE process, and every assertion between the save and the restore is a place -/// the entry can be abandoned. What a sibling test would then inherit is not -/// merely a stale value but one no host would produce -- a path to a directory -/// that no longer exists once the probe directory is removed, or the 1200-unit -/// value that `a_long_drive_entry_round_trips_through_the_reader` installs on -/// purpose. The reader above already names this ("the entry is then never -/// restored") without defending against it; this is the defence. -/// -/// A failed restore is dropped rather than asserted, for the reason given on -/// [`try_set_drive_entry_units`]. -struct BorrowedDriveEntry { - drive: char, - saved: Option, -} - -impl BorrowedDriveEntry { - fn take(drive: char) -> Self { - Self { - drive, - saved: drive_entry(drive), - } - } -} - -impl Drop for BorrowedDriveEntry { - fn drop(&mut self) { - let restored = try_set_drive_entry_units(self.drive, self.saved.as_ref()); - - // Silence is bought only where it buys something. While unwinding, a - // panic here aborts the process and destroys the report of the failure - // that started the unwind, so a failed restore is worth less than the - // diagnosis it would replace. On the ordinary path there is no such - // trade: staying quiet would let the suite carry on with corrupted - // process-global state and fail somewhere unrelated, which is the - // hardest kind of failure to read. - assert!( - restored || std::thread::panicking(), - "restoring ={}: failed, leaving process-global state corrupted for \ - every test that follows", - self.drive - ); - } -} - -#[test] -fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one() { - // The arm the sibling test can only BOUND. Without controlling the entry, - // an implementation that always used the drive root would satisfy every - // assertion there, because a host with no `=X:` entry cannot tell the two - // rules apart. - // - // **Controlling the entry is not the hazard it looks like**, and that is - // what unblocked this test. The objection was that `=X:` is process-global - // while these tests share a process. But the call under test writes that - // entry itself whenever it is absent or rejected, so this state is already - // mutated by the code being exercised. What keeps the tests from - // interfering is not that -- it is that each takes a drive letter no other - // one can choose. - // - // The probe directory is created FIRST so the letter can avoid its drive as - // well as the current one: an entry naming a directory on the same drive it - // is recorded for would be honoured, the test would pass, and the - // cross-drive property below would go unexercised. - let probe_dir = probe_directory("verbatim"); - let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_from(probe_drives::VERBATIM_ENTRY, probe_dir.drive()); - let _restore = BorrowedDriveEntry::take(drive); - - assert_ne!( - Some(drive.to_ascii_uppercase()), - probe_dir.drive().map(|d| d.to_ascii_uppercase()), - "the probe directory must be on a different drive, or the assertion \ - below cannot show the entry is honoured ACROSS drives" - ); - - set_drive_entry(drive, Some(probe)); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{probe}\foo"), - "an entry naming an existing directory is honoured verbatim, even onto \ - a different drive -- so \"that drive's own current directory\" is the \ - convention the entry usually holds, not a guarantee about the result" - ); - - // Verbatim means verbatim, including the join. The module doc records that - // an accepted entry ending in a separator yields a DOUBLED one, and nothing - // pinned it -- so the observation could have stopped being true without CI - // noticing, which is the drift this change exists to close rather than - // commit again. - set_drive_entry(drive, Some(&format!(r"{probe}\"))); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{probe}\\foo"), - "an entry is accepted with a trailing separator and concatenated \ - without normalising the join" - ); - - // An entry that does not name an existing directory is rejected, and the - // call rewrites it to the drive root rather than leaving it stale. - // - // Derived from the probe directory rather than hard-coded: a literal like - // `C:\no-such-directory-for-this-test` is only missing until some host - // happens to have it, and the test would then assert the rejected case - // against an accepted one. A child of a directory this test just created - // cannot exist unless something else creates it in between. - let missing = probe_dir.path.join("no-such-child"); - assert!( - !missing.exists(), - "precondition: the rejected entry must name nothing: {}", - missing.display() - ); - set_drive_entry( - drive, - Some(missing.to_str().expect("the probe path is UTF-8")), - ); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{drive}:\foo"), - "an entry that names nothing is rejected in favour of the drive root" - ); - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(format!(r"{drive}:\").as_str()), - "and the call REWROTE the entry: this is a query that mutates the \ - process environment block" - ); - - // Absent entirely, the entry is created rather than merely read. - set_drive_entry(drive, None); - assert_eq!(drive_entry(drive), None, "precondition: entry cleared"); - let _ = resolve(&format!("{drive}:foo")); - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(format!(r"{drive}:\").as_str()), - "resolving created the entry on a host that had none" - ); -} - -#[test] -fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { - // Acceptance needs BOTH a shape and an existence check, and a draft of the - // module doc claimed it was "a filesystem query rather than a syntax test" - // -- having measured only the existence half. Every value below names an - // existing directory, so anything rejected here is rejected on shape alone. - // - // Pinned because the distinction is not guessable and the doc asserts it. - let probe_dir = probe_directory("shape"); - let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); - let drive = probe_drive_from(probe_drives::REJECTED_ENTRY, probe_dir.drive()); - let _restore = BorrowedDriveEntry::take(drive); - - // The control: this exact directory IS accepted in canonical form, so the - // rejections below cannot be blamed on the directory itself. - set_drive_entry(drive, Some(accepted)); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{accepted}\foo"), - "control: the same directory in canonical form is accepted" - ); - - // Same directory, spellings that are not fully-qualified `X:\...` form. - // Each names something that exists; each is rejected anyway. - for spelling in [ - accepted.replace('\\', "/"), - format!(r"{accepted}\."), - format!( - r"{accepted}\..\{}", - accepted.rsplit('\\').next().unwrap_or("") - ), - format!(r"\\?\{accepted}"), - ] { - set_drive_entry(drive, Some(&spelling)); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{drive}:\foo"), - "{spelling:?} names an existing directory but is rejected on shape" - ); - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(format!(r"{drive}:\").as_str()), - "and the rejected entry is written back as the drive root" - ); - } - - // The type check, which is a separate necessary condition from both the - // shape above and the existence check in the sibling test. The module doc - // has listed an existing FILE among the rejections since the drive-entry - // work, and nothing pinned it -- so the one observation distinguishing - // "names a directory" from "names something" lived only in prose. - // - // Not a file this test creates: `ProbeDir` may be the read-only - // `%SystemRoot%` fallback, where creating one needs privileges the suite - // must not assume. This one is present on every Windows host by - // construction, and setting an entry to a file does not touch the file. - let system_file = std::path::PathBuf::from( - std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), - ) - .join("System32") - .join("kernel32.dll"); - assert!( - system_file.is_file(), - "precondition: the rejected entry must name an existing FILE: {}", - system_file.display() - ); - let system_file = system_file.to_str().expect("the system path is UTF-8"); - - set_drive_entry(drive, Some(system_file)); - assert_eq!( - resolve(&format!("{drive}:foo")), - format!(r"{drive}:\foo"), - "{system_file} exists and is canonical, and is rejected anyway because \ - it is not a directory -- so existence alone is not the gate" - ); - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(format!(r"{drive}:\").as_str()), - "and an entry naming a file is written back as the drive root too" - ); -} - -#[test] -fn a_long_drive_entry_round_trips_through_the_reader() { - // The reader grows its buffer, and this is what proves it. Windows reports - // an undersized buffer by returning the REQUIRED capacity rather than the - // units written, so a reader that treats the two alike slices past its own - // buffer and panics -- while preserving a legitimate entry, which is the - // worst moment for it, because the entry is then never restored. - // - // 1200 units is comfortably past the 256 the reader starts with and past - // the 1024 an earlier fixed-size version used, and is a legitimate value: a - // per-drive entry is a path, and long paths reach far beyond this. - let drive = probe_drive_from(probe_drives::LONG_ENTRY, None); - let _restore = BorrowedDriveEntry::take(drive); - - let long = format!(r"C:\{}", "a".repeat(1200)); - set_drive_entry(drive, Some(&long)); - - let read_back = drive_entry(drive).expect("the entry was just set"); - assert_eq!( - read_back.to_string_lossy(), - long, - "a long entry survives the read, so the buffer grew instead of truncating" - ); -} - -#[test] -fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { - // The guard exists for the unwinding path, and a suite that passes never - // takes it -- so trusting it would mean shipping an untested defence - // against the exact failure it is there for. This takes the path on - // purpose. - let drive = probe_drive_from(probe_drives::BORROW_GUARD, None); - - // The outer guard is not ceremony. This test installs a sentinel to watch - // the inner guard put back, and without it that install would destroy - // whatever the process inherited -- so the test for not losing borrowed - // state would itself lose some. The inner guard still takes the unwinding - // path; the outer one covers this test's own borrow. - let _outer = BorrowedDriveEntry::take(drive); - let sentinel = format!(r"C:\borrowed-entry-{}", std::process::id()); - set_drive_entry(drive, Some(&sentinel)); - - let outcome = std::panic::catch_unwind(|| { - let _restore = BorrowedDriveEntry::take(drive); - set_drive_entry(drive, Some(r"C:\the-borrowed-value")); - panic!("expected: this panic exercises the restore-on-unwind path"); - }); - assert!( - outcome.is_err(), - "precondition: the borrower must actually panic, or the unwinding path \ - is not the thing being measured" - ); - - assert_eq!( - drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), - Some(sentinel.as_str()), - "the guard put the entry back while unwinding, where an end-of-test \ - restore would have been skipped" - ); -} - -#[test] -fn an_empty_drive_entry_is_distinguished_from_an_absent_one() { - // **This pins a correction, not a discovery.** The reader used to fold both - // into `None`, on a recorded measurement that an empty value and an absent - // name are the same state. They are not; the measurement behind that claim - // never cleared the last error, so it could only have read whatever an - // earlier call left behind -- the same "stated more precisely than the - // evidence reaches" failure this crate keeps meeting, committed inside the - // comment that called itself measured. - // - // The consequence is what makes it worth a test rather than a fix: with the - // two collapsed, restoring an inherited EMPTY entry deletes it, so the guard - // written to preserve process state destroys it in exactly one case. - let drive = probe_drive_from(probe_drives::EMPTY_VS_ABSENT, None); - let _outer = BorrowedDriveEntry::take(drive); - - set_drive_entry(drive, Some("")); - let empty = drive_entry(drive); - assert_eq!( - empty.as_ref().map(|v| v.to_string_lossy()), - Some(String::new()), - "an entry set to the empty string reads back as PRESENT and empty" - ); - - set_drive_entry(drive, None); - assert_eq!( - drive_entry(drive), - None, - "and a deleted entry reads back as absent, which is the answer the \ - empty one must not be confused with" - ); - - // The two are distinct in the round trip as well as in the read, which is - // the property restoration actually depends on. - set_drive_entry_units(drive, empty.as_ref()); - assert_eq!( - drive_entry(drive).as_ref().map(|v| v.to_string_lossy()), - Some(String::new()), - "restoring an empty entry puts back an empty entry, not an absent one" - ); -} diff --git a/crates/windows-namespace-request-sys/src/full_path/tests/drive_entry.rs b/crates/windows-namespace-request-sys/src/full_path/tests/drive_entry.rs new file mode 100644 index 000000000..91fd779f8 --- /dev/null +++ b/crates/windows-namespace-request-sys/src/full_path/tests/drive_entry.rs @@ -0,0 +1,897 @@ +// Copyright (c) Mike Grier. +// Split from tests.rs at 3e48630. + +//! The per-drive current-directory (`=X:`) arm, and the fixture that controls +//! it. +//! +//! Separated from the parent because it is the one cluster here with a fixture +//! of its own: a probe directory, a drive-letter allocator, a reader and writer +//! for the hidden entries, and a guard that puts a borrowed one back. Every +//! test below either sets that state or is about what the call does with it, +//! and nothing outside this module needs any of it. +//! +//! The two drive-relative ROOTING tests moved with the fixture rather than +//! staying beside the other rooting forms. That is a privacy constraint, not a +//! preference: a child module can see its ancestors' private items, but a +//! parent cannot see its child's, so a test left in the parent could not reach +//! the helpers here. They are also the two tests that are about this arm, so +//! the constraint and the responsibility agree. + +use super::*; + +#[test] +fn a_drive_relative_path_carries_its_component_and_the_current_drive_uses_the_process_directory() { + // **The name says what the two assertions reach, and an earlier one did + // not.** This was + // `a_drive_relative_path_is_rooted_at_that_drive_and_not_the_process_directory`, + // which claims more than anything here shows, in two separate ways. The + // other-drive assertion is `ends_with("\\foo")`, which accepts ANY base + // including the process directory -- so it cannot say "not the process + // directory". And "rooted at that drive" is not even true in general: an + // accepted entry is used verbatim and may name a directory on a different + // drive entirely, which is the sibling test's whole point. + // + // The body already said it only bounds the arm. The name did not, and the + // name is what a reader takes away -- the same defect as the test called + // `..._is_neither_consulted_nor_rewritten` before it was renamed. + // + // The third rooting form, with two arms, which is the part that gets + // missed: + // + // * For a drive OTHER than the current one, Windows reads the hidden + // `=X:` entry recorded for it. + // * For the CURRENT drive the entry makes no difference to the result and + // the process current directory wins. Measured: setting `=Q:` while the + // process is on `Q:` changes nothing. + // + // **This test does not mutate `=X:`, but the call it exercises may.** + // Measured: resolving `X:foo` for a non-current drive checks that drive's + // entry and WRITES it to `X:\` when the entry is absent or rejected. An + // accepted entry is left alone, and the current-drive form writes nothing + // -- so this is not "every resolution", but it does mean an ordinary host + // with no entry has one written merely by running this test. That is + // a property of the call, documented in the module doc; it is noted here so + // the next reader does not take "reads process state" at face value, as + // four revisions of that doc did. + // + // **This test only BOUNDS the other-drive arm**, because it does not control + // the entry: with no `=X:` set, an implementation that always used the + // drive root would satisfy everything here. The arm is pinned properly by + // `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one`, + // which sets the entry and draws its letter from a candidate list disjoint + // from this one's, so the two cannot race. + let cwd = current_directory(); + let cwd_drive = cwd.chars().next().filter(char::is_ascii_alphabetic); + + // The other-drive arm needs no drive letter from the current directory -- + // under a UNC current directory every letter is "other" -- so it runs + // unconditionally and this test never degenerates to a silent skip. + let other = probe_drive_from(probe_drives::ROOTED_AT_THAT_DRIVE, None); + + // This test controls no entry, but the CALL does: resolving for a + // non-current drive writes `=X:` whenever the recorded entry is absent or + // rejected, and on most hosts a letter chosen for being unused has no entry + // at all. So merely observing the arm mutates process-global state, and + // this was the one mutating case here without a guard -- the borrow is + // needed exactly because the mutation is not the test's own doing. + let _restore = BorrowedDriveEntry::take(other); + let resolved = resolve(&format!("{other}:foo")); + + // Only what is invariant without controlling the entry. An earlier version + // required the result to start with `X:\`, which the verbatim rule breaks; + // its replacement compared against the current directory, which `other` + // differs from by construction, so it could fire only if the entry happened + // to equal the process directory exactly -- the same vacuity, respelled. + // What survives every entry value is that the component is carried through. + assert!( + resolved.ends_with(r"\foo"), + "the component is carried through whatever the entry holds: {resolved}" + ); + + // The current-drive arm, where the process directory wins over any `=X:`. + // Only expressible when the current directory has a drive letter at all. + if let Some(drive) = cwd_drive { + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{}\foo", cwd.trim_end_matches('\\')), + "on the current drive, the per-drive entry makes no difference to \ + the result and the process directory is used" + ); + } +} + +#[test] +fn the_current_drives_entry_does_not_affect_resolution_and_is_not_rewritten() { + // The module doc states both halves of the current-drive arm as fact. Until + // now nothing pinned either, and the assertion just above -- which looks + // like it does -- cannot: it resolves `X:foo` WITHOUT controlling the entry + // and compares against the process directory, and Windows keeps the current + // drive's entry equal to that directory. So it reads the same either way. + // Vacuous in precisely the way this crate keeps rediscovering, and the + // reason the two arms need opposite fixtures: the sibling tests must AVOID + // the current drive, and this one must be on it. + // + // **The name says what is observable, and an earlier one did not.** This + // was `..._is_neither_consulted_nor_rewritten`, which claims the entry is + // not READ -- and installing a value and watching the outcome cannot + // separate "not read" from "read and ignored". That is the same overreach + // this branch removed from the probe's "without consulting a device", and + // the test correcting it committed it in its own name. What the two + // assertions below reach is the pair of observable effects: the entry makes + // no difference to the result, and it is not written back. + let cwd = current_directory(); + let Some(drive) = cwd.chars().next().filter(char::is_ascii_alphabetic) else { + // A UNC current directory has no drive letter, so there is no + // current-drive arm to exercise. Not a skip of something testable. + return; + }; + let process_directory = format!(r"{}\foo", cwd.trim_end_matches('\\')); + + let probe_dir = probe_directory("current-drive"); + let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let _restore = BorrowedDriveEntry::take(drive); + + // The anti-vacuity check, made permanent rather than performed once by + // hand: unless the two arms would give DIFFERENT answers, every assertion + // below passes without distinguishing them, which is the failure this test + // was written to correct. + assert_ne!( + process_directory, + format!(r"{probe}\foo"), + "precondition: the entry must name somewhere other than the process \ + directory, or honouring it and ignoring it look identical" + ); + + // No difference to the result. The entry is one the OTHER arm would honour + // verbatim -- an existing directory in canonical form -- and it names + // somewhere the process directory cannot be, because this test just created + // it under a process-unique name. If the entry were HONOURED, the result + // would be under `probe`. + set_drive_entry(drive, Some(probe)); + assert_eq!( + resolve(&format!("{drive}:foo")), + process_directory, + "the current drive's entry was set to {probe}, an entry the non-current \ + arm honours verbatim, and the process directory won anyway" + ); + + // Not rewritten, which needs a REJECTED entry to be visible: an accepted one + // is left alone on both arms, so leaving it alone shows nothing. A child of + // the probe directory cannot exist, and on the non-current arm that is + // replaced by the drive root. + let missing = probe_dir.path.join("no-such-child"); + let missing = missing.to_str().expect("the probe path is UTF-8"); + set_drive_entry(drive, Some(missing)); + let _ = resolve(&format!("{drive}:foo")); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(missing), + "an entry the non-current arm would have replaced with {drive}:\\ is \ + left untouched on the current drive" + ); +} + +/// A directory that exists, is in canonical `X:\...` form, and is neither a +/// drive root nor the process current directory. +/// +/// **The form is enforced, not assumed, and that distinction has now cost two +/// rounds.** An earlier version derived the probe from `current_directory()`, +/// which yields `C:` at a drive root -- drive-relative, not a directory -- and +/// both tests failed there. Its replacement used `std::env::temp_dir()`, which +/// is `%TMP%`/`%TEMP%` verbatim and carries no guarantee of a drive letter: with +/// temp redirected to a share, the probe is a UNC path, which +/// `GetFullPathNameW` rejects as an entry *on shape* -- the very rule the +/// caller is trying to pin. Folder redirection makes that an ordinary +/// configuration, not a contrived one. +/// +/// So the temp directory is used only when it is drive-rooted, and otherwise +/// the fallback is `%SystemRoot%`, which is guaranteed to exist, to be +/// canonical, and not to be a drive root. Nothing is created in the fallback +/// case, so `created` records whether there is anything to remove. +/// +/// Removal is a [`Drop`], matching this crate's own `Fixture` in +/// [`crate::handle`]'s tests. An earlier version cleaned up with a statement at +/// the end of each test and argued that a guard writing during unwinding could +/// panic and abort -- which is true of the `=X:` entry restore beside it, and +/// not of removing a directory. Conflating the two left a directory behind +/// after every failing assertion. +struct ProbeDir { + path: std::path::PathBuf, + created: bool, + + /// Held for the fixture's whole lifetime, because this fixture opens + /// handles and this suite serialises that. + /// + /// `handle_allocation()`'s read guard means "I may open handles"; its write + /// guard means "no other test may, while I reason about a specific handle + /// value". Eleven tests across four modules take the write guard to assert + /// things like a closed handle's value not being reused, and any test + /// allocating a handle beside them can make those assertions fail for a + /// reason that has nothing to do with what they pin. + /// + /// This fixture allocates: `create_dir` and `remove_dir` here, and the + /// `Path::exists` / `is_file` preconditions its tests run while it is + /// alive -- `std`'s Windows metadata goes through `CreateFileW`. Holding + /// the guard on the fixture covers all of them for the whole test, which a + /// guard taken at each call site would not. + /// + /// A test holding one of these must not take a second read guard: `std` + /// does not promise recursive read locking is deadlock-free. + _allocating: std::sync::RwLockReadGuard<'static, ()>, +} + +impl ProbeDir { + /// The drive letter this probe lives on, if it has one. + /// + /// The caller needs it to pick a probe drive that is *not* this one: + /// resolving `W:foo` against an entry naming a directory that is itself on + /// `W:` cannot demonstrate that an accepted entry is used verbatim across + /// drives, which is the property being pinned. + fn drive(&self) -> Option { + self.path + .as_os_str() + .to_string_lossy() + .chars() + .next() + .filter(char::is_ascii_alphabetic) + } +} + +impl Drop for ProbeDir { + fn drop(&mut self) { + if self.created { + let _ = std::fs::remove_dir(&self.path); + } + } +} + +fn probe_directory(tag: &str) -> ProbeDir { + // Taken before the first filesystem call, and handed to the fixture so it + // outlives this function. See `ProbeDir::_allocating`. + let _allocating = handle_allocation() + .read() + .expect("the lock is not poisoned"); + + // The full shape an accepted `=X:` entry must have, not just its first + // three characters: rooted at `X:\`, with no `.` or `..` component and no + // forward slash. `a_rejected_drive_entry_is_replaced_by_the_drive_root` + // shows each of those spellings is rejected while naming the same existing + // directory, so a base carrying one would turn that test's CONTROL + // assertion -- "the same directory in canonical form is accepted" -- into a + // rejection, and it would fail for a reason unrelated to what it pins. + // + // **A review read that as reachable through a non-canonical `%TMP%`. It is + // not, and the measurement is here so the next reader need not repeat it.** + // `std::env::temp_dir` goes through `GetTempPath2W`, which normalises what + // it finds: `C:/Users/.../Temp`, `...\Temp\.`, `...\Temp\..\Temp`, + // `...\Temp\\` and even the drive-relative `C:Users\...` all came back as + // `C:\Users\...\Temp\`. The one spelling passed through verbatim is + // `\\?\C:\...`, which is not drive-rooted and so takes the fallback below. + // + // The check is widened anyway. It costs nothing, it covers the fallback + // base too, and it is the difference between a precondition that is + // enforced and one that is argued -- which is the distinction this whole + // change exists to hold. + let canonical_drive_rooted = |p: &std::path::Path| { + let s = p.as_os_str().to_string_lossy().into_owned(); + let mut chars = s.chars(); + matches!( + (chars.next(), chars.next(), chars.next()), + (Some(d), Some(':'), Some('\\')) if d.is_ascii_alphabetic() + ) && !s.contains('/') + && !s.split('\\').any(|c| c == "." || c == "..") + }; + + let temp = std::env::temp_dir(); + if canonical_drive_rooted(&temp) { + let path = temp.join(format!("wnrs-{}-{tag}", std::process::id())); + + // `create_dir`, not `create_dir_all`, and the difference is ownership + // rather than parents. `create_dir_all` SUCCEEDS on a directory that + // already exists, so setting `created` after it recorded a claim this + // fixture had not established -- and `Drop` then removes the path on + // the strength of that claim. The name is `%TEMP%\wnrs--`, + // which an interrupted earlier run leaves behind and which Windows can + // hand back to a later process when it reuses the PID. The blast radius + // is small, because `remove_dir` refuses a non-empty directory -- but + // "small" is not the point. Deleting something on an ownership claim + // nothing checked is the same defect as asserting a mechanism nothing + // measured, and this file exists to stop doing that. + // + // An existing directory is still perfectly usable as a probe; it is + // just not ours to remove. + let created = match std::fs::create_dir(&path) { + Ok(()) => true, + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => false, + Err(e) => panic!("create the probe directory {}: {e}", path.display()), + }; + + return ProbeDir { + path, + created, + _allocating, + }; + } + + // Not creating anything here, so no write permission is needed on a host + // whose temp directory is redirected off a drive letter. + // + // **It must also differ from the process current directory**, which the + // temp branch gets for free -- it creates a uniquely named child -- and this + // branch does not. Cargo launched from `%SystemRoot%` on a host with a UNC + // temp directory would otherwise hand back the current directory itself, + // and a probe indistinguishable from the current directory cannot separate + // "the entry was honoured" from "the entry was ignored". `System32` is the + // second candidate for the same reason `probe_drive_from` takes a list: + // one value that is usually right is not a guarantee. + let system_root = std::path::PathBuf::from( + std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), + ); + let cwd = current_directory(); + let cwd = cwd.trim_end_matches('\\'); + let distinct = |p: &std::path::Path| { + !p.as_os_str() + .to_string_lossy() + .trim_end_matches('\\') + .eq_ignore_ascii_case(cwd) + }; + + let path = [system_root.clone(), system_root.join("System32")] + .into_iter() + .find(|p| canonical_drive_rooted(p) && distinct(p)) + .unwrap_or_else(|| { + panic!( + "no fallback probe directory is both canonical and distinct \ + from the current directory {cwd}" + ) + }); + + ProbeDir { + path, + created: false, + _allocating, + } +} +/// The candidate drive letters, one list per test that mutates a `=X:` entry. +/// +/// **Centralised so the properties these tests depend on are CHECKED rather +/// than restated.** Both were previously prose -- a doc comment saying the +/// lists are disjoint, and an archived note enumerating them -- and prose +/// drifted: the archive named five lists after the sixth had been added, so a +/// reader picking letters for a seventh would have consulted an inventory +/// missing three of the eighteen letters already in use. Nothing checked +/// either claim, because nothing could: the lists were literals at six call +/// sites with no table to read. +/// +/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] now reads +/// this table, so adding a list that collides -- or one too short for +/// [`probe_drive_from`]'s guarantee -- fails a test instead of a review. +mod probe_drives { + pub const ROOTED_AT_THAT_DRIVE: &[char] = &['X', 'Y', 'P']; + pub const VERBATIM_ENTRY: &[char] = &['W', 'U', 'N']; + pub const REJECTED_ENTRY: &[char] = &['V', 'T', 'M']; + pub const LONG_ENTRY: &[char] = &['R', 'S', 'K']; + pub const BORROW_GUARD: &[char] = &['G', 'H', 'J']; + pub const EMPTY_VS_ABSENT: &[char] = &['E', 'F', 'B']; + + /// Every list above. A new list that is not added here is not covered by + /// the disjointness test, so keep them together. + pub const ALL: &[(&str, &[char])] = &[ + ("ROOTED_AT_THAT_DRIVE", ROOTED_AT_THAT_DRIVE), + ("VERBATIM_ENTRY", VERBATIM_ENTRY), + ("REJECTED_ENTRY", REJECTED_ENTRY), + ("LONG_ENTRY", LONG_ENTRY), + ("BORROW_GUARD", BORROW_GUARD), + ("EMPTY_VS_ABSENT", EMPTY_VS_ABSENT), + ]; +} + +#[test] +fn the_probe_drive_candidate_lists_are_disjoint_and_large_enough() { + for (name, list) in probe_drives::ALL { + // `probe_drive_from` excludes at most two letters -- the current drive + // and the probe directory's drive -- so three candidates guarantee a + // survivor. This is the premise of the panic in that function, checked + // here rather than left to the caller as the doc comment used to. + assert!( + list.len() >= 3, + "{name} has {} candidates, and at most two can be excluded, so \ + fewer than three cannot guarantee a survivor", + list.len() + ); + + let mut seen = list.to_vec(); + seen.sort_unstable(); + seen.dedup(); + assert_eq!(seen.len(), list.len(), "{name} repeats a letter"); + } + + for (a_name, a) in probe_drives::ALL { + for (b_name, b) in probe_drives::ALL { + if a_name == b_name { + continue; + } + let shared: Vec = a + .iter() + .copied() + .filter(|c| b.iter().any(|d| d.eq_ignore_ascii_case(c))) + .collect(); + assert!( + shared.is_empty(), + "{a_name} and {b_name} share {shared:?}, so the two tests can \ + select the same drive and race under libtest's \ + thread-per-test model" + ); + } + } +} + +/// A drive letter to probe with, drawn from `candidates` and guaranteed to be +/// neither the current drive nor `avoid`. +/// +/// **Every candidate is checked, which an earlier version did not do.** It took +/// a preferred letter and a fallback, tested only the preferred one, and +/// returned the fallback unvalidated -- so when the preferred letter was +/// excluded the caller could still be handed the current drive. Measured: with +/// the process on `U:` and `%TEMP%` on a `subst`-ed `W:`, the verbatim test +/// selected `U` and then asserted the *other-drive* contract while exercising +/// the *current-drive* arm, which is the one case where the entry makes no +/// difference to the result. +/// It failed, but the mode is worse than a failure: the helper's own doc +/// promised a guarantee it never enforced. +/// +/// Three candidates against at most two exclusions, so one always survives. The +/// panic remains because that argument is about the caller's list, which this +/// function cannot see -- but the argument is no longer only an argument: +/// [`the_probe_drive_candidate_lists_are_disjoint_and_large_enough`] checks it +/// against every list in [`probe_drives`]. +/// +/// Callers pass disjoint lists, so no two tests can select the same letter and +/// race under libtest's thread-per-test model. +fn probe_drive_from(candidates: &[char], avoid: Option) -> char { + let cwd = current_directory(); + // A UNC current directory has no drive letter, so nothing collides there. + let current = cwd.chars().next().filter(char::is_ascii_alphabetic); + let taken = |c: char| { + current.is_some_and(|d| d.eq_ignore_ascii_case(&c)) + || avoid.is_some_and(|d| d.eq_ignore_ascii_case(&c)) + }; + + *candidates.iter().find(|&&c| !taken(c)).unwrap_or_else(|| { + panic!( + "every candidate of {candidates:?} is excluded by the current \ + drive ({current:?}) or the probe drive ({avoid:?})" + ) + }) +} +/// Reads one of the hidden `=X:` per-drive current-directory entries. +/// +/// Through Win32 rather than `std::env`, which rejects a key containing `=` +/// outright and so cannot address these at all. +fn drive_entry(drive: char) -> Option { + let name = Wtf16String::from(format!("={drive}:").as_str()); + // Start small and grow to whatever Windows asks for. The API's two return + // conventions differ: on success it reports the units written EXCLUDING the + // terminator, and on an undersized buffer it reports the capacity REQUIRED + // INCLUDING it. Treating the second as the first indexes past the buffer and + // panics -- while trying to preserve a legitimate long entry, so the failure + // would land before the test could restore the process state it borrowed. + let mut buffer = vec![0u16; 256]; + loop { + // Zero is TWO different answers, and the last error is the only thing + // that separates them -- so it is cleared first, because the value left + // by some earlier call would otherwise be read as this one's. + // + // SAFETY: no preconditions. + unsafe { windows_sys::Win32::Foundation::SetLastError(ERROR_SUCCESS) }; + + // SAFETY: the name is NUL-terminated and the buffer is writable for + // the length passed. + let written = unsafe { + windows_sys::Win32::System::Environment::GetEnvironmentVariableW( + name.as_terminated_ptr(), + buffer.as_mut_ptr(), + u32::try_from(buffer.len()).unwrap_or(u32::MAX), + ) + }; + let written = written as usize; + if written == 0 { + // **An earlier version of this comment claimed, as measured, that + // an empty value and an absent name are the same state and cannot + // be told apart. That was wrong, and wrong in this crate's + // signature way: the measurement behind it never cleared the last + // error, so it could only ever have seen whatever was already + // there.** Cleared first and re-measured, the two are distinct, for + // an ordinary name and an `=X:` name alike: + // + // set to "" -> returns 0, last error ERROR_SUCCESS + // deleted -> returns 0, last error ERROR_ENVVAR_NOT_FOUND + // + // The difference is not academic here. Collapsing both to `None` + // makes the restoration in `BorrowedDriveEntry` DELETE an inherited + // empty entry rather than put it back -- losing exactly the process + // state the guard exists to preserve. + // + // SAFETY: no preconditions. + let last = unsafe { windows_sys::Win32::Foundation::GetLastError() }; + return match last { + ERROR_SUCCESS => Some(Wtf16String::from_units(&[])), + ERROR_ENVVAR_NOT_FOUND => None, + // Anything else is neither answer, and folding it into "absent" + // would make the guard delete an entry over a transient error. + other => panic!("reading ={drive}: failed with error {other}"), + }; + } + if written < buffer.len() { + // Kept as WTF-16 units rather than going through String: a lossy + // conversion would replace an unpaired surrogate, so restoring the + // entry afterwards would write back something the process did not + // start with. + return Some(Wtf16String::from_units(&buffer[..written])); + } + buffer = vec![0u16; written]; + } +} +/// Sets or clears one of the hidden `=X:` entries. +fn set_drive_entry(drive: char, value: Option<&str>) { + set_drive_entry_units(drive, value.map(Wtf16String::from).as_ref()); +} + +/// [`set_drive_entry`], taking the exact units a [`drive_entry`] read returned. +/// +/// Restoration goes through this rather than through `&str`, so an entry +/// containing an unpaired surrogate is put back byte for byte. +fn set_drive_entry_units(drive: char, value: Option<&Wtf16String>) { + assert!( + try_set_drive_entry_units(drive, value), + "set ={drive}: entry" + ); +} + +/// [`set_drive_entry_units`] without the assertion, reporting success instead. +/// +/// Separate because the restoration in [`BorrowedDriveEntry`] runs during +/// unwinding, where a panic would abort the process and destroy the report of +/// the failure that started the unwind. +fn try_set_drive_entry_units(drive: char, value: Option<&Wtf16String>) -> bool { + let name = Wtf16String::from(format!("={drive}:").as_str()); + let value_ptr = value + .as_ref() + .map_or(core::ptr::null(), |v| v.as_terminated_ptr()); + // SAFETY: both pointers are NUL-terminated; a null value clears the entry. + let ok = unsafe { + windows_sys::Win32::System::Environment::SetEnvironmentVariableW( + name.as_terminated_ptr(), + value_ptr, + ) + }; + ok != 0 +} + +/// Borrows one drive's `=X:` entry and puts it back when the test ends, +/// **whether or not the test panicked**. +/// +/// Restoring on the last line of the test is not enough, and the hazard is not +/// theoretical: `=X:` is process-global, `cargo test` runs tests as threads in +/// ONE process, and every assertion between the save and the restore is a place +/// the entry can be abandoned. What a sibling test would then inherit is not +/// merely a stale value but one no host would produce -- a path to a directory +/// that no longer exists once the probe directory is removed, or the 1200-unit +/// value that `a_long_drive_entry_round_trips_through_the_reader` installs on +/// purpose. The reader above already names this ("the entry is then never +/// restored") without defending against it; this is the defence. +/// +/// A failed restore is dropped rather than asserted, for the reason given on +/// [`try_set_drive_entry_units`]. +struct BorrowedDriveEntry { + drive: char, + saved: Option, +} + +impl BorrowedDriveEntry { + fn take(drive: char) -> Self { + Self { + drive, + saved: drive_entry(drive), + } + } +} + +impl Drop for BorrowedDriveEntry { + fn drop(&mut self) { + let restored = try_set_drive_entry_units(self.drive, self.saved.as_ref()); + + // Silence is bought only where it buys something. While unwinding, a + // panic here aborts the process and destroys the report of the failure + // that started the unwind, so a failed restore is worth less than the + // diagnosis it would replace. On the ordinary path there is no such + // trade: staying quiet would let the suite carry on with corrupted + // process-global state and fail somewhere unrelated, which is the + // hardest kind of failure to read. + assert!( + restored || std::thread::panicking(), + "restoring ={}: failed, leaving process-global state corrupted for \ + every test that follows", + self.drive + ); + } +} + +#[test] +fn a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one() { + // The arm the sibling test can only BOUND. Without controlling the entry, + // an implementation that always used the drive root would satisfy every + // assertion there, because a host with no `=X:` entry cannot tell the two + // rules apart. + // + // **Controlling the entry is not the hazard it looks like**, and that is + // what unblocked this test. The objection was that `=X:` is process-global + // while these tests share a process. But the call under test writes that + // entry itself whenever it is absent or rejected, so this state is already + // mutated by the code being exercised. What keeps the tests from + // interfering is not that -- it is that each takes a drive letter no other + // one can choose. + // + // The probe directory is created FIRST so the letter can avoid its drive as + // well as the current one: an entry naming a directory on the same drive it + // is recorded for would be honoured, the test would pass, and the + // cross-drive property below would go unexercised. + let probe_dir = probe_directory("verbatim"); + let probe = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let drive = probe_drive_from(probe_drives::VERBATIM_ENTRY, probe_dir.drive()); + let _restore = BorrowedDriveEntry::take(drive); + + assert_ne!( + Some(drive.to_ascii_uppercase()), + probe_dir.drive().map(|d| d.to_ascii_uppercase()), + "the probe directory must be on a different drive, or the assertion \ + below cannot show the entry is honoured ACROSS drives" + ); + + set_drive_entry(drive, Some(probe)); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{probe}\foo"), + "an entry naming an existing directory is honoured verbatim, even onto \ + a different drive -- so \"that drive's own current directory\" is the \ + convention the entry usually holds, not a guarantee about the result" + ); + + // Verbatim means verbatim, including the join. The module doc records that + // an accepted entry ending in a separator yields a DOUBLED one, and nothing + // pinned it -- so the observation could have stopped being true without CI + // noticing, which is the drift this change exists to close rather than + // commit again. + set_drive_entry(drive, Some(&format!(r"{probe}\"))); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{probe}\\foo"), + "an entry is accepted with a trailing separator and concatenated \ + without normalising the join" + ); + + // An entry that does not name an existing directory is rejected, and the + // call rewrites it to the drive root rather than leaving it stale. + // + // Derived from the probe directory rather than hard-coded: a literal like + // `C:\no-such-directory-for-this-test` is only missing until some host + // happens to have it, and the test would then assert the rejected case + // against an accepted one. A child of a directory this test just created + // cannot exist unless something else creates it in between. + let missing = probe_dir.path.join("no-such-child"); + assert!( + !missing.exists(), + "precondition: the rejected entry must name nothing: {}", + missing.display() + ); + set_drive_entry( + drive, + Some(missing.to_str().expect("the probe path is UTF-8")), + ); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), + "an entry that names nothing is rejected in favour of the drive root" + ); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(format!(r"{drive}:\").as_str()), + "and the call REWROTE the entry: this is a query that mutates the \ + process environment block" + ); + + // Absent entirely, the entry is created rather than merely read. + set_drive_entry(drive, None); + assert_eq!(drive_entry(drive), None, "precondition: entry cleared"); + let _ = resolve(&format!("{drive}:foo")); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(format!(r"{drive}:\").as_str()), + "resolving created the entry on a host that had none" + ); +} + +#[test] +fn a_rejected_drive_entry_is_replaced_by_the_drive_root() { + // Acceptance needs BOTH a shape and an existence check, and a draft of the + // module doc claimed it was "a filesystem query rather than a syntax test" + // -- having measured only the existence half. Every value below names an + // existing directory, so anything rejected here is rejected on shape alone. + // + // Pinned because the distinction is not guessable and the doc asserts it. + let probe_dir = probe_directory("shape"); + let accepted = probe_dir.path.to_str().expect("the probe path is UTF-8"); + let drive = probe_drive_from(probe_drives::REJECTED_ENTRY, probe_dir.drive()); + let _restore = BorrowedDriveEntry::take(drive); + + // The control: this exact directory IS accepted in canonical form, so the + // rejections below cannot be blamed on the directory itself. + set_drive_entry(drive, Some(accepted)); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{accepted}\foo"), + "control: the same directory in canonical form is accepted" + ); + + // Same directory, spellings that are not fully-qualified `X:\...` form. + // Each names something that exists; each is rejected anyway. + for spelling in [ + accepted.replace('\\', "/"), + format!(r"{accepted}\."), + format!( + r"{accepted}\..\{}", + accepted.rsplit('\\').next().unwrap_or("") + ), + format!(r"\\?\{accepted}"), + ] { + set_drive_entry(drive, Some(&spelling)); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), + "{spelling:?} names an existing directory but is rejected on shape" + ); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(format!(r"{drive}:\").as_str()), + "and the rejected entry is written back as the drive root" + ); + } + + // The type check, which is a separate necessary condition from both the + // shape above and the existence check in the sibling test. The module doc + // has listed an existing FILE among the rejections since the drive-entry + // work, and nothing pinned it -- so the one observation distinguishing + // "names a directory" from "names something" lived only in prose. + // + // Not a file this test creates: `ProbeDir` may be the read-only + // `%SystemRoot%` fallback, where creating one needs privileges the suite + // must not assume. This one is present on every Windows host by + // construction, and setting an entry to a file does not touch the file. + let system_file = std::path::PathBuf::from( + std::env::var("SystemRoot").expect("SystemRoot is set on Windows"), + ) + .join("System32") + .join("kernel32.dll"); + assert!( + system_file.is_file(), + "precondition: the rejected entry must name an existing FILE: {}", + system_file.display() + ); + let system_file = system_file.to_str().expect("the system path is UTF-8"); + + set_drive_entry(drive, Some(system_file)); + assert_eq!( + resolve(&format!("{drive}:foo")), + format!(r"{drive}:\foo"), + "{system_file} exists and is canonical, and is rejected anyway because \ + it is not a directory -- so existence alone is not the gate" + ); + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(format!(r"{drive}:\").as_str()), + "and an entry naming a file is written back as the drive root too" + ); +} + +#[test] +fn a_long_drive_entry_round_trips_through_the_reader() { + // The reader grows its buffer, and this is what proves it. Windows reports + // an undersized buffer by returning the REQUIRED capacity rather than the + // units written, so a reader that treats the two alike slices past its own + // buffer and panics -- while preserving a legitimate entry, which is the + // worst moment for it, because the entry is then never restored. + // + // 1200 units is comfortably past the 256 the reader starts with and past + // the 1024 an earlier fixed-size version used, and is a legitimate value: a + // per-drive entry is a path, and long paths reach far beyond this. + let drive = probe_drive_from(probe_drives::LONG_ENTRY, None); + let _restore = BorrowedDriveEntry::take(drive); + + let long = format!(r"C:\{}", "a".repeat(1200)); + set_drive_entry(drive, Some(&long)); + + let read_back = drive_entry(drive).expect("the entry was just set"); + assert_eq!( + read_back.to_string_lossy(), + long, + "a long entry survives the read, so the buffer grew instead of truncating" + ); +} + +#[test] +fn a_borrowed_drive_entry_is_restored_even_when_the_borrower_panics() { + // The guard exists for the unwinding path, and a suite that passes never + // takes it -- so trusting it would mean shipping an untested defence + // against the exact failure it is there for. This takes the path on + // purpose. + let drive = probe_drive_from(probe_drives::BORROW_GUARD, None); + + // The outer guard is not ceremony. This test installs a sentinel to watch + // the inner guard put back, and without it that install would destroy + // whatever the process inherited -- so the test for not losing borrowed + // state would itself lose some. The inner guard still takes the unwinding + // path; the outer one covers this test's own borrow. + let _outer = BorrowedDriveEntry::take(drive); + let sentinel = format!(r"C:\borrowed-entry-{}", std::process::id()); + set_drive_entry(drive, Some(&sentinel)); + + let outcome = std::panic::catch_unwind(|| { + let _restore = BorrowedDriveEntry::take(drive); + set_drive_entry(drive, Some(r"C:\the-borrowed-value")); + panic!("expected: this panic exercises the restore-on-unwind path"); + }); + assert!( + outcome.is_err(), + "precondition: the borrower must actually panic, or the unwinding path \ + is not the thing being measured" + ); + + assert_eq!( + drive_entry(drive).map(|v| v.to_string_lossy()).as_deref(), + Some(sentinel.as_str()), + "the guard put the entry back while unwinding, where an end-of-test \ + restore would have been skipped" + ); +} + +#[test] +fn an_empty_drive_entry_is_distinguished_from_an_absent_one() { + // **This pins a correction, not a discovery.** The reader used to fold both + // into `None`, on a recorded measurement that an empty value and an absent + // name are the same state. They are not; the measurement behind that claim + // never cleared the last error, so it could only have read whatever an + // earlier call left behind -- the same "stated more precisely than the + // evidence reaches" failure this crate keeps meeting, committed inside the + // comment that called itself measured. + // + // The consequence is what makes it worth a test rather than a fix: with the + // two collapsed, restoring an inherited EMPTY entry deletes it, so the guard + // written to preserve process state destroys it in exactly one case. + let drive = probe_drive_from(probe_drives::EMPTY_VS_ABSENT, None); + let _outer = BorrowedDriveEntry::take(drive); + + set_drive_entry(drive, Some("")); + let empty = drive_entry(drive); + assert_eq!( + empty.as_ref().map(|v| v.to_string_lossy()), + Some(String::new()), + "an entry set to the empty string reads back as PRESENT and empty" + ); + + set_drive_entry(drive, None); + assert_eq!( + drive_entry(drive), + None, + "and a deleted entry reads back as absent, which is the answer the \ + empty one must not be confused with" + ); + + // The two are distinct in the round trip as well as in the read, which is + // the property restoration actually depends on. + set_drive_entry_units(drive, empty.as_ref()); + assert_eq!( + drive_entry(drive).as_ref().map(|v| v.to_string_lossy()), + Some(String::new()), + "restoring an empty entry puts back an empty entry, not an absent one" + ); +} From dbbeccefedb209cb7702dd44c376200bdbcbcd9d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Thu, 10 Sep 2026 17:35:57 -0400 Subject: [PATCH 36/36] fix(namespace-request): assert which device each name reaches, and repoint the links the split moved Copilot's review of the split. Its two main asks were already done -- the split itself, and the NUL suffix cases -- so the live findings are what it saw in the new shape. **A device assertion that only proved "some device".** `a_legacy_device_name_short_circuits_rooting` asserted `resolved.starts_with(r"\\.\")` over eight names, which would pass if `CON` resolved to `\\.\NUL` -- and the contract names which device each spelling reaches. Both device tests now assert the complete mapping. That also pins what the device form does to a spelling: `CON:`, `CON::`, `CON.` and `CON ` all give `\\.\CON`, while `con` and `cOn:` give `\\.\con` and `\\.\cOn`, so trailing punctuation is dropped and case is carried through. Verified by sabotage -- expecting `\\.\NUL` for every name, which is Copilot's own scenario, now fails on `CON`. **Three links the split invalidated**, all mine: the archive's pointers to the verbatim test and the candidate table, and Tier 2's pointer to the empty-entry regression test, all now resolve to `drive_entry.rs`. Two others were checked and left -- they cite tests that stayed in the facade. Also: the probes archive stated the `=X:` write-back unconditionally, where an accepted entry is left alone; and the `## Moved` group heading this branch added carried a timestamp where the format reserves that for the anchored item heading. **Declined, and queued as `M-inf.2` instead.** A comment asked for M26.5's completed body in `CHECKLIST-thread-ambient.md` to become a one-line stub. The rule is right and the file does violate it, but M26.5 is not exceptional -- stubbing only the reported item would have made it inconsistent with the five siblings written the same way. Counted rather than assumed: every group in that file is complete and due for migration -- M22 (8 items), M23 (6), M24 (6), M25 (7), M26 (6), M27 (6), M28 (4), M29 (5) -- with only `M26+` open. That is roughly 400 lines of another feature's bookkeeping, and moving it through a `GetFullPathNameW` documentation branch would bury the change this branch exists to make. Verified: 236 lib + 37 doc + 32 integration tests, clippy --all-targets, cargo fmt, encoding over 631 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHECKLIST.md | 21 +++++++++++ .../COMPLETED-CHECKLIST.md | 4 +-- .../DESIGN-RATIONALE.md | 2 +- .../src/full_path/tests.rs | 36 ++++++++++++++----- .../COMPLETED-CHECKLIST.md | 6 ++-- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/CHECKLIST.md b/CHECKLIST.md index 4df84e5d4..4d01818e8 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -195,3 +195,24 @@ Ungated work with no identified predecessor deliverable. probe because a `LOGON32_LOGON_NEW_CREDENTIALS` token answered the question with a passing control, so the fallback was redundant -- not because the crash was understood. Parked rather than dropped so the unexplained result is not mistaken for a tested one. + +- [ ] **M-inf.2** -- Archive the eight completed milestone groups in + [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) into + [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). + + **Raised by a review that named one item, and measured to be eight groups.** The comment asked for + M26.5's completed multi-line body to be replaced by a one-line stub, per the checklist-hygiene rule + that an active checklist is an action queue. That rule is right and the file does violate it -- but + M26.5 is not exceptional: its five siblings in M26 are written the same way, so stubbing only the + reported item would have made it inconsistent with the group it belongs to rather than more + consistent with the rule. + + Counted rather than assumed, every group in the file is complete and due for migration under the + "move the completed group" rule: M22 (8 items), M23 (6), M24 (6), M25 (7), M26 (6), M27 (6), + M28 (4) and M29 (5). Only `M26+` has open items, and it is what keeps the file alive. + + Not taken in PR #86 because that branch corrects `GetFullPathNameW` documentation and touched + M26.5 only to fix one technical premise inside it. Migrating roughly 400 lines of another feature's + bookkeeping through it would bury the change it exists to make. The migration is mechanical, is its + own commit, and needs the group headings dated per the archive format -- date-only on the `## Moved` + line, with any precise timestamp reserved for an anchored item heading. diff --git a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md index 738624fd5..01500bc5a 100644 --- a/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md +++ b/crates/windows-namespace-request-sys/COMPLETED-CHECKLIST.md @@ -27,7 +27,7 @@ Append-only. Newest groups at the bottom. their disjoint drive letters. `a_drive_relative_path_uses_that_drives_entry_verbatim_and_rewrites_a_bad_one` - in [tests.rs](src/full_path/tests.rs) now pins all three behaviours: an entry + in [drive_entry.rs](src/full_path/tests/drive_entry.rs) now pins all three behaviours: an entry naming an existing directory is honoured verbatim (onto a *different* drive, which is what makes "that drive's own current directory" a convention rather than a guarantee); an entry naming nothing is rejected in favour of the drive @@ -41,7 +41,7 @@ Append-only. Newest groups at the bottom. letter and returned the second unchecked, so the guarantee the pair implied did not hold. The lists are now three letters each, every candidate is checked against both the current drive and the probe drive, and the lists themselves - live in one table in [tests.rs](src/full_path/tests.rs) with a test enforcing + live in one table in [drive_entry.rs](src/full_path/tests/drive_entry.rs) with a test enforcing that they stay disjoint and long enough. This note deliberately does NOT enumerate them: an earlier version did, and named five lists after a sixth had been added -- so a reader picking letters for a seventh would have consulted diff --git a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md index 15aeec950..26bbe2f0a 100644 --- a/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md +++ b/crates/windows-namespace-request-sys/DESIGN-RATIONALE.md @@ -187,7 +187,7 @@ state; with the two answers collapsed, restoring an inherited *empty* entry one case, and only that case. Pinned by `an_empty_drive_entry_is_distinguished_from_an_absent_one` in -[tests.rs](src/full_path/tests.rs), and verified by re-introducing the collapse, +[drive_entry.rs](src/full_path/tests/drive_entry.rs), and verified by re-introducing the collapse, which makes it fail. The general form is worth keeping separately from the specific fact: **a diff --git a/crates/windows-namespace-request-sys/src/full_path/tests.rs b/crates/windows-namespace-request-sys/src/full_path/tests.rs index 222389eeb..d938f4219 100644 --- a/crates/windows-namespace-request-sys/src/full_path/tests.rs +++ b/crates/windows-namespace-request-sys/src/full_path/tests.rs @@ -312,13 +312,19 @@ fn a_legacy_device_name_short_circuits_rooting() { // The exception to "roots a path that is not fully qualified", and the one // a caller passing an untrusted name has to know about: these do not become // files under the current directory. + // + // **Asserted as the complete mapping, not as "somewhere in the device + // namespace".** `starts_with(r"\\.\")`, which this used, would pass if + // `CON` resolved to `\\.\NUL` -- and the contract names which device each + // spelling reaches, so a test binding only to the prefix does not bind to + // the contract. Measured: the name is carried through unchanged. for name in [ "CON", "NUL", "PRN", "AUX", "CONIN$", "CONOUT$", "COM1", "LPT9", ] { - let resolved = resolve(name); - assert!( - resolved.starts_with(r"\\.\"), - "{name} names a device, so it must not be rooted: {resolved}" + assert_eq!( + resolve(name), + format!(r"\\.\{name}"), + "{name} reaches ITS device, not merely some device" ); } } @@ -327,11 +333,23 @@ fn a_legacy_device_name_short_circuits_rooting() { fn the_device_form_accepts_trailing_colons_dots_spaces_and_any_casing() { // Every spelling the module doc claims reaches a device. A filter written // from a narrower reading of the rule would let these through. - for spelling in ["CON", "CON:", "CON::", "CON.", "CON ", "con", "cOn:"] { - let resolved = resolve(spelling); - assert!( - resolved.starts_with(r"\\.\"), - "{spelling:?} is a device spelling: {resolved}" + // + // The full result again rather than the prefix, which also pins what the + // device form does to the spelling: trailing colons, dots and spaces are + // dropped, and case is carried through untouched. + for (spelling, expected) in [ + ("CON", r"\\.\CON"), + ("CON:", r"\\.\CON"), + ("CON::", r"\\.\CON"), + ("CON.", r"\\.\CON"), + ("CON ", r"\\.\CON"), + ("con", r"\\.\con"), + ("cOn:", r"\\.\cOn"), + ] { + assert_eq!( + resolve(spelling), + expected, + "{spelling:?} is a device spelling, and reaches this exact device" ); } } diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 98c8e638f..9e8797022 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -94,7 +94,7 @@ piece of work rather than a correction to that one. newline even when redirected -- had stdout been block-buffered this milestone would have needed a per-line flush too. -## Moved 2026-09-09 22:54:01 -04:00 -- M2.6: what `GetFullPathNameW` does, and whether it stays +## Moved 2026-09-09 -- M2.6: what `GetFullPathNameW` does, and whether it stays ### M2.6 -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. *(completed 2026-09-09 22:54:01 UTC-04:00)* @@ -125,7 +125,9 @@ request as it was written, and quotes the module doc as it read before the corre *(Later correction: the second half stood, the first did not. "Touches no filesystem" was measured false while carrying out this item -- resolving `X:foo` for a non-current drive distinguishes an - existing directory from an existing file from a missing one, and rewrites the `=X:` entry. What + existing directory from an existing file from a missing one, and rewrites the `=X:` entry when that + check REJECTS it -- an accepted entry is left alone, so the write is conditional rather than part of + every such resolution. What Microsoft documents is only that the call does not VERIFY its result. Two smaller things in the paragraph above also turned out to be stated too broadly: the per-drive entry is consulted for a drive OTHER than the current one, and on the current drive it makes no difference to the result --