Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Windows 11 leaks a kernel token for every process that creates a process

On Windows 11 25H2 (build 26200.9106; win32kfull.sys 10.0.26100.8875) the kernel leaks a primary-token object — the security token every process is born with, kernel pool tag Toke — for almost every process that goes on to create a process of its own (a creator, below). The holder is win32kfull!CForegroundLaunch::_CheckAllowForeground, the foreground-launch policy check that win32k (the kernel-mode window manager) runs when a process is created: it references the parent's primary token to read its logon id and never releases it. Each leaked token keeps an entry alive in a kernel hash table that NtCreateUserProcess walks under an exclusive lock, so process creation slows for the whole boot cycle: in Git Bash a fork+exec costs about 20 ms after a reboot and 570–810 ms 4.6 days later, with millions of dead tokens and about 10 GB of paged pool behind it, and only a reboot shrinks it.

The check reaches the token only while win32k's foreground lock is armed — while user input, or as far as measured the creation of a new console window, arrived within the last ForegroundLockTimeout milliseconds (a per-user setting; Windows default 200000). Setting that timeout to 0 stops the leak; that is the workaround below, with its side effect. The leak reproduces on demand in about nine minutes with repro/minimal-repro.sh. The fix is one missing PsDereferencePrimaryToken in win32kfull. Everything here was measured on one machine; the few claims not directly measured are marked inference.

Am I affected?

  • Build. Observed on Windows 11 25H2, build 26200.x (26200.9106 here; 26200.8655 and 26200.8737 in the public reports below). Other builds are untested.

  • Workload. Heavy process creation from Git for Windows bash — the MSYS2 runtime, called MSYS below: AI coding-agent tools that run bash hooks and status-line scripts, git hooks, editors and IDEs driving git.exe. Native creators such as cmd.exe leak too while the foreground lock is armed.

  • The check. Run

    powershell -ExecutionPolicy Bypass -File repro/pool-tags.ps1
    

    Its second line, TAG Toke <n>, is the number of live token objects, system-wide, no elevation needed. On an idle box it does not grow. Affected boxes reach hundreds of thousands within hours and millions after days, and the count never comes back down.

What is happening

When a process creates a child, win32k applies its foreground-launch policy to the new process and examines the parent — the creator. That check, win32kfull!CForegroundLaunch::_CheckAllowForeground, calls nt!PsReferencePrimaryToken on the parent's token, passes it to nt!SeQueryAuthenticationIdToken to read the logon id, and drops the pointer: its disassembly contains exactly one reference call and no dereference on any path. The check is reached on two paths, both Microsoft code: an NtUserConsoleControl system call handled by xxxUserNotifyConsoleApplication (the console host notifying win32k of a console application attaching — inference from the handler's name), and win32k's per-process initialization callout. The token is reached only while the foreground lock is armed: win32kfull's CanForceForeground(parent) must return false, and its last leg returns true once more than ForegroundLockTimeout ms have passed since win32k's last-input time. Kernel object reference tracing — a ledger per object of every reference and release, each with its stack — confirms the holder: all 642 leaked tokens whose complete ledgers were read carry this reference, none carries a matching release, the count of these references equals the token's residual reference count in every ledger but one truncated one, and every other reference balances. When the process exits its own reference goes and the leaked one stays: the token sits dead — no handles, not in use — until the next reboot, holding its entry in nt!SeLuidToIndexMapping, the hash table NtCreateUserProcess walks under an exclusive push lock.

The reference, verbatim from the ledger of one leaked token (evidence/leaked-token-ledger.txt, sequence 1bc0d21):

nt!ObpTraceObjectReferenceIfActive+1a
nt!PsReferencePrimaryTokenWithTag+79
nt!PsReferencePrimaryToken+e
win32kfull!CForegroundLaunch::_CheckAllowForeground+3b5
win32kfull!CForegroundLaunch::ApplyForegroundPolicyConsole+1e9
win32kfull!ForegroundLaunch::ApplyForegroundPolicyConsole+4b
win32kfull!xxxUserNotifyConsoleApplication+113
win32kfull!xxxConsoleControl+469
win32kfull!NtUserConsoleControl+b9
win32k!NtUserConsoleControl+43
nt!KiSystemServiceCopyEnd+25

Reproduce it

Requirements: Windows 11 (build above), Git for Windows (tested with 2.54.0, MSYS runtime 3.6.7), PowerShell 5 or later; no elevation, nothing is killed. Use a box with no other Git Bash or MSYS tooling running and keep your hands off it for the nine minutes — the foreground lock decides what the run reads.

git clone <this repo>
cd windows-token-leak
bash repro/minimal-repro.sh 60

