Skip to content

mpsc Sender.close() doesn't wake blocked receivers when buffer has data + null sentinel breaks typed channels #3

@consigcody94

Description

@consigcody94

Summary

Found two concurrency bugs in the mpsc channel implementation via code audit.

Bug 1: Sender.close() doesn't wake blocked receivers when buffer has data (HIGH)

File: src/sync/mpsc.ts, lines 201-208

`Sender.close()` only calls `wakeRecvWaitersWithNull()` when `senderCount === 0 && buffer.isEmpty()`. If the last sender closes while there is still data in the buffer AND a receiver is waiting, the receiver will never be woken. It remains blocked forever.

Fix: After decrementing `senderCount`, if it reaches 0, wake all receivers regardless of buffer state. Receivers will drain the buffer first and then get `null`.

Same issue exists in `UnboundedSender.close()` at lines 386-390.

Bug 2: AsyncIterator uses null as termination sentinel (HIGH)

File: src/sync/mpsc.ts, lines 321-327

The `[Symbol.asyncIterator]` uses `if (value === null) return;` to detect channel closure. But `null` is a valid value for `Receiver` or `Receiver<T | null>`. Sending `null` terminates the iterator prematurely, losing all subsequent messages. Same in `UnboundedReceiver` at lines 448-454.

Tokio avoids this with `Option`. Consider using a wrapper object or documenting the limitation.

Bug 3: select() swallows errors from losing branches (MEDIUM)

File: src/sync/select.ts, lines 24-29

When `Promise.race` picks the winner, losing branches that already rejected become unhandled promise rejections.

Fix: Add `.catch(() => {})` to losing promises after the race completes.

Bug 4: Watch changed() rejects before checking unseen values (MEDIUM)

File: src/sync/watch.ts, lines 132-134

`changed()` checks `senderClosed` before checking `version !== lastSeenVersion`. If the sender sends a final value then closes, a receiver that hasn't seen the latest value gets `RecvError` instead of being told the value changed. Tokio returns Ok if there is an unseen value, even if the sender closed.

Fix: Swap the order: check version mismatch first, then sender closed.

Found via code audit.

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