Skip to content

fix: recover send-on-closed-channel panic in byteChannel sink#8

Merged
myleshorton merged 2 commits into
mainfrom
fisk/fix-closed-channel-panic
Jun 8, 2026
Merged

fix: recover send-on-closed-channel panic in byteChannel sink#8
myleshorton merged 2 commits into
mainfrom
fisk/fix-closed-channel-panic

Conversation

@myleshorton

Copy link
Copy Markdown
Contributor

Problem

byteChannel.UpdateFrom did an unguarded send to a caller-owned channel:

func (s *byteChannel) UpdateFrom(r io.Reader) error {
	b, err := ioutil.ReadAll(r)
	if err != nil { return err }
	s.ch <- b   // sink.go:80 — panics if ch was closed
	return nil
}

A Runner (Start) calls syncOnceUpdateFrom on a ticker. The channel is owned by the caller (ToChannel takes a caller channel), and consumers close it on shutdown / config reload:

  • getlantern/frontedfronted.go:257 (keeps the masquerade list current)
  • getlantern/radiancekindling/dnstt/parser.go:162

When a tick fires the send while/after the consumer has closed the channel, s.ch <- b panics with send on closed channel, which crashes the whole process.

Observed crashing the macOS v9 client (9.1.9) — four Runner goroutines panicked simultaneously on shutdown. User-visible as "beta cannot be used, and can't even submit an in-app report" (a crashed app can't upload the report). Freshdesk #176970 / lantern-forum-cn #1543.

Fix

The sink can't know whether the caller's channel is closed, so make the send close-safe: recover the panic and return it as a sink error. Runner.syncOnce already treats sink failures as recoverable via OnSinkError (keepcurrent.go:128-130) — so a closed channel now degrades to a logged sink error instead of a process crash. No consumer changes required; fixes every consumer at once.

Tests

Added sink_test.go:

  • TestByteChannelClosedDoesNotPanic — send to a closed channel returns an error, no panic (regression).
  • TestByteChannelDelivers — healthy send still delivers unchanged.

go build, go vet, and the full test suite pass.

Notes

  • This was not fixed on main before this PR, and it's cross-platform (keepcurrent + fronted run on every client), so any v9 build could hit the crash whenever fronted/dnstt reloads at the wrong instant — macOS just happened to capture the crash log.
  • Optional follow-up (separate, not required to stop the crash): consumers could Stop() the Runner before close(ch) to coordinate shutdown cleanly.

🤖 Generated with Claude Code

byteChannel.UpdateFrom did an unguarded `s.ch <- b` to a caller-owned
channel. When the consumer closes that channel (shutdown / config reload)
while a keepcurrent Runner is mid-sync on its ticker, the send panics with
"send on closed channel" and crashes the whole process — observed taking
down the macOS v9 client (Freshdesk #176970). Consumers that close the
channel: fronted fronted.go:257 and radiance kindling/dnstt/parser.go:162.

The sink doesn't own the channel (ToChannel takes a caller channel) so it
can't know whether it's been closed. Recover the panic and surface it as a
sink error, which Runner already treats as recoverable via OnSinkError
instead of dying. Adds regression tests.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents a process-crashing send on closed channel panic in the ToChannel sink by making byteChannel.UpdateFrom recover from a closed-channel send and return it as a sink error, and adds regression tests to validate both the closed-channel and normal-delivery behaviors.

Changes:

  • Add a defer/recover guard around the channel send in byteChannel.UpdateFrom to convert the panic into an error.
  • Add unit tests covering closed-channel behavior (no panic, returns error) and successful delivery.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
sink.go Adds panic recovery in the byte-channel sink to avoid crashing when the caller closes the channel.
sink_test.go Adds regression + happy-path tests for byte-channel sink behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sink.go
Comment thread sink_test.go Outdated

@garmr-ulfr garmr-ulfr left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

…t ref

Addresses Copilot review on #8:
- byteChannel.UpdateFrom now recovers *only* the "send on closed channel"
  panic and re-panics anything else, so unrelated/programmer bugs still
  surface instead of being silently turned into a sink error.
- sink_test.go: make the regression comment self-contained (drop the
  internal Freshdesk ticket reference) since this is a public repo.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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.

3 participants