Stream: git-wasmtime

Topic: wasmtime / issue #14618 Hitting `bail_bug!` + panic combi...


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

alexcrichton assigned dicej to issue #14618.

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

alexcrichton opened issue #14618:

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"))

</details>

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:

  1. The host drops an unstarted call_async future. Default
    configuration. Cancelling the call goes through
    cancel_guest_subtask_without_lowered_parameters → cleanup_thread.
    The cancelled thread never ran, so next_switch_item isn't its own:
    it belongs to whatever the event loop was about to do. When a guest
    makes an async call to another gue
    [message truncated]

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

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

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

alexcrichton added the wasm-proposal:component-threading label to Issue #14618.

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

alexcrichton added the bug label to Issue #14618.

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

dicej closed issue #14618:

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"))

</details>

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:

  1. The host drops an unstarted call_async future. Default
    configuration. Cancelling the call goes through
    cancel_guest_subtask_without_lowered_parameters → cleanup_thread.
    The cancelled thread never ran, so next_switch_item isn'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
    [message truncated]

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

dicej commented on issue #14618:

Fixed by #14624


Last updated: Oct 11 2026 at 04:10 UTC