Skip to content

wasm: gracefully closed datagram writer can wake forever in poll_send_datagram #375

Description

@kixelated

Summary

Session::poll_send_datagram in rs/web-transport-wasm/src/session.rs may never terminate if the datagram writable is ever gracefully closed rather than errored. It would wake repeatedly across polls without making progress and without returning an error.

This is pre-existing and not introduced by #374 — see "Relationship to #374" below.

Mechanism

Per the Streams spec, a closed (not errored) writable fulfills ready and reports desiredSize == 0:

Against such a writer, each call to poll_send_datagram does:

  1. poll_settled at the top drains the fulfilled ready future from the shared send_datagram slot and returns Ok(()).
  2. The loop reads desiredSize, sees zero, and arms a fresh writer.ready().
  3. That JsFuture returns Pending on its first poll, then fulfills on a microtask and wakes the task.
  4. Back to step 1, forever.

No single invocation spins — the loop terminates within a call because a freshly built JsFuture always polls Pending first. The repetition is across wakes, so it presents as a task that never completes and never errors while burning CPU.

Why the existing closed() guard never prevented this

Before #374 there was a fallback that waited on writer.closed() when ready had fulfilled but capacity was still zero. That arm was unreachable: ready and closed shared the single Op slot that poll_settled drains at the top of every call, so the loop always began with an empty slot, and reaching the arm required a fulfilled ready carried over from a previous call. It never ran, on any code path, which is why it never protected against this.

Note the naive repair makes things worse rather than better: giving ready and closed separate slots activates the arm, and then a clone that loses the capacity race on an open writer parks on closed() — which never fulfills — hanging the send for the life of the session. That is the deadlock pinned by concurrent_datagram_senders in rs/web-transport-wasm/examples/harness.rs.

Suggested fix

Observe writer.closed() through its own operation slot while continuing to resubscribe to the writer's current ready promise. Clean closure then becomes terminal without ever parking an open-writer race loser exclusively on closed(). Both harness cases must keep passing.

Testing gap

closed_datagram_writer_terminates currently closes the session, which errors the writable in Chromium, so ready rejects and the error exits through the existing error arm. That validates the errored path only. A case is needed that exercises a spec-compliant cleanly-closed datagram writable, rather than relying on one browser's current teardown behavior.

Open question worth settling first: whether a WebTransport datagram writable can reach a cleanly-closed state in practice at all, or whether every teardown path errors it. If the latter, this is latent rather than reachable, and the fix is hardening.

Relationship to #374

#374 removes the unreachable closed() fallback and replaces it with continue. It is behavior-neutral on every reachable path — the arm never executed before or after. The wake loop described here exists identically on main today. Found while adversarially reviewing that PR.

(written by Opus 5)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions