Stream: git-wasmtime

Topic: wasmtime / issue #10995 Why does the trapped func call re...


view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 09:22):

sammyne opened issue #10995:

I make the simple wasm host as

use std::path::PathBuf;

use anyhow::Context;
use clap::Parser;
use wasmtime::component::{Component, Linker, Val};
use wasmtime::{Config, Engine, Store, Trap};
use wasmtime_wasi::WasiCtxBuilder;

fn main() -> anyhow::Result<()> {
    let Cli { path } = Cli::parse();

    let mut config = Config::default();
    config.wasm_component_model(true);
    let engine = Engine::new(&config)?;
    let mut linker = Linker::new(&engine);

    // Add the command world (aka WASI CLI) to the linker
    wasmtime_wasi::add_to_linker_sync(&mut linker).context("link command world")?;

    let ctx = WasiCtxBuilder::new().inherit_stdout().build_p1();
    let mut store = Store::new(&engine, ctx);

    let component = Component::from_file(&engine, path).context("Component file not found")?;

    const INTERFACE: &str = "sammyne:helloworld/greeter@1.0.0";
    const FUNC_NAME: &str = "say-hello";

    let instance = linker
        .instantiate(&mut store, &component)
        .context("instantiate")?;

    // ref: https://docs.rs/wasmtime/30.0.2/wasmtime/component/struct.Instance.html#method.get_func
    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    // make name prettry large to trigger OOM on purpose.
    let name = "a".repeat(1024 * 1024 * 1024);

    for i in 0.. {
        let params = [new_hello_request(name.clone())];
        let mut results = [Val::Bool(false)];
        match say_hello.call(&mut store, &params, &mut results) {
            Ok(_) => {}
            Err(err) => {
                match err.downcast_ref::<Trap>() {
                    None => println!("#{i} non-trap err: {err}"),
                    Some(c) => println!("#{i} trap err: {c}\n{err}"),
                }
                break;
            }
        }
        say_hello
            .post_return(&mut store)
            .with_context(|| format!("#{i} post return '{FUNC_NAME}'"))?;
    }

    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    let params = [new_hello_request(name.clone())];
    let mut results = [Val::Bool(false)];
    say_hello
        .call(&mut store, &params, &mut results)
        .context("call after trap")?;

    println!("hello world");

    Ok(())
}

/// A CLI for executing WebAssembly components that
/// implement the `example` world.
#[derive(Parser)]
#[clap(name = "hello-world-host", version = env!("CARGO_PKG_VERSION"))]
struct Cli {
    /// WASM 组件的路径
    #[clap(short, long)]
    path: PathBuf,
}

fn new_hello_request(name: String) -> Val {
    Val::Record(vec![("name".to_owned(), Val::String(name))])
}

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 09:26):

sammyne edited issue #10995:

I make the simple wasm host as

use std::path::PathBuf;

use anyhow::Context;
use clap::Parser;
use wasmtime::component::{Component, Linker, Val};
use wasmtime::{Config, Engine, Store, Trap};
use wasmtime_wasi::WasiCtxBuilder;

fn main() -> anyhow::Result<()> {
    let Cli { path } = Cli::parse();

    let mut config = Config::default();
    config.wasm_component_model(true);
    let engine = Engine::new(&config)?;
    let mut linker = Linker::new(&engine);

    // Add the command world (aka WASI CLI) to the linker
    wasmtime_wasi::add_to_linker_sync(&mut linker).context("link command world")?;

    let ctx = WasiCtxBuilder::new().inherit_stdout().build_p1();
    let mut store = Store::new(&engine, ctx);

    let component = Component::from_file(&engine, path).context("Component file not found")?;

    const INTERFACE: &str = "sammyne:helloworld/greeter@1.0.0";
    const FUNC_NAME: &str = "say-hello";

    let instance = linker
        .instantiate(&mut store, &component)
        .context("instantiate")?;

    // ref: https://docs.rs/wasmtime/30.0.2/wasmtime/component/struct.Instance.html#method.get_func
    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    // make name prettry large to trigger OOM on purpose.
    let name = "a".repeat(1024 * 1024 * 1024);

    for i in 0.. {
        let params = [new_hello_request(name.clone())];
        let mut results = [Val::Bool(false)];
        match say_hello.call(&mut store, &params, &mut results) {
            Ok(_) => {}
            Err(err) => {
                match err.downcast_ref::<Trap>() {
                    None => println!("#{i} non-trap err: {err}"),
                    Some(c) => println!("#{i} trap err: {c}\n{err}"),
                }
                break;
            }
        }
        say_hello
            .post_return(&mut store)
            .with_context(|| format!("#{i} post return '{FUNC_NAME}'"))?;
    }

    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    let params = [new_hello_request(name.clone())];
    let mut results = [Val::Bool(false)];
    say_hello
        .call(&mut store, &params, &mut results)
        .context("call after trap")?;

    println!("hello world");

    Ok(())
}

