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
Commit: 249bdf8fca (main2), audited 2026-10-02. Introduced by 0d76404cfd
(#14416).Platform: Linux 7.0.0-31-generic x86_64
- Auditor: Claude Opus 5.5 (claude-opus-5-5); repro re-verified by coordinator
Severity: medium-low (guest-triggered host panic). Requires the off-by-default
task-group-hookcargo 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 everyTaskGrouptable
entry and callshandle_finish, whileGuestTasks/HostTasks holding
thoseTaskGroupIds remain. Any laterWaitable::delete_from
(concurrent.rs:5581) callsdecrement_group_ref_count
(task_group_hook.rs:198), whoseget_mut(group)?fails with
"resource not present". Thecall_asyncdrop guard
SignalOnDrop::drop->host_future_dropped(..).unwrap()
(concurrent/func.rs:269) then panics for a call parked inpendingby
backpressure.Also:
unforced_current_threadstill points into a finished group, so the
hook getshandle_exitafterhandle_finish, and ids can be reused after
finish.Distinct from prior audit #017 (same
unwrap, but there the task is
missing frompending; here the group was deleted).Repro
Component with two callback-lifted async exports:
bgdoes
backpressure.inc,task.return, then YIELD with anunreachable
callback;foonever starts. Host:bg.call_async, thenfoo.call_async.cd repro CARGO_TARGET_DIR=<repo>/target/audit-repro CARGO_INCREMENTAL=0 cargo run --releaseObserved:
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 presentControl:
NO_HOOK=1-> no panic;fooreturnsErr(unreachable trap).Suggested fix
Don't delete
TaskGroupentries on trap: mark them finished, call
handle_finishonce, and makedecrement_group_ref_count/
handle_thread_switchtolerate finished groups. Neverunwrap()in
SignalOnDrop::drop.Unconfirmed follow-on
Component instantiation doesn't check
may_enterand creates a new group
viaenter_guest_sync_call; on a poisoned store this can reuse a freed
TaskGroupslot, so a stale task's decrement hits the wrong group (the
#14247 problem the feature was meant to solve).</details>
alexcrichton added the wasm-proposal:component-model-async label to Issue #14504.
dicej assigned dicej to issue #14504.
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.
dicej commented on issue #14504:
Nevermind, this wasn't related to #14459, but should be easy to fix anyway.
alexcrichton added the bug label to Issue #14504.
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
Commit: 249bdf8fca (main2), audited 2026-10-02. Introduced by 0d76404cfd
(#14416).Platform: Linux 7.0.0-31-generic x86_64
- Auditor: Claude Opus 5.5 (claude-opus-5-5); repro re-verified by coordinator
Severity: medium-low (guest-triggered host panic). Requires the off-by-default
task-group-hookcargo 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 everyTaskGrouptable
entry and callshandle_finish, whileGuestTasks/HostTasks holding
thoseTaskGroupIds remain. Any laterWaitable::delete_from
(concurrent.rs:5581) callsdecrement_group_ref_count
(task_group_hook.rs:198), whoseget_mut(group)?fails with
"resource not present". Thecall_asyncdrop guard
SignalOnDrop::drop->host_future_dropped(..).unwrap()
(concurrent/func.rs:269) then panics for a call parked inpendingby
backpressure.Also:
unforced_current_threadstill points into a finished group, so the
hook getshandle_exitafterhandle_finish, and ids can be reused after
finish.Distinct from prior audit #017 (same
unwrap, but there the task is
missing frompending; here the group was deleted).Repro
Component with two callback-lifted async exports:
bgdoes
backpressure.inc,task.return, then YIELD with anunreachable
callback;foonever starts. Host:bg.call_async, thenfoo.call_async.cd repro CARGO_TARGET_DIR=<repo>/target/audit-repro CARGO_INCREMENTAL=0 cargo run --releaseObserved:
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 presentControl:
NO_HOOK=1-> no panic;fooreturnsErr(unreachable trap).Suggested fix
Don't delete
TaskGroupentries on trap: mark them finished, call
handle_finishonce, and makedecrement_group_ref_count/
handle_thread_switchtolerate finished groups. Neverunwrap()in
SignalOnDrop::drop.Unconfirmed follow-on
Component instantiation doesn't check
may_enterand creates a new group
viaenter_guest_sync_call; on a poisoned store this can reuse a freed
TaskGroupslot, 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