{"record":{"id":"45744609b2c62770","repo":"tailscale/tailscale","slug":"enablecurrentthreadprivileges-v-w","errorCode":null,"errorMessage":"EnableCurrentThreadPrivileges(%#v): %w","messagePattern":"EnableCurrentThreadPrivileges\\(%#v\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/winutil/restartmgr_windows.go","lineNumber":810,"sourceCode":"\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"UTF16PtrFromString(wd): %w\", err)\n\t}\n\n\tenv, err := token.Environ(false)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token environment: %w\", err)\n\t}\n\tenv16 := NewEnvBlock(env)\n\n\t// The privileges in privNames are required for CreateProcessAsUser to be\n\t// able to start processes as other users in other logon sessions.\n\tprivNames := []string{\n\t\t\"SeAssignPrimaryTokenPrivilege\",\n\t\t\"SeIncreaseQuotaPrivilege\",\n\t}\n\tdropPrivs, err := EnableCurrentThreadPrivileges(privNames)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"EnableCurrentThreadPrivileges(%#v): %w\", privNames, err)\n\t}\n\tdefer dropPrivs()\n\n\tcreateFlags := extraFlags | windows.CREATE_UNICODE_ENVIRONMENT | windows.DETACHED_PROCESS\n\tsi := windows.StartupInfo{\n\t\tCb:      uint32(unsafe.Sizeof(windows.StartupInfo{})),\n\t\tDesktop: defaultDesktop,\n\t}\n\tvar pi windows.ProcessInformation\n\tif err := windows.CreateProcessAsUser(token, exePath16, commandLine16, nil, nil,\n\t\tfalse, createFlags, env16, wd16, &si, &pi); err != nil {\n\t\treturn nil, fmt.Errorf(\"CreateProcessAsUser: %w\", err)\n\t}\n\treturn &pi, nil\n}\n\n// NewEnvBlock processes a slice of strings containing \"NAME=value\" pairs\n// representing a process envionment into the environment block format used by","sourceCodeStart":792,"sourceCodeEnd":828,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/util/winutil/restartmgr_windows.go#L792-L828","documentation":"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.","triggerScenarios":"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.","commonSituations":"Running the launcher outside the Windows service for testing; service configured as NETWORK SERVICE instead of LocalSystem; hardened machines where those privileges were removed.","solutions":["Run the launching process as LocalSystem (WTSQueryUserToken in this same path requires it anyway)","Verify with 'whoami /priv' that both privileges are listed before invoking the API","If a custom service account is mandatory, grant it the two privileges via policy"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// verify the launching process holds the privileges before starting\nfunc canLaunchAsUser() bool {\n    var tok windows.Token\n    if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &tok); err != nil {\n        return false\n    }\n    defer tok.Close()\n    // SYSTEM holds SeAssignPrimaryTokenPrivilege/SeIncreaseQuotaPrivilege by default;\n    // user tokens do not. Check elevation/identity as the practical gate.\n    return tok.IsElevated()\n}","typeGuard":null,"tryCatchPattern":"pi, err := startProcessInSession(sessID, cli)\nif err != nil {\n    if errors.Is(err, windows.ERROR_NOT_ALL_ASSIGNED) || strings.Contains(err.Error(), \"EnableCurrentThreadPrivileges\") {\n        return errors.New(\"launcher must run as LocalSystem with SeAssignPrimaryTokenPrivilege\")\n    }\n    return err\n}","preventionTips":["Run the launcher as LocalSystem; verify with 'whoami /priv' on the service context","Ensure group policy has not stripped SeAssignPrimaryTokenPrivilege or SeIncreaseQuotaPrivilege","Fail fast at service startup if privileges are missing"],"tags":["go","windows","privileges","seassignprimarytokenprivilege","not-all-assigned","service-account"],"backgroundTag":"privilege-not-held","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}