Skip to content

fix: reclaim host stream/future transmits when the guest drops its end#13515

Closed
gfx wants to merge 1 commit into
bytecodealliance:mainfrom
wado-lang:fix-component-host-transmit-leak
Closed

fix: reclaim host stream/future transmits when the guest drops its end#13515
gfx wants to merge 1 commit into
bytecodealliance:mainfrom
wado-lang:fix-component-host-transmit-leak

Conversation

@gfx

@gfx gfx commented May 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #13514.

When the guest drops its end of a stream/future while the host consumer/producer is still HostReady, the transmit was never reclaimed. This finalizes the stranded host end so the TransmitState and both handles are freed.

Fix

crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs — two HostReady arms were no-ops; both now call delete_transmit (which also drops the host producer/consumer):

  • host_drop_reader (guest dropped the read end → host producer): the read end is already Dropped, so finalize unconditionally.
  • host_drop_writer (guest dropped the write end → host consumer): the write end isn't forced Dropped here, so finalize only once the writer is actually gone.

Tests

Two regression tests in component-async-tests, driving only the public StreamReader::pipe / FutureReader::new APIs:

  • streams::async_host_consumer_drop (+ a minimal host-consumer-drop-guest test program) — covers host_drop_writer.
  • streams::async_host_producer_drop (reuses the existing closed-streams guest) — covers host_drop_reader.

Both fail on main (leftover concurrent-state entries via assert_concurrent_state_empty) and pass with this change.

Verification

cargo test -p component-async-tests --test test_all → 82 passed, 0 failed.

Copilot AI review requested due to automatic review settings May 30, 2026 09:03
@gfx gfx requested a review from a team as a code owner May 30, 2026 09:03
@gfx gfx requested review from dicej and removed request for a team May 30, 2026 09:03

This comment was marked as spam.

@gfx gfx force-pushed the fix-component-host-transmit-leak branch 2 times, most recently from 7025008 to ac7f447 Compare May 30, 2026 09:05
When the guest drops its end of a stream/future while the host
consumer/producer is still `HostReady`, the host end was never finalized, so
the `TransmitState` and both handles leaked from the concurrent-state table
(eventually trapping with "resource table has no free keys").

The `HostReady` arms of `host_drop_reader` and `host_drop_writer` were no-ops;
both now `delete_transmit`. `host_drop_writer` only finalizes once the writer is
actually `Dropped`.

Adds two `component-async-tests` regression tests (one per path) that fail on
`main` and pass here.

Fixes bytecodealliance#13514.

Assisted-by: Claude Code
@gfx gfx force-pushed the fix-component-host-transmit-leak branch from ac7f447 to 89cc10d Compare May 30, 2026 09:11
@github-actions github-actions Bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label May 30, 2026
@gfx gfx changed the title Reclaim host stream/future transmits when the guest drops its end fix: reclaim host stream/future transmits when the guest drops its end May 31, 2026
@alexcrichton alexcrichton requested review from alexcrichton and removed request for dicej June 1, 2026 14:39

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've got a question about the conditional drop, but otherwise this all looks reasonable to me

Comment on lines +2585 to +2599
// A host consumer (e.g. one registered via `StreamReader::pipe`)
// is only driven on guest writes; it is never re-polled to
// observe the guest dropping the write end. Reclaim the transmit
// (state + both handles) so it does not leak. Unlike
// `host_drop_reader`, the write end is not forced to `Dropped`
// earlier in this function, so only finalize once the writer is
// actually gone -- otherwise we would discard a still-live host
// writer. The consumer is dropped along with the matched value.
if matches!(
self.concurrent_state_mut().get_mut(transmit_id)?.write,
WriteState::Dropped
) {
log::trace!("host_drop_writer: finalize host consumer, delete {transmit_id:?}");
self.concurrent_state_mut().delete_transmit(transmit_id)?;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand why this is conditional, can you explain this a bit more? For example if the write state is not dropped, meaning that this isn't executed, then I believe it's only possible to get here by flowing through the WriteState::HostReady, but that means that the host actually owns the writer. This function is only called when the guest drops the writer, so I'm not sure how that's possible.

Are there tests for this conditional branch? Or if this branch goes away and the delete_transmit unconditionally happens, what bad would happen? (e.g. could a test be written to exercise that?)

@pchickey

pchickey commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gfx The BA asks that all AI agent interactions by our contributors are mediated by the contributor, not sent to the BA repos directly https://github.com/bytecodealliance/governance/blob/main/AI_TOOL_POLICY.md#details - in the future please do not request the copilot agent to leave reviews on PRs, if you want to use copilot to review your work you can do that prior to submission.

@gfx

gfx commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for review. Will handle the review later, but I'm marking this PR as draft for now.

@gfx gfx marked this pull request as draft June 8, 2026 00:23
pull Bot pushed a commit to eduardomourar/wasmtime that referenced this pull request Jun 16, 2026
…nce#13659)

* cm-async: Fix leaking transmit state with "half hosts"

This commit fixes issues in the implementation of futures and streams
for component-model-async where when a guest dropped its half of a
future/stream connection, and the other half was owned by the host, then
the host-owned half would be leaked within the store.

This is similar in spirit to bytecodealliance#13515 but has a smaller test and
additionally takes into account some review comments.

Closes bytecodealliance#13515

* Fix clippy
alexcrichton added a commit that referenced this pull request Jun 16, 2026
* validate content-length is decimal digits in wasi-http (#13636)

* Fix panic with component-model-threading (#13651)

This commit fixes an `assert!` tripping when using
component-model-threads combined with `thread.yield cancellable`.

* Fix a multiplication overflow on 32-bit platforms (#13653)

This commit updates the bounds checks for async stream reads/writes to
do a checked multiplication of the size-by-count instead of unchecked
multiplication. This couldn't ever overflow on a 64-bit platform, but it
can overflow on 32-bit platforms. The added test here fails on 32-bit
platforms before this commit, for example.

* Adjust stack alignment in nostd fibers (#13657)

This commit adjust how fiber stacks are allocated/aligned to 16 byte
boundaries in the nostd implementation of fibers. This was found where
testing as-is with MIRI flags UB where an unaligned pointer write
happens. This occurs because while the base pointer of a stack is
aligned it means that the size of the stack is not aligned, producing an
unaligned "top addr".

The fix in this commit is to change the element type of the stack to a
type that naturally has an alignment of 16, so unaligned allocations
aren't possible in the first place. Unfortunately this can't be added to
CI, however, because the nostd implementation of fibers requires the
'stackswitch' module which Miri does not support, and the Miri
implementation of fibers is backed by threads and doesn't do any of
this. For now this'll end up just being manually verified as "no Miri
errors until it executes `naked_asm!`".

* Flush stdout on every chunk accepted from the p3 stream (#13654)

* Flush stdout on every chunk accepted from the p3 stream

This is aggresive and it may be more flushing than necessary.

* Address code review comments

- clear pending when finish is true,
- factor out poll_flush

* cm-async: Fix leaking transmit state with "half hosts" (#13659)

* cm-async: Fix leaking transmit state with "half hosts"

This commit fixes issues in the implementation of futures and streams
for component-model-async where when a guest dropped its half of a
future/stream connection, and the other half was owned by the host, then
the host-owned half would be leaked within the store.

This is similar in spirit to #13515 but has a smaller test and
additionally takes into account some review comments.

Closes #13515

* Fix clippy

* cm-async: Clear pending waitable-set on drop (#13663)

This commit updates the `{future,stream}.drop-{readable,writable}` paths
to use `join(None)` to clear out any pending waitable set. This fixes a
number of minor issues related to that sticking around by accident, and
this additionally matches the upstream specification.

* cm-async: Deduplicate/fix future/stream lift/lower (#13664)

This commit removes duplication between the `Instance::guest_transfer`
function as well as the `lift_{future,stream}_to_transmit` and
`lower_transmit_to_{future,stream}` functions. Notably the lift/lower
functions had an additional check for futures which `guest_transfer` did
not contain, and now additionally there's just one definition of
lifting/lowering as opposed to multiple.

* Trap if futures/streams are trasnferred while in a waitable set (#13665)

Adjust behavior to account for WebAssembly/component-model#664

---------

Co-authored-by: netliomax25-code <netliomax25@gmail.com>
Co-authored-by: Tomasz Andrzejak <andreiltd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Component model: host stream/future transmits leak when the guest drops its end while the host consumer/producer is HostReady

4 participants