Files
tailscale/control/controlclient/auto_test.go
Brad Fitzpatrick d64aaffc06 control/controlclient: fix map context race
Capture Auto.mapCtx while holding Auto.mu before using it for
incremental map update forwarding. Pause and restart paths can replace
the context under the same mutex, so using it after unlocking races
with those writers.

Add a race regression test for the UserProfiles path that repeatedly
cancels the map context while incremental profile updates are
forwarded.

Fixes #19953

Change-Id: Icc55c4a0dffbc16d6507a2b446b3909d4d0a0278
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-06-01 13:44:19 -07:00

67 lines
1.3 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlclient
import (
"context"
"sync"
"testing"
"time"
"tailscale.com/tailcfg"
)
type userProfileUpdateObserver struct{}
func (userProfileUpdateObserver) SetControlClientStatus(Client, Status) {}
func (userProfileUpdateObserver) UpdateUserProfiles(map[tailcfg.UserID]tailcfg.UserProfileView) bool {
return true
}
func TestMapRoutineStateUpdateUserProfilesConcurrentCancelMapCtx(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
c := &Auto{
logf: func(string, ...any) {},
observer: userProfileUpdateObserver{},
mapCtx: ctx,
mapCancel: cancel,
loggedIn: true,
inMapPoll: true,
}
mrs := mapRoutineState{c: c}
start := make(chan struct{})
var wg sync.WaitGroup
for range 4 {
wg.Go(func() {
<-start
for range 2000 {
c.mu.Lock()
c.cancelMapCtxLocked()
c.mu.Unlock()
}
})
}
for range 4 {
wg.Go(func() {
<-start
for range 2000 {
mrs.UpdateUserProfiles(nil)
}
})
}
close(start)
wg.Wait()
waitCtx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
if err := c.observerQueue.Wait(waitCtx); err != nil {
t.Fatal(err)
}
c.observerQueue.Shutdown()
c.mapCancel()
}