SagerNet / SagerNet/sing-tun

gvisor: LinkEndpointFilter.Attach never forwards nil, so GVisor.Close leaks all inbound dispatcher goroutines and fds

Open Beginner friendly
#85 1 comment 0 reactions 0 assignees View on GitHub

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.start goroutines (ProcessorsPerChannel);
  • the qdisc fifo dispatchLoop goroutine;
  • 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.