tailscale/util/winutil/gp/policylock_windows.go
Nick Khyl cab0e1a6f7 uti/syspolicy: user policy support, auto-refresh and initial preparation for policy structs
This updates the syspolicy package to support multiple policy sources in the
three policy scopes: user, profile, and device, and provides a merged resultant
policy. A policy source is a syspolicy/source.Store that has a name and provides
access to policy settings for a given scope. It can be registered with
syspolicy/rsop.RegisterStore. Policy sources and policy stores can be either
platform-specific or platform-agnostic. On Windows, we have the Registry-based,
platform-specific policy store implemented as
syspolicy/source.PlatformPolicyStore. This store provides access to the Group
Policy and MDM policy settings stored in the Registry. On other platforms, we
currently provide a wrapper that converts a syspolicy.Handler into a
syspolicy/source.Store. However, we should update them in follow-up PRs. An
example of a platform-agnostic policy store would be a policy deployed from the
control, a local policy config file, or even environment variables.

We maintain the current, most recent version of the resultant policy for each
scope in an rsop.Policy. This is done by reading and merging the policy settings
from the registered stores the first time the resultant policy is requested,
then re-reading and re-merging them if a store implements the source.Changeable
interface and reports a policy change. Policy change notifications are debounced
to avoid re-reading policy settings multiple times if there are several changes
within a short period. The rsop.Policy can notify clients if the resultant
policy has changed. However, we do not currently expose this via the syspolicy
package and plan to do so differently along with a struct-based policy hierarchy
in the next PR.

To facilitate this, all policy settings should be registered with the
setting.Register function. The syspolicy package does this automatically for all
policy settings defined in policy_keys.go.

The new functionality is available through the existing syspolicy.Read* set of
functions. However, we plan to expose it via a struct-based policy hierarchy,
along with policy change notifications that other subsystems can use, in the
next PR. We also plan to send the resultant policy back from tailscaled to the
clients via the LocalAPI.

This is primarily a foundational PR to facilitate future changes, but the
immediate observable changes on Windows include:
- The service will use the current policy setting values instead of those read
  at OS boot time.
- The GUI has access to policy settings configured on a per-user basis.
On Android:
- We now report policy setting usage via clientmetrics.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-02 20:01:13 -05:00

