tailscale/tailscale · error

EnableCurrentThreadPrivileges(%#v): %w

Error message

EnableCurrentThreadPrivileges(%#v): %w

What it means

Before CreateProcessAsUser, EnableCurrentThreadPrivileges enables SeAssignPrimaryTokenPrivilege and SeIncreaseQuotaPrivilege on the calling thread; both are required to assign the session token as the child's primary token. It fails - classically with ERROR_NOT_ALL_ASSIGNED - when the calling process's token does not hold those privileges, i.e. the caller is not SYSTEM.

Source

Thrown at util/winutil/restartmgr_windows.go:810

	if err != nil {
		return nil, fmt.Errorf("UTF16PtrFromString(wd): %w", err)
	}

	env, err := token.Environ(false)
	if err != nil {
		return nil, fmt.Errorf("token environment: %w", err)
	}
	env16 := NewEnvBlock(env)

	// The privileges in privNames are required for CreateProcessAsUser to be
	// able to start processes as other users in other logon sessions.
	privNames := []string{
		"SeAssignPrimaryTokenPrivilege",
		"SeIncreaseQuotaPrivilege",
	}
	dropPrivs, err := EnableCurrentThreadPrivileges(privNames)
	if err != nil {
		return nil, fmt.Errorf("EnableCurrentThreadPrivileges(%#v): %w", privNames, err)
	}
	defer dropPrivs()

	createFlags := extraFlags | windows.CREATE_UNICODE_ENVIRONMENT | windows.DETACHED_PROCESS
	si := windows.StartupInfo{
		Cb:      uint32(unsafe.Sizeof(windows.StartupInfo{})),
		Desktop: defaultDesktop,
	}
	var pi windows.ProcessInformation
	if err := windows.CreateProcessAsUser(token, exePath16, commandLine16, nil, nil,
		false, createFlags, env16, wd16, &si, &pi); err != nil {
		return nil, fmt.Errorf("CreateProcessAsUser: %w", err)
	}
	return &pi, nil
}

// NewEnvBlock processes a slice of strings containing "NAME=value" pairs
// representing a process envionment into the environment block format used by

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Run the launching process as LocalSystem (WTSQueryUserToken in this same path requires it anyway)
  2. Verify with 'whoami /priv' that both privileges are listed before invoking the API
  3. If a custom service account is mandatory, grant it the two privileges via policy
Defensive patterns

Strategy: validation

Validate before calling

// verify the launching process holds the privileges before starting
func canLaunchAsUser() bool {
    var tok windows.Token
    if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &tok); err != nil {
        return false
    }
    defer tok.Close()
    // SYSTEM holds SeAssignPrimaryTokenPrivilege/SeIncreaseQuotaPrivilege by default;
    // user tokens do not. Check elevation/identity as the practical gate.
    return tok.IsElevated()
}

Try / catch

pi, err := startProcessInSession(sessID, cli)
if err != nil {
    if errors.Is(err, windows.ERROR_NOT_ALL_ASSIGNED) || strings.Contains(err.Error(), "EnableCurrentThreadPrivileges") {
        return errors.New("launcher must run as LocalSystem with SeAssignPrimaryTokenPrivilege")
    }
    return err
}

Prevention

When it happens

Trigger: startProcessInSession* invoked by a process running as a regular user or a restricted service account; group policy stripping the privileges; the privilege names failing LsaLookupPrivilegeValue.

Common situations: Running the launcher outside the Windows service for testing; service configured as NETWORK SERVICE instead of LocalSystem; hardened machines where those privileges were removed.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/45744609b2c62770. Report an issue: GitHub.