The script runs three 60 s legs in their own minimized console windows, reading the Toke counter before each, after it, and again after a 60 s wait for the kernel's deferred reclaim of dead tokens; that settled delta is the leak. repro/out/<timestamp>-minimal-repro.txt gets four lines:

  1. idle baseline — nothing started; the box's ambient leak, to subtract.
  2. two MSYS loops, two consoles — two bash fork+exec loops (repro/leak-loop-msys.sh; each iteration forks a subshell that execs true.exe, so one creator per iteration), each in its own console.
  3. two native cmd loops, two consoles — the same with cmd.exe creators (repro/leak-loop-native.cmd).
  4. one MSYS loop — a single bash loop in its own console.

At the Windows default timeout with no input, line 2 leaks and lines 3 and 4 do not; at 0 nothing leaks; at 2147483647, or with input arriving, lines 2–4 all leak at about one token per creator. The reference run (evidence/reference-repro-run.txt, made before the idle-baseline line existed, lock state unknown):

two MSYS loops, two consoles         Toke 195793 -> 197434 -> settled 197437   delta +1644
two native cmd loops, two consoles   Toke 197437 -> 197454 -> settled 197444   delta +7
one MSYS loop (control)              Toke 197444 -> 198414 -> settled 198400   delta +956

The first line is the leak: 1,644 tokens over 3,542 iterations, none reclaimed. The single loop is not a clean control — the bash running the script is itself an MSYS process alive in another console.

Workaround

Set ForegroundLockTimeout to 0:

powershell -ExecutionPolicy Bypass -File repro/foreground-lock-timeout.ps1 -Set 0

No elevation: it is a per-user setting, written live into win32k and persisted to HKCU\Control Panel\Desktop\ForegroundLockTimeout. With 0 the lock is never armed, _CheckAllowForeground returns before the reference, and the leak stops. On the same idle box in the same boot, the two-MSYS-loop line goes from +4,092 with the timeout at 2147483647 (evidence/workaround-before.txt) to −94 with it at 0 (evidence/workaround-after.txt), within the idle baseline's noise; the native and single-loop lines go from +1,926 and +2,154 to +1 and +125. The side effect is the documented one: any application may take the foreground at any time (the classic focus-stealing tweak). Two practical notes:

  • The set call is refused while the lock is armed. win32k rejects it with Win32 error 87 (ERROR_INVALID_PARAMETER) unless the caller may force the foreground. Run it on an idle box, from an existing console, not within 200 s of any input or of a window being created or activated; if it reports 87, leave the machine alone for 200 s and run it again.
  • Check the live value first. The script with no arguments prints the live value and the registry value. On the machine measured here the registry holds the default, 200000, but something sets the live value to 2147483647 after logon — which application is not identified. At that value the lock stays armed for 24.9 days after any input, so the leak is permanently on; at the default it is armed for 200 s after each input.

What Microsoft needs to fix

win32kfull.sys 10.0.26100.8875, CForegroundLaunch::_CheckAllowForeground: the PsReferencePrimaryToken call at +0x39f has no matching PsDereferencePrimaryToken on any path out of the function. One call fixes it. Both chains that reach it are Microsoft code; no third-party driver appears in any frame and nothing in user mode holds the object. A corrective kernel driver could release the leaked references itself (a dead primary token's whole residual reference count is leaked references), but on a Secure Boot machine it needs Microsoft's attestation signature; none has been built.

Related reports

  • Microsoft Q&A 5952454 (2026-07-21, build 26200.8655): about one leaked token per spawn from a Git Bash loop, 1–1.5 GB/day of Toke, "multi-second process launches"; no fix as of July 2026.
  • openai/codex#30926 (2026-07-02, open): Codex Desktop spawning git.exe (Git for Windows 2.54.0) leaks tokens at the spawn rate; corroborators report 5.3 M Toke objects on 26200.8737.

Both report this leak and neither names the holder. Toke growth under WSL2 driven by wcifs.sys file contexts is a different leak.

Evidence

  • evidence/leaked-token-ledger.txt — one leaked token's complete !obtrace ledger: 749 references, 748 dereferences; the unreleased one is the _CheckAllowForeground entry at sequence 1bc0d21.
  • evidence/reference-repro-run.txt — the repro run quoted above.
  • evidence/workaround-before.txt, evidence/workaround-after.txt — the same repro on the same idle box and boot with ForegroundLockTimeout at 2147483647 and at 0. Each file's first line states its conditions; the trailing "reading" line is the script's generic hint, not an analysis.

The full analysis — 657 reference ledgers, the condition tree of _CheckAllowForeground from the disassembly, ETW pool traces with allocation stacks, and the kernel dumps — is available on request.

About

Windows 11 25H2 leaks a kernel token (pool tag Toke) for every process that creates a process; holder named, repro, workaround

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages