wingo opened issue #11606:
Summary
I can stat-at "/", whereas the filesystem spec says that any path that starts with / should result in
not-permitted.Test case
use std::process; extern crate wit_bindgen; wit_bindgen::generate!({ inline: r" package test:test; world test { include wasi:filesystem/imports@0.3.0-rc-2025-08-15; include wasi:cli/command@0.3.0-rc-2025-08-15; } ", additional_derives: [PartialEq, Eq, Hash, Clone], // Work around https://github.com/bytecodealliance/wasm-tools/issues/2285. features:["clocks-timezone"], async: [ "wasi:cli/run@0.3.0-rc-2025-08-15#run", ], generate_all }); use wasi::filesystem::types::{PathFlags,ErrorCode}; async fn test_filesystem() { match &wasi::filesystem::preopens::get_directories()[..] { [(dirfd, _)] => { assert_eq!(dirfd.stat_at(PathFlags::empty(), "/".to_string()).await, Err(ErrorCode::NotPermitted)); }, [..] => { eprintln!("usage: run with one open dir"); process::exit(1) } } } struct Component; export!(Component); impl exports::wasi::cli::run::Guest for Component { async fn run() -> Result<(), ()> { test_filesystem().await; Ok(()) } } fn main() { unreachable!("main is a stub"); }Run as:
mkdir fs-tests.dir wasmtime --dir fs-tests.dir -Wcomponent-model-async=y -Sp3=y target/wasm32-wasip2/release/test.wasm
wingo added the bug label to Issue #11606.
wingo edited issue #11606:
Summary
I can stat-at "/", whereas the filesystem spec says that any path that starts with / should result in
not-permitted.Test case
use std::process; extern crate wit_bindgen; wit_bindgen::generate!({ inline: r" package test:test; world test { include wasi:filesystem/imports@0.3.0-rc-2025-08-15; include wasi:cli/command@0.3.0-rc-2025-08-15; } ", additional_derives: [PartialEq, Eq, Hash, Clone], // Work around https://github.com/bytecodealliance/wasm-tools/issues/2285. features:["clocks-timezone"], async: [ "wasi:cli/run@0.3.0-rc-2025-08-15#run", ], generate_all }); use wasi::filesystem::types::{PathFlags,ErrorCode}; async fn test_filesystem() { match &wasi::filesystem::preopens::get_directories()[..] { [(dirfd, _)] => { assert_eq!(dirfd.stat_at(PathFlags::empty(), "/".to_string()).await, Err(ErrorCode::NotPermitted)); }, [..] => { eprintln!("usage: run with one open dir"); process::exit(1) } } } struct Component; export!(Component); impl exports::wasi::cli::run::Guest for Component { async fn run() -> Result<(), ()> { test_filesystem().await; Ok(()) } } fn main() { unreachable!("main is a stub"); }Run as:
mkdir fs-tests.dir wasmtime --dir fs-tests.dir -Wcomponent-model-async=y -Sp3=y target/wasm32-wasip2/release/test.wasmOutput:
assertion `left == right` failed left: Ok(DescriptorStat { type: DescriptorType::Directory, link-count: 1, size: 4096, data-access-timestamp: Some(Datetime { seconds: 1755768485, nanoseconds: 715466563 }), data-modification-timestamp: Some(Datetime { seconds: 1755095399, nanoseconds: 980128269 }), status-change-timestamp: Some(Datetime { seconds: 1755086421, nanoseconds: 967089397 }) }) right: Err(ErrorCode { code: 30, name: "not-permitted", message: "Operation not permitted, similar to `EPERM` in POSIX." })
alexcrichton added the wasi:impl label to Issue #11606.
alexcrichton added the wasi label to Issue #11606.
wingo commented on issue #11606:
Same bug for
metadata-hash-at("/").
wingo commented on issue #11606:
FWIW, this is actually a sandbox escape, but only for
/, and so far I have only been able to repro withstatandmetadata-hash-at, and not with anything that goes throughopenatinstead of straight tostatx. Still testing all the foo-at methods though.
alexcrichton commented on issue #11606:
This might be something where we end up having to update the specification as my gut is that it's pretty heavily baked in everywhere that
/fooworks in WASI (e.g. wasi-libc and all its dependents).Could you elaborate a bit more on the sandbox escape point though? Are you thinking that this ends up looking at the host's
/root folder? I would expect that this would be looking at the folder itself passed to openat/statat/etc, so/is more like a relative root instead of the absolute root. I could very well be wrong though, I'm not intimately familiar with these syscalls
wingo commented on issue #11606:
If you strace the process, it does:
statx(3, "/", AT_STATX_SYNC_AS_STAT|AT_SYMLINK_NOFOLLOW, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=STATX_ATTR_MOUNT_ROOT, stx_mode=S_IFDIR|0755, stx_size=4096, ...}) = 0Which I think is statting the real
/.
wingo edited a comment on issue #11606:
If you strace the process, it does:
statx(3, "/", AT_STATX_SYNC_AS_STAT|AT_SYMLINK_NOFOLLOW, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=STATX_ATTR_MOUNT_ROOT, stx_mode=S_IFDIR|0755, stx_size=4096, ...}) = 0Which I think is statting the real
/. I could be misinterpreting though, I don't know whatSTATX_ATTR_MOUNT_ROOTdoes.
wingo commented on issue #11606:
The docs seem pretty clear that if you pass an absolute path to statx, the dirfd is ignored: https://man.archlinux.org/man/statx.2.en#An
alexcrichton commented on issue #11606:
For the sandbox escape part, all you can do is learn that there's a root filesystem right? You can't read/write anything about it? Although to discuss this further, if you feel this is a CVE mind running through our security policy?
For WASI here I basically don't know if this is going to be a practical change. There's a lot of code in the wild doing filesystem stuff and I'm not sure that anyone has a good handle on what it's all doing. For example my personal interpretation of
/would be "stat the root" and I feel like I've almost certainly written code to that effect in the past. I have no idea if anyone's relying on that still and how many places would need to be updated.
wingo commented on issue #11606:
I don't have an opinion regarding CVE :) And I think it just gets you stat access to /, I haven't found anything else you can do with it. I'm sympathetic wrt wanting to stat the root but I think wasmtime should be handling these calls itself, as you can't use these interfaces to stat
/direven when/diris in preopens.
sunfishcode commented on issue #11606:
From the description here, I don't think a CVE is warranted here, but this is a bug, and I'll look into fixing it.
sunfishcode closed issue #11606:
Summary
I can stat-at "/", whereas the filesystem spec says that any path that starts with / should result in
not-permitted.Test case
use std::process; extern crate wit_bindgen; wit_bindgen::generate!({ inline: r" package test:test; world test { include wasi:filesystem/imports@0.3.0-rc-2025-08-15; include wasi:cli/command@0.3.0-rc-2025-08-15; } ", additional_derives: [PartialEq, Eq, Hash, Clone], // Work around https://github.com/bytecodealliance/wasm-tools/issues/2285. features:["clocks-timezone"], async: [ "wasi:cli/run@0.3.0-rc-2025-08-15#run", ], generate_all }); use wasi::filesystem::types::{PathFlags,ErrorCode}; async fn test_filesystem() { match &wasi::filesystem::preopens::get_directories()[..] { [(dirfd, _)] => { assert_eq!(dirfd.stat_at(PathFlags::empty(), "/".to_string()).await, Err(ErrorCode::NotPermitted)); }, [..] => { eprintln!("usage: run with one open dir"); process::exit(1) } } } struct Component; export!(Component); impl exports::wasi::cli::run::Guest for Component { async fn run() -> Result<(), ()> { test_filesystem().await; Ok(()) } } fn main() { unreachable!("main is a stub"); }Run as:
mkdir fs-tests.dir wasmtime --dir fs-tests.dir -Wcomponent-model-async=y -Sp3=y target/wasm32-wasip2/release/test.wasmOutput:
assertion `left == right` failed left: Ok(DescriptorStat { type: DescriptorType::Directory, link-count: 1, size: 4096, data-access-timestamp: Some(Datetime { seconds: 1755768485, nanoseconds: 715466563 }), data-modification-timestamp: Some(Datetime { seconds: 1755095399, nanoseconds: 980128269 }), status-change-timestamp: Some(Datetime { seconds: 1755086421, nanoseconds: 967089397 }) }) right: Err(ErrorCode { code: 30, name: "not-permitted", message: "Operation not permitted, similar to `EPERM` in POSIX." })
Last updated: Dec 06 2025 at 06:05 UTC