Repository navigation
Hitting bail_bug! + panic combining threading & yields #14618
Copy link
Copy link
Closed
Labels
bugIncorrect behavior in the current implementation that needs fixingIncorrect behavior in the current implementation that needs fixingwasm-proposal:component-model-asyncIssues related to the WebAssembly Component Model async proposalIssues related to the WebAssembly Component Model async proposalwasm-proposal:component-threadingIssues related to cooperative threading in the component modelIssues related to cooperative threading in the component model
Description
This test:
;;! component_model_async = true
;;! component_model_threading = true
;; $A makes an async-lowered call to $J's callback-lifted "f", so $A's fiber is
;; parked waiting for "f" to suspend or return. "f" then makes a sync-lowered
;; call to $K's sync-typed "g", which spawns a thread and yields to it. When
;; that thread exits, $K has a sync-typed call in progress, so control must
;; switch back to "g"'s thread rather than to $A. $A must only be resumed once
;; "f" returns.
(component
(component $K
(core module $libc (table (export "t") 1 funcref))
(core instance $libc (instantiate $libc))
(core type $ft (func (param i32)))
(core func $thread.new-indirect (canon thread.new-indirect $ft (core table $libc "t")))
(core func $thread.yield-then-resume (canon thread.yield-then-resume))
(core module $m
(import "" "thread.new-indirect" (func $new (param i32 i32) (result i32)))
(import "" "thread.yield-then-resume" (func $ytr (param i32) (result i32)))
(import "" "t" (table 1 funcref))
(func $start (param i32))
(elem (i32.const 0) func $start)
(func (export "g") (result i32)
(drop (call $ytr (call $new (i32.const 0) (i32.const 0))))
(i32.const 42))
)
(core instance $i (instantiate $m (with "" (instance
(export "thread.new-indirect" (func $thread.new-indirect))
(export "thread.yield-then-resume" (func $thread.yield-then-resume))
(export "t" (table $libc "t"))))))
(func (export "g") (result u32) (canon lift (core func $i "g")))
)
(component $J
(import "g" (func $g (result u32)))
(core func $g (canon lower (func $g)))
(core func $task.return (canon task.return (result u32)))
(core module $m
(import "" "g" (func $g (result i32)))
(import "" "task.return" (func $ret (param i32)))
(func (export "f") (result i32)
(call $ret (call $g))
(i32.const 0 (; EXIT ;)))
(func (export "cb") (param i32 i32 i32) (result i32) unreachable)
)
(core instance $i (instantiate $m (with "" (instance
(export "g" (func $g))
(export "task.return" (func $task.return))))))
(func (export "f") async (result u32)
(canon lift (core func $i "f") async (callback (core func $i "cb"))))
)
(component $A
(import "f" (func $f async (result u32)))
(core module $libc (memory (export "memory") 1))
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) async (memory (core memory $libc "memory"))))
(core func $task.return (canon task.return (result u32)))
(core module $m
(import "" "f" (func $f (param i32) (result i32)))
(import "" "task.return" (func $ret (param i32)))
(import "libc" "memory" (memory 1))
(func (export "run") (result i32)
;; The subtask status should be RETURNED, and the result 42.
(if (i32.ne (call $f (i32.const 0)) (i32.const 2 (; RETURNED ;)))
(then unreachable))
(call $ret (i32.load (i32.const 0)))
(i32.const 0 (; EXIT ;)))
(func (export "cb") (param i32 i32 i32) (result i32) unreachable)
)
(core instance $i (instantiate $m
(with "" (instance
(export "f" (func $f))
(export "task.return" (func $task.return))))
(with "libc" (instance $libc))))
(func (export "run") async (result u32)
(canon lift (core func $i "run") async (callback (core func $i "cb"))))
)
(instance $k (instantiate $K))
(instance $j (instantiate $J (with "g" (func $k "g"))))
(instance $a (instantiate $A (with "f" (func $j "f"))))
(func (export "run") (alias export $a "run"))
)
(assert_return (invoke "run") (u32.const 42))fails with
$ cargo run wast foo.wast -W component-model-threading
warning: linker stderr: ld: __eh_frame section too large (max 16MB) to encode dwarf unwind offsets in compact unwind table, performance of exception handling might be affected
|
= note: `#[warn(linker_messages)]` on by default
warning: `wasmtime-cli` (bin "wasmtime") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/wasmtime wast foo.wast -W component-model-threading`
thread 'main' (1310505) panicked at crates/wasmtime/src/runtime/bug.rs:63:13:
BUG: switch item already set
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' (1310505) panicked at crates/wasmtime/src/runtime/fiber.rs:497:9:
attempted to drop in-progress fiber without first calling `StoreFiber::dispose`
stack backtrace:
0: 0x1051fcb44 - std[b23ccf54642dae0e]::backtrace_rs::backtrace::libunwind::trace
at /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/std/src/../../backtrace/src/backtrac
a similar test
Details
;;! component_model_async_stackful = true
;;! component_model_more_async_builtins = true
;; $B is called asynchronously by $C and, before it ever suspends, cancels
;; one of its own subtasks that is queued behind backpressure in $A, then
;; makes another async call. Per the spec the cancel returns
;; CANCELLED_BEFORE_STARTED, the next call is queued (STARTING), and
;; everything returns normally. Wasmtime: `BUG: switch item already set`.
(component
(component $A
(core module $m
(import "" "backpressure.inc" (func $bp-inc))
(import "" "task.return" (func $task-return))
(func (export "f") (call $bp-inc) (call $task-return)))
(core func $bp-inc (canon backpressure.inc))
(core func $task-return (canon task.return))
(core instance $i (instantiate $m (with "" (instance
(export "backpressure.inc" (func $bp-inc))
(export "task.return" (func $task-return))))))
(func (export "f") async (canon lift (core func $i "f") async)))
(component $B
(import "f" (func $f async))
(core module $libc (memory (export "mem") 1))
(core instance $libc (instantiate $libc))
(core func $f (canon lower (func $f) async (memory (core memory $libc "mem"))))
(core func $subtask-cancel (canon subtask.cancel async))
(core func $task-return (canon task.return))
(core module $m
(import "" "f" (func $f (result i32)))
(import "" "subtask.cancel" (func $subtask-cancel (param i32) (result i32)))
(import "" "task.return" (func $task-return))
(func (export "run")
;; returns eagerly (RETURNED = 2), leaving backpressure enabled in $A
(if (i32.ne (call $f) (i32.const 2)) (then unreachable))
;; queued behind backpressure: subtask 1, STARTING = 0
(if (i32.ne (call $f) (i32.const 0x10)) (then unreachable))
;; CANCELLED_BEFORE_STARTED = 3
(if (i32.ne (call $subtask-cancel (i32.const 1)) (i32.const 3)) (then unreachable))
;; queued again: subtask 2, STARTING
(if (i32.ne (call $f) (i32.const 0x20)) (then unreachable))
(call $task-return)))
(core instance $i (instantiate $m (with "" (instance
(export "f" (func $f))
(export "subtask.cancel" (func $subtask-cancel))
(export "task.return" (func $task-return))))))
(func (export "run") async (canon lift (core func $i "run") async)))
(component $C
(import "run" (func $run async))
(core module $libc (memory (export "mem") 1))
(core instance $libc (instantiate $libc))
(core func $run (canon lower (func $run) async (memory (core memory $libc "mem"))))
(core func $task-return (canon task.return))
(core module $m
(import "" "run" (func $run (result i32)))
(import "" "task.return" (func $task-return))
(func (export "run")
;; $B returns without blocking
(if (i32.ne (call $run) (i32.const 2)) (then unreachable))
(call $task-return)))
(core instance $i (instantiate $m (with "" (instance
(export "run" (func $run))
(export "task.return" (func $task-return))))))
(func (export "run") async (canon lift (core func $i "run") async)))
(instance $a (instantiate $A))
(instance $b (instantiate $B (with "f" (func $a "f"))))
(instance $c (instantiate $C (with "run" (func $b "run"))))
(export "run" (func $c "run")))
(assert_return (invoke "run"))
fails with:
$ cargo run wast foo.wast -W component-model-async-stackful,component-model-more-async-builtins
warning: linker stderr: ld: __eh_frame section too large (max 16MB) to encode dwarf unwind offsets in compact unwind table, performance of exception handling might be affected
|
= note: `#[warn(linker_messages)]` on by default
warning: `wasmtime-cli` (bin "wasmtime") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/wasmtime wast foo.wast -W component-model-async-stackful,component-model-more-async-builtins`
thread 'main' (1312064) panicked at crates/wasmtime/src/runtime/bug.rs:63:13:
BUG: switch item already set
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
If useful, an LLM summary is:
Details
cleanup_thread (crates/wasmtime/src/runtime/component/concurrent.rs)
starts with take_next_switch_item(). That moves the store-wide
next_switch_item (normally "resume the caller") into switch_item
("run this next"). If switch_item is already set, set_switch_item
hits bail_bug!("switch item already set"). The rejected work item holds
a fiber, which is dropped without StoreFiber::dispose.
Three paths reach it:
- The host drops an unstarted
call_asyncfuture. Default
configuration. Cancelling the call goes through
cancel_guest_subtask_without_lowered_parameters→cleanup_thread.
The cancelled thread never ran, sonext_switch_itemisn't its own:
it belongs to whatever the event loop was about to do. When a guest
makes an async call to another guest, the event loop setsswitch_item
(start the callee) andnext_switch_item(resume the caller). The event
loop yields to the executor every 128 work items
(MAX_TURNS_WITHOUT_YIELD). If that yield lands right there and the
host then drops the future, the panic follows. Dropping a future is
routine (timeouts, disconnects,select!), and a guest can keep the
event loop busy to make the window more frequent. - A guest's
subtask.cancelof an unstarted subtask, before the
canceller has suspended. The same code path. It needs
component-model-async-stackfuland
component-model-more-async-builtins. In release builds it's an error
(bail_bug!returnsErr), not a panic. - A thread exits while another thread is already in
switch_item.
It needscomponent-model-threading:- A makes an async call to callback-lifted
f, so A's resume is in
next_switch_item. fmakes a sync call to sync-typedg, which spawns thread T and
yields to it.- When T exits,
g's thread is put intoswitch_item, then
cleanup_threadtries to install A's resume as well.
- A makes an async call to callback-lifted
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugIncorrect behavior in the current implementation that needs fixingIncorrect behavior in the current implementation that needs fixingwasm-proposal:component-model-asyncIssues related to the WebAssembly Component Model async proposalIssues related to the WebAssembly Component Model async proposalwasm-proposal:component-threadingIssues related to cooperative threading in the component modelIssues related to cooperative threading in the component model
Activity
add test from bytecodealliance#14618, which is also addressed by the …
dicej commentedon Oct 9, 2026
Fixed by #14624
save and restore `next_switch_item` for sync-to-sync calls (bytecodea…
[50.0.x] backport #14574 and #14624 (#14630)