syncs: add means of declare locking assumptions for debug mode validation

Updates #17852

Change-Id: I42a64a990dcc8f708fa23a516a40731a19967aba
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/18076/head
Brad Fitzpatrick 2 weeks ago committed by Brad Fitzpatrick
parent 3f9f0ed93c
commit 74ed589042

@ -876,6 +876,7 @@ func (b *LocalBackend) initPrefsFromConfig(conf *conffile.Config) error {
} }
func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config) { func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config) {
syncs.RequiresMutex(&b.mu)
if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) { if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) {
return return
} }
@ -894,6 +895,7 @@ func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config)
} }
func (b *LocalBackend) setStateLocked(state ipn.State) { func (b *LocalBackend) setStateLocked(state ipn.State) {
syncs.RequiresMutex(&b.mu)
if b.state == state { if b.state == state {
return return
} }
@ -906,6 +908,7 @@ func (b *LocalBackend) setStateLocked(state ipn.State) {
// setConfigLocked uses the provided config to update the backend's prefs // setConfigLocked uses the provided config to update the backend's prefs
// and other state. // and other state.
func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error { func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
syncs.RequiresMutex(&b.mu)
p := b.pm.CurrentPrefs().AsStruct() p := b.pm.CurrentPrefs().AsStruct()
mp, err := conf.Parsed.ToPrefs() mp, err := conf.Parsed.ToPrefs()
if err != nil { if err != nil {
@ -927,6 +930,7 @@ var assumeNetworkUpdateForTest = envknob.RegisterBool("TS_ASSUME_NETWORK_UP_FOR_
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) pauseOrResumeControlClientLocked() { func (b *LocalBackend) pauseOrResumeControlClientLocked() {
syncs.RequiresMutex(&b.mu)
if b.cc == nil { if b.cc == nil {
return return
} }
@ -1204,6 +1208,7 @@ func (b *LocalBackend) Prefs() ipn.PrefsView {
} }
func (b *LocalBackend) sanitizedPrefsLocked() ipn.PrefsView { func (b *LocalBackend) sanitizedPrefsLocked() ipn.PrefsView {
syncs.RequiresMutex(&b.mu)
return stripKeysFromPrefs(b.pm.CurrentPrefs()) return stripKeysFromPrefs(b.pm.CurrentPrefs())
} }
@ -1335,6 +1340,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
} }
func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
syncs.RequiresMutex(&b.mu)
cn := b.currentNode() cn := b.currentNode()
nm := cn.NetMap() nm := cn.NetMap()
if nm == nil { if nm == nil {
@ -1873,6 +1879,8 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
if !buildfeatures.HasSystemPolicy { if !buildfeatures.HasSystemPolicy {
return false return false
} }
syncs.RequiresMutex(&b.mu)
if controlURL, err := b.polc.GetString(pkey.ControlURL, prefs.ControlURL); err == nil && prefs.ControlURL != controlURL { if controlURL, err := b.polc.GetString(pkey.ControlURL, prefs.ControlURL); err == nil && prefs.ControlURL != controlURL {
prefs.ControlURL = controlURL prefs.ControlURL = controlURL
anyChange = true anyChange = true
@ -1941,6 +1949,8 @@ func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange
if !buildfeatures.HasUseExitNode { if !buildfeatures.HasUseExitNode {
return false return false
} }
syncs.RequiresMutex(&b.mu)
if exitNodeIDStr, _ := b.polc.GetString(pkey.ExitNodeID, ""); exitNodeIDStr != "" { if exitNodeIDStr, _ := b.polc.GetString(pkey.ExitNodeID, ""); exitNodeIDStr != "" {
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr) exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
@ -2182,6 +2192,8 @@ func (b *LocalBackend) resolveAutoExitNodeLocked(prefs *ipn.Prefs) (prefsChanged
if !buildfeatures.HasUseExitNode { if !buildfeatures.HasUseExitNode {
return false return false
} }
syncs.RequiresMutex(&b.mu)
// As of 2025-07-08, the only supported auto exit node expression is [ipn.AnyExitNode]. // As of 2025-07-08, the only supported auto exit node expression is [ipn.AnyExitNode].
// //
// However, to maintain forward compatibility with future auto exit node expressions, // However, to maintain forward compatibility with future auto exit node expressions,
@ -2295,6 +2307,8 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) setWgengineStatusLocked(s *wgengine.Status) { func (b *LocalBackend) setWgengineStatusLocked(s *wgengine.Status) {
syncs.RequiresMutex(&b.mu)
es := b.parseWgStatusLocked(s) es := b.parseWgStatusLocked(s)
cc := b.cc cc := b.cc
@ -4312,6 +4326,7 @@ func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ip
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn.PrefsView, mp *ipn.MaskedPrefs) error { func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn.PrefsView, mp *ipn.MaskedPrefs) error {
syncs.RequiresMutex(&b.mu)
var errs []error var errs []error
if mp.RunSSHSet && mp.RunSSH && !envknob.CanSSHD() { if mp.RunSSHSet && mp.RunSSH && !envknob.CanSSHD() {
@ -4362,6 +4377,7 @@ func (b *LocalBackend) checkEditPrefsAccessLocked(actor ipnauth.Actor, prefs ipn
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) changeDisablesExitNodeLocked(prefs ipn.PrefsView, change *ipn.MaskedPrefs) bool { func (b *LocalBackend) changeDisablesExitNodeLocked(prefs ipn.PrefsView, change *ipn.MaskedPrefs) bool {
syncs.RequiresMutex(&b.mu)
if !buildfeatures.HasUseExitNode { if !buildfeatures.HasUseExitNode {
return false return false
} }
@ -4403,6 +4419,7 @@ func (b *LocalBackend) changeDisablesExitNodeLocked(prefs ipn.PrefsView, change
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) adjustEditPrefsLocked(prefs ipn.PrefsView, mp *ipn.MaskedPrefs) { func (b *LocalBackend) adjustEditPrefsLocked(prefs ipn.PrefsView, mp *ipn.MaskedPrefs) {
syncs.RequiresMutex(&b.mu)
// Zeroing the ExitNodeID via localAPI must also zero the prior exit node. // Zeroing the ExitNodeID via localAPI must also zero the prior exit node.
if mp.ExitNodeIDSet && mp.ExitNodeID == "" && !mp.InternalExitNodePriorSet { if mp.ExitNodeIDSet && mp.ExitNodeID == "" && !mp.InternalExitNodePriorSet {
mp.InternalExitNodePrior = "" mp.InternalExitNodePrior = ""
@ -4480,6 +4497,7 @@ func (b *LocalBackend) onEditPrefsLocked(_ ipnauth.Actor, mp *ipn.MaskedPrefs, o
// startReconnectTimerLocked sets a timer to automatically set WantRunning to true // startReconnectTimerLocked sets a timer to automatically set WantRunning to true
// after the specified duration. // after the specified duration.
func (b *LocalBackend) startReconnectTimerLocked(d time.Duration) { func (b *LocalBackend) startReconnectTimerLocked(d time.Duration) {
syncs.RequiresMutex(&b.mu)
if b.reconnectTimer != nil { if b.reconnectTimer != nil {
// Stop may return false if the timer has already fired, // Stop may return false if the timer has already fired,
// and the function has been called in its own goroutine, // and the function has been called in its own goroutine,
@ -4522,11 +4540,13 @@ func (b *LocalBackend) startReconnectTimerLocked(d time.Duration) {
} }
func (b *LocalBackend) resetAlwaysOnOverrideLocked() { func (b *LocalBackend) resetAlwaysOnOverrideLocked() {
syncs.RequiresMutex(&b.mu)
b.overrideAlwaysOn = false b.overrideAlwaysOn = false
b.stopReconnectTimerLocked() b.stopReconnectTimerLocked()
} }
func (b *LocalBackend) stopReconnectTimerLocked() { func (b *LocalBackend) stopReconnectTimerLocked() {
syncs.RequiresMutex(&b.mu)
if b.reconnectTimer != nil { if b.reconnectTimer != nil {
// Stop may return false if the timer has already fired, // Stop may return false if the timer has already fired,
// and the function has been called in its own goroutine, // and the function has been called in its own goroutine,
@ -4542,6 +4562,7 @@ func (b *LocalBackend) stopReconnectTimerLocked() {
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) editPrefsLocked(actor ipnauth.Actor, mp *ipn.MaskedPrefs) (ipn.PrefsView, error) { func (b *LocalBackend) editPrefsLocked(actor ipnauth.Actor, mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
syncs.RequiresMutex(&b.mu)
p0 := b.pm.CurrentPrefs() p0 := b.pm.CurrentPrefs()
// Check if the changes in mp are allowed. // Check if the changes in mp are allowed.
@ -5660,6 +5681,7 @@ func (b *LocalBackend) enterStateLocked(newState ipn.State) {
} }
func (b *LocalBackend) hasNodeKeyLocked() bool { func (b *LocalBackend) hasNodeKeyLocked() bool {
syncs.RequiresMutex(&b.mu)
// we can't use b.Prefs(), because it strips the keys, oops! // we can't use b.Prefs(), because it strips the keys, oops!
p := b.pm.CurrentPrefs() p := b.pm.CurrentPrefs()
return p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() return p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero()
@ -5680,9 +5702,11 @@ func (b *LocalBackend) NodeKey() key.NodePublic {
// //
// b.mu must be held // b.mu must be held
func (b *LocalBackend) nextStateLocked() ipn.State { func (b *LocalBackend) nextStateLocked() ipn.State {
syncs.RequiresMutex(&b.mu)
if b.health.IsUnhealthy(ipn.StateStoreHealth) { if b.health.IsUnhealthy(ipn.StateStoreHealth) {
return ipn.NoState return ipn.NoState
} }
var ( var (
cc = b.cc cc = b.cc
cn = b.currentNode() cn = b.currentNode()
@ -5758,6 +5782,8 @@ func (b *LocalBackend) nextStateLocked() ipn.State {
// //
// requires b.mu to be held. // requires b.mu to be held.
func (b *LocalBackend) stateMachineLocked() { func (b *LocalBackend) stateMachineLocked() {
syncs.RequiresMutex(&b.mu)
b.enterStateLocked(b.nextStateLocked()) b.enterStateLocked(b.nextStateLocked())
} }
@ -5767,6 +5793,7 @@ func (b *LocalBackend) stateMachineLocked() {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) stopEngineAndWaitLocked() { func (b *LocalBackend) stopEngineAndWaitLocked() {
syncs.RequiresMutex(&b.mu)
b.logf("stopEngineAndWait...") b.logf("stopEngineAndWait...")
st, _ := b.e.ResetAndStop() // TODO: what should we do if this returns an error? st, _ := b.e.ResetAndStop() // TODO: what should we do if this returns an error?
b.setWgengineStatusLocked(st) b.setWgengineStatusLocked(st)
@ -5787,6 +5814,7 @@ func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
// returned value is non-nil, the caller must call Shutdown on it after // returned value is non-nil, the caller must call Shutdown on it after
// releasing b.mu. // releasing b.mu.
func (b *LocalBackend) resetControlClientLocked() controlclient.Client { func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
syncs.RequiresMutex(&b.mu)
if b.cc == nil { if b.cc == nil {
return nil return nil
} }
@ -5813,6 +5841,8 @@ func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
// resetAuthURLLocked resets authURL, canceling any pending interactive login. // resetAuthURLLocked resets authURL, canceling any pending interactive login.
func (b *LocalBackend) resetAuthURLLocked() { func (b *LocalBackend) resetAuthURLLocked() {
syncs.RequiresMutex(&b.mu)
b.authURL = "" b.authURL = ""
b.authURLTime = time.Time{} b.authURLTime = time.Time{}
b.authActor = nil b.authActor = nil
@ -5842,6 +5872,8 @@ func (b *LocalBackend) ShouldExposeRemoteWebClient() bool {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) setWebClientAtomicBoolLocked(nm *netmap.NetworkMap) { func (b *LocalBackend) setWebClientAtomicBoolLocked(nm *netmap.NetworkMap) {
syncs.RequiresMutex(&b.mu)
shouldRun := !nm.HasCap(tailcfg.NodeAttrDisableWebClient) shouldRun := !nm.HasCap(tailcfg.NodeAttrDisableWebClient)
wasRunning := b.webClientAtomicBool.Swap(shouldRun) wasRunning := b.webClientAtomicBool.Swap(shouldRun)
if wasRunning && !shouldRun { if wasRunning && !shouldRun {
@ -5854,6 +5886,8 @@ func (b *LocalBackend) setWebClientAtomicBoolLocked(nm *netmap.NetworkMap) {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) setExposeRemoteWebClientAtomicBoolLocked(prefs ipn.PrefsView) { func (b *LocalBackend) setExposeRemoteWebClientAtomicBoolLocked(prefs ipn.PrefsView) {
syncs.RequiresMutex(&b.mu)
if !buildfeatures.HasWebClient { if !buildfeatures.HasWebClient {
return return
} }
@ -5982,6 +6016,8 @@ func (b *LocalBackend) RefreshExitNode() {
// refreshExitNodeLocked is like RefreshExitNode but requires b.mu be held. // refreshExitNodeLocked is like RefreshExitNode but requires b.mu be held.
func (b *LocalBackend) refreshExitNodeLocked() { func (b *LocalBackend) refreshExitNodeLocked() {
syncs.RequiresMutex(&b.mu)
if b.resolveExitNodeLocked() { if b.resolveExitNodeLocked() {
b.authReconfigLocked() b.authReconfigLocked()
} }
@ -5997,6 +6033,8 @@ func (b *LocalBackend) refreshExitNodeLocked() {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) resolveExitNodeLocked() (changed bool) { func (b *LocalBackend) resolveExitNodeLocked() (changed bool) {
syncs.RequiresMutex(&b.mu)
if !buildfeatures.HasUseExitNode { if !buildfeatures.HasUseExitNode {
return false return false
} }
@ -6058,6 +6096,7 @@ func (b *LocalBackend) reconcilePrefsLocked(prefs *ipn.Prefs) (changed bool) {
// //
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) resolveExitNodeInPrefsLocked(prefs *ipn.Prefs) (changed bool) { func (b *LocalBackend) resolveExitNodeInPrefsLocked(prefs *ipn.Prefs) (changed bool) {
syncs.RequiresMutex(&b.mu)
if !buildfeatures.HasUseExitNode { if !buildfeatures.HasUseExitNode {
return false return false
} }

@ -16,3 +16,8 @@ type Mutex = sync.Mutex
// //
// It's only not a sync.RWMutex when built with the ts_mutex_debug build tag. // It's only not a sync.RWMutex when built with the ts_mutex_debug build tag.
type RWMutex = sync.RWMutex type RWMutex = sync.RWMutex
// RequiresMutex declares the caller assumes it has the given
// mutex held. In non-debug builds, it's a no-op and compiles to
// nothing.
func RequiresMutex(mu *sync.Mutex) {}

@ -15,4 +15,8 @@ type RWMutex struct {
sync.RWMutex sync.RWMutex
} }
func RequiresMutex(mu *sync.Mutex) {
// TODO: check
}
// TODO(bradfitz): actually track stuff when in debug mode. // TODO(bradfitz): actually track stuff when in debug mode.

Loading…
Cancel
Save