Repository navigation
Task-group-hook cleanup is too eager, leading to a host panic #14504
Description
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 0d76404
(add (off-by-default)task-group-hookfeature #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-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 #17 (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).
Activity
dicej commentedon Oct 2, 2026
This could be a variation of #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 commentedon Oct 5, 2026
Nevermind, this wasn't related to #14459, but should be easy to fix anyway.
fix over-eager deletion of task groups on trap
fix over-eager deletion of task groups on trap
fix over-eager deletion of task groups on trap (bytecodealliance#14555)