1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
// Copyright (c) Tailscale Inc & AUTHORS
// 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{})
go func() {
defer close(done)
err := gpLock.Lock()
if err == nil {
defer gpLock.Unlock()
}
// We closed gpLock before the enter function returned.
// (*PolicyLock).Lock is expected to fail.
if err == nil || !errors.Is(err, ErrInvalidLockState) {
t.Errorf("(*PolicyLock).Lock: got %v; want %v", err, ErrInvalidLockState)
}
// gpLock must not be held as Lock() failed.
if lockCnt := gpLock.lockCnt.Load(); lockCnt != 0 {
t.Errorf("lockCnt: got %v; want 0", lockCnt)
}
}()
<-init
// Close gpLock right before the enter function returns.
if err := gpLock.Close(); err != nil {
t.Fatalf("(*PolicyLock).Close failed: %v", err)
}
<-done
}, 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(l *PolicyLock), enter func(bool) (policyLockHandle, error), leave func(policyLockHandle) error) {
t.Helper()
l := NewMachinePolicyLock()
l.enterFn, l.leaveFn = enter, leave
t.Cleanup(func() {
if err := l.Close(); err != nil {
t.Fatalf("(*PolicyLock).Close failed: %v", err)
}
})
f(l)
}
|