Skip to content

fix: listing sandbox escaping symlinks - #6731

Open
Fliqqr wants to merge 14 commits into
mainfrom
fix-symlink-showing
Open

fix: listing sandbox escaping symlinks#6731
Fliqqr wants to merge 14 commits into
mainfrom
fix-symlink-showing

Conversation

@Fliqqr

@Fliqqr Fliqqr commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

fd_readdir was exposing symlink entries that should not have been visible inside the WASIX sandbox, including host-backed symlinks that could resolve outside the mounted or preopened directory tree.

Changes

  • Updated fd_readdir to filter directory entries through a symlink visibility check instead of listing all symlinks blindly.
  • Added host-fs symlink policy logic to distinguish safe symlinks from sandbox-escaping symlinks.
  • Resolved symlink visibility against the effective mount root and preopen root, including nested directories and subtree mounts.
  • Handled edge cases for absolute links, chained links, broken links, and symlink loops when deciding whether an entry should be shown.
  • Reused mount-aware symlink resolution so allowed host symlinks still resolve correctly to guest paths.
  • Added coverage for readdir behavior and symlink traversal with new WASIX and Rust tests for visible vs hidden symlinks.

@Fliqqr Fliqqr changed the title fix: sandbox escaping symlinks fix: listing sandbox escaping symlinks Jun 17, 2026
@sadhbh-c0d3

Copy link
Copy Markdown
Contributor

I’m not sure fd_readdir should hide the escaping symlink entry.

For the sandbox symlink issue we fixed earlier, the bug was that follow-style operations could resolve through an “evil” symlink and escape the mount/preopen boundary. The intended model after that work was:

  • literal symlink operations still see the link itself: readdir, lstat, readlink
  • follow-style operations must stay mount-contained or fail: open, followed stat, traversal through the link, etc.

With this PR, readdir hides the link, but no-follow operations can still observe it, so the behavior becomes inconsistent and less POSIX-like. I think we should either keep listing the symlink and rely on the resolver to reject following it, or explicitly make “escaping host symlinks are invisible” a broader policy and apply it consistently to readlink / no-follow stat too.

There’s also a coverage gap here: the new host policy downcasts mount entries directly to host_fs::FileSystem, so wrapped host mounts such as ArcFileSystem / OverlayFileSystem may silently skip this visibility policy.

Note that when I delivered symbolc links to Wasmer I have ran these tests:

cargo test -p wasmer-wasix path_resolution_preserves_posix_directory_component_rules --lib
cargo test -p wasmer-wasix symlinked_directory_components_resolve_to_target_entries --lib
cargo test -p wasmer-wasix backing_absolute_host_symlink_targets_stay_within_guest_mount --lib
cargo test --test wasm_tests fs_sandbox_symlink -- --nocapture
cargo test --test wasm_tests ported_fs_sandbox -- --nocapture
cargo test --test wasm_tests ported_wasi_sees_virtual_root -- --nocapture
cargo test --test wasm_tests fs_sandbox_test -- --nocapture

and from https://github.com/wasmerio/anti-regression-shield these specifically:

test.test_tarfile.TestExtractionFilters.test_parent_symlink
test.test_tarfile.TestExtractionFilters.test_parent_symlink2
test.test_tarfile.TestExtractionFilters
test.test_zipfile._path.test_path.TestPath.test_is_symlink

I think would be great to run these tests with your changes.

@marxin
marxin requested a review from Arshia001 June 18, 2026 07:34

@Arshia001 Arshia001 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is correct in principle. However, we're adding a lot of #[cfg]ed code into an already huge file, and even doing downcasts that depend on exactly how the FS tree is set up right now AFAICT. This makes the implementation brittle. We should move the host-related logic into host FS. The WASIX FS doesn't really need to understand any of this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread lib/wasix/src/syscalls/wasi/fd_readdir.rs

@Arshia001 Arshia001 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not a fan of the implementation. Not every VirtualFS implementer needs to know what a "host" is.

@Arshia001 Arshia001 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still architecturally unsound IMO. Filtering must be done at the FileSystem impl level, not at the WASIX level. This also avoids the need for the whole symlink_policy method on FileSystem.

Comment thread lib/virtual-fs/src/host_fs.rs Outdated

let metadata = fs::symlink_metadata(&path)?;

if !metadata.file_type().is_symlink() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is technically a wrong input, not a success. I'd use InvalidInput here.

Comment thread lib/virtual-fs/src/host_fs.rs Outdated
return Ok(SymlinkPolicy::Visible);
}

let target = match fs::read_link(&path) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we unify the file_type().is_symlink() above and this read_link() into one call? I'm guessing read_link must report an error containing "this is not a symlink" if you call it on the wrong thing?

Comment thread lib/virtual-fs/src/path.rs Outdated
@@ -0,0 +1,51 @@
use std::path::{Component, Path, PathBuf};

pub(crate) fn resolve_path_within(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably belongs in the host_fs module, since that's the only module that does handling of host paths (i.e. std::path). Note that WASIX uses unix-style paths regardless of host OS, including on Windows.

Comment thread lib/wasix/src/fs/mod.rs Outdated
))
}

pub(crate) fn readdir_entry_visible(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why we're reporting entries from the FS impls, and then filtering out at this layer. Each FS impl should be responsible for making sure they only report visible entries.

@Fliqqr
Fliqqr force-pushed the fix-symlink-showing branch from ad70b04 to 3094118 Compare July 23, 2026 09:44
@Fliqqr
Fliqqr requested a review from Arshia001 July 24, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants