net/netmon: fix goroutine leak in winMon if the monitor is never started

When the portable Monitor creates a winMon via newOSMon, we register
address and route change callbacks with Windows. Once a callback is hit,
it starts a goroutine that attempts to send the event into messagec and returns.
The newly started goroutine then blocks until it can send to the channel.
However, if the monitor is never started and winMon.Receive is never called,
the goroutines remain indefinitely blocked, leading to goroutine leaks and
significant memory consumption in the tailscaled service process on Windows.
Unlike the tailscaled subprocess, the service process creates but never starts
a Monitor.

This PR adds a check within the callbacks to confirm the monitor's active status,
and exits immediately if the monitor hasn't started.

Updates #9864

Signed-off-by: Nick Khyl <nickk@tailscale.com>
pull/10670/head
Nick Khyl 11 months ago committed by Nick Khyl
parent 2e956713de
commit c9836b454d

@ -220,6 +220,13 @@ func (m *Monitor) RegisterRuleDeleteCallback(callback RuleDeleteCallback) (unreg
} }
} }
// isActive reports whether this monitor has been started and not yet closed.
func (m *Monitor) isActive() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.started && !m.closed
}
// Start starts the monitor. // Start starts the monitor.
// A monitor can only be started & closed once. // A monitor can only be started & closed once.
func (m *Monitor) Start() { func (m *Monitor) Start() {

@ -29,6 +29,7 @@ type winMon struct {
logf logger.Logf logf logger.Logf
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
isActive func() bool
messagec chan eventMessage messagec chan eventMessage
addressChangeCallback *winipcfg.UnicastAddressChangeCallback addressChangeCallback *winipcfg.UnicastAddressChangeCallback
routeChangeCallback *winipcfg.RouteChangeCallback routeChangeCallback *winipcfg.RouteChangeCallback
@ -44,9 +45,10 @@ type winMon struct {
noDeadlockTicker *time.Ticker noDeadlockTicker *time.Ticker
} }
func newOSMon(logf logger.Logf, _ *Monitor) (osMon, error) { func newOSMon(logf logger.Logf, pm *Monitor) (osMon, error) {
m := &winMon{ m := &winMon{
logf: logf, logf: logf,
isActive: pm.isActive,
messagec: make(chan eventMessage, 1), messagec: make(chan eventMessage, 1),
noDeadlockTicker: time.NewTicker(5000 * time.Hour), // arbitrary noDeadlockTicker: time.NewTicker(5000 * time.Hour), // arbitrary
} }
@ -130,6 +132,16 @@ func (m *winMon) Receive() (message, error) {
// unicastAddressChanged is the callback we register with Windows to call when unicast address changes. // unicastAddressChanged is the callback we register with Windows to call when unicast address changes.
func (m *winMon) unicastAddressChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibUnicastIPAddressRow) { func (m *winMon) unicastAddressChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibUnicastIPAddressRow) {
if !m.isActive() {
// Avoid starting a goroutine that sends events to messagec,
// or sending messages to messagec directly, if the monitor
// hasn't started and Receive is not yet reading from messagec.
//
// Doing so can lead to goroutine leaks or deadlocks, especially
// if the monitor is never started.
return
}
what := "addr" what := "addr"
if ip := row.Address.Addr(); ip.IsValid() && tsaddr.IsTailscaleIP(ip.Unmap()) { if ip := row.Address.Addr(); ip.IsValid() && tsaddr.IsTailscaleIP(ip.Unmap()) {
what = "tsaddr" what = "tsaddr"
@ -141,6 +153,16 @@ func (m *winMon) unicastAddressChanged(_ winipcfg.MibNotificationType, row *wini
// routeChanged is the callback we register with Windows to call when route changes. // routeChanged is the callback we register with Windows to call when route changes.
func (m *winMon) routeChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibIPforwardRow2) { func (m *winMon) routeChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibIPforwardRow2) {
if !m.isActive() {
// Avoid starting a goroutine that sends events to messagec,
// or sending messages to messagec directly, if the monitor
// hasn't started and Receive is not yet reading from messagec.
//
// Doing so can lead to goroutine leaks or deadlocks, especially
// if the monitor is never started.
return
}
what := "route" what := "route"
ip := row.DestinationPrefix.Prefix().Addr().Unmap() ip := row.DestinationPrefix.Prefix().Addr().Unmap()
if ip.IsValid() && tsaddr.IsTailscaleIP(ip) { if ip.IsValid() && tsaddr.IsTailscaleIP(ip) {

Loading…
Cancel
Save