Validate the Windows final component through a same-handle reparse-point open (#703) - #739
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (11)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Summary
Related issue
WalkthroughChangesWindows reparse-point validation
Sequence Diagram(s)sequenceDiagram
participant Filter
participant open_file_checked
participant WindowsHandle
participant Reader
Filter->>open_file_checked: request file open
open_file_checked->>WindowsHandle: open with reparse-aware flags
WindowsHandle-->>open_file_checked: return handle attributes
open_file_checked->>WindowsHandle: reject prohibited reparse point
open_file_checked->>Reader: provide validated handle
Suggested labels: Priority: ➖ Normal Change: Bug fix · Severity of issue fixed: Medium 🚥 Pre-merge checks | ✅ 13 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (13 passed)
Full details: User-Facing DocumentationExplanation Treat the user-facing documentation as incomplete. The pull request changes Windows Resolution Update Full details: Testing (Compile-Time / Ui)Explanation Require a compile-time test. The PR adds the Windows Resolution Add a Windows-gated trybuild compile-pass test, or a clear Rust compile-time equivalent, that evaluates A guarded handle opens bright Comment |
Reviewer's GuideWindows file reads now open the final component without traversing reparse points, validate attributes from that same handle, and read through it, eliminating the pre-open check race while preserving capability-based access, shared regular-file diagnostics, and the symlink-following opt-in. New Windows tests and ADR/documentation updates cover the behavior and rationale. Sequence diagram for race-free Windows file validationsequenceDiagram
participant Filter
participant OpenFileChecked
participant ParentHandle
participant FileHandle
Filter->>OpenFileChecked: open_file_checked(path, limits)
OpenFileChecked->>ParentHandle: open_with(entry, custom_flags)
Note over ParentHandle: FILE_FLAG_OPEN_REPARSE_POINT
Note over ParentHandle: FILE_FLAG_BACKUP_SEMANTICS
ParentHandle-->>OpenFileChecked: FileHandle
OpenFileChecked->>FileHandle: metadata()
FileHandle-->>OpenFileChecked: file_attributes()
alt FILE_ATTRIBUTE_REPARSE_POINT set
OpenFileChecked-->>Filter: not_regular_file_error(path)
else ordinary regular file
Filter->>FileHandle: read bytes
FileHandle-->>Filter: file contents
end
Flow diagram for Windows reparse-point policy modesflowchart TD
A["open_file_checked"] --> B["apply_open_flags"]
B --> C{"follow_symlinks?"}
C -->|No| D["OPEN_REPARSE_POINT + BACKUP_SEMANTICS"]
C -->|Yes| E["BACKUP_SEMANTICS"]
D --> F["open_with"]
E --> F
F --> G["metadata from opened handle"]
G --> H{"Default policy and reparse attribute?"}
H -->|Yes| I["not_regular_file_error"]
H -->|No| J["shared is_file check and read"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
22d0897 to
ac3d3e9
Compare
Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`.
Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4eee40e77c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
CodeRabbit review — dispositionsRan Fixed — spelling ( Declined — skip-on-missing- let Some(link) = fallible::file_symlink_fixture(&root)? else {
skip_without_symlink_support(case);
return Ok(());
};with fn skip_without_symlink_support(case: FilterCase) {
eprintln!(
"skipped: {} — this host cannot create a symlink fixture",
case.name
);
}The parallel Also declined — the same finding restated three times against different line |
…ides Three guides still described the pre-open Windows check that this branch removed, or stated the policy as symlink-only when it in fact refuses every reparse point. `users-guide.md` and `stdlib-yaml-and-jinja-guide.md` now say that the Windows refusal covers junctions, volume mount points, and other tags such as deduplication or cloud placeholders, and that `follow_symlinks=true` is the opt-in for all of them. `security-network-command-audit.md` replaces the "pre-open `symlink_metadata` check on Windows" remediation with `FILE_FLAG_OPEN_REPARSE_POINT` plus the handle's `FILE_ATTRIBUTE_REPARSE_POINT`, and records that the judgement and the read share one handle. Reported by chatgpt-codex-connector on PR #739. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Addressed in
One note on scope: the Verified: |
|
ADR renumbered 027 → 032 ( A reviewer opening The ADR was originally written as 026, renumbered to 027 when A sweep of every remote branch (not just
The commit is a
|
* Record the deferred split-build-dir harness trim (#693) The 156s figure that motivated trimming `harness_compiles_under_a_split_build_dir` came from the contended distribution, where the two isolated-Cargo tests each roughly halved the other. #687 moved the other one off the Windows lane, so this test got faster without being touched and the saving a trim could return fell with it. Measured across the three runs after #687, the test's exclusive tail is 62.3s, 87.2s and 85.5s, so trimming it is worth about 85s rather than 156s. The lane itself fell from a 1468s median to about 848s across #687, #690 and #691, which makes that 85s roughly ten percent of what remains. Record the decision not to trim it now, the ten-run revisit gate under which that decision is reconsidered, and the three alternatives already ruled out with their measured costs: `cargo check` (114s against 102s cold, and it writes nothing into the target directory so the uplift the regression exists to catch stops happening), warming the compiler cache (about three percent), and sharing a target directory (blocked by E0460 races with the `#[once]` fixture). Describe what a fixture-crate replacement would have to carry, so the design is on record while the trim is deferred: the fidelity argument naming the regression it still guards and the coverage it drops, and the Windows response-file pressure, which a one-dependency fixture would stop exercising unless it generates enough search paths or the contract moves to its own dedicated test. * Point the harness comments at the deferred trim (#693) Neither `.config/nextest.toml` nor the test's doc comment recorded that the trim measured at 156s is now worth about 85s, so both still read as though the larger figure were available. Add the three post-#687 runs (125.3s, 170.6s and 170.7s against the 274.7s contended median) and the exclusive tails that give the 85s figure, and point the "candidate for tightening or deletion" note at the ten-run revisit gate that "Deferring the split-build-dir harness trim" in docs/developers-guide.md defines. The test's doc comment gains the fidelity argument — that the subject is the real `test_support` build rather than a fixture crate, and that the developers' guide holds the criteria for revisiting that — and the response-file note, that the long `-L dependency=` set plus the long temporary roots is what keeps the Windows response-file path exercised, so any replacement must preserve that pressure or move it to a dedicated test. No executable logic and no timeout value changes. * Address CodeRabbit findings on the trim record (#693) Two findings, both on the same passage of the nextest comment, and one prose nit on the guide. The contention narrative was genuinely ambiguous: "on those same runs" tied the 125.3s, 170.6s and 170.7s values to the runs where the two isolated-Cargo tests contended, when they are the three runs *after* the second build left the Windows lane. Say which direction the change ran — contention had been increasing each test's elapsed time, and its removal produced the shorter durations — so the sentence cannot be read as claiming the values were measured under contention. The guide's cross-reference to the new fixture-crate subsection used an inline link too long for mdtablefix to wrap at 80 columns, which check-fmt rejects. Move it to the file's reference-link convention. The link label is `fixture-constraints` rather than the longer `fixture-crate-constraints`: mdtablefix treats `[text][label]` as one atomic token, and the longer label leaves no valid wrap point, so it wanted to strand the sentence's full stop on a line of its own. The visible text, the anchor target, and the convention are unchanged. Also stop quoting the guide's subsection title inline in the nextest comment; naming the section rather than reproducing its heading reads the same and does not invite drift. * Record the serialization group in the deferred-trim evidence (#693) Main's nested-cargo-builds test group serializes this test with the other build-capable child Cargo tests. It landed after the three runs the deferred trim was sized from, so those samples describe a shape that no longer exists. Under the group the test holds the single slot and everything behind it waits, so a trim returns its whole occupancy rather than its exclusive tail: 113s to 152s on the three Windows runs after the group landed, against the 85s the uncontended reading gave. The decision to defer still stands and the trim remains open at the ten-run gate, now against the serialized shape. Recorded in the developers' guide decision record, the nextest.toml budget comment, and the harness test's doc comment. * Count the group without a fixed number (#693) Main added a member to the nested-cargo-builds group while this branch was open, so the prose count of the other members was already stale by one. Name the membership instead of counting it, so a later addition to the group does not silently falsify the sentence. The group's contract test discovers members rather than listing only the pinned ones, which is why it stays correct through such an addition. * Correct the serialized-trim figures against the run logs (#693) Re-deriving every figure from the three Windows job logs showed three claims in the record were not what the logs support. The harness is not the last test to finish in any of the three runs; the prose said "two of the three". The group's cheap tail members trail it by under seven seconds in each case. The saving is not the group occupancy in every run. It is the run's end less whichever of the trimmed group chain and the last non-group test finishes later: 152.0s, 149.0s and 113.4s, where only the first two equal the occupancy and the third is capped because non-group work becomes the binding constraint once the harness is gone. The table's middle column is the group chain's end with the trim applied, not the run's end, so it is now named that way. All nine figures in the table reproduce from the logs; the changes are to what the prose claimed about them. * Record the fourth post-group sample for the harness trim (#693) The guide stated that trimming `harness_compiles_under_a_split_build_dir` returned 113s to 152s, from three Windows runs under the `nested-cargo-builds` group. This pull request's own Windows lane supplies a fourth: run 35400200137, the harness held the group's slot for 115.7s and a trim there returns 95.0s, below the low end of the published range. Add the row, widen the range to 95s to 152s, and correct the per-run analysis, which claimed the figure was the occupancy exactly in one run and short of it in one. It is short in the last two: 113s against 125s, and 95s against 116s, both because unrelated non-group work becomes the run's next binding constraint once the harness is gone. The four samples are also not uniform, so say so: the first three are trunk pushes and this one is a pull-request lane. The deferral decision is unchanged; only the evidence for it moves. Both files keep their existing shape, and `.config/nextest.toml` remains a comment-only change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Anchor the post-group sample instead of counting it (#693) The post-group write-up counted its own samples: "the four runs", "the last two", "estimated from five". Every push to this branch starts another Windows run, so each count was stale by the time the next one landed, and the fifth run — 35403273264, occupancy 141.6s, trim returns 136.3s — left three of them wrong. Anchor the claims rather than re-counting them. The table gains the fifth run, the range stays 95s to 152s, and the prose now says across-the-sample and names the date the sample was taken. The "falls short of occupancy" case is stated as a pattern rather than as an ordinal claim about which runs did it, since it has now happened in three of the five. The caution paragraph notes the sample mixes trunk pushes with pull-request lanes and is a snapshot rather than a running total, so later runs belong to the revisit gate rather than to this table. The revisit gate now reads "the sample above" instead of a figure, and the nextest.toml range carries the same 2026-09-18 date. The deferral decision is unchanged. `.config/nextest.toml` remains a comment-only change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Wrap the serialized-value link definition (#693) The definition line ran to 88 columns. markdownlint's MD013 does not flag it — it permits an over-length line when nothing follows the last space before the limit, and this line's only space sits at column 19 — but the repository already wraps long definitions this way, at `[github-actions-validation-test]` further up the same file. Put the fragment on an indented continuation line. The reference still resolves: every `][serialized-value]` use has a matching definition, and no definition is left unused. Co-Authored-By: Claude Code <noreply@anthropic.com> * Anchor the trim figure to its mechanism, not a running range (#693) The seventh Windows sample returned 162.9s, above the 152s upper bound the guide had published. The bound was the wrong shape: it read like a settled range when it was only a running maximum over a sample that keeps growing, since every push spawns a fresh Windows run. Replace it with the mechanism. A trim can never return more than the test's own duration, because the duration *is* the occupancy it gives back; and it returns exactly that whenever the shortened group chain is still what bounds the run. It returns less only when unrelated non-group work binds first. The figure therefore tracks the test's own cost rather than converging on the 85s exclusive tail, which belonged to an uncontended lane that no longer exists. The claim "four of the seven runs above" required the `35405043577` row, which the table was missing; added, and the count verified programmatically. Both the guide and the nextest comment are dated to the 2026-09-18 sample so later runs accrue to the revisit gate rather than falsifying a bound. * State the tail invariant structurally, not as a 0.1s margin (#693) The paragraph below the post-group table claimed the group's tail members trail the harness by "under seven seconds in every run". Re-derived across all seven rows, the worst gap is 6.9s against that 7.0s bound. The claim is true, but it is the same shape of hazard that already forced two rewrites here: a hard numeric bound over a sample that grows by one run on every push. The invariant underneath it does not decay, because it follows from the group's structure rather than from the measurement -- tail members cannot start until the harness frees the single slot, so they necessarily finish after it. State that, and drop the number. The figures the paragraph exists to support are unchanged; the seven-row table, its trim-returns column and the mechanism below it were re-verified against the raw job logs while checking this. * Separate the group's rationale from the coverage it owes (#693) CodeRabbit found the paragraph that follows the post-group table claiming the response-file pressure "makes the group entry necessary in the first place". That conflates two independent things, and the guide says the opposite two sections earlier: `nested-cargo-builds` exists because four nextest workers each starting a four-job child Cargo build on four vCPUs is what it prevents. Response-file pressure is a coverage requirement a replacement would owe, not a reason to serialize anything. A build-capable replacement does not change the contention rationale at all, so stating them as one requirement would misdirect exactly the fixture work this section exists to constrain. Split them, and keep the response-file clause attached to the coverage obligation where the constraints section already develops it. * Qualify the doc comment's occupancy claim the way the guide does (#693) The comment on `harness_compiles_under_a_split_build_dir` said a trim "would return its whole occupancy rather than only the tail it finishes on", unconditionally. The guide conditions exactly this figure: the trim returns the whole occupancy only when the shortened group chain still bounds the run, and returns less when unrelated work becomes the run's next binding constraint once the slot frees. That qualification is not decoration. It is the mechanism the guide was rewritten around, and the comment stated the stronger, unqualified form of it next to the measurements that contradict it. The two now agree. Raised as an unreproduced CodeRabbit finding on an aborted review pass. It was not corroborated on re-review, but it is correct on its merits, so it is fixed rather than dismissed on provenance. * Move the trim decision out of the guide and into ADR-027 (#693) The decision record was living in `docs/developers-guide.md` as two subsections. That is the wrong home for it. The style guide reserves the developer's guide for current responsibilities and points design rationale and trade-offs at decision records, and it asks the guide to stay synchronized with them. A deferral with a revisit gate is a decision with a status, not a description of how the suite works today. `docs/adr-027-defer-split-build-dir-harness-trim.md` now carries it: context, the options already measured and rejected, the rule that a trim can never return more than the test's own duration, the serialized-lane sample, the ten-run revisit gate, and the consequences. The guide keeps a short summary of the decision and links to the ADR; it still owns the constraints a fixture-crate replacement would have to preserve, because those are implementation requirements rather than decision rationale. The serialized-measurement table moved rather than being duplicated. Two copies of a seven-row sample that each push can date further is the drift this branch already corrected twice, so the ADR is the single owner and the guide points at it. `contents.md` gains the ADR-027 entry, and the three inbound references that named the old section -- in the guide's cross-reference block, in the Windows override comment in `.config/nextest.toml`, and in the doc comment on `harness_compiles_under_a_split_build_dir` -- now name the ADR. The nextest.toml change remains comment-only: verified structurally identical to `origin/main` under `tomllib`, and no non-comment line appears in its diff. Every figure in the ADR was re-derived programmatically from its own table: seven rows, four returning the full duration, the 115.7s to 162.9s duration span, the 95s to 163s saving span, the three short rows, and zero ceiling violations. Nine gates green on this tree: check-fmt, markdownlint, lint, typecheck, test (3196 passed, 5 skipped), doc-coverage (98.80%), and nixie. * Correct the ADR header and renumber it to 028 (#693) Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`. * Drop the full stop from the ADR's Date value (#693) The style guide gives the Date field the format `YYYY-MM-DD`, and the trailing full stop on `2026-09-19.` was mine rather than the convention's. The three most recent ADRs before this one -- 023, 024 and 026 -- carry the bare date; 022 and 025 carry the stop, so the repository is mixed and this is genuinely minor. The spec is unambiguous, so the bare form wins. `make check-fmt` and `make markdownlint` pass (143 files, 0 errors), with mdtablefix leaving all 142 files unchanged. The ADR's presence in markdownlint's scope was confirmed three ways: a count reconciliation against the config's ignore set, a glob check against its ignores, and a scoped probe linting the file alone. The change is a single character on one line. * Name the third Windows run behind the 170.7s figure (#693) CodeRabbit flagged an inconsistency: the guide said "the new shape has three samples" while its table carried only two run columns. The premise was wrong -- the third sample is real -- but the finding was right that the set was not traceable, and the fault was mine. My first commit replaced main's "on those same two runs ... 170.6s" with "125.3s, 170.6s and 170.7s on the three runs that followed" and never named the third run. The exclusive tails 62.3s, 87.2s and 85.5s came across from the measurement work, so the prose was consistent with itself; only the identifier was missing. The third run is 34080385050, recovered from the branch that produced the other two, `measure-windows-isolated-cargo-tests`. It is third in sequence after 34075197897 and 34079222917 and it reports the harness at 170.741s, which is the 170.7s the comment cites. It is now a table column rather than a bare number, with its own values measured the same way as the other two: 6.8s for the packaging test, a 253.8s nextest phase, and a 358s `Test` step. The extraction was validated before it was trusted. Run against the two runs whose values were already published, it reproduces the per-test durations to the decimal and the `Test` step to the second. It also caught its own first error: a naive phase measurement (`cargo nextest` invocation to summary) disagreed with both published values, because the published figure is nextest's own summary line, which excludes build time. The corrected reading matches to 0.1s on both, which is what licenses using it on the third. `nextest.toml` gains the three identifiers for the same reason, so the comment's 170.7s is traceable without the guide. The guide's lead-in sentence also said "the first two under the new shape" while enumerating two runs; it now names all three, which the scrutineer flagged as a stale clause after the first pass. `make check-fmt` leaves all 142 files unchanged and `make markdownlint` reports 0 errors across 143, so the hand-written table padding is canonical. `.config/nextest.toml` still parses under `tomllib`. --------- Co-authored-by: leynos <leynos@rohga> Co-authored-by: Claude Code <noreply@anthropic.com>
Replace the pre-open symlink_metadata check in open_file_checked with an open that does not traverse a reparse point and a judgement taken from the handle it returns, so the policy decision and the read share one handle on Windows as they already do on Unix through O_NOFOLLOW. The new windows_reparse submodule sets FILE_FLAG_OPEN_REPARSE_POINT via cap_std's Windows-only OpenOptionsExt::custom_flags and refuses an opened handle carrying FILE_ATTRIBUTE_REPARSE_POINT. Testing the attribute bit rather than the tag also rejects junctions and volume mount points, which std does not report as symlinks. No unsafe, no new dependency, and no lint relaxation is required. Record the decision in ADR-026 and update the design and developers guides, which described reject_windows_symlink and its known race. Refs #703
The prohibition was justified by a claim that junctions are not name surrogates. They are: `IO_REPARSE_TAG_MOUNT_POINT` is `0xA000_0003`, so bit 29 is set, and `std` reports a junction as a symlink. The same holds for a volume mount point. The claim mattered because it was doing load-bearing work in the argument for testing the attribute bit. That argument survives, but on the real gap: a tag that is *not* a name surrogate — a deduplication or cloud placeholder — is missed by `FileType::is_symlink` and, worse, is reported by `std` as `is_file() == true`, so the shared regular-file check would accept it. That is what makes `reject_reparse_point` load-bearing rather than redundant with the check beside it. No change in behaviour; the policy already tested the attribute bit. The prose now states the reason the code is written that way. Refs #703
The policy was only exercised through `open_flags` and the three ABI constants. Nothing asserted the property the policy actually turns on: that testing the attribute bit refuses the tags a name-surrogate test would miss. Extract `is_prohibited_reparse_point` as a pure predicate and pin it against real tag values — symlink and mount point for the surrogate case, NFS, deduplication and cloud for the non-surrogate case — cross-checking each fixture's own bit 29 so the table cannot drift from what it claims to test. A plain file's attributes must not be refused. Add a Windows integration test that creates a real directory junction with `mklink /J` and asserts all four filters refuse it. A junction needs no privilege, so unlike the symlink fixture there is nothing to skip on an ordinary host: the fixture reports `Ok(None)` only when `cmd.exe` is absent, and every other failure propagates rather than passing green over an unexercised policy. `require_real_junction` re-reads the reparse attribute without following the link, so a fixture that degraded to a plain directory cannot invert the assertions. The junction is refused by `reject_reparse_point` and, independently, by the shared regular-file check, since `std` reports a junction as a symlink. Driving both would need a non-surrogate reparse point, which no unprivileged fixture can create; the unit test above covers that gap instead. Refs #703
`make check-fmt` runs `mdtablefix --wrap` over every tracked Markdown file, and the hand-wrapped paragraphs in the three touched documents did not match its line breaking. Rewrapped in place with the same flags the Makefile passes. The reflow is cosmetic: a token-stream comparison of each file before and after shows the two sequences identical, so no wording changed. Refs #703
The documentation style guide requires sentence case for headings, and the ADR template spells the sections that way: "Decision drivers", "Decision outcome", "Known risks and limitations", "Architectural rationale". ADR-026 was the only ADR in the tree that used title case for all four. The date keeps its trailing period. The same template writes the field as `YYYY-MM-DD.`, and ADR-025 and ADR-022 follow it, so the formatting review suggestion to drop the period conflicts with the documented convention and is not applied. No behavioural change; headings only, and nothing links to these anchors. Refs #703
Add a handle-level test that only passes if FILE_FLAG_OPEN_REPARSE_POINT reaches the open. The integration test cannot detect that: std reports a junction as a symlink, so metadata.is_file() refuses the resolved directory regardless, and the policy would look correct while the flag went unapplied. Asserting the property on the opened handle's attributes is the only place the distinction shows, so a dropped flag now fails a test instead of silently widening the policy. The fixture is extracted into a helper, which also takes the test from 69 to 41 counted lines against the 70-line threshold: it had a single line of headroom on a lane only Windows CI compiles, so any small future edit would have broken that job with no local signal. Narrow the OpenOptionsExt import in fs_utils back to #[cfg(unix)]. Its only call site is inside #[cfg(unix)] apply_unix_open_flags; the Windows arm reaches custom_flags through windows_reparse, which has its own import. Commit 77c99e7 had widened it to any(unix, windows), which leaves the import live but unused on Windows and fails lint-clippy under -D warnings. Linux gates cannot see this, because the file's Windows code is cfg-gated. Both findings come from a local cross-compile probe: the main crate cannot be built for x86_64-pc-windows-msvc here because ring needs MSVC lib.exe, so the probe mirrors the module tree with file modules (Clippy resets its nesting counter at a file module and counts it through an inline one), copies windows_reparse byte for byte, and compiles the Windows-gated integration fixtures that the Linux gates never reach. Refs #703
main gained adr-026-manifest-environment-access-policy.md (PR #666) while this branch was in review, so the Windows reparse-point ADR moves to 027. Also drops the trailing full stop from the Date line, matching the two most recent ADRs on main (024, 026), which write the bare YYYY-MM-DD.
The unit-test junction fixture spawned `cmd.exe` unconditionally and `expect`ed the spawn to succeed, so a host without `cmd.exe` failed the test instead of reporting that its subject was unavailable. That is both a false negative for a valid Windows host and an inconsistency with the sibling integration fixture, `fallible::junction_fixture` in tests/std_filter_tests/support.rs, which maps `ErrorKind::NotFound` to a skip. Map that one error kind to `None` from `junction_fixture`, and return early from the caller with a skip notice on captured test output. Every other spawn error and a non-zero `mklink` exit still assert: a junction needs no privilege, so a missing `cmd.exe` is the only unavailability that is a skip, and a real fault must not be masked as one. The skip is printed rather than silent so a green run cannot quietly hide the regression on a host that can build a junction; the `#[expect(clippy::print_stderr, ...)]` is fulfilled by that branch.
…ossible assertion The Windows CI lane failed three ways, all in the reparse-point module. This addresses each, and adds a local oracle that reproduces the lane in seconds. `module_must_have_inner_docs` wanted a `//!` line as the test module's first item. Adding one pushed the module past `module_max_lines`' 400-line limit, so the tests move to a `windows_reparse_tests.rs` sibling, reached through `#[path]`, as `status_tests.rs` already does. `no_expect_outside_tests` rejected five `expect` calls in `junction_fixture`. The lint exempts `#[test]` functions, but a plain helper is production code to it however it is gated, and `#[cfg(test)]` on the enclosing module does not register: rustc strips `cfg` attributes before the HIR the lint inspects. The fixture now returns `Result` and propagates, with `ensure!` rather than `assert!` because the repo denies `panic_in_result_fn`. The opt-in half of the junction test asserted something impossible. `mklink /J` records an absolute target, and `cap_std`'s resolver refuses to follow a reparse point whose destination leaves the capability, reporting `escape_attempt()` as `PermissionDenied`; a capability open cannot traverse a junction under either policy. That half is dropped, and the security-critical half — the default policy's handle *is* the reparse point — is kept and strengthened with an ordinary-directory control, so the refusal is shown to be about the link rather than about directories generally. The handle is taken ambiently because it must be. ADR-027 gains the capability-limitation risk and a Verification section that says which layer carries which property, since `std` reports a junction as a symlink and the integration test alone cannot see the flag. Verified with `cargo dylint --target x86_64-pc-windows-msvc` against the probe crate, which reproduces the Windows lint verdict exactly, plus clippy on the same target. A deliberate syntax error in the test file fails that run, so the oracle is genuinely compiling the Windows-gated code.
The `-ize` form is the en-GB-oxendict preference this repository enforces elsewhere, and the surrounding prose already uses it. No behavioural change: the edit is inside a doc comment. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ides Three guides still described the pre-open Windows check that this branch removed, or stated the policy as symlink-only when it in fact refuses every reparse point. `users-guide.md` and `stdlib-yaml-and-jinja-guide.md` now say that the Windows refusal covers junctions, volume mount points, and other tags such as deduplication or cloud placeholders, and that `follow_symlinks=true` is the opt-in for all of them. `security-network-command-audit.md` replaces the "pre-open `symlink_metadata` check on Windows" remediation with `FILE_FLAG_OPEN_REPARSE_POINT` plus the handle's `FILE_ATTRIBUTE_REPARSE_POINT`, and records that the judgement and the read share one handle. Reported by chatgpt-codex-connector on PR #739. Co-Authored-By: Claude Code <noreply@anthropic.com>
The earlier renumber to 027 was itself a collision. PR #699 minted docs/adr-027-command-placeholder-contract.md at 2026-09-18T23:52Z, 31 minutes before this branch's ac3d3e9 landed its own 027. Renumbering off main's ceiling (026) missed it because an ADR number is spent as soon as any branch mints it, not when it merges. A sweep of every remote branch shows 028, 029, 030, and 031 are taken too — 029 by two branches independently: 026 merged on main 027 PR #699 028 issue-693-trim-the-split-build-dir-harness-test... 029 docs/hexagonal-hardening-and-checking and make-the-build-standard-the-default 030 docs/hexagonal-hardening-and-checking 031 docs/hexagonal-hardening-and-checking 032 free Take 032 and leave 027 to #699, which claimed it first. Update the four references in lockstep: the ADR's own H1, the docs/contents.md index entry, and the two inbound links in developers-guide.md and netsuke-design.md. No other file mentions the number.
CodeRabbit raised one concern three times: that `follow_symlinks=true` does not override capability containment, so the docs must not imply it does. The premise is right, but the mechanism it named — "an absolute target that leaves the capability" — is wrong, and writing it down would have misdescribed what an operator can rely on. The resolver never compares the resolved path against the capability root. It refuses a link destination containing a prefix or root component outright, so it cannot tell an absolute target that stays inside from one that does not: both are refused. The trigger is the target's *absoluteness*, not its escaping. Measured rather than inferred. Two symlinks pointing at the same file inside one capability, one written relatively and one absolutely, under the opt-in policy: the relative link opened, the absolute link was refused with "a path led outside of the filesystem". Same file, same containment, different result — only the spelling differed. Documented at all four sites that state the policy, since a partial correction would leave the wrong mechanism standing somewhere: `users-guide.md`, `stdlib-yaml-and-jinja-guide.md`, ADR-032's known risks, and the Windows unit test whose doc comment explains why the junction handle is taken ambiently. The ADR records the superseded wording explicitly rather than quietly replacing it, so a reader who met the earlier draft can see which claim was wrong and why. Verified: the junction fixture's target is a relative `"file"`, so the opt-in integration test that ADR-032 cites as covering the follow path does in fact use the supported spelling. Refs #703.
`make check-fmt` failed on `docs/users-guide.md` in both the Linux `build-test` job and `Windows / lint-windows` for 2ffd72d: mdtablefix wanted +2 -2 on exactly one line. The line was 80 characters. Hand-wrapping prose to fit the 80-column budget is not the same as satisfying mdtablefix's own wrap, which chose a different break point. The previous commit hand-wrapped; this one runs the formatter over the file instead, which is the only way to get the same answer the gate does. Only the wrap point moved — no wording changed, and no other paragraph was touched. Verified with the gate's own invocation: mdtablefix --check --git --include-untracked --wrap --renumber \ --breaks --ellipsis --fences -> 142 files left unchanged, exit 0 Refs #703.
ADR-032's Verification section described two Windows-only tests as carrying the guarantee, which reads as though continuous integration had confirmed them. It has not: the Windows test lane halts on the unrelated pre-existing failure tracked as issue 743 before nextest reaches `stdlib::path`. Measured from the job log rather than assumed: the run ends at 1078/2901 tests, and the strings `windows_reparse` and `junction` appear zero times in the whole log. A decision record that overstates its own evidence is worse than one that names the gap, so the gap is now named — what is verified (compiles, lint-clean, tests compile, on a local `x86_64-pc-windows-msvc` probe) and what is not (that the tests pass on the platform they govern), with the two-tool reason the lint evidence needs both `cargo dylint` and `cargo clippy`. Docs-only; no behaviour change. Formatted with mdtablefix rather than by hand, so `mdtablefix --check` reports 142 files unchanged, exit 0. Refs #703.
The evidence section pointed readers at the local probe crate as the main route to Windows verification, which sold the CI evidence short. The `Windows / lint-windows` lane runs `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `--all-targets` compiles the library's `cfg(test)` module and the integration test targets. Since `windows_reparse_tests.rs` is a `#[cfg(test)]` child of the lib, every Windows-gated line — the module, its tests, and the junction fixture — is compiled on Windows itself under `-D warnings`, on the platform's own toolchain rather than an approximation of it. That job is green on this head. The probe crate is still worth recording, but as what it is: the local development loop, needed because the main crate cannot be cross-compiled here (`ring` needs MSVC's `lib.exe`), not the guarantee. The limit is narrowed rather than removed. Compiling a test under `-D warnings` does not run it, and the lane that would run it still stops 1800 tests short of `stdlib::path`. Co-Authored-By: Claude Code <noreply@anthropic.com>
My previous commit hand-wrapped a paragraph in this file and `check-fmt`
failed in `build-test`: `docs/adr-032-...md +2 -2`, `1 file would be
reformatted`, `make: *** [Makefile:314: check-fmt] Error 1`. The lines were
within 80 columns; the break position was wrong. That is the trap recorded in
the formatter memory, and I walked into it a second time in this same file.
Fixed by running the tool rather than the ruler:
mdtablefix --in-place --wrap --renumber --breaks --ellipsis --fences \
docs/adr-032-windows-reparse-point-same-handle-open.md
only the break between "-- all-features" and "Whitaker's" moved. Verified with
the invocation the gate itself uses,
`mdtablefix --check --git --include-untracked --wrap --renumber --breaks
--ellipsis --fences` -> `142 files left unchanged`, exit 0.
Markdown-only; no code or behaviour touched.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`1078/2901 (1077 passed, 1 failed, 2 skipped)` is the one claim in this record most likely to age — it moves the moment issue 743 is fixed and a green Windows lane starts running these tests. Cite it against commit `2d8e5305` and name the test it died on, so a later reader can tell a stale number from a contradicted one. Re-measured on `2d8e5305` rather than carried over from the earlier run: same figures, and `windows_reparse`/`junction` still appear zero times in the job log. Verified with `mdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences` -> `142 files left unchanged`, exit 0, and the file was formatted with the tool rather than by hand. Co-Authored-By: Claude Code <noreply@anthropic.com>
CodeRabbit finding 2 was the one finding in its batch with a valid basis: this guide sat at 397 lines on `main` and my earlier prose took it to 405. The other four findings cite a file-length rule that `AGENTS.md:31` scopes to "code file" and that only pylint enforces (`pyproject.toml:175`, `max-module-lines`), against documents already 2000-7300 lines on `main`; those are not this PR's to fix and are not defects. The cap claim is still worth honouring where this branch caused the crossing, so the added prose is trimmed from 12 inserted lines to 5. The dropped material enumerated reparse tags and restated the `follow_symlinks` containment behaviour, both of which `users-guide.md#configure-file-reading-limits` already carries in full — and this guide already cross-linked to that manual once, at line 387, so the pattern is established. Sibling guides updated alongside cover the security audit rather than the user-facing policy. Result: 400 lines, no longer over the threshold. Verified with `mdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences` -> `142 files left unchanged`, exit 0; the file was formatted with the tool. Co-Authored-By: Claude Code <noreply@anthropic.com>
The Windows-gated tests only run on a Windows host, and the Windows test lane has been known to stop short of `stdlib::path` (ADR-032 records the figure), so the policy's two load-bearing branches had no evidence that ran on every push. A `const` assertion does not depend on a test lane reaching the module: rustc evaluates it whenever the module is compiled, and `Windows / lint-windows` compiles every Windows-gated line through `cargo clippy --all-targets`, which is green on every head of this branch. Both `pub(super)` functions are now pinned in `const` contexts: `open_flags(false)` must set `FILE_FLAG_OPEN_REPARSE_POINT`, `open_flags(true)` must not, either policy must keep `FILE_FLAG_BACKUP_SEMANTICS` so a directory can still be opened and rejected by the shared regular-file check, and `is_prohibited_reparse_point` must accept an attribute value carrying `FILE_ATTRIBUTE_REPARSE_POINT` and refuse an ordinary one. No production behaviour changes, no dependency is added, no function's visibility changes, and the runtime tests are untouched — they still cover the handle behaviour and the diagnostics that a compile-time assertion cannot see. The oracle was shown live rather than assumed. Against a probe crate mirroring the module tree, the true code compiles clean (exit 0); dropping the flag from the default branch aborts the build with `E0080: evaluation panicked: the default policy must not traverse a reparse point` and exit 101; and a broken attribute test fails independently on its own assertion, so neither branch is merely riding on its neighbour's assertion. Co-Authored-By: Claude Code <noreply@anthropic.com>
Two documents described the `follow_symlinks` opt-in in symlink-only terms
while on Windows it governs the final component's reparse point as a whole,
which is the finding CodeRabbit raised on `docs/stdlib-yaml-and-jinja-guide.md`
line 178 and `docs/security-network-command-audit.md` line 153. The premise is
sound even though the wording of the finding inverted the mechanism: neither
file mentions a pre-open metadata check, and `users-guide.md` already carries
the requested content in full. Fixing the narrower defect the premise points at:
- guide: `permits the final component to be a symlink` ->
`waives that final-component refusal`; net-zero on line count (400).
- audit: the same correction, and the sentence is reflowed rather than
patch-edited, so the clause order stays readable (163 lines).
- ADR-032: `## Verification` now records three layers — the compile-time
assertion added in `5d2dab68`, the unit test, and the integration test —
and `### How far this evidence actually extends` is rewritten, because the
section it replaces concluded that none of these tests had ever run in CI.
That was true when it was written and issue 743 is now fixed on `main`.
The rewritten section also corrects a claim of mine that was wrong, not merely
stale: I had argued that zero occurrences of the fixtures' skip line proved the
junction was built, because nextest captures a passing test's stderr. It does
not. A probe crate shows the marker is absent from nextest output under the
default profile, and the `success-output = "immediate"` entries in
`.config/nextest.toml` cover three unrelated test groups with no
`--success-output` in CI. The argument now rests on the fixtures' single narrow
quiet arm (`ErrorKind::NotFound` from spawning `cmd`, every other outcome
failing) and on `require_real_junction` checking the reparse attribute before
use, with the unmeasured premise — that the runner image can spawn `cmd.exe` —
stated as such. The record of the error is left in place rather than tidied
away.
Docs only; no code, test, or build configuration changes. Applied with
`mdtablefix --in-place` and verified as a no-op against the hand-written
wording, so the result is formatter-canonical rather than hand-wrapped.
Co-Authored-By: Claude Code <noreply@anthropic.com>
d4accd7 to
108fad1
Compare
|
@coderabbitai approve |
✅ Action performedComments resolved and changes approved. |
Summary
Closes #703.
open_file_checkeddecided the Windows file-type and symlink policy from apre-open
symlink_metadatalookup and then opened the path as a separate step.Those two operations are not atomic: a final component that is an ordinary file
at check time can be replaced before the open, so a caller able to write to the
containing directory can win the race and have the read follow a prohibited
reparse point. Unix never had this window, because
O_NOFOLLOWis applied tothe open itself.
The Windows path now reaches the same guarantee from inside the open. The final
component is opened with
FILE_FLAG_OPEN_REPARSE_POINT, so the open does nottraverse the reparse point and the handle that comes back is the reparse
point; the policy decision is then read from that handle's own attributes, and
the caller reads bytes through the same handle. A concurrent rename changes
which entry the path names, but cannot change what an already-open handle
refers to, so the decision and the read cannot diverge.
Two details worth review:
stdcallssymlinks.
FileType::is_symlinktests the tag value: it is true only forname-surrogate tags (bit 29 set) — symlinks, junctions, volume mount points —
and false for every other tag, such as a deduplication or cloud placeholder,
for which
stdalso reportsis_file() == true. TestingFILE_ATTRIBUTE_REPARSE_POINTinstead asks "is this a reparse point at all",which needs no enumeration of tags and refuses ones a future Windows release
may mint.
FILE_FLAG_BACKUP_SEMANTICSis unconditional, so a directory can beopened and then rejected by the existing shared regular-file check with the
documented diagnostic, rather than the open failing with a different error
than Unix reports.
reject_windows_symlinkis deleted; nothing replaces its pre-open lookup.Why no
unsafe, no new dependency, no lint relaxationThe original plan anticipated raw Win32 FFI (
CreateFileW+GetFileInformationByHandleEx), awindows-sysdependency, anunsafe_codedowngrade, and a
dylint.tomlexclusion. None proved necessary:cap_std'sOpenOptionsExt::custom_flagsis OR-ed straight intodwFlagsAndAttributes,and
MetadataExt::file_attributes()is populated fromBY_HANDLE_FILE_INFORMATIONread from the open handle. Both are safe, public,and already reachable through the
cap_std::fs_utf8re-exports the moduleuses. This is also the same trait family the Unix arm already uses to set
O_NOFOLLOW, so the two platforms reach their guarantees through parallelmechanisms.
CreateFileWadditionally has no directory-handle-relative form,so an FFI route would have discarded the capability sandbox for an ambient
absolute path. Recorded in ADR-032.
Testing
Four layers carry the guarantee: a compile-time assertion, a unit test, an
integration test, and the opt-in test.
Compile time, on every Windows build. A
const _: () = { ... }block inwindows_reparse_tests.rspins both branches ofopen_flagsand both outcomesof
is_prohibited_reparse_point. Runtime tests in that file execute only on aWindows host, so a regression could have reached a merge on the strength of a
green Linux run; a
constassertion does not, because rustc evaluates itwhenever the module is compiled and
Windows / lint-windowscompiles it onevery push through
cargo clippy --all-targets. It fails the Windows build inseconds rather than a test run in minutes. Added in response to the Codex
review's "Compile-Time / UI" finding.
Integration, all four filters. A Windows-only test asserts that a junction
fixture — created with
mklink /J, which needs no privilege — is rejected bycontents,linecount,hash, anddigestunder the default policy. Thefixture follows the repository's "create the requested file type or skip
because that file type is unavailable" rule:
require_real_junctionassertsthe entry really carries
FILE_ATTRIBUTE_REPARSE_POINTbefore rendering, sothe test cannot pass against a plain directory. Because the refusal happens on
the opened handle, it surfaces as the regular-file diagnostic; a traversal
would have rendered the target directory's entries instead of erroring at all.
Unit, on the handle. The integration test alone cannot detect a dropped
FILE_FLAG_OPEN_REPARSE_POINT, becausestdreports a junction as a symlinkand the shared regular-file check refuses the resolved directory anyway. A
handle-level test therefore asserts the property where it actually shows: the
default policy's open of a junction returns a handle carrying the reparse bit,
and the ordinary target directory is asserted not to carry one, so the
refusal is shown to be about the link rather than about directories generally.
That is the only test that fails if the flag stops being passed.
Opt-in.
follow_symlinks_opt_in_reads_the_link_targetcovers the retainedfollow path end to end using a relative-target file symlink. A junction
cannot serve here:
mklink /Jrecords an absolute target, andcap_std'sresolver refuses an absolute link destination outright (
escape_attempt(),reported as
PermissionDenied) — under either policy. The handle-level testtherefore takes the junction through the ambient authority, which is the only
way to reach the reparse point itself. This is recorded in ADR-032 under "Known
risks and limitations".
Note the mechanism precisely, because an earlier revision of this body and of
the docs stated it wrongly. The resolver does not compare the resolved path
against the capability root; it refuses a link destination containing a prefix
or root component, and so cannot distinguish an absolute target that stays
inside from one that does not. Relativity of the link target, not containment
of the resolved path, is what is tested. Measured on Linux rather than
inferred: two symlinks pointing at the same file inside one capability, one
written relatively and one absolutely, under
follow_symlinks=true— therelative link opened and the absolute link was refused with "a path led outside
of the filesystem". Same file, same containment, different result; only the
spelling of the target differed.
Windows verification: what CI already does, and what the probe crate is for
Native Windows CI compiles and lints every Windows-gated line, tests included.
Windows / lint-windowsrunsmake lint-clippy, which expands tocargo clippy --workspace --all-targets --all-features -- -D warnings, thenWhitaker's dylint suite over the same target and feature selection.
--all-targetscompiles the library'scfg(test)module and the integrationtest targets, so
windows_reparse.rs,windows_reparse_tests.rs, and thejunction fixture in
file_type_tests.rsare all built on Windows itself under-D warnings. That job is green on this head.For most of this branch's life that was the widest evidence available, because
the test lane never reached the module. It is no longer the widest: the test
lane now executes the cases as well, lower on this page. It remains the half
that compiles every Windows-gated line, and the two are described separately
because compiling a test under
-D warningsis not running it.The local probe crate below is therefore the development loop, not the
guarantee. It exists because the main crate cannot be cross-compiled on this
host (
ringneeds MSVClib.exe), so Windows-gated edits were iteratedagainst it between CI runs. Its value is that it returns a Windows lint verdict
in seconds; passing
--target x86_64-pc-windows-msvcafter dylint's--separator reaches the same lints the CI job applies:
Both are clean, and both are required, because each is blind to what the
other checks.
cargo dylintshells out tocargo check, so it applies theWhitaker lints and no clippy lint at all;
cargo clippydoes the reverse.Running only the first leaves every clippy lint on Windows-only code
unvalidated. Neither direction is inferred — each was falsified by injecting a
defect into the mirrored test module and confirming a non-zero exit:
cargo dylintcargo clippySome(1u32).unwrap()unwrap_usedstd::fs::metadata(".")no_std_fs_operationsBoth tools were re-run clean on this head after the defects were reverted. The
first row also corrects this body's earlier claim that clippy is the weaker
tool: on this codebase the relationship is genuinely complementary, and clippy
alone is what let two Whitaker findings reach CI earlier on this branch
(
module_must_have_inner_docsand fiveno_expect_outside_tests), both nowfixed.
The Windows test lane executes this PR's junction tests, and they pass.
This section said the opposite until the rebase, because
Windows / build-test-windowswas red repository-wide until061182b1landedon
mainwith the write-side shutdown fix for the network-fixture race trackedas #743. Before that fix the
lane halted inside
stdlib::networkand never reachedstdlib::path: on2d8e5305and on34553e03alike the run ended at 1078/2901 tests and thestring
windows_reparseappeared zero times in the job log. This branch isnow rebased onto
061182b1, and on head5d2dab68the lane completes:2898 of those are
main's; the extra 8 are this branch's. The new cases:The result easiest to fake is the one worth checking hardest: these tests skip
their own fixture when the host has no
cmd.exeto reachmklinkthrough, so agreen run does not by itself prove a junction was built. It is worth being
exact about this, because my first draft of this section claimed the log proved
it and it does not.
This repository's skip convention returns
Ok(()), so a skipped fixture isrecorded as a pass, not a skip. Neither total can distinguish "asserted
against a junction" from "quietly did nothing". Two things can.
The fixtures have exactly one quiet arm, and it is narrow. Both
junction_fixturevariants returnNoneonly onErrorKind::NotFoundfromCommand::new("cmd"); amklinkthat exits non-zero trips anensure!quotingits stderr, and any other spawn error propagates. On a host where
cmd.execanbe spawned, the only ways to finish are "the junction was created" or "the test
failed".
cmd.exeships withwindows-latest, so that arm is not reached.A created junction is checked before it is used.
require_real_junctionreads the entry's attributes without following the link and fails unless
FILE_ATTRIBUTE_REPARSE_POINTis set, precisely so a plain directory cannotstand in for a reparse point.
The residual uncertainty is that the first point reasons about the runner image
rather than measuring it: nextest hides the captured output of passing tests by
default — the
success-output = "immediate"entries in.config/nextest.tomlcover three unrelated test groups, and the CI lane passes no
--success-output— so the log never records that
cmdwas spawnable. The absence of the skiplines proves nothing on its own and is not relied on. What the run count does
establish is that the cases ran at all:
mainreports 2898 tests, this head2906, and all eight new cases appear as
PASS.Windows / lint-windowsis green on the same head; it is the compile-time half,and
build-test-windowsis now the runtime half.What the Windows lane still cannot show
A test cannot force a rename to land between two filesystem calls, because the
change removed the second call. The decision is read from the handle the read
uses, so the absence of a window is an argument from handle semantics, not
something a test can schedule. The runtime behaviour is now demonstrated on the
platform it governs; the construction argument is what covers the race itself.
Pre-existing Windows failure that the rebase carries the fix for
Recorded because this branch's earlier heads carried it, not because it is live.
Windows / build-test-windowsfailed for every PR and formainitself,from a race in a test introduced by
8e09a3a4 Bump ureq from 2.12.1 to 3.4.0 (#438):malformed_status_line_failurewrote a malformed status line from a serverthread and joined it, but did not drain the request or shut the write side down
first, so the client's connection was reset before the malformed response was
read. Fixed on
mainby061182b1 Complete local HTTP fixture responses with a write-side shutdown (#743) (#749), which this branch is rebased onto.Checklist
reparse points without a separate pre-open metadata check
O_NOFOLLOWpath unchanged;open_file_checkedremains the singleshared entry point for all four filters
creating the requested file type rather than substituting a regular file
unsafe_codestays atforbid; no new dependency; no lint relaxation5d2dab68:check-fmt(142 filesunchanged),
lint,typecheck,markdownlint(143 files, 0 errors),nixie(all diagrams validated),doc-coverage(98.79% vs an 80%threshold),
doctest(123 passed, 0 failed), and — for the first time onthis branch —
make testend to end.make test— green,3225 tests run: 3225 passed, 5 skippedonhead
5d2dab68, a full run with no cancellation. This is the first headof the branch on which the gate passes whole: earlier heads failed
locale_stub_ui_tests::harness_compiles_under_a_split_build_diragainstnextest's 300s budget. That test passed here in 57.9s.
maindid nottouch it (none of the six incoming commits names it), so the difference is
load: the earlier 449s standalone / 333s near-idle measurements were this
host's load sensitivity, not a defect. Recorded because the earlier heads'
checklist entries said the opposite, and a reader comparing heads deserves
to know which way it moved and why.
Windows / lint-windowspasses on CI, green on head5d2dab68as it wason
34553e03,87c41ee9,4eb1f08e,2d8e5305, and0b233a16. It isnot only a lint result: it runs
cargo clippy --workspace --all-targets --all-features -- -D warnings, and--all-targetscompiles thelibrary's
#[cfg(test)]children, so the Windows-gated module, itstests, and the junction fixture are all built natively on Windows under
-D warnings.5d2dab68.Windows / build-test-windowsis green and the eight newcases execute there, including the four
reading_filters_reject_a_junctioncases and
the_default_handle_is_the_junction_not_its_target. This wasthe last open item on the branch; it closed when the rebase onto
061182b1brought in the Windows: ureq 3.4.0 classifies a malformed status line as a connection abort, failing redirect error_tests and halting the suite at 1080/2891 #743 fix. See "The Windows test lane executesthis PR's junction tests, and they pass".
Defects found after the PR opened
Recorded so the history is readable rather than tidy. All three were caught by
CI on this branch rather than by local gates, and the third was found while
reading a job log for an unrelated reason.
follow_symlinks=truedoes not override capability containment. The premisewas right and the wording I had shipped was wrong in a specific way: it said
a link is refused when its destination leaves the capability, when in fact
the resolver refuses an absolute link destination outright and never
compares the resolved path to the capability root. Corrected in all four
places that stated it, with the superseded wording quoted in ADR-032 so a
reader who met the earlier claim can see which was wrong and why.
users-guide.mdwasunder 80 columns but broke at the wrong position, so
make check-fmtfailedin both
build-testandWindows / lint-windows. Fixed by runningmdtablefixover the file rather than wrapping by hand — the tool's answeris the only one the gate accepts. Only the wrap point moved.
the most useful entry here. Reviewing the CI job logs for an unrelated
reason, I found this PR had understated its own Windows verification: the
record pointed readers at a throwaway
/tmpprobe crate as the main route,when
Windows / lint-windowsalready runscargo clippy --workspace --all-targets --all-features -- -D warnings, and--all-targetscompilesthe library's
#[cfg(test)]children — so the Windows-gated module, itstests, and the junction fixture are all built on Windows natively under
-D warnings. Correcting that in ADR-032 and the PR body was worth doing.The correction itself then failed
check-fmtat the identicalmake: *** [Makefile:314: check-fmt] Error 1, because I hand-wrote the newprose. Nothing interesting about that failure; the point is that knowing the
rule did not prevent it, twice.
None of the three changed behaviour, and all three are fixed on the current
head. The third narrowed a claim as well as widening one: compiling a test
under
-D warningsdoes not run it, so this PR still claims no Windows CIexecution of the new tests.
Review findings, and what happened to each
The final
coderabbit review --agentpass on this PR returned five findings andposted no PR comments, so they are recorded here rather than left invisible.
All five are the same class — "this document exceeds the repository's 400-line
limit, decompose it" — and four have a premise the repository does not support.
The cap is a code-file rule.
AGENTS.md:31reads "No single code filemay be longer than 400 lines". Its only enforcement is
pyproject.toml:175,max-module-lines = 400under[tool.pylint.main]—pylint is a Python linter and never reads Markdown. No Markdown gate imposes a
line cap (
.markdownlint-cli2.jsoncsets only MD004/MD010/MD013/MD029, and noMakefile target counts lines in a
.md).Four findings point at documents this PR did not push over any limit:
developers-guide.mdnetsuke-design.mdusers-guide.mdThe only file here this PR lengthened is
developers-guide.md, by 16 lines,in a 7300-line document. All three were far over 400 long before this branch
existed, and none of the three is shorter than its base. Decomposing a
7000-line guide is a real piece of work, but it is not this PR's change and
doing it here would bury a filesystem hardening diff under thousands of moved
lines. Not actioned, deliberately.
One finding was right, and is fixed.
stdlib-yaml-and-jinja-guide.mdsat at397 lines on
main; my prose took it to 405. That is a threshold thisbranch actually crossed, so the added text was trimmed back: the policy
paragraph went from 13 text lines on
mainto 21, then to 15 after the trim,and the file landed at 400. The dropped material enumerated reparse tags and
restated the
follow_symlinkscontainment behaviour thatusers-guide.md#configure-file-reading-limitsalready documents in full. Theguide already pointed readers at the same manual once on
main, at line 379(
#configure-network-access, now line 382), so citing the manual from thisparagraph follows the file's existing idiom; the new
#configure-file-reading-limitsanchor is what this branch adds. The guide is now at 400.
The distinction matters for reviewability rather than for line counts: a rule
whose scope is "code file" does not become a defect when it is aimed at prose
the PR did not lengthen.
Two notes on the review process itself. The findings visible via
coderabbit review findingsincluded three symlink-related items restating "anabsolute target that leaves the capability" — the exact wrong mechanism this
branch corrects. Those are stale cache from an earlier session (timestamped
before either review run, carrying no
git.json); both runs made against thisbranch persisted zero comments, and the wording they quote is no longer in the
tree. They are not output about this head and were not acted on.
The one actionable review finding, and how it finally closed
The Codex finding above ("Update user docs for the reparse-point policy") was
the only one that survived scrutiny, and it was right about all three files.
Recording the reasoning here because my first response to it was wrong, and the
error is more instructive than the fix.
The finding was made against
4eee40e7, which is not an ancestor of thecurrent head. That commit belongs to an earlier epoch of this branch, forked
from
a273fad3; the branch was later rebuilt on a different base. Reading afinding against the current tree therefore answers a different question than
the one the reviewer asked, and I initially did exactly that. Against
4eee40e7, all three sub-claims hold:The implementation had already moved to the same-handle design at that commit
(
reject_windows_symlinkis gone fromfs_utils.rs), and the review's ownsubject is a compile-time finding against that commit — so the docs were
stale relative to the code, exactly as the reviewer said. The fix landed later
the same day in
e8ebc5f4(docs: describe the Windows reparse-point policy in the user-facing guides), which added the all-tags paragraph tousers-guide.mdand replaced the pre-open wording in the other two files. Thereview was submitted at 16:21Z and that commit is timestamped 18:08Z, so the
ordering is consistent with the finding having prompted the fix.
One sub-claim was still narrower than it looked, and CodeRabbit's second pass
found the remainder: two sentences described the opt-in in symlink-only
terms, while on Windows it governs every reparse tag —
That is a genuine defect: a reader on a cloud-placeholder or deduplicated file
would conclude the opt-in does not apply to them. Both now say the opt-in
"waives that final-component refusal", which is the
users-guide.mdphrasing,and each paragraph's antecedent defines the refusal precisely — every reparse
tag on Windows — a few lines above. The change is net-zero on line count in
both files, so the branch does not cross the 400-line threshold it just came
under.
This is the useful shape to remember, in the opposite direction to the one I
first wrote down: a finding can be correct and still look wrong, because the
tree in front of you has moved past it. Check the commit the finding was made
against before deciding its premise is inverted — especially after a rebase,
where the reviewed commit may not be in the lineage at all.
References
stdlib file filters and define a safe symlink/file-type policy
#743 —
Windows / build-test-windowsred repository-wide since the ureq 3.4.0 bumpdocs/adr-032-windows-reparse-point-same-handle-open.md—renumbered twice. It began at 026, moved to 027 when
mainmerged its own026, then to 032: #699 had
minted
adr-027thirty-one minutes earlier, and a sweep of every remotebranch showed 028-031 spent as well (029 twice). Document the command placeholder contract in the README (4.4.1) #699 keeps 027.
https://lody.ai/leynos/sessions/5a6c2be3-7231-444e-86e7-a74daf140eb8
Summary by Sourcery
Harden Windows file-reading policy by opening final components without traversing reparse points and validating the resulting handle before reading.
Bug Fixes:
Enhancements:
Documentation:
Tests: