Aditya-9-6 opened PR #14387 from Aditya-9-6:fix-stat-symlink-trailing-slash to bytecodealliance:main:
Closes #14380
Problem
When \stat\ing a path with a trailing slash where the final component is a symlink to a directory with \FollowSymlinks::No\, POSIX semantics require resolving the symlink because the trailing slash forces resolution of the target directory.
On Linux with \openat2\, \stat_fast\ delegates to \statat\ which resolves the symlink and returns the directory metadata. But in \manually::stat\, \stat_unchecked\ returns the symlink metadata, and the check:
\\\
ust
if options.follow == FollowSymlinks::No || !stat.file_type().is_symlink()
\\\exited immediately before dereferencing. Because \ctx.dir_required\ is set (due to the trailing slash) and \stat.is_dir()\ is false, it returned \ENOTDIR\.
Fix
Check \!ctx.trailing_slash\ in \manually::stat\ when \options.follow == FollowSymlinks::No\, mirroring \maybe_last_component_symlink\ in \manually::open\. If a trailing slash is present, execution falls through to \ctx.symlink\ to dereference the link. If the target is a directory, it returns the directory's metadata; if the target is a regular file, it fails with \ENOTDIR\.
Also exposes \manually\ as \pub(crate)\ within \ilesystem::primitives\ so that unit tests can directly verify \manually::stat\ across all platforms.
Aditya-9-6 requested rvolosatovs for a review on PR #14387.
Aditya-9-6 requested wasmtime-wasi-reviewers for a review on PR #14387.
github-actions[bot] added the label wasi on PR #14387.
alexcrichton commented on PR #14387:
One small note: the PR description here has lots of \ in it which looks like it's intended to be used to format something, but that's not recognized by github, so the PR description is probably not rendering as you intended.
:memo: rvolosatovs submitted PR review:
Could we do something like this?
diff --git a/crates/wasi/src/filesystem/primitives/manually/open.rs b/crates/wasi/src/filesystem/primitives/manually/open.rs index 3df82678a7..4bc06491d3 100644 --- a/crates/wasi/src/filesystem/primitives/manually/open.rs +++ b/crates/wasi/src/filesystem/primitives/manually/open.rs @@ -350,6 +350,13 @@ impl<'start> Context<'start> { Ok(()) } + /// Whether a symlink in the last component should be left as-is rather + /// than dereferenced. A trailing slash requires a directory, so it + /// forces dereferencing even with `FollowSymlinks::No`. + fn stops_at_symlink(&self, follow: FollowSymlinks) -> bool { + follow == FollowSymlinks::No && !self.trailing_slash + } + /// Check whether this is the last component and we don't need /// to dereference; otherwise call `Self::symlink`. fn maybe_last_component_symlink( @@ -359,7 +366,7 @@ impl<'start> Context<'start> { follow: FollowSymlinks, err: io::Error, ) -> io::Result<()> { - if follow == FollowSymlinks::No && !self.trailing_slash && self.components.is_empty() { + if self.stops_at_symlink(follow) && self.components.is_empty() { self.canonical_path.push(one); self.canonical_path.complete(); return Err(err); @@ -451,12 +458,9 @@ pub(crate) fn stat(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io: // `stat_unchecked` on it. let stat = stat_unchecked(&ctx.base, one.as_ref(), FollowSymlinks::No)?; - // If we weren't asked to follow symlinks (and there was no - // trailing slash requiring a directory), or it wasn't a + // If we weren't asked to follow symlinks, or it wasn't a // symlink, we're done. - if (options.follow == FollowSymlinks::No && !ctx.trailing_slash) - || !stat.file_type().is_symlink() - { + if ctx.stops_at_symlink(options.follow) || !stat.file_type().is_symlink() { if stat.is_dir() { if ctx.dir_precluded { return Err(errors::is_directory()); @@ -476,8 +480,7 @@ pub(crate) fn stat(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io: ctx.dir_precluded = true; } - // If it was a symlink and we're asked to follow symlinks (or - // a trailing slash forces dereferencing to a directory), + // If it was a symlink and we're asked to follow symlinks, // dereference it. ctx.symlink(&one, &mut symlink_count)? } else { diff --git a/crates/wasi/src/filesystem/primitives/mod.rs b/crates/wasi/src/filesystem/primitives/mod.rs index d010c9ffe1..90f4919a03 100644 --- a/crates/wasi/src/filesystem/primitives/mod.rs +++ b/crates/wasi/src/filesystem/primitives/mod.rs @@ -18,7 +18,7 @@ mod open_parent; mod open_unchecked_error; mod errors; -pub(crate) mod manually; +mod manually; #[cfg(test)] mod tests; @@ -64,6 +64,9 @@ pub(crate) fn open(start: &fs::File, path: &Path, options: &OpenOptions) -> io:: /// "foo/bar/baz", if "foo" or "bar" are symlinks, they will always be /// followed. This enum value only determines whether "baz" is followed. /// +/// As POSIX requires, a trailing slash forces the last component to resolve +/// to a directory, so "foo/bar/baz/" follows "baz" even with `No`. +/// /// Instead of passing bare `bool`s as parameters, pass a distinct enum so that /// the intent is clear. #[derive(Copy, Clone, Debug, Eq, PartialEq)] diff --git a/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs b/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs index 21ff5511a1..50697065c0 100644 --- a/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs +++ b/crates/wasi/src/filesystem/primitives/tests/fs_additional.rs @@ -1382,13 +1382,8 @@ fn trailing_slash_requires_a_directory() { } if symlink_supported() { - #[cfg(unix)] - use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file}; - #[cfg(windows)] - use std::os::windows::fs::{symlink_dir, symlink_file}; - - check!(symlink_dir("dir", tmpdir.path().join("sym_dir"))); - check!(symlink_file("file", tmpdir.path().join("sym_file"))); + check!(h::symlink_dir(&start, "dir", "sym_dir")); + check!(h::symlink_file(&start, "file", "sym_file")); let stat_sym = check!(p::stat(&start, "sym_dir".as_ref(), p::FollowSymlinks::No)); assert!(stat_sym.file_type().is_symlink());
:speech_balloon: rvolosatovs created PR review comment:
I think we should use
h::symlink_{dir,file}instead like the other tests
:speech_balloon: rvolosatovs created PR review comment:
since this is also used in
maybe_last_component_symlink, could introduce a shared helper?
:speech_balloon: rvolosatovs created PR review comment:
this appears to be redundant?
Aditya-9-6 updated PR #14387.
Aditya-9-6 edited PR #14387:
Closes #14380
Problem
When
stating a path with a trailing slash where the final component is a symlink to a directory withFollowSymlinks::No, POSIX semantics require resolving the symlink because the trailing slash forces resolution of the target directory.On Linux with
openat2,stat_fastdelegates tofstatatwhich resolves the symlink and returns the directory metadata. But inmanually::stat,stat_uncheckedreturns the symlink metadata, and the early check:if options.follow == FollowSymlinks::No || !stat.file_type().is_symlink()exited immediately before dereferencing. Because
ctx.dir_requiredis set (due to the trailing slash) andstat.is_dir()is false, it returnedENOTDIR.Fix
- Introduced a shared
stops_at_symlink(&self, follow: FollowSymlinks) -> boolhelper onContext(follow == FollowSymlinks::No && !self.trailing_slash).- Used
stops_at_symlinkin bothmaybe_last_component_symlinkandmanually::stat. If a trailing slash is present, execution inmanually::statfalls through toctx.symlinkto dereference the link. If the target is a directory, it returns the directory's metadata; if the target is a regular file, it fails withENOTDIR.- Added test coverage in
fs_additional::trailing_slash_requires_a_directoryutilizingh::symlink_dirandh::symlink_file.
:memo: Aditya-9-6 submitted PR review.
:speech_balloon: Aditya-9-6 created PR review comment:
Reverted, thank you!
:memo: Aditya-9-6 submitted PR review.
:speech_balloon: Aditya-9-6 created PR review comment:
Updated to use \h::symlink_dir\ and \h::symlink_file\.
:memo: Aditya-9-6 submitted PR review.
:speech_balloon: Aditya-9-6 created PR review comment:
Extracted \stops_at_symlink\ helper on \Context\ and used it in both places.
Aditya-9-6 commented on PR #14387:
Thanks @alexcrichton and @rvolosatovs for the review! I've fixed the PR description markdown and addressed all comments: extracted the shared \stops_at_symlink\ helper, switched to \h::symlink_{dir,file}\, and kept \mod manually\ private.
:thumbs_up: rvolosatovs submitted PR review.
rvolosatovs added PR #14387 wasi: stat symlinks with trailing slashes correctly in manual resolver to the merge queue.
:check: rvolosatovs merged PR #14387.
rvolosatovs removed PR #14387 wasi: stat symlinks with trailing slashes correctly in manual resolver from the merge queue.
Last updated: Oct 11 2026 at 04:10 UTC