cmd/tailscaled,util/syspolicy/source,util/winutil/gp: disallow acquiring the GP lock during service startup

In v1.78, we started acquiring the GP lock when reading policy settings. This led to a deadlock during
Tailscale installation via Group Policy Software Installation because the GP engine holds the write lock
for the duration of policy processing, which in turn waits for the installation to complete, which in turn
waits for the service to enter the running state.

In this PR, we prevent the acquisition of GP locks (aka EnterCriticalPolicySection) during service startup
and update the Windows Registry-based util/syspolicy/source.PlatformPolicyStore to handle this failure
gracefully. The GP lock is somewhat optional; it’s safe to read policy settings without it, but acquiring
the lock is recommended when reading multiple values to prevent the Group Policy engine from modifying
settings mid-read and to avoid inconsistent results.

Fixes #14416

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2025-01-16 15:48:07 -06:00
committed by Nick Khyl
parent 413fb5b933
commit f0db47338e
4 changed files with 138 additions and 8 deletions

View File

@@ -55,6 +55,7 @@ import (
"tailscale.com/util/osdiag"
"tailscale.com/util/syspolicy"
"tailscale.com/util/winutil"
"tailscale.com/util/winutil/gp"
"tailscale.com/version"
"tailscale.com/wf"
)
@@ -70,6 +71,22 @@ func init() {
}
}
// permitPolicyLocks is a function to be called to lift the restriction on acquiring
// [gp.PolicyLock]s once the service is running.
// It is safe to be called multiple times.
var permitPolicyLocks = func() {}
func init() {
if isWindowsService() {
// We prevent [gp.PolicyLock]s from being acquired until the service enters the running state.
// Otherwise, if tailscaled starts due to a GPSI policy installing Tailscale, it may deadlock
// while waiting for the write counterpart of the GP lock to be released by Group Policy,
// which is itself waiting for the installation to complete and tailscaled to start.
// See tailscale/tailscale#14416 for more information.
permitPolicyLocks = gp.RestrictPolicyLocks()
}
}
const serviceName = "Tailscale"
// Application-defined command codes between 128 and 255
@@ -109,13 +126,13 @@ func tstunNewWithWindowsRetries(logf logger.Logf, tunName string) (_ tun.Device,
}
}
func isWindowsService() bool {
var isWindowsService = sync.OnceValue(func() bool {
v, err := svc.IsWindowsService()
if err != nil {
log.Fatalf("svc.IsWindowsService failed: %v", err)
}
return v
}
})
// syslogf is a logger function that writes to the Windows event log (ie, the
// one that you see in the Windows Event Viewer). tailscaled may optionally
@@ -180,6 +197,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
syslogf("Service running")
// It is safe to allow GP locks to be acquired now that the service
// is running.
permitPolicyLocks()
for {
select {
case <-doneCh: