Null channel
| Vulnerability potential | Low |
| DDoS potential | Medium |
The channel used in this communication might be null
Impact
In Go a channel variable that was never created with make holds its zero value,
nil. Unlike a nil map or nil pointer, operating on a nil channel does not
panic: a send to a nil channel and a receive from a nil channel both block
forever. A select case whose channel is nil is simply never selected. The
practical effect is a silently stuck goroutine. If the goroutine holds a request,
a connection, or a lock, that resource is leaked indefinitely; if main itself
blocks, the program hangs. Because nothing crashes and no error is returned, the
defect is easy to miss in testing and only manifests as a hang or steadily
growing goroutine count under specific conditions.
Vulnerability potential
This issue is mainly a denial-of-service and liveness concern.
- A goroutine permanently blocked on a nil channel never releases its stack, any held mutexes, or referenced objects. If the nil-channel path is reachable from request handling, an attacker who triggers it repeatedly leaks goroutines and memory until the service degrades or is killed (OOM), a denial of service.
- A hang on a nil channel can deadlock a pipeline stage and stall all upstream producers, amplifying a single stuck path into a full outage. Direct memory-safety impact is negligible because Go is memory-safe here.
Technical details
A channel value is internally a pointer to a runtime hchan structure. The zero
value of any channel type is nil (no hchan). The Go runtime defines the
blocking-forever behaviour deliberately so that nil channels can be used to
disable a select case dynamically (a useful idiom). The danger is the
unintended nil: a struct field channel that was never initialized, a channel
returned from a function on an error path, or a channel zeroed by a var
declaration without a following make.
Why it does not panic
Sends/receives on closed channels panic or return immediately, but the runtime
special-cases nil to park the goroutine on a wait that is never woken. Combined
with select, a nil channel case is unreachable, so a select consisting only
of nil cases (and no default) blocks forever too.
Common sources
Forgetting make(chan T); returning the zero value of a named return on an
error path; resetting a channel field to nil and using it again.
Catching the issue
Static analysis
go vet does not flag nil-channel blocking directly, but staticcheck (SA
checks) and nilness-style analyzers can detect channels used before
initialization. Review rule: every channel field/variable must be initialized
with make before first use unless nil is deliberately used to disable a select
case (and that intent is commented).
Runtime detection
The Go runtime’s deadlock detector prints “all goroutines are asleep -
deadlock!” only when every goroutine is blocked; a single leaked goroutine is
not caught. Instead, monitor runtime.NumGoroutine(), take goroutine profiles
(go tool pprof, /debug/pprof/goroutine), and add timeouts (context,
time.After in select) so a stuck channel op fails loudly instead of hanging.
How to reproduce
Observe that the program prints nothing and the runtime reports a deadlock, because the receive is on a nil channel that is never made.
package main
import "fmt"
func main() {
var ch chan int // nil: never created with make
fmt.Println("waiting...")
<-ch // blocks forever; runtime: all goroutines asleep - deadlock
fmt.Println("unreachable")
}