/// A CLI for executing WebAssembly components that
/// implement the `example` world.
#[derive(Parser)]
#[clap(name = "hello-world-host", version = env!("CARGO_PKG_VERSION"))]
struct Cli {
    /// WASM 组件的路径
    #[clap(short, long)]
    path: PathBuf,
}

fn new_hello_request(name: String) -> Val {
    Val::Record(vec![("name".to_owned(), Val::String(name))])
}

The say_hello outside the for loop triggering Trap failed with error as

Error: call after trap

Caused by:
    wasm trap: cannot enter component instance

Really appreciate if someone could explain the reason of rendering the instance non-enterable.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 09:34):

bjorn3 commented on issue #10995:

https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#component-invariants

Components define a "lockdown" state that prevents continued execution after a trap. This both prevents continued execution with corrupt state and also allows more-aggressive compiler optimizations (e.g., store reordering). This was considered early in Core WebAssembly standardization but rejected due to the lack of clear trapping boundary. With components, each component instance is given a mutable "lockdown" state that is set upon trap and implicitly checked at every execution step by component functions. Thus, after a trap, it's no longer possible to observe the internal state of a component instance.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 09:36):

bjorn3 edited a comment on issue #10995:

https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#component-invariants

Components define a "lockdown" state that prevents continued execution after a trap. This both prevents continued execution with corrupt state and also allows more-aggressive compiler optimizations (e.g., store reordering). This was considered early in Core WebAssembly standardization but rejected due to the lack of clear trapping boundary. With components, each component instance is given a mutable "lockdown" state that is set upon trap and implicitly checked at every execution step by component functions. Thus, after a trap, it's no longer possible to observe the internal state of a component instance.

Also from the perspective of the guest wasm module it is straight up UB to re-enter after a trap if the module was compiled by LLVM, even when not using the component model.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 09:44):

sammyne commented on issue #10995:

https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#component-invariants

Components define a "lockdown" state that prevents continued execution after a trap. This both prevents continued execution with corrupt state and also allows more-aggressive compiler optimizations (e.g., store reordering). This was considered early in Core WebAssembly standardization but rejected due to the lack of clear trapping boundary. With components, each component instance is given a mutable "lockdown" state that is set upon trap and implicitly checked at every execution step by component functions. Thus, after a trap, it's no longer possible to observe the internal state of a component instance.

Also from the perspective of the guest wasm module it is straight up UB to re-enter after a trap if the module was compiled by LLVM, even when not using the component model.

Two further questions

Q1: It should be possible the check the tables, resources etc by means of the bound Store. Why "it's no longer possible to observe the internal state of a component instance"?

Q2: As for re-enter after trap, is the UB caused by table or resources leaked by previous trap? If not reusing the table and resources, it should run as if the re-enter is a fresh run.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 10:03):

bjorn3 commented on issue #10995:

Q1: It should be possible the check the tables, resources etc by means of the bound Store. Why "it's no longer possible to observe the internal state of a component instance"?

AFAIK there is no way to get a reference to those things from a component. Stores don't provide any way to iterate over the things that are contained inside it.

Q2: As for re-enter after trap, is the UB caused by table or resources leaked by previous trap? If not reusing the table and resources, it should run as if the re-enter is a fresh run.

It is caused by reusing the linear memory and globals of the component. The only way to prevent reusing that is by literally recreating the component.

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

sammyne commented on issue #10995:

Q1: It should be possible the check the tables, resources etc by means of the bound Store. Why "it's no longer possible to observe the internal state of a component instance"?

AFAIK there is no way to get a reference to those things from a component. Stores don't provide any way to iterate over the things that are contained inside it.

Q2: As for re-enter after trap, is the UB caused by table or resources leaked by previous trap? If not reusing the table and resources, it should run as if the re-enter is a fresh run.

