Stream: git-wasmtime

Topic: wasmtime / issue #14504 Task-group-hook cleanup is too ea...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 02 2026 at 21:30):

alexcrichton opened issue #14504:

this input:

use wasmtime::component::{Component, Linker, TaskGroupHook, TaskGroupId};
use wasmtime::{Config, Engine, Result, Store};

struct Hook;

impl TaskGroupHook for Hook {
    fn handle_start(&mut self, id: TaskGroupId) -> Result<()> {
        println!("start  {id:?}");
        Ok(())
    }
    fn handle_enter(&mut self, id: TaskGroupId) -> Result<()> {
        println!("enter  {id:?}");
        Ok(())
    }
    fn handle_exit(&mut self, id: TaskGroupId) -> Result<()> {
        println!("exit   {id:?}");
        Ok(())
    }
    fn handle_finish(&mut self, id: TaskGroupId) -> Result<()> {
        println!("finish {id:?}");
        Ok(())
    }
}

const WAT: &str = r#"
(component
  (core module $m
    (import "" "task.return" (func $task.return))
    (import "" "backpressure.inc" (func $backpressure.inc))
    ;; `bg`: turn on backpressure, return a result, then keep running in the
    ;; background by yielding; the next callback invocation traps.
    (func (export "bg") (result i32)
      (call $backpressure.inc)
      (call $task.return)
      (i32.const 1 (; YIELD ;)))
    (func (export "bg-cb") (param i32 i32 i32) (result i32)
      unreachable)
    ;; `foo`: never gets to run because of the backpressure.
    (func (export "foo") (result i32)
      (call $task.return)
      (i32.const 0 (; EXIT ;)))
    (func (export "foo-cb") (param i32 i32 i32) (result i32)
      unreachable)
  )
  (core func $task.return (canon task.return))
  (core func $backpressure.inc (canon backpressure.inc))
  (core instance $i (instantiate $m (with "" (instance
    (export "task.return" (func $task.return))
    (export "backpressure.inc" (func $backpressure.inc))))))
  (func (export "bg") async
    (canon lift (core func $i "bg") async (callback (core func $i "bg-cb"))))
  (func (export "foo") async
    (canon lift (core func $i "foo") async (callback (core func $i "foo-cb"))))
)
"#;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    let mut config = Config::new();
    config.wasm_component_model_async(true);
    let engine = Engine::new(&config)?;
    let component = Component::new(&engine, WAT)?;
    let mut store = Store::new(&engine, ());
    if std::env::var_os("NO_HOOK").is_none() {
        store.task_group_hook(Hook);
    }
    let linker = Linker::new(&engine);
    let instance = linker.instantiate_async(&mut store, &component).await?;
    let bg = instance.get_typed_func::<(), ()>(&mut store, "bg")?;
    let foo = instance.get_typed_func::<(), ()>(&mut store, "foo")?;

    println!("calling bg");
    bg.call_async(&mut store, ()).await?;
    println!("bg returned; calling foo");
    let r = foo.call_async(&mut store, ()).await;
    println!("foo result: {r:?}");
    Ok(())
}

yields:

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/task-group-drop-guard-panic`
start  TaskGroupId(TaskGroup(0))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(0))
calling bg
start  TaskGroupId(TaskGroup(0))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
bg returned; calling foo
start  TaskGroupId(TaskGroup(4))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(4))
exit   TaskGroupId(TaskGroup(0))

thread 'main' (266787) panicked at /home/alex/code/wasmtime2/crates/wasmtime/src/runtime/component/concurrent/func.rs:269:61:
called `Result::unwrap()` on an `Err` value: resource not present
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit   TaskGroupId(TaskGroup(0))

cc @dicej

and LLM report, if helpful, is

<details>

task-group-hook: trap deletes TaskGroups still referenced -> host panic

Severity: medium-low (guest-triggered host panic). Requires the off-by-default
task-group-hook cargo feature and a hook installed via
Store::task_group_hook.

Root cause

On guest trap, set_trapped -> clean_up_task_groups
(concurrent/task_group_hook.rs:108-145) deletes every TaskGroup table
entry and calls handle_finish, while GuestTasks/HostTasks holding
those TaskGroupIds remain. Any later Waitable::delete_from
(concurrent.rs:5581) calls decrement_group_ref_count
(task_group_hook.rs:198), whose get_mut(group)? fails with
"resource not present". The call_async drop guard
SignalOnDrop::drop -> host_future_dropped(..).unwrap()
(concurrent/func.rs:269) then panics for a call parked in pending by
backpressure.

Also: unforced_current_thread still points into a finished group, so the
hook gets handle_exit after handle_finish, and ids can be reused after
finish.

Distinct from prior audit #017 (same unwrap, but there the task is
missing from pending; here the group was deleted).

Repro

Component with two callback-lifted async exports: bg does
backpressure.inc, task.return, then YIELD with an unreachable
callback; foo never starts. Host: bg.call_async, then foo.call_async.

cd repro
CARGO_TARGET_DIR=<repo>/target/audit-repro CARGO_INCREMENTAL=0 cargo run --release

Observed:

finish TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(4))
exit   TaskGroupId(TaskGroup(0))
thread 'main' panicked at crates/wasmtime/src/runtime/component/concurrent/func.rs:269:61:
called `Result::unwrap()` on an `Err` value: resource not present

Control: NO_HOOK=1 -> no panic; foo returns Err(unreachable trap).

Suggested fix

Don't delete TaskGroup entries on trap: mark them finished, call
handle_finish once, and make decrement_group_ref_count /
handle_thread_switch tolerate finished groups. Never unwrap() in
SignalOnDrop::drop.

Unconfirmed follow-on

Component instantiation doesn't check may_enter and creates a new group
via enter_guest_sync_call; on a poisoned store this can reuse a freed
TaskGroup slot, so a stale task's decrement hits the wrong group (the
#14247 problem the feature was meant to solve).

</details>

view this post on Zulip Wasmtime GitHub notifications bot (Oct 02 2026 at 21:30):

alexcrichton added the wasm-proposal:component-model-async label to Issue #14504.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 02 2026 at 21:31):

dicej assigned dicej to issue #14504.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 02 2026 at 21:40):

dicej commented on issue #14504:

This could be a variation of https://github.com/bytecodealliance/wasmtime/issues/14459 in that we're not tracking guest task lifetimes properly. I'm currently working on that issue, and I'll try running this test when I've resolved it.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 05 2026 at 20:03):

dicej commented on issue #14504:

Nevermind, this wasn't related to #14459, but should be easy to fix anyway.

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

alexcrichton added the bug label to Issue #14504.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 10 2026 at 01:26):

dicej closed issue #14504:

this input:

use wasmtime::component::{Component, Linker, TaskGroupHook, TaskGroupId};
use wasmtime::{Config, Engine, Result, Store};

struct Hook;

impl TaskGroupHook for Hook {
    fn handle_start(&mut self, id: TaskGroupId) -> Result<()> {
        println!("start  {id:?}");
        Ok(())
    }
    fn handle_enter(&mut self, id: TaskGroupId) -> Result<()> {
        println!("enter  {id:?}");
        Ok(())
    }
    fn handle_exit(&mut self, id: TaskGroupId) -> Result<()> {
        println!("exit   {id:?}");
        Ok(())
    }
    fn handle_finish(&mut self, id: TaskGroupId) -> Result<()> {
        println!("finish {id:?}");
        Ok(())
    }
}

const WAT: &str = r#"
(component
  (core module $m
    (import "" "task.return" (func $task.return))
    (import "" "backpressure.inc" (func $backpressure.inc))
    ;; `bg`: turn on backpressure, return a result, then keep running in the
    ;; background by yielding; the next callback invocation traps.
    (func (export "bg") (result i32)
      (call $backpressure.inc)
      (call $task.return)
      (i32.const 1 (; YIELD ;)))
    (func (export "bg-cb") (param i32 i32 i32) (result i32)
      unreachable)
    ;; `foo`: never gets to run because of the backpressure.
    (func (export "foo") (result i32)
      (call $task.return)
      (i32.const 0 (; EXIT ;)))
    (func (export "foo-cb") (param i32 i32 i32) (result i32)
      unreachable)
  )
  (core func $task.return (canon task.return))
  (core func $backpressure.inc (canon backpressure.inc))
  (core instance $i (instantiate $m (with "" (instance
    (export "task.return" (func $task.return))
    (export "backpressure.inc" (func $backpressure.inc))))))
  (func (export "bg") async
    (canon lift (core func $i "bg") async (callback (core func $i "bg-cb"))))
  (func (export "foo") async
    (canon lift (core func $i "foo") async (callback (core func $i "foo-cb"))))
)
"#;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    let mut config = Config::new();
    config.wasm_component_model_async(true);
    let engine = Engine::new(&config)?;
    let component = Component::new(&engine, WAT)?;
    let mut store = Store::new(&engine, ());
    if std::env::var_os("NO_HOOK").is_none() {
        store.task_group_hook(Hook);
    }
    let linker = Linker::new(&engine);
    let instance = linker.instantiate_async(&mut store, &component).await?;
    let bg = instance.get_typed_func::<(), ()>(&mut store, "bg")?;
    let foo = instance.get_typed_func::<(), ()>(&mut store, "foo")?;

    println!("calling bg");
    bg.call_async(&mut store, ()).await?;
    println!("bg returned; calling foo");
    let r = foo.call_async(&mut store, ()).await;
    println!("foo result: {r:?}");
    Ok(())
}

yields:

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/task-group-drop-guard-panic`
start  TaskGroupId(TaskGroup(0))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(0))
calling bg
start  TaskGroupId(TaskGroup(0))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
bg returned; calling foo
start  TaskGroupId(TaskGroup(4))
enter  TaskGroupId(TaskGroup(0))
exit   TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(4))
exit   TaskGroupId(TaskGroup(0))

