-
Notifications
You must be signed in to change notification settings - Fork 0
Validate the Windows final component through a same-handle reparse-point open (#703) #739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
leynos
merged 21 commits into
main
from
issue-703-validate-the-windows-final-component-through-a-same-handle-reparse-point-open
Sep 20, 2026
+1,081
−69
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
297bad0
Harden the Windows final component through a same-handle reparse open
b1490be
Correct the reparse-tag rationale in the design documents
9facc58
Test the reparse policy against real tag values and add a junction test
3bc4cbc
Reflow the ADR-026 prose to the mdtablefix canonical form
d241138
Use sentence case for the ADR-026 section headings
a99b9bc
Test the reparse policy on the handle and narrow a Windows-only import
704aa78
Renumber ADR-026 to ADR-027 after main claimed 026
30e7c34
test(windows): skip the junction fixture when cmd.exe is absent
6b0ad8b
fix(windows): satisfy the Windows-only Whitaker lints and drop an imp…
4c3ef48
docs(tests): spell "recognizes" in the junction fixture comment
2cb3ef4
docs: describe the Windows reparse-point policy in the user-facing gu…
d8aeaae
Renumber ADR-027 to ADR-032 after three later claims
cbec7e5
Correct the follow_symlinks containment mechanism
6c3f44c
Rewrap the users-guide paragraph mdtablefix's way
c755c1a
State the ADR's evidence limit plainly
3862bf7
docs(adr-032): credit native Windows CI for compiling the gated code
306f1d2
style(adr-032): let mdtablefix choose the wrap point
241b528
docs(adr-032): pin the test-lane figure to its commit
7b4599b
docs(stdlib guide): trim the reparse detail to a cross-link
5232908
test(windows): assert the reparse policy at compile time
108fad1
docs: correct the opt-in wording and the ADR evidence record
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| //! Windows final-component policy: open the reparse point itself and judge the | ||
| //! handle that the read will use. | ||
| //! | ||
| //! Unix reaches this guarantee inside `open` with `O_NOFOLLOW`, so the policy | ||
| //! decision and the read share one call. Windows has no flag of that name, but | ||
| //! `FILE_FLAG_OPEN_REPARSE_POINT` has the same effect: the handle that comes | ||
| //! back refers to the reparse point rather than to whatever it points at. The | ||
| //! judgement then reads that handle's own attributes, so nothing between the | ||
| //! decision and the read can redirect either one — a concurrent rename in the | ||
| //! containing directory changes which entry the path names, but it cannot | ||
| //! change what an already-open handle refers to. | ||
| //! | ||
| //! The policy rejects **every** reparse point, not only the ones `std` reports | ||
| //! as symlinks. `FileType::is_symlink` is a test on the tag *value*: it is true | ||
| //! only when the tag is a name surrogate (bit 29 set), which covers symlinks, | ||
| //! junctions, and volume mount points, but is false for every other tag — a | ||
| //! deduplication or cloud placeholder, for example. An open that follows one of | ||
| //! those would hand back the target's handle while a symlink-shaped check saw | ||
| //! nothing to refuse. Testing the attribute bit instead asks "is this a reparse | ||
| //! point at all", which is the policy the callers actually want and needs no | ||
| //! knowledge of which tags a future Windows release may mint. | ||
| use camino::Utf8Path; | ||
| use cap_std::fs::MetadataExt as _; | ||
| use cap_std::fs_utf8::{Metadata, OpenOptions, OpenOptionsExt}; | ||
| use minijinja::Error; | ||
|
|
||
| use super::fs_utils::not_regular_file_error; | ||
|
|
||
| /// `FILE_FLAG_OPEN_REPARSE_POINT`: open the reparse point instead of following | ||
| /// it. The flag is ignored when the entry is not a reparse point, so it costs | ||
| /// an ordinary file nothing. | ||
| const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; | ||
|
|
||
| /// `FILE_FLAG_BACKUP_SEMANTICS`: permit a directory to be opened at all. | ||
| /// | ||
| /// Without it an open of a directory fails outright, and the caller would | ||
| /// report an open error where Unix reports the regular-file diagnostic. The | ||
| /// flag exists to let the shared `is_file` check reject a directory on its | ||
| /// attributes, exactly as it does on Unix. | ||
| const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000; | ||
|
|
||
| /// `FILE_ATTRIBUTE_REPARSE_POINT`: the entry carries a reparse tag. | ||
| /// | ||
| /// This is the attribute the policy consumes; the tag value itself is | ||
| /// deliberately never inspected, so a tag this build has never heard of is | ||
| /// refused rather than waved through. | ||
| const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400; | ||
|
|
||
| /// The `dwFlagsAndAttributes` bits the policy passes to the open. | ||
| /// | ||
| /// `FILE_FLAG_BACKUP_SEMANTICS` is unconditional, so a directory can be opened | ||
| /// and then rejected by the shared regular-file check rather than by the open | ||
| /// failing. `FILE_FLAG_OPEN_REPARSE_POINT` is added only by the default policy, | ||
| /// which must not traverse a reparse point. | ||
| pub(super) const fn open_flags(follow_symlinks: bool) -> u32 { | ||
| let mut flags = FILE_FLAG_BACKUP_SEMANTICS; | ||
| if !follow_symlinks { | ||
| flags |= FILE_FLAG_OPEN_REPARSE_POINT; | ||
| } | ||
| flags | ||
| } | ||
|
|
||
| /// Apply the Windows half of the open policy to `options`. | ||
| /// | ||
| /// Mirrors `apply_unix_open_flags`: the default policy asks the open itself not | ||
| /// to follow a reparse point, and the opt-in policy leaves the open to resolve | ||
| /// the link as usual. | ||
| pub(super) fn apply_open_flags(options: &mut OpenOptions, follow_symlinks: bool) { | ||
| options.custom_flags(open_flags(follow_symlinks)); | ||
| } | ||
|
|
||
| /// Whether `file_attributes` describes a reparse point the policy refuses. | ||
| /// | ||
| /// The test is on the attribute bit alone, and deliberately never on the tag | ||
| /// value. A tag-value test would have to enumerate acceptable tags, and a | ||
| /// name-surrogate test — the shape `FileType::is_symlink` uses — misses every | ||
| /// tag that is not a name surrogate, such as a deduplication or cloud | ||
| /// placeholder. | ||
| pub(super) const fn is_prohibited_reparse_point(file_attributes: u32) -> bool { | ||
| file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 | ||
| } | ||
|
|
||
| /// Reject an opened handle whose entry is a reparse point. | ||
| /// | ||
| /// `metadata` must come from the handle the caller will read, never from a | ||
| /// separate path lookup: that is what makes the refusal race-free. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns the non-regular-file diagnostic when the handle carries the | ||
| /// reparse-point attribute. | ||
| pub(super) fn reject_reparse_point(metadata: &Metadata, path: &Utf8Path) -> Result<(), Error> { | ||
| if is_prohibited_reparse_point(metadata.file_attributes()) { | ||
| return Err(not_regular_file_error(path)); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "windows_reparse_tests.rs"] | ||
| mod tests; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.