Skip to content

sec: drop BroadcastMu to remove head-of-line blocking on slow subscribers - #22

Merged
EthanY33 merged 1 commit into
mainfrom
sec/drop-broadcast-mutex
May 7, 2026
Merged

sec: drop BroadcastMu to remove head-of-line blocking on slow subscribers#22
EthanY33 merged 1 commit into
mainfrom
sec/drop-broadcast-mutex

Conversation

@EthanY33

@EthanY33 EthanY33 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #12.

Broadcast held BroadcastMu for the entire iteration over the subscriber set. Under PolicyDisconnect's 10s writeDeadline, a single TCP-paused subscriber wedged every publisher to that channel for up to 10 seconds. An attacker who could open one socket and stop ACKing-down-the-stack effectively held the channel hostage.

Drop the mutex. Per-subscriber FIFO ordering — the only guarantee the protocol promises — is preserved by Go's chan-send ordering inside each Conn's send chan: messages arrive at the subscriber in the order Send was called. Cross-subscriber ordering ("every subscriber sees publish A before publish B") was never a documented contract.

Concurrent broadcasts can now interleave their Send calls. Each subscriber still sees its own messages FIFO.

What changed

  • internal/registry/registry.go: removed Channel.BroadcastMu field.
  • internal/hub/channel.go: Broadcast no longer locks BroadcastMu. Updated comment to explain the ordering guarantee.

internal/* is not part of any external API; the field rename is safe.

Test plan

  • go build ./... clean.
  • go test ./internal/... passes (pre-existing CGO sqlite failure is environmental, unrelated).
  • Stress: 100 publishers concurrently spam channel "X". Subscriber consumes all messages. With the mutex, throughput was bounded by the slowest publisher. After this change, total throughput scales with publisher count up to subscriber's chan capacity.
  • Adversarial: one subscriber TCP-stalls. Other subscribers still receive new publishes within tens of ms (was: blocked for up to 10s).

Conflict notes

Touches internal/hub/channel.go and internal/registry/registry.go. PR #20 (channel GC) also touches both files, with a new ErrChannelDeleted error and a Deleted atomic.Bool field. Conflicts are textually adjacent but logically independent — both can coexist. Whichever lands second will need a small textual rebase.

…bers

Closes #12.

Broadcast held BroadcastMu for the entire iteration over the
subscriber set. Under PolicyDisconnect's 10s writeDeadline, a single
TCP-paused subscriber wedged every publisher to that channel for
the duration. An attacker who could open a socket and stop
ACKing-down-the-stack effectively held the channel hostage.

Drop the mutex. Per-subscriber ordering — the only guarantee
wirefan's protocol promises — is preserved by Go's channel-send
ordering inside each Conn's send chan: messages arrive at the
subscriber in the order Send was called. Cross-subscriber ordering
("every subscriber sees publish A before publish B") was never a
documented contract.

Concurrent broadcasts can now interleave their Send calls; each
subscriber still sees its own messages FIFO.

The Channel.BroadcastMu field is removed entirely (internal/* is
not part of any external API).
@EthanY33
EthanY33 merged commit 22fd26d into main May 7, 2026
2 checks passed
@EthanY33
EthanY33 deleted the sec/drop-broadcast-mutex branch May 7, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Low] SQLite DSN path validation TBD before --db-path lands

1 participant