293 lines
8.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package gp
import (
"errors"
"fmt"
"runtime"
"sync"
"sync/atomic"
"golang.org/x/sys/windows"
)
// PolicyLock allows pausing the application of policy to safely read Group Policy
// settings. A PolicyLock is an R-lock that can be held by multiple readers simultaneously,
// preventing the Group Policy Client service (which maintains its W-counterpart) from
// modifying policies while they are being read.
//
// It is not possible to pause group policy processing for longer than 10 minutes.
// If the system needs to apply policies and the lock is being held for more than that,
// the Group Policy Client service will release the lock and continue policy processing.
//
// To avoid deadlocks when acquiring both machine and user locks, acquire the
// user lock before the machine lock.
type PolicyLock struct {
scope Scope
token windows.Token
// hooks for testing
enterFn func(bool) (policyLockHandle, error)
leaveFn func(policyLockHandle) error
closing chan struct{} // closing is closed when the Close method is called.
mu sync.Mutex
handle policyLockHandle
lockCnt atomic.Int32 // A non-zero LSB indicates that the lock can be acquired.
}
// policyLockHandle is the underlying lock handle returned by enterCriticalPolicySection.
type policyLockHandle uintptr
type policyLockResult struct {
handle policyLockHandle
err error
}
var (
// ErrInvalidLockState is returned by (*PolicyLock).Lock if the lock has a zero value or has already been closed.
ErrInvalidLockState = errors.New("the lock has not been created or has already been closed")
)
// NewMachinePolicyLock creates a PolicyLock that facilitates pausing the
// application of computer policy. To avoid deadlocks when acquiring both
// machine and user locks, acquire the user lock before the machine lock.
func NewMachinePolicyLock() *PolicyLock {
lock := &PolicyLock{
scope: MachinePolicy,
closing: make(chan struct{}),
enterFn: enterCriticalPolicySection,
leaveFn: leaveCriticalPolicySection,
}
lock.lockCnt.Store(1) // mark as initialized
return lock
}
// NewUserPolicyLock creates a PolicyLock that facilitates pausing the
// application of the user policy for the specified user. To avoid deadlocks
// when acquiring both machine and user locks, acquire the user lock before the
// machine lock.
//
// The token indicates which user's policy should be locked for reading.
// If specified, the token must have TOKEN_DUPLICATE access,
// the specified user must be logged in interactively.
// and the caller retains ownership of the token.
//
// Otherwise, a zero token value indicates the current user. It should not
// be used by services or other applications running under system identities.
func NewUserPolicyLock(token windows.Token) (*PolicyLock, error) {
lock := &PolicyLock{
scope: UserPolicy,
closing: make(chan struct{}),
enterFn: enterCriticalPolicySection,
leaveFn: leaveCriticalPolicySection,
}
if token != 0 {
err := windows.DuplicateHandle(
windows.CurrentProcess(),
windows.Handle(token),
windows.CurrentProcess(),
(*windows.Handle)(&lock.token),
windows.TOKEN_QUERY|windows.TOKEN_DUPLICATE|windows.TOKEN_IMPERSONATE,
false,
0)
if err != nil {
return nil, err
}
}
lock.lockCnt.Store(1) // mark as initialized
return lock, nil
}
// Lock locks l.
// It returns ErrNotInitialized if l has a zero value or has already been closed,
// or an Errno if the underlying Group Policy lock cannot be acquired.
//
// As a special case, it fails with windows.ERROR_ACCESS_DENIED
// if l is a user policy lock, and the corresponding user is not logged in
// interactively at the time of the call.
func (l *PolicyLock) Lock() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.lockCnt.Add(2)&1 == 0 {
// The lock cannot be acquired because it has either never been properly
// created or its Close method has already been called. However, we need
// to call Unlock to both decrement lockCnt and leave the underlying
// CriticalPolicySection if we won the race with another goroutine and
// now own the lock.
l.Unlock()
return ErrInvalidLockState
}
if l.handle != 0 {
// The underlying CriticalPolicySection is already acquired.
// It is an R-Lock (with the W-counterpart owned by the Group Policy service),
// meaning that it can be acquired by multiple readers simultaneously.
// So we can just return.
return nil
}
return l.lockSlow()
}
// lockSlow calls enterCriticalPolicySection to acquire the underlying GP read lock.
// It waits for either the lock to be acquired, or for the Close method to be called.
//
// l.mu must be held.
func (l *PolicyLock) lockSlow() (err error) {
defer func() {
if err != nil {
// Decrement the counter if the lock cannot be acquired,
// and complete the pending close request if we're the last owner.
if l.lockCnt.Add(-2) == 0 {
l.closeInternal()
}
}
}()
// In some cases in production environments, the Group Policy service may
// hold the corresponding W-Lock for extended periods of time (minutes
// rather than seconds or milliseconds). We need to make our wait operation
// cancellable. So, if one goroutine invokes (*PolicyLock).Close while another
// initiates (*PolicyLock).Lock and waits for the underlying R-lock to be
// acquired by enterCriticalPolicySection, the Close method should cancel
// the wait.
initCh := make(chan error)
resultCh := make(chan policyLockResult)
go func() {
closing := l.closing
if l.scope == UserPolicy && l.token != 0 {
// Impersonate the user whose critical policy section we want to acquire.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := impersonateLoggedOnUser(l.token); err != nil {
initCh <- err
return
}
defer func() {
if err := windows.RevertToSelf(); err != nil {
// RevertToSelf errors are non-recoverable.
panic(fmt.Errorf("could not revert impersonation: %w", err))
}
}()
}
close(initCh)
var machine bool
if l.scope == MachinePolicy {
machine = true
}
handle, err := l.enterFn(machine)
send_result:
for {
select {
case resultCh <- policyLockResult{handle, err}:
// lockSlow has received the result.
break send_result
default:
select {
case <-closing:
// The lock is being closed, and we lost the race to l.closing
// it the calling goroutine.
if err == nil {
l.leaveFn(handle)
}
break send_result
default:
// The calling goroutine did not enter the select block yet.
runtime.Gosched() // allow other routines to run
continue send_result
}
}
}
}()
// lockSlow should not return until the goroutine above has been fully initialized,
// even if the lock is being closed.
if err = <-initCh; err != nil {
return err
}
select {
case result := <-resultCh:
if result.err == nil {
l.handle = result.handle
}
return result.err
case <-l.closing:
return ErrInvalidLockState
}
}
// Unlock unlocks l.
// It panics if l is not locked on entry to Unlock.
func (l *PolicyLock) Unlock() {
l.mu.Lock()
defer l.mu.Unlock()
lockCnt := l.lockCnt.Add(-2)
if lockCnt < 0 {
panic("negative lockCnt")
}
if lockCnt > 1 {
// The lock is still being used by other readers.
// We compare against 1 rather than 0 because the least significant bit
// of lockCnt indicates that l has been initialized and a close
// has not been requested yet.
return
}
if l.handle != 0 {
// Impersonation is not required to unlock a critical policy section.
// The handle we pass determines which mutex will be unlocked.
leaveCriticalPolicySection(l.handle)
l.handle = 0
}
if lockCnt == 0 {
// Complete the pending close request if there's no more readers.
l.closeInternal()
}
}
// Close releases resources associated with l.
// It is a no-op for the machine policy lock.
func (l *PolicyLock) Close() error {
lockCnt := l.lockCnt.Load()
if lockCnt&1 == 0 {
// The lock has never been initialized, or close has already been called.
return nil
}
close(l.closing)
// Unset the LSB to indicate a pending close request.
for !l.lockCnt.CompareAndSwap(lockCnt, lockCnt&^int32(1)) {
lockCnt = l.lockCnt.Load()
}
if lockCnt != 0 {
// The lock is still being used and will be closed upon the final Unlock call.
return nil
}
return l.closeInternal()
}
func (l *PolicyLock) closeInternal() error {
if l.token != 0 {
if err := l.token.Close(); err != nil {
return err
}
l.token = 0
}
l.closing = nil
return nil
}