Skip to content

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

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

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
Copy link
Copy Markdown
Contributor

@gfx gfx commented May 30, 2026

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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

@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
Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

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

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

3 participants