RFA-317 · Case file with fixtures · Case 289 of 694 · Runtime evidence
DirEntry::metadata Does Not Follow the Final Symlink
Despite its similar name, DirEntry::metadata follows symlink_metadata semantics for the final entry. Use fs::metadata when the destination object is required, and keep race limits visible.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- Unix
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- DirEntry::metadata follows no-final-symlink semantics even though the similarly named free metadata function follows symbolic links.
- First discriminating check
- Create a controlled link and query entry metadata, entry file_type, and followed path metadata as three explicit observations.
I changed a directory walker from fs::metadata(entry.path()) to entry.metadata() because the shorter form looked equivalent. Symbolic links changed classification. The entry method reported the link itself, while the free function had reported the target.
The failing program creates a link to a regular file, finds its DirEntry, and expects entry.metadata().is_file() to be true. The metadata file type is symlink instead.
Similar method names carry different follow policies
DirEntry::metadata does not traverse a final symbolic link. On Unix it is equivalent in this respect to calling symlink_metadata on the path.
fs::metadata follows symbolic links. Applied to the same entry path, it describes the destination regular file.
RFA-305 compares fs::metadata with fs::symlink_metadata. This case adds the less obvious fact that DirEntry::metadata belongs to the no-follow side despite sharing the shorter metadata name.
Directory walking must choose what it visits
A walker has at least two decisions: how to classify an entry and whether to recurse through a directory symlink. Accidentally following links can escape the intended root, revisit the same directory through cycles, or scan a much larger tree.
Accidentally not following them can omit data that the product considers part of the logical tree. Neither policy is universally correct.
I store entry type and resolved target type separately when policy needs both. A Boolean is_dir cannot explain whether a path is a direct directory, a link to one, a broken link, or an inaccessible target.
file_type can be cheaper and clearer
DirEntry::file_type also does not traverse symlinks. When I only need to distinguish file, directory, and link, it states the question directly and may avoid extra system calls on supported platforms.
Full metadata is appropriate for size, timestamps, permissions, and other fields. I do not request it only to inspect a type if directory entry data already carries that fact.
Platform cost differs: the standard documentation explains that metadata and file-type queries can require different calls depending on Unix, Windows, and filesystem support. I benchmark a real deployment rather than assuming every entry already contains every field.
Stored entry information can become stale
A DirEntry is not a stable capability to one immutable object. The filesystem can change after read_dir produced it. Calling entry.path() and then another metadata function can observe a replacement.
For backup, cleanup, or sandbox boundaries, I consider handle-relative traversal, device and inode identity where appropriate, and platform no-follow options. A string path plus earlier classification is not proof that a later open reaches the same object.
The exact defenses depend on the operating system. The Rust standard methods give useful building blocks but do not turn multi-step path traversal into an atomic security boundary.
Relative symlink targets add another coordinate
The fixture stores target.txt in the link. That text is resolved relative to the directory containing the link. entry.path() returns the path of the link entry, while read_link would return its stored target text. Followed metadata resolves that text to an object.
I keep these three concepts distinct in logs: encountered entry path, stored link target, and resolved destination. Printing only one “path” makes debugging cycles and escapes unnecessarily difficult.
Canonicalization can provide a resolved absolute path for some workflows, but it requires existence, follows links, and introduces its own race between resolution and later action.
What I test
The repaired program asks both questions. Entry metadata identifies the link; free-function metadata identifies its file destination. Cleanup happens after both observations.
My walker tests direct files and directories, links to each, dangling links, relative and absolute targets, cycles, permission errors, disappearing entries, and links that point outside the root. I run platform-specific expectations instead of pretending Unix setup applies everywhere.
I also test the follow policy at the operation that matters, such as open or recursion, not only at the metadata display layer.
The core principle is that filesystem APIs encode a link-follow boundary in each operation. Names alone are not enough: DirEntry::metadata does not follow the final symlink, while fs::metadata does. A robust walker makes entry identity and destination identity separate, reviewable states.