It is caused by reusing the linear memory and globals of the component. The only way to prevent reusing that is by literally recreating the component.

Let's assume the linear memory is reused for the re-entered instance, previous bookkeeping of which allocated memory chunks should be reused also, the new allocation request will be satisified in the unallocated chunks. It just leaked memory. How is the UB triggered? Could you give an example explaining the UB?

view this post on Zulip Wasmtime GitHub notifications bot (Jun 10 2025 at 11:25):

bjorn3 commented on issue #10995:

LLVM is allowed to temporarily write garbage to memory (eg a global variable) for as long as it fixes it up before anyone can observe it according to the rules of the LLVM abstract machine. At the same time it assumes that after a trap nobody will be around to observe anything anymore, so it will just optimize away the restore operation if preceded by a trap.

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

alexcrichton commented on issue #10995:

You might also be interested in https://github.com/WebAssembly/component-model/issues/502 which is about inspecting components after a trap as well.

view this post on Zulip Wasmtime GitHub notifications bot (Jun 11 2025 at 02:05):

sammyne closed issue #10995:

I make the simple wasm host as

use std::path::PathBuf;

use anyhow::Context;
use clap::Parser;
use wasmtime::component::{Component, Linker, Val};
use wasmtime::{Config, Engine, Store, Trap};
use wasmtime_wasi::WasiCtxBuilder;

fn main() -> anyhow::Result<()> {
    let Cli { path } = Cli::parse();

    let mut config = Config::default();
    config.wasm_component_model(true);
    let engine = Engine::new(&config)?;
    let mut linker = Linker::new(&engine);

    // Add the command world (aka WASI CLI) to the linker
    wasmtime_wasi::add_to_linker_sync(&mut linker).context("link command world")?;

    let ctx = WasiCtxBuilder::new().inherit_stdout().build_p1();
    let mut store = Store::new(&engine, ctx);

    let component = Component::from_file(&engine, path).context("Component file not found")?;

    const INTERFACE: &str = "sammyne:helloworld/greeter@1.0.0";
    const FUNC_NAME: &str = "say-hello";

    let instance = linker
        .instantiate(&mut store, &component)
        .context("instantiate")?;

    // ref: https://docs.rs/wasmtime/30.0.2/wasmtime/component/struct.Instance.html#method.get_func
    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    // make name prettry large to trigger OOM on purpose.
    let name = "a".repeat(1024 * 1024 * 1024);

    for i in 0.. {
        let params = [new_hello_request(name.clone())];
        let mut results = [Val::Bool(false)];
        match say_hello.call(&mut store, &params, &mut results) {
            Ok(_) => {}
            Err(err) => {
                match err.downcast_ref::<Trap>() {
                    None => println!("#{i} non-trap err: {err}"),
                    Some(c) => println!("#{i} trap err: {c}\n{err}"),
                }
                break;
            }
        }
        say_hello
            .post_return(&mut store)
            .with_context(|| format!("#{i} post return '{FUNC_NAME}'"))?;
    }

    let say_hello = {
        let instance_idx = instance
            .get_export(&mut store, None, INTERFACE)
            .with_context(|| format!("miss interface: {INTERFACE}"))?;
        let func_idx = instance
            .get_export(&mut store, Some(&instance_idx), FUNC_NAME)
            .with_context(|| format!("locate func '{FUNC_NAME}'"))?;
        instance
            .get_func(&mut store, &func_idx)
            .with_context(|| format!("load func '{FUNC_NAME}'"))?
    };

    let params = [new_hello_request(name.clone())];
    let mut results = [Val::Bool(false)];
    say_hello
        .call(&mut store, &params, &mut results)
        .context("call after trap")?;

    println!("hello world");

    Ok(())
}

/// A CLI for executing WebAssembly components that
/// implement the `example` world.
#[derive(Parser)]
#[clap(name = "hello-world-host", version = env!("CARGO_PKG_VERSION"))]
struct Cli {
    /// WASM 组件的路径
    #[clap(short, long)]
    path: PathBuf,
}

fn new_hello_request(name: String) -> Val {
    Val::Record(vec![("name".to_owned(), Val::String(name))])
}

The say_hello outside the for loop triggering Trap failed with error as

Error: call after trap

Caused by:
    wasm trap: cannot enter component instance

Really appreciate if someone could explain the reason of rendering the instance non-enterable.


Last updated: Dec 06 2025 at 06:05 UTC