Stream: git-wasmtime

Topic: wasmtime / issue #11606 [v0.3] dirfd.stat_at("/") unexpec...


view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 10:10):

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

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 10:10):

wingo added the bug label to Issue #11606.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 10:10):

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.wasm

Output:

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." })

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 14:31):

alexcrichton added the wasi:impl label to Issue #11606.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 14:31):

alexcrichton added the wasi label to Issue #11606.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 15:10):

wingo commented on issue #11606:

Same bug for metadata-hash-at("/").

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 15:14):

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 with stat and metadata-hash-at, and not with anything that goes through openat instead of straight to statx. Still testing all the foo-at methods though.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 04 2025 at 20:17):

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 /foo works 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

view this post on Zulip Wasmtime GitHub notifications bot (Sep 05 2025 at 06:09):

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, ...}) = 0

Which I think is statting the real /.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 05 2025 at 06:12):

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, ...}) = 0

Which I think is statting the real /. I could be misinterpreting though, I don't know what STATX_ATTR_MOUNT_ROOT does.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 05 2025 at 06:17):

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

view this post on Zulip Wasmtime GitHub notifications bot (Sep 05 2025 at 14:12):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 05 2025 at 17:34):

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 /dir even when /dir is in preopens.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 08 2025 at 20:41):

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.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 24 2025 at 21:11):

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.wasm

Output:

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