Stream: git-wasmtime

Topic: wasmtime / PR #14387 wasi: stat symlinks with trailing sl...


view this post on Zulip Wasmtime GitHub notifications bot (Sep 23 2026 at 16:28):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 23 2026 at 16:28):

Aditya-9-6 requested rvolosatovs for a review on PR #14387.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 23 2026 at 16:28):

Aditya-9-6 requested wasmtime-wasi-reviewers for a review on PR #14387.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 23 2026 at 17:44):

github-actions[bot] added the label wasi on PR #14387.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 23 2026 at 19:37):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 24 2026 at 17:00):

: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());

view this post on Zulip Wasmtime GitHub notifications bot (Sep 24 2026 at 17:00):

:speech_balloon: rvolosatovs created PR review comment:

I think we should use h::symlink_{dir,file} instead like the other tests

view this post on Zulip Wasmtime GitHub notifications bot (Sep 24 2026 at 17:00):

:speech_balloon: rvolosatovs created PR review comment:

since this is also used in maybe_last_component_symlink, could introduce a shared helper?

view this post on Zulip Wasmtime GitHub notifications bot (Sep 24 2026 at 17:00):

:speech_balloon: rvolosatovs created PR review comment:

this appears to be redundant?

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:22):

Aditya-9-6 updated PR #14387.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:22):

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 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 fstatat which resolves the symlink and returns the directory metadata. But in manually::stat, stat_unchecked returns the symlink metadata, and the early check:

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

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:memo: Aditya-9-6 submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:speech_balloon: Aditya-9-6 created PR review comment:

Reverted, thank you!

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:memo: Aditya-9-6 submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:speech_balloon: Aditya-9-6 created PR review comment:

Updated to use \h::symlink_dir\ and \h::symlink_file\.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:memo: Aditya-9-6 submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

:speech_balloon: Aditya-9-6 created PR review comment:

Extracted \stops_at_symlink\ helper on \Context\ and used it in both places.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 05:23):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 06:53):

:thumbs_up: rvolosatovs submitted PR review.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 06:54):

rvolosatovs added PR #14387 wasi: stat symlinks with trailing slashes correctly in manual resolver to the merge queue.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 07:20):

:check: rvolosatovs merged PR #14387.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 25 2026 at 07:20):

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