Stream: git-wasmtime

Topic: wasmtime / issue #14674 Error contexts sent through futur...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 18:36):

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 drop

An LLM summary, if useful, is:

<details>

When an error-context is 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 a bail_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>

view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 18:36):

alexcrichton added the wasm-proposal:component-model-error-context label to Issue #14674.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 21:03):

alexcrichton commented on issue #14674:

Perhaps a related issue is that error-context on 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 present currently.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 21:26):

alexcrichton added the bug label to Issue #14674.


Last updated: Oct 31 2026 at 23:03 UTC