Windows 11 25H2 (26200.8655): unbounded paged-pool leak in "Toke" tag

Le, Steven 0 Reputation points
2026-07-20T20:07:45.09+00:00

On a fully-patched Windows 11 25H2 (build 26200.8655), kernel paged pool under the Toke tag (security tokens) grows continuously and is never reclaimed until reboot — about 1–1.5 GB/day under developer workloads, reaching 10+ GB within a week and causing system-wide slowdown (sustained hard page faults, multi-second process launches).

Root cause (ETW pool trace via wpr -start Pool, symbols resolved from ntkrnlmp.pdb): every leaked allocation comes from a single kernel stack:

ExAllocatePoolWithTag (tag "Toke")

ExAllocatePool2

ObpAllocateObject

ObCreateObjectEx

SepDuplicateToken

SeSubProcessToken

PspInitializeProcessSecurity

PspAllocateProcess

NtCreateUserProcess

KiSystemServiceCopyEnd

The duplicated child primary token (~28 KB here — domain account with a large AD group SID list) is never freed after the child process exits. A second variant also appears via NtAlpcConnectPortEx → SeCreateClientSecurity → SepDuplicateToken. Over a 138-second trace: 29,302 Toke allocs vs 28,497 frees, 940 never freed, evenly distributed in time. No third-party driver appears on any leaked allocation stack — the leaking path is entirely ntoskrnl.

Reproduction (100% on my machine): from Git for Windows bash:

for i in {1..200}; do cmd.exe /c exit; done

The Toke pool tag grows ~5–6 MB (about one leaked token per spawn) and never returns. Launching the same binaries from PowerShell without an attached console does NOT leak, so the trigger is tied to MSYS/console-attached process creation, not process creation in general. Only a reboot reclaims the memory.

Environment note: machine also runs SentinelOne (25.2.6.442) and Microsoft Defender for Endpoint. Their drivers do not appear on the leaked allocation stacks, but I can't fully exclude an EDR holding token references taken during a process-create callback without object-reference tracing.

Questions:

  1. Is this a known issue in build 26200?
  2. Is a fix expected in a future cumulative update?
  3. What's the correct channel to submit the full trace analysis and 21.8 GB ETL if useful?
Windows for business | Windows Client for IT Pros | Performance | Other

Locked Question. You can vote on whether it's helpful, but you can't add comments or replies or follow the question.

0 comments No comments

3 answers

Sort by: Most helpful
  1. Omer Inbar 0 Reputation points
    2026-10-04T00:39:21.6766667+00:00

    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 calls PsReferencePrimaryToken on the parent process, reads its logon LUID, then drops the pointer. There is no PsDereferencePrimaryToken anywhere 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 CanForceForeground returns 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

    Was this answer helpful?

    0 comments No comments
  2. Ben Toner 0 Reputation points
    2026-09-08T16:49:55.0733333+00:00

    I have what looks like the same leak on 26200.9106 (same tag, same allocation stack, same slowdown from Git Bash spawns), and on my machine it traces to a Windows kernel bug: win32kfull!CForegroundLaunch::_CheckAllowForeground references the parent's primary token and never releases it, so each process that creates a process leaves a Toke object behind. That is from kernel object reference tracing on one box; I can't say whether yours is the same path without tracing it, though the symptoms match.

    What stopped it here: setting ForegroundLockTimeout to 0 (per-user, no elevation). The check only runs while the foreground lock is armed, so with 0 it never reaches the token; the cost is that any app can take the foreground. Write-up, a nine-minute repro and the script, if you want to check your machine against it: https://github.com/bentoner/windows-token-leak

    Was this answer helpful?

    0 comments No comments
  3. Xuan Nhu 1,370 Reputation points Independent Advisor
    2026-07-21T01:15:21.1833333+00:00

    Hi Steven,

    Thank you for providing such a detailed investigation.

    Based on the information you've shared, I am not aware of any publicly documented known issue, KB article, or Release Health notice for Windows 11 25H2 (build 26200.8655) describing an unbounded "Toke" paged-pool leak.

    Your ETW traces indicate that the allocations originate during token creation (SepDuplicateToken / SeSubProcessToken) and persist after the child process exits. While this is certainly unexpected behavior, the stack alone cannot determine which component is holding the final reference. Security products (such as Microsoft Defender for Endpoint or third-party EDR solutions) may legitimately retain token references through process creation callbacks, so they cannot be completely ruled out based on allocation stacks alone.

    To further isolate the issue, I would recommend:

    • Reproducing the issue in a clean environment, if possible, without third-party security software.
    • Collecting a kernel memory dump or additional ETW traces that include object reference information.
    • Comparing the behavior on another Windows 11 build or Insider Preview build to determine whether the issue has already been addressed.

    Regarding your questions:

    1. Known issue: I am not aware of a publicly documented issue for build 26200.8655 matching this behavior.
    2. Future fix: There is no published information indicating that this issue is scheduled to be addressed in a future cumulative update.
    3. Submitting traces: If you have a reproducible scenario and ETL files of this size, the recommended approach is to open a Microsoft Unified/Premier Support case. The support team can securely collect the ETL, memory dumps, and additional diagnostics, then engage the Windows engineering team if the investigation indicates a product issue.

    Given the depth of your analysis and the reproducible test case, I believe opening a support case would be the most appropriate next step, as engineering has access to private symbols and internal diagnostics that are not available through the public forums.Hi Steven,

    Thank you for providing such a detailed investigation.

    Based on the information you've shared, I am not aware of any publicly documented known issue, KB article, or Release Health notice for Windows 11 25H2 (build 26200.8655) describing an unbounded "Toke" paged-pool leak.

    Your ETW traces indicate that the allocations originate during token creation (SepDuplicateToken / SeSubProcessToken) and persist after the child process exits. While this is certainly unexpected behavior, the stack alone cannot determine which component is holding the final reference. Security products (such as Microsoft Defender for Endpoint or third-party EDR solutions) may legitimately retain token references through process creation callbacks, so they cannot be completely ruled out based on allocation stacks alone.

    To further isolate the issue, I would recommend:

    • Reproducing the issue in a clean environment, if possible, without third-party security software.
    • Collecting a kernel memory dump or additional ETW traces that include object reference information.
    • Comparing the behavior on another Windows 11 build or Insider Preview build to determine whether the issue has already been addressed.

    Regarding your questions:

    1. Known issue: I am not aware of a publicly documented issue for build 26200.8655 matching this behavior.
    2. Future fix: There is no published information indicating that this issue is scheduled to be addressed in a future cumulative update.
    3. Submitting traces: If you have a reproducible scenario and ETL files of this size, the recommended approach is to open a Microsoft Unified/Premier Support case. The support team can securely collect the ETL, memory dumps, and additional diagnostics, then engage the Windows engineering team if the investigation indicates a product issue.

    Given the depth of your analysis and the reproducible test case, I believe opening a support case would be the most appropriate next step, as engineering has access to private symbols and internal diagnostics that are not available through the public forums.

    Was this answer helpful?

    0 comments No comments