thread 'main' (266787) panicked at /home/alex/code/wasmtime2/crates/wasmtime/src/runtime/component/concurrent/func.rs:269:61:
called `Result::unwrap()` on an `Err` value: resource not present
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit   TaskGroupId(TaskGroup(0))

cc @dicej

and LLM report, if helpful, is

<details>

task-group-hook: trap deletes TaskGroups still referenced -> host panic

Severity: medium-low (guest-triggered host panic). Requires the off-by-default
task-group-hook cargo feature and a hook installed via
Store::task_group_hook.

Root cause

On guest trap, set_trapped -> clean_up_task_groups
(concurrent/task_group_hook.rs:108-145) deletes every TaskGroup table
entry and calls handle_finish, while GuestTasks/HostTasks holding
those TaskGroupIds remain. Any later Waitable::delete_from
(concurrent.rs:5581) calls decrement_group_ref_count
(task_group_hook.rs:198), whose get_mut(group)? fails with
"resource not present". The call_async drop guard
SignalOnDrop::drop -> host_future_dropped(..).unwrap()
(concurrent/func.rs:269) then panics for a call parked in pending by
backpressure.

Also: unforced_current_thread still points into a finished group, so the
hook gets handle_exit after handle_finish, and ids can be reused after
finish.

Distinct from prior audit #017 (same unwrap, but there the task is
missing from pending; here the group was deleted).

Repro

Component with two callback-lifted async exports: bg does
backpressure.inc, task.return, then YIELD with an unreachable
callback; foo never starts. Host: bg.call_async, then foo.call_async.

cd repro
CARGO_TARGET_DIR=<repo>/target/audit-repro CARGO_INCREMENTAL=0 cargo run --release

Observed:

finish TaskGroupId(TaskGroup(0))
finish TaskGroupId(TaskGroup(4))
exit   TaskGroupId(TaskGroup(0))
thread 'main' panicked at crates/wasmtime/src/runtime/component/concurrent/func.rs:269:61:
called `Result::unwrap()` on an `Err` value: resource not present

Control: NO_HOOK=1 -> no panic; foo returns Err(unreachable trap).

Suggested fix

Don't delete TaskGroup entries on trap: mark them finished, call
handle_finish once, and make decrement_group_ref_count /
handle_thread_switch tolerate finished groups. Never unwrap() in
SignalOnDrop::drop.

Unconfirmed follow-on

Component instantiation doesn't check may_enter and creates a new group
via enter_guest_sync_call; on a poisoned store this can reuse a freed
TaskGroup slot, so a stale task's decrement hits the wrong group (the
#14247 problem the feature was meant to solve).

</details>


Last updated: Oct 11 2026 at 04:10 UTC