alexcrichton opened issue #14674:
This input:
;;! component_model_async = true ;;! component_model_error_context = true ;; `$B` sends an error-context to `$A` through a future, and `$A` drops the ;; copy it received. `$B` still owns its own handle, so dropping it should ;; succeed. (component (component $A (core module $libc (memory (export "memory") 1)) (core instance $libc (instantiate $libc)) (type $FT (future error-context)) (core func $future.read (canon future.read $FT async (memory (core memory $libc "memory")))) (core func $error-context.drop (canon error-context.drop)) (core module $m (import "libc" "memory" (memory 1)) (import "" "future.read" (func $future.read (param i32 i32) (result i32))) (import "" "error-context.drop" (func $error-context.drop (param i32))) (func (export "take") (param $f i32) (if (i32.ne (call $future.read (local.get $f) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) (then unreachable)) (call $error-context.drop (i32.load (i32.const 0)))) ) (core instance $i (instantiate $m (with "libc" (instance $libc)) (with "" (instance (export "future.read" (func $future.read)) (export "error-context.drop" (func $error-context.drop)))))) (func (export "take") (param "f" $FT) (canon lift (core func $i "take")))) (instance $a (instantiate $A)) (component $B (import "take" (func $take (param "f" (future error-context)))) (core module $libc (memory (export "memory") 1)) (core instance $libc (instantiate $libc)) (type $FT (future error-context)) (core func $take (canon lower (func $take))) (core func $future.new (canon future.new $FT)) (core func $future.write (canon future.write $FT async (memory (core memory $libc "memory")))) (core func $error-context.new (canon error-context.new (memory (core memory $libc "memory")))) (core func $error-context.drop (canon error-context.drop)) (core module $m (import "libc" "memory" (memory 1)) (import "" "take" (func $take (param i32))) (import "" "future.new" (func $future.new (result i64))) (import "" "future.write" (func $future.write (param i32 i32) (result i32))) (import "" "error-context.new" (func $error-context.new (param i32 i32) (result i32))) (import "" "error-context.drop" (func $error-context.drop (param i32))) (func (export "run") (local $futures i64) (local $e i32) (local.set $e (call $error-context.new (i32.const 0) (i32.const 0))) (local.set $futures (call $future.new)) (i32.store (i32.const 0) (local.get $e)) (if (i32.ne (call $future.write (i32.wrap_i64 (i64.shr_u (local.get $futures) (i64.const 32))) (i32.const 0)) (i32.const -1 (; BLOCKED ;))) (then unreachable)) (call $take (i32.wrap_i64 (local.get $futures))) (call $error-context.drop (local.get $e))) ) (core instance $i (instantiate $m (with "libc" (instance $libc)) (with "" (instance (export "take" (func $take)) (export "future.new" (func $future.new)) (export "future.write" (func $future.write)) (export "error-context.new" (func $error-context.new)) (export "error-context.drop" (func $error-context.drop)))))) (func (export "run") async (canon lift (core func $i "run")))) (instance $b (instantiate $B (with "take" (func $a "take")))) (export "run" (func $b "run")) ) (assert_return (invoke "run"))fails with
$ wasmtime wast -W component-model-error-context=y repro.wast Error: failed to run script file 'repro.wast' 0: failed directive on repro.wast:78 1: error while executing at wasm backtrace: 2: BUG: retrieve concurrent state for error context during dropAn LLM summary, if useful, is:
<details>
When an
error-contextis lowered as part of a future or stream payload,
lower_error_context_to_index
(crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs)
inserts it into the receiving component's table without incrementing
global_error_context_ref_counts. The direct transfer path,
error_context_transfer, does increment it.So if a component sends an error-context through a future and the receiver
drops its copy, the global count goes from 1 to 0 and the shared state is
deleted while the sender still has a handle. The sender dropping its handle
then hits abail_bug!. If another error-context is created first, it
reuses the freed slot, and the sender's handle refers to that one instead,
which may belong to a different component.This requires
component-model-error-context, which is off by default.</details>
alexcrichton added the wasm-proposal:component-model-error-context label to Issue #14674.
alexcrichton commented on issue #14674:
Perhaps a related issue is that
error-contexton the host has no reference count at all. I suspect there's a lot of issues related to this and we've generally just ignored error-context for now. This test however:#[tokio::test] async fn host_error_context_outlives_guest_handle() -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); config.wasm_component_model_error_context(true); let engine = Engine::new(&config)?; let component = Component::new( &engine, r#" (component (core module $libc (memory (export "memory") 1) (func (export "realloc") (param i32 i32 i32 i32) (result i32) (i32.const 256))) (core instance $libc (instantiate $libc)) (core func $error-context.new (canon error-context.new (memory (core memory $libc "memory")))) (core func $error-context.debug-message (canon error-context.debug-message (memory (core memory $libc "memory")) (realloc (core func $libc "realloc")))) (core func $error-context.drop (canon error-context.drop)) (core func $task.return (canon task.return (result error-context))) (core module $m (import "libc" "memory" (memory 1)) (import "" "error-context.new" (func $error-context.new (param i32 i32) (result i32))) (import "" "error-context.debug-message" (func $error-context.debug-message (param i32 i32))) (import "" "error-context.drop" (func $error-context.drop (param i32))) (import "" "task.return" (func $task.return (param i32))) (data (i32.const 0) "hello") ;; Returns a new error-context to the host, then drops its own handle. (func (export "make") (result i32) (local $e i32) (local.set $e (call $error-context.new (i32.const 0) (i32.const 5))) (call $task.return (local.get $e)) (call $error-context.drop (local.get $e)) (i32.const 0 (; EXIT ;))) (func (export "make-cb") (param i32 i32 i32) (result i32) unreachable) ;; Returns the length of the error-context's debug message. (func (export "take") (param $e i32) (result i32) (call $error-context.debug-message (local.get $e) (i32.const 64)) (call $error-context.drop (local.get $e)) (i32.load (i32.const 68))) ) (core instance $i (instantiate $m (with "libc" (instance $libc)) (with "" (instance (export "error-context.new" (func $error-context.new)) (export "error-context.debug-message" (func $error-context.debug-message)) (export "error-context.drop" (func $error-context.drop)) (export "task.return" (func $task.return)))))) (func (export "make") async (result error-context) (canon lift (core func $i "make") async (callback (core func $i "make-cb")))) (func (export "take") (param "e" error-context) (result u32) (canon lift (core func $i "take"))) ) "#, )?; let mut store = Store::new(&engine, ()); let instance = Linker::new(&engine) .instantiate_async(&mut store, &component) .await?; let make = instance.get_typed_func::<(), (ErrorContext,)>(&mut store, "make")?; let take = instance.get_typed_func::<(ErrorContext,), (u32,)>(&mut store, "take")?; // The host still holds `e` after the guest drops its own handle, so // passing it back should work. let (e,) = make.call_async(&mut store, ()).await?; let (len,) = take.call_async(&mut store, (e,)).await?; assert_eq!(len, 5); Ok(()) }fails with
resource not presentcurrently.
alexcrichton added the bug label to Issue #14674.
Last updated: Oct 31 2026 at 23:03 UTC