Same leak here on 26200.9550 (win32kfull.sys 10.0.26100.9549): 4.53 M Token objects alive with only 2,191 Token handles after 10 days, about 13 GB of kernel pool. Building on Ben Toner's finding, here is why the registry workaround alone may not stick, and a fix that survives restarts.
Confirmed in the disassembly (WinDbg + Microsoft public symbols):
-
win32kfull!CForegroundLaunch::_CheckAllowForeground+0x3a9 callsPsReferencePrimaryTokenon the parent process, reads its logon LUID, then drops the pointer. There is noPsDereferencePrimaryTokenanywhere in the function, so every process that creates a child keeps its token forever. The allocation stack in the question (SeSubProcessToken) is where the token is created; this is what keeps it alive. - The block is skipped only when
CanForceForegroundreturns TRUE, which depends on the live ForegroundLockTimeout (FLT) versus the time since the last input.
Why the live FLT reads 2147483647: at logon, win32kfull!LoadCPUserPreferences (called from xxxUpdatePerUserSystemParameters) reads the per-user registry values and then stores 0x7FFFFFFF into the FLT slot. So the registry value never reaches the live setting: here the live value read 2147483647 while the registry held 200000, and a registry ForegroundLockTimeout=0 is overwritten the same way. What works is setting the live value with SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, ...) after every logon. Check yours: if the live value is 2147483647 while the registry says otherwise, you are in the same state.
Fix, verified here: after each logon, run python flt_fix.py --fix (script below) in a terminal window you opened yourself, or put a one-try version in your PowerShell profile so every new window does it (the repo linked below has a --profile mode for that). Windows accepts the change only from the foreground program, so a Startup-folder shortcut is unreliable: here it failed (error 87, another window already had focus) on 1 of 3 boots. After the fix, 100,000 cmd /c cmd /c rem parent+child pairs in 9 minutes changed the live Toke count by +176 (noise). Before, that would have been about +100,000. Already-leaked tokens are freed only by a restart; with Fast Startup on, "Shut down" keeps them, so use Restart.
Trade-off: with FLT 0, any app may take the foreground. If the set is refused (error 87), run it from a foreground terminal.
"""Windows 11 'Toke' kernel token leak: check, fix and verify the ForegroundLockTimeout workaround.
python flt_fix.py show the live ForegroundLockTimeout and the live 'Toke' object count
python flt_fix.py --fix set the live ForegroundLockTimeout to 0 (retries for 2 min, for use at logon)
python flt_fix.py --test N run N 'cmd /c cmd /c rem' parent+child pairs and report the 'Toke' change
The live value has to be set after every logon: setting it only in the registry does not survive one.
Trade-off: with 0, any app may take the foreground.
"""
import ctypes, subprocess, sys, time
from ctypes import wintypes
user32 = ctypes.WinDLL("user32", use_last_error=True)
ntdll = ctypes.WinDLL("ntdll")
SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT, SPIF_SENDCHANGE = 0x2000, 0x2001, 2
def flt():
v = wintypes.DWORD()
user32.SystemParametersInfoW(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ctypes.byref(v), 0)
return v.value
def toke():
"""Live 'Toke' pool objects (allocs - frees) from NtQuerySystemInformation(SystemPoolTagInformation)."""
size = 1 << 20
while True:
buf, ret = ctypes.create_string_buffer(size), wintypes.ULONG()
if ntdll.NtQuerySystemInformation(22, buf, size, ctypes.byref(ret)) & 0xFFFFFFFF == 0xC0000004:
size *= 2
continue
break
raw = buf.raw
for i in range(int.from_bytes(raw[0:4], "little")):
o = 8 + i * 40 # x64 SYSTEM_POOLTAG is 40 bytes
if raw[o:o + 4] == b"Toke":
return int.from_bytes(raw[o + 4:o + 8], "little") - int.from_bytes(raw[o + 8:o + 12], "little")
return -1
def fix():
for i in range(1, 25):
ok = user32.SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, None, SPIF_SENDCHANGE)
err = ctypes.get_last_error()
if flt() == 0:
print(f"live ForegroundLockTimeout = 0 (try {i})")
return 0
time.sleep(5)
print(f"failed: set={bool(ok)} error={err} live={flt()} - run it from a foreground terminal")
return 1
def test(n):
t0 = toke()
for _ in range(n):
subprocess.run(["cmd.exe", "/d", "/c", "cmd.exe", "/d", "/c", "rem"], stdout=subprocess.DEVNULL)
time.sleep(5)
t1 = toke()
print(f"{n} pairs: Toke {t0:,} -> {t1:,} ({t1 - t0:+,}); leaking would be about +{n:,}, fixed is noise")
if __name__ == "__main__":
a = sys.argv[1:]
if a[:1] == ["--fix"]:
sys.exit(fix())
print(f"live ForegroundLockTimeout = {flt()} live Toke objects = {toke():,}")
if a[:1] == ["--test"]:
test(int(a[1]) if len(a) > 1 else 1000)
python flt_fix.py shows the live FLT and Toke count, --fix sets it, and --test 1000 measures the leak directly (about +1000 if leaking, near 0 if fixed).
This is a workaround. Microsoft: please add the missing PsDereferencePrimaryToken in CForegroundLaunch::_CheckAllowForeground, and check whether the unconditional 0x7FFFFFFF store in LoadCPUserPreferences is intended. I'm also filing this on Feedback Hub.
Full write-up, disassembly listings and the script: https://github.com/gunrgbcr/windows-token-leak-logon-fix