gvisor: LinkEndpointFilter.Attach never forwards nil, so GVisor.Close leaks all inbound dispatcher goroutines and fds
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 237
- Forks
- 228
- PR merge metrics
- No merged PRs in 30d
Description
Summary
GVisor.Close() stops the inbound machinery via t.endpoint.Attach(nil). The fdbased
endpoint's teardown only runs on a literal nil dispatcher:
// internal/fdbased_darwin/endpoint.go (Attach)
if dispatcher == nil && e.dispatcher != nil {
for _, dispatcher := range e.inboundDispatchers {
dispatcher.Stop()
}
...
e.Wait()
return
}
But t.endpoint is a LinkEndpointFilter, whose Attach wraps the dispatcher
unconditionally:
// stack_gvisor_filter.go
func (w *LinkEndpointFilter) Attach(dispatcher stack.NetworkDispatcher) {
w.LinkEndpoint.Attach(&networkDispatcherFilter{dispatcher, w.BroadcastAddress, w.Writer})
}
So on Close the underlying endpoint receives a non-nil networkDispatcherFilter{nil, …},
neither branch of Attach matches, and Stop()/Wait() never run. Every GVisor.Close
permanently leaks:
- the recvMMsg dispatcher goroutine, parked in kevent (pinning an OS thread). The tun fd is
closed elsewhere, and closing an fd silently removes its kqueue registrations — so the
goroutine waits forever on a stop-fd that will never be written; - the
processor.startgoroutines (ProcessorsPerChannel); - the qdisc fifo
dispatchLoopgoroutine; - the stopfd pipe fds (plus the kqueue fd).
Affects any process that recreates the gvisor stack in-place (config reload / reconnect in a
library embedding). Long-run numbers from such a process on macOS: ~15 goroutines + several fds
leaked per stack recreation; after 18 days of periodic reconnects, ~730 leaked goroutines, ~70
kevent-pinned OS threads, hundreds of fds. Verified on v0.8.9 and current dev (the
ForwardDispatcher refactor keeps the unconditional wrap). The filter is cross-platform, so
the Linux fdbased endpoint should be affected the same way; verified on darwin.
Reproduction (in-process, darwin, -tags with_gvisor)
before := runtime.NumGoroutine()
for i := 0; i < 10; i++ {
tunIf, _ := tun.New(tunOpts) // any working tun options
stack, _ := tun.NewStack("gvisor", tun.StackOptions{Tun: tunIf, TunOptions: tunOpts, ...})
stack.Start()
stack.Close()
tunIf.Close()
}
time.Sleep(time.Second)
after := runtime.NumGoroutine() // grows by ~(2 + ProcessorsPerChannel) per iteration
A goroutine profile after the loop shows the piles at
fdbased_darwin.(*processor).start, qdisc/fifo.(*queueDispatcher).dispatchLoop, and
rawfile_darwin.BlockingRecvMMsgUntilStopped (in kevent).
Fix
Forward nil as nil:
func (w *LinkEndpointFilter) Attach(dispatcher stack.NetworkDispatcher) {
if dispatcher == nil {
w.LinkEndpoint.Attach(nil)
return
}
w.LinkEndpoint.Attach(&networkDispatcherFilter{dispatcher, w.BroadcastAddress, w.Writer})
}
Running this one-liner in production: goroutine and fd counts are flat across stack
recreations since.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with LinkEndpointFilter.Attach in stack_gvisor_filter.go and compare its dispatcher wrapping with the fdbased endpoint's nil-dispatcher teardown. Run the darwin reproduction with -tags with_gvisor, then verify that repeated stack creation and Close calls no longer increase goroutine or file-descriptor counts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100