fix: reclaim host stream/future transmits when the guest drops its end#13515
fix: reclaim host stream/future transmits when the guest drops its end#13515gfx wants to merge 1 commit into
Conversation
7025008 to
ac7f447
Compare
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
ac7f447 to
89cc10d
Compare
alexcrichton
left a comment
There was a problem hiding this comment.
Thanks! I've got a question about the conditional drop, but otherwise this all looks reasonable to me
| // 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)?; | ||
| } |
There was a problem hiding this comment.
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?)
@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. |
|
Thank you for review. Will handle the review later, but I'm marking this PR as draft for now. |
…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
* 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>
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 theTransmitStateand both handles are freed.Fix
crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs— twoHostReadyarms were no-ops; both now calldelete_transmit(which also drops the host producer/consumer):host_drop_reader(guest dropped the read end → host producer): the read end is alreadyDropped, so finalize unconditionally.host_drop_writer(guest dropped the write end → host consumer): the write end isn't forcedDroppedhere, so finalize only once the writer is actually gone.Tests
Two regression tests in
component-async-tests, driving only the publicStreamReader::pipe/FutureReader::newAPIs:streams::async_host_consumer_drop(+ a minimalhost-consumer-drop-guesttest program) — covershost_drop_writer.streams::async_host_producer_drop(reuses the existingclosed-streamsguest) — covershost_drop_reader.Both fail on
main(leftover concurrent-state entries viaassert_concurrent_state_empty) and pass with this change.Verification
cargo test -p component-async-tests --test test_all→ 82 passed, 0 failed.