Unwritten channel
| Vulnerability potential | None |
| DDoS potential | Medium |
Nothing ever writes to a channel
Impact
A channel is read from, but no goroutine ever sends to it and it is never closed.
The receiving goroutine blocks forever on <-ch. Any goroutine waiting on that
receive is leaked, along with everything it references; if the blocked receive is
on the path to producing output or releasing a resource, that work never
completes. When the receiver is main (or the only remaining runnable
goroutine), the Go runtime detects the global stall and aborts with “all
goroutines are asleep - deadlock!”. When other goroutines remain runnable, there
is no crash — just a silently stuck reader and a resource leak.
Vulnerability potential
This issue is a liveness / denial-of-service concern with no direct memory-safety impact.
- A reader permanently blocked on a never-written channel holds its goroutine, stack, and any captured objects forever. If such readers are created per request/connection, repeated triggering leaks goroutines and memory until the service is starved or OOM-killed.
- A stalled receive in a pipeline blocks the consumer stage and can cascade into a full hang of dependent work, turning one missing producer into a service-wide outage.
Technical details
A receive on a channel completes when either a value is sent or the channel is closed (closing wakes all blocked receivers, which then read the zero value). With no sender and no close, neither event occurs, so the receiver stays parked on the channel’s receive wait queue indefinitely.
Missing close as the usual cause
Idiomatic Go signals “no more values” by closing the channel; a for v := range
ch loop ends only on close. Forgetting to close — or forgetting to start the
producer at all, or the producer returning early on an error path before sending —
leaves the reader hanging. A select whose only viable case reads such a channel
is likewise stuck unless it has a default or a timeout/cancellation case.
Distinguishing from a nil channel
Here the channel is a real, made channel; it simply has no writer. The blocking mechanism is the empty receive queue, not the nil special case, but the observable hang is the same.
Catching the issue
Deadlock detector and timeouts
The runtime’s all-goroutines-asleep detector catches the case where the stuck
reader is the last runnable goroutine. For partial stalls, add a timeout or
cancellation:
select { case v := <-ch: ...; case <-ctx.Done(): ...; case <-time.After(d): ... }
so a missing producer turns into a handled error rather than an infinite wait.
Leak detection and review
Use go.uber.org/goleak in tests and goroutine profiles in production to spot
readers parked in chanrecv. Review rule: every channel has an identified
producer that always either sends the expected values or closes the channel
(typically via defer close(ch) in the producer), guaranteeing every receiver
eventually unblocks.
How to reproduce
Observe a deadlock: the receive blocks forever because nothing sends to or closes the channel.
package main
import "fmt"
func main() {
ch := make(chan int) // made, but no writer and never closed
fmt.Println("receiving...")
v := <-ch // blocks forever; runtime: all goroutines asleep - deadlock
fmt.Println("got", v)
}