feat(core): reach a directory tree only through pinned handles (#445)
User description
Part of #359 (R-15 Gate 4, slice H1a-1). Stacked successor: H1a-2 (removal primitives, test-support only).
What this adds
A new parallel module pinned (DurableFs is unchanged): PinnedDir::open_root opens one directory with no-follow semantics, and open_dir, open_file, entry_kind, entries(limit) and is_still_named reach everything below it through the handle of a directory that was opened, never through a path again. A file is read through PinnedFile (read_at_most, identity, link_count). A link, junction or reparse point is refused at every component below the root; a name must be exactly one ordinary component (on Windows also no data stream, wildcard, trailing dot/space or device name).
- Unix:
*atcalls on directory descriptors (O_NOFOLLOW,AT_SYMLINK_NOFOLLOW,Dir::read_from); a file is opened non-blocking and without a controlling terminal, then type-checked on the descriptor. - Windows: a directory is pinned by a handle that does not share delete; reparse points are opened as themselves and refused; identity = volume serial + 128-bit file id.
Status
H1A_PRIMITIVES=IMPLEMENTED_NON_DESTRUCTIVE, H1_DELETION_MECHANISMS=NOT_IMPLEMENTED (H1a-2), H1_PROVED=NO, R1_PRODUCTION_RECLAMATION=NOT_ADMITTED, GATE4_TERMINAL=NO, RELEASE_AUTHORIZED=NO. This PR changes nothing in the file system and has no caller. The H1a design review (both deletion risks) is on #359 (comment 6186489812); the removal code is H1a-2, so that the destructive part is reviewed on its own.
Please look at
- One new
unsafefunction (windows.rs::standard_info, link count viaGetFileInformationByHandleEx),#[allow(unsafe_code)]local with a SAFETY comment, the pattern ofroot_lock::sys. The existingfile_identityis madepub(crate)to be reused. No new dependency, feature or package;Cargo.lockis untouched. - The root is still opened by path once (its ancestors are resolved by the OS); everything below is handle-relative.
Proof
Lib unit tests (pinned::tests::read), so the macOS/Windows evidence job runs the same cases as Linux: links and other kinds refused for root, child dir, file and entry_kind; FIFO and symlink-to-file not opened (Unix); every non-single-component name refused by every operation; bounded listing and reading; hard-link count; a held directory keeps its object when its path is renamed and refilled (Unix); Windows: junction refused, reinterpreted names refused, a pinned directory and its ancestors cannot be renamed or removed while held. The local environment is Linux, so the hosted Windows job is the only oracle for the Windows cases; ENVIRONMENT_LIMITED is not a pass.
Local: cargo fmt, clippy -D warnings on Linux and x86_64-pc-windows-msvc (all targets), focused tests, default-feature cargo check --locked.
Reviews
Codex quota is deliberate for this boundary; CodeRabbit and CodeAnt run as usual. Three-channel census (threads, top-level comments incl. CodeAnt nitpicks, review bodies incl. CodeRabbit outside-diff) before merge.
Summary by Sourcery
Introduce non-destructive pinned directory-tree primitives that inspect authenticated filesystem objects without resolving paths below the opened root.
New Features:
- Add a public
pinnedmodule for safe, read-only traversal of directories and files through pinned handles with bounded listing and reading, object identity, and hard-link counts.
Bug Fixes:
- Prevent traversal from following symbolic links, junctions, reparse points, special files, or operating-system-reinterpreted names.
Enhancements:
- Share typed Windows handle-query support with existing root-lock functionality and preserve the existing
DurableFsbehavior.
Documentation:
- Document the H1a-1 pinned I/O design, platform guarantees, proof coverage, and remaining deletion work.
Tests:
- Add cross-platform unit coverage for handle-relative access, redirection rejection, name validation, bounds, identity, link counts, and platform-specific pinning behavior.
Chores:
- Record the new non-destructive H1a-1 capability in the unreleased changelog.
Summary by cubic
Adds a new pinned module for handle-relative access to a directory tree (Gate 4 slice H1a-1), non-destructive and with no callers yet. PinnedDir::open_root opens a directory without following a final symlink, resolves it once to a link-free path, and every operation below it — open_dir, open_file, entry_kind, entries(limit), is_still_named — goes through opened directory handles, never path strings. Links, junctions, and reparse points are refused at every component, and names must be exactly one ordinary path component. A root path that ends in a separator, . or .. is refused, because the system resolves the entry before it and would follow a link there even under O_NOFOLLOW; on Windows a root whose last segment ends in a dot or space is refused too, since the path layer strips them. On Unix this uses *at calls on directory descriptors; on Windows a directory is pinned with a handle that does not share delete and children, listings, and entry kinds all go through that handle (NtCreateFile relative to it and the handle's directory information), so relative roots, changed working directories, or retargeted ancestor links cannot redirect a child. The parent handle is checked for reparse data before each use, and every child must report a real path whose parent is the pinned directory, which catches a directory converted into a reparse point in place. DurableFs is unchanged.
Highlights
PinnedFilereads from the start of the file with a caller-given size bound and exposes identity and hard-link count; on Unix a file is looked at before it is opened, so a device node is never opened.root_lock's handle queries share one typed helper,query_handle<T: HandleInfo>, whose information class belongs to the structure through an unsafe trait; the module adds one newunsafefunction (GetFinalPathNameByHandleW) and oneunsafe impl.- Unit tests cover link refusal, name validation (including
COM¹..COM³andLPT¹..LPT³device names), bounded listing/reading, hard-link counting, ancestor-link retargeting, trailing-separator root refusal, and Windows pinning semantics — including in-place junction conversion and that a child keeps its ancestors pinned after its parent is dropped; they run on the Linux, macOS, and Windows evidence jobs.
Summary by CodeRabbit
- New Features
- Added secure access to directories and files on Unix and Windows, using held-open directory handles. Links and reparse points are rejected, and names are validated.
- Added bounded directory listings and file reads, along with file identity, link-count, and directory-location checks.
- Documentation
- Documented platform-specific guarantees and limitations. The new access capabilities do not include filesystem removal operations.
CodeAnt-AI Description
Add directory access that stays tied to opened objects
What Changed
- Adds directory and file access that continues to use the opened directory even if its path changes, and refuses links or redirected entries.
- Limits file reads and directory listings to caller-set bounds; each file read starts from the beginning.
- Adds these capabilities without changing existing storage workflows; nothing calls them yet, and they do not modify the file system.
Impact
✅ Blocks access through linked or redirected entries
✅ Limits file reads and directory listings
✅ Keeps access tied to the opened directory if its path changes
💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
@codeant-ai ask: Your question here
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
@codeant-ai ask: Can you suggest a safer alternative to storing this secret?
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
@codeant-ai: Your feedback here
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
@codeant-ai: Do not flag unused imports.
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
@codeant-ai: review
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.