Skip to content

fix(eventstream): M9 broker leaks fd + goroutine when a subscriber is evicted mid-publish - #34

Merged
TeoSlayer merged 1 commit into
mainfrom
sec/findings-eventstream
Jul 26, 2026
Merged

fix(eventstream): M9 broker leaks fd + goroutine when a subscriber is evicted mid-publish#34
TeoSlayer merged 1 commit into
mainfrom
sec/findings-eventstream

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Addresses finding M9.

M9 — evicted subscriber leaks its descriptor, goroutine and rate bucket

service.go

When publishWith drops a subscriber after maxConsecutivePublishFailures, it called b.removeSub(s) and nothing else. That subscriber is by definition a peer that is not draining, so:

  • its handleConn goroutine stays parked in ReadEvent on a stream nobody will ever write to — it never reaches the deferred teardown, so the descriptor is held for the lifetime of the daemon;
  • the inner writer goroutine in the publish path (the one the publishWriteTimeout select abandons) stays blocked in write(s, evt) on the same stuck stream, and a fresh one accumulates on each publish until the eviction threshold is reached;
  • b.rate[s] is never deleted, so the rate map grows without bound.

Eviction now performs the full teardown — removeSub + forgetPublisher + Close — and closing the stream is what returns both goroutines through their normal paths.

subscriber.Close now tolerates a nil stream, matching the existing nil handling in remote(); broker unit tests construct subscribers without one.

Tests

zz_evict_teardown_test.go — fails against the pre-fix eviction loop (handleConn still parked after eviction), passes here. Drives a real net.Pipe-backed subscriber that subscribes then goes quiet, fails it past the tolerance, and asserts handleConn returns, the rate bucket is dropped, and the stream is closed.

No wire-format or protocol changes.

🤖 Generated with Claude Code

…ter it

A subscriber dropped after repeated publish failures was removed from
the topic map and nothing else. Such a peer is by definition not
draining, so its handleConn goroutine stayed parked in ReadEvent and any
writer still blocked on the same stream stayed blocked — leaking the
goroutine, the descriptor and the rate-bucket entry for the lifetime of
the daemon. Eviction now also drops the rate bucket and closes the
stream, which returns both goroutines through their normal teardown.

subscriber.Close tolerates a nil stream, matching remote().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@TeoSlayer
TeoSlayer merged commit 55e3ff6 into main Jul 26, 2026
5 checks passed
@TeoSlayer
TeoSlayer deleted the sec/findings-eventstream branch July 26, 2026 14:10
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.

2 participants