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.
-
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 ascmd.exeleak too while the foreground lock is armed. -
The check. Run
powershell -ExecutionPolicy Bypass -File repro/pool-tags.ps1Its 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.
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
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:
- idle baseline — nothing started; the box's ambient leak, to subtract.
- two MSYS loops, two consoles — two bash fork+exec loops
(
repro/leak-loop-msys.sh; each iteration forks a subshell that execstrue.exe, so one creator per iteration), each in its own console. - two native cmd loops, two consoles — the same with
cmd.execreators (repro/leak-loop-native.cmd). - 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.
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.
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.
- 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 MTokeobjects 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/leaked-token-ledger.txt— one leaked token's complete!obtraceledger: 749 references, 748 dereferences; the unreleased one is the_CheckAllowForegroundentry at sequence1bc0d21.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 withForegroundLockTimeoutat 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.