Files
tailscale/util/winutil/gp/gp_windows_test.go
T
Brad Fitzpatrick f844c8bc32 util/winutil/gp: deflake TestGroupPolicyReadLockClose
The test goroutine read lockCnt immediately after Lock returned, racing
with Close: close(lk.closing) wakes lockSlow's select, whose deferred
Add(-2) on lockCnt can run before Close's CAS clears the LSB. When that
happens, lockCnt is briefly 1 (3 - 2) instead of 0 (1 + 2 - 2 - 1),
producing "lockCnt: got 1; want 0".

Move the lockCnt assertion into the main test goroutine, after both
Close has returned and the Lock goroutine has finished, so both updates
have settled before we read.

Fixes #19647

Change-Id: Ia67036ff73a1beb528cbd621460db9048f3066ad
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-05-05 14:02:35 -07:00

203 lines
5.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package gp
import (
"errors"
"sync"
"testing"
"time"
"tailscale.com/util/cibuild"
)
func TestWatchForPolicyChange(t *testing.T) {
if cibuild.On() {
// Unlike tests that also use the GP API in net\dns\manager_windows_test.go,
// this one does not require elevation. However, a Group Policy change notification
// never arrives when this tests runs on a GitHub-hosted runner.
t.Skipf("test requires running on a real Windows environment")
}
done, close := setupMachinePolicyChangeNotifier(t)
defer close()
// RefreshMachinePolicy is a non-blocking call.
if err := RefreshMachinePolicy(true); err != nil {
t.Fatalf("RefreshMachinePolicy failed: %v", err)
}
// We should receive a policy change notification when
// the Group Policy service completes policy processing.
// Otherwise, the test will eventually time out.
<-done
}
func TestGroupPolicyReadLock(t *testing.T) {
if cibuild.On() {
// Unlike tests that also use the GP API in net\dns\manager_windows_test.go,
// this one does not require elevation. However, a Group Policy change notification
// never arrives when this tests runs on a GitHub-hosted runner.
t.Skipf("test requires running on a real Windows environment")
}
done, close := setupMachinePolicyChangeNotifier(t)
defer close()
doWithMachinePolicyLocked(t, func() {
// RefreshMachinePolicy is a non-blocking call.
if err := RefreshMachinePolicy(true); err != nil {
t.Fatalf("RefreshMachinePolicy failed: %v", err)
}
// Give the Group Policy service a few seconds to attempt to refresh the policy.
// It shouldn't be able to do so while the lock is held, and the below should time out.
timeout := time.NewTimer(5 * time.Second)
defer timeout.Stop()
select {
case <-timeout.C:
case <-done:
t.Fatal("Policy refresh occurred while the policy lock was held")
}
})
// We should receive a policy change notification once the lock is released
// and GP can refresh the policy.
// Otherwise, the test will eventually time out.
<-done
}
func TestHammerGroupPolicyReadLock(t *testing.T) {
const N = 10_000
enter := func(bool) (policyLockHandle, error) { return 1, nil }
leave := func(policyLockHandle) error { return nil }
doWithCustomEnterLeaveFuncs(t, func(gpLock *PolicyLock) {
var wg sync.WaitGroup
wg.Add(N)
for range N {
go func() {
defer wg.Done()
if err := gpLock.Lock(); err != nil {
t.Errorf("(*PolicyLock).Lock failed: %v", err)
return
}
defer gpLock.Unlock()
if gpLock.handle == 0 {
t.Error("(*PolicyLock).handle is 0")
return
}
}()
}
wg.Wait()
}, enter, leave)
}
func TestGroupPolicyReadLockClose(t *testing.T) {
init := make(chan struct{})
enter := func(bool) (policyLockHandle, error) {
close(init)
time.Sleep(500 * time.Millisecond)
return 1, nil
}
leave := func(policyLockHandle) error { return nil }
doWithCustomEnterLeaveFuncs(t, func(gpLock *PolicyLock) {
done := make(chan struct{})
var lockErr error
go func() {
defer close(done)
lockErr = gpLock.Lock()
if lockErr == nil {
defer gpLock.Unlock()
}
}()
<-init
// Close gpLock right before the enter function returns.
if err := gpLock.Close(); err != nil {
t.Fatalf("(*PolicyLock).Close failed: %v", err)
}
// Wait for Lock to fully unwind before reading lockCnt.
// Otherwise we race with Close clearing the LSB:
// close(lk.closing) wakes lockSlow's select, whose defer
// runs Add(-2) on lockCnt before Close completes its CAS,
// briefly leaving lockCnt at 1 instead of 0.
<-done
// We closed gpLock before the enter function returned.
// (*PolicyLock).Lock is expected to fail.
if lockErr == nil || !errors.Is(lockErr, ErrInvalidLockState) {
t.Errorf("(*PolicyLock).Lock: got %v; want %v", lockErr, ErrInvalidLockState)
}
// gpLock must not be held as Lock() failed.
if lockCnt := gpLock.lockCnt.Load(); lockCnt != 0 {
t.Errorf("lockCnt: got %v; want 0", lockCnt)
}
}, enter, leave)
}
func TestGroupPolicyReadLockErr(t *testing.T) {
wantErr := errors.New("failed to acquire the lock")
enter := func(bool) (policyLockHandle, error) { return 0, wantErr }
leave := func(policyLockHandle) error { t.Error("leaveCriticalPolicySection must not be called"); return nil }
doWithCustomEnterLeaveFuncs(t, func(gpLock *PolicyLock) {
err := gpLock.Lock()
if err == nil {
defer gpLock.Unlock()
}
if err != wantErr {
t.Errorf("(*PolicyLock).Lock: got %v; want %v", err, wantErr)
}
// gpLock must not be held when Lock() fails.
// The LSB indicates that the lock has not been closed.
if lockCnt := gpLock.lockCnt.Load(); lockCnt&^(1) != 0 {
t.Errorf("lockCnt: got %v; want 0", lockCnt)
}
}, enter, leave)
}
func setupMachinePolicyChangeNotifier(t *testing.T) (chan struct{}, func()) {
done := make(chan struct{})
var watcher *ChangeWatcher
watcher, err := NewChangeWatcher(MachinePolicy, func() {
close(done)
})
if err != nil {
t.Fatalf("NewChangeWatcher failed: %v", err)
}
return done, func() {
if err := watcher.Close(); err != nil {
t.Errorf("(*ChangeWatcher).Close failed: %v", err)
}
}
}
func doWithMachinePolicyLocked(t *testing.T, f func()) {
gpLock := NewMachinePolicyLock()
defer gpLock.Close()
if err := gpLock.Lock(); err != nil {
t.Fatalf("(*PolicyLock).Lock failed: %v", err)
}
defer gpLock.Unlock()
f()
}
func doWithCustomEnterLeaveFuncs(t *testing.T, f func(*PolicyLock), enter func(bool) (policyLockHandle, error), leave func(policyLockHandle) error) {
t.Helper()
lock := NewMachinePolicyLock()
lock.enterFn, lock.leaveFn = enter, leave
t.Cleanup(func() {
if err := lock.Close(); err != nil {
t.Fatalf("(*PolicyLock).Close failed: %v", err)
}
})
f(lock)
}