Operation on closed channel
| Vulnerability potential | Low |
| DDoS potential | Medium |
Operation on closed channel
Impact
Certain operations on a closed Go channel cause a runtime panic that, if not recovered, terminates the entire program:
- Sending to a closed channel (
ch <- v) panics with “send on closed channel”. - Closing an already-closed channel (
close(ch)) panics with “close of closed channel”. - Closing a nil channel panics with “close of nil channel”.
Receiving from a closed channel does not panic — it returns immediately with
the element type’s zero value and ok == false — which is by design, but a
receiver that ignores the ok flag will silently process an endless stream of
zero values (e.g. a busy for range-like loop reading zeros), spinning the CPU.
A single unrecovered panic on one goroutine crashes the whole process, so a
mis-timed close can take a server down.
Vulnerability potential
This issue is principally a denial-of-service concern.
- If the close/send ordering depends on external timing or input (e.g. a close
driven by client disconnect racing with a send driven by a request), an
attacker can provoke the “send on closed channel” panic and crash the process,
denying service. Without
recover, one panic ends the program. - A receiver that ignores
okafter close can be driven into a tight loop consuming zero values, wasting CPU. The memory-safety impact is otherwise low since Go remains memory-safe.
Technical details
A channel has a closed flag in its runtime hchan. close sets it and wakes
all blocked senders and receivers. The runtime then enforces:
- send checks
closedand panics if set; closechecksclosed/nil and panics on double-close or nil;- receive on a closed, drained channel returns
zero, false.
Ownership discipline
The idiomatic rule is that the sole sender owns the close: only the goroutine
(or coordinator) responsible for producing values closes the channel, and exactly
once. Multiple senders must never close directly; instead use a separate done
signal, a sync.Once around the close, or a dedicated closer that waits for all
senders to finish (e.g. via sync.WaitGroup).
Receive idiom
Use v, ok := <-ch or for v := range ch (which stops on close) so closure is
handled explicitly rather than yielding a stream of zero values.
Catching the issue
Race detector
go test -race / -race builds catch the data race between a close and a
concurrent send/close that underlies most of these panics, reporting both
stacks before the panic occurs nondeterministically in production.
Static analysis and review
staticcheck flags some closed-channel misuses. Review rules: a channel is
closed in exactly one place, by its owner, after all sends are done; senders
never close; nil channels are never closed; receivers always test ok.
Defensive recovery
Where a send/close race cannot be fully designed out, wrapping the operation so a panic is recovered (and logged) prevents a whole-process crash, though fixing the ownership model is preferable.
How to reproduce
Observe a panic “send on closed channel” that crashes the program.
package main
func main() {
ch := make(chan int, 1)
close(ch)
ch <- 1 // panic: send on closed channel
}