Skip to content

Every exec() from Git Bash leaks one kernel Token object (paged pool) until reboot: ~660k tokens / 1.3 GB after 3 weeks of agent use #6471

Open

Description

@nonacc

Existing issues matching what you're seeing

  • I was not able to find an open or closed issue matching what I'm seeing

Git for Windows version

git version 2.55.0.windows.3
cpu: x86_64
built from commit: 52ca1113d651127f89477a8763f86ab20f645e1d
sizeof-long: 4
sizeof-size_t: 8
shell-path: D:/git-sdk-64/usr/bin/sh
rust: disabled
feature: fsmonitor--daemon
gettext: enabled
libcurl: 8.21.0
OpenSSL: OpenSSL 3.5.7 9 Jun 2026
zlib: 1.3.2
SHA-1: SHA1_DC
SHA-256: SHA256_BLK
default-ref-format: files
default-hash: sha1

MSYS2 runtime: msys-2.0.dll 3.6.9-b4195d69133078c498a1bf811c4fb0c61fc3c8af

Windows version

Windows 11

Windows CPU architecture

x86_64 (64-bit)

Additional Windows version information

Microsoft Windows [Version 10.0.26200.9457]

Options set during installation

Editor Option: VIM
Path Option: Cmd
SSH Option: OpenSSH
CURL Option: WinSSL
CRLF Option: CRLFAlways
Bash Terminal Option: MinTTY
Git Pull Behavior Option: Merge
Use Credential Manager: Enabled
Performance Tweaks FSCache: Enabled
Enable Symlinks: Disabled
Enable FSMonitor: Disabled

Other interesting things

The machine runs AI coding agents (Claude Code, Codex), which run every shell command through Git Bash. That is hundreds of thousands of fork+exec cycles over a few weeks of uptime.

Root cause, in Windows: the console host takes the reference. Reported upstream at microsoft/terminal#20781.

Terminal/shell

Bash (usr\bin\bash.exe), started from PowerShell / Windows Terminal / an agent's process (no mintty involved).

Commands that trigger the issue

for i in $(seq 300); do /usr/bin/true; done

Measure kernel Token objects before and after. The self-contained PowerShell repro (no admin; counts tokens via NtQueryObject(ObjectTypesInformation), with a cmd.exe control) is in the upstream issue.

Expected behaviour

Kernel Token objects go back to baseline once the processes exit.

Actual behaviour

Every exec() from an MSYS2 process leaks one kernel Token object (~2 KB of paged pool) until reboot:

300 iterations Token objects
control: cmd.exe → where.exe −7 to +2
fork + exec (/usr/bin/true) +238 to +345 (7 of 8 runs)
exec only (bash -c 'exec /usr/bin/true') +54 to +354
fork only (( : ), no exec) 0 to +29

After 22 days of uptime there were ~660,000 Token objects (1.3 GB paged pool, Toke tag) but only ~2,650 token handles system-wide. The leaked tokens also keep logon sessions alive (277 stale sessions of a sandbox account whose commands run through MSYS).

An xperf object-reference trace shows that the leaked token is the primary token of the process that calls exec(). Its one unreleased reference is taken in conhost.exe by ConsoleControl::NotifyConsoleApplication (presumably NtUserConsoleControl, → win32kfull.sys) when that process connects to the console. The same reference is released for the exec'd program. So the bug is in Windows. But it is triggered specifically by how the MSYS2 runtime hands the console over during exec, so it may be avoidable in the runtime, and you will know best whether it is. Plain CreateProcess in every variant I tried does not leak.

Repository

No response

Activity

  1. dscho commented on Oct 20, 2026

    @dscho
    Member

    I have a tentative fix in hand: cygwingitgadget/cygwin#15. If you have the time, please feel free to build an MSYS2 runtime with that patch and to test whether this fixes your issue.

  2. dscho commented on Oct 20, 2026

    @dscho
    Member

    @nonacc actually, I forgot that I had already ran a tentative build of the MSYS2 runtime with this fix: https://github.com/dscho/Cygwin-msys2-fork/actions/runs/38449886036 has an install artifact that contains a fixed msys-2.0.dll. Could you please (temporarily) exchange the copy of that DLL in your setup with that fixed version and verify that the token leak is gone?

  3. martindroneharmony commented on Oct 20, 2026

    @martindroneharmony

    Tested the fixed msys-2.0.dll from https://github.com/dscho/Cygwin-msys2-fork/actions/runs/38449886036 (3.6.11-426a8d2290bd668c0de8b57b3dfea51fdc4b5fc3) on Windows 11 26H2, build 10.0.26300, in a copy of Git for Windows 2.55.0.windows.5 (verified that bash.exe loads the fixed DLL). Unfortunately it does not stop the leak here.

    Method: kernel Token object count via NtQueryObject(ObjectTypesInformation), 1000 runs of /usr/bin/true per measurement, each run in its own console window, 3 rounds alternating:

    • runtime 3.6.10 (installed): +1003, +1144, +496
    • runtime 3.6.11 with the fix: +1273, +1096, +1603
    • control, cmd.exe starting cmd /c exit 1000 times: +11, -12, +207
    • idle 60 s (background noise from other processes): -43, +650, +227

    Note: without a console attached (bash started from a process that has none), neither runtime leaks, which matches the conhost root cause described above.

    Happy to test another build.

  4. dscho commented on Oct 21, 2026

    @dscho
    Member

    @martindroneharmony ah, I missed something important. You ran /usr/bin/true 1,000 times directly in separate console windows. That does not exercise exec() (_P_OVERLAY), so our fix in spawn.cc never executed.

    Even more importantly, with the help of GPT-6.1 Sol I found https://learn.microsoft.com/en-gb/answers/questions/5952454/windows-11-25h2-26200-8655-unbounded-paged-pool-le, which seems to talk precisely about the same problem. And it suggests a work-around: Setting SPI_SETFOREGROUNDLOCKTIMEOUT to 0 via SystemParametersInfoW(), to bypass _CheckAllowForeground () and thereby stopping the leak. Here is a PowerShell command to do that, and according to my research you'd only need to run this once per logon session:

    Add-Type @'
    using System; using System.Runtime.InteropServices;
    public static class SPIHelper {
      [DllImport("user32.dll", SetLastError = true)]
      public static extern bool SystemParametersInfoW(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
      public static uint GetFLT() {
        uint flt = 0;
        IntPtr p = Marshal.AllocHGlobal(4);
        try {
          SystemParametersInfoW(0x2000 /* SPI_GETFOREGROUNDLOCKTIMEOUT */, 0, p, 0);
          flt = (uint)Marshal.ReadInt32(p);
        } finally { Marshal.FreeHGlobal(p); }
        return flt;
      }
      public static bool SetFLT(uint val) {
        return SystemParametersInfoW(0x2001 /* SPI_SETFOREGROUNDLOCKTIMEOUT */, 0, (IntPtr)val, 2 /* SPIF_SENDCHANGE */);
      }
    }
    '@ ; $res = [SPIHelper]::SetFLT(0) ; 'SetFLT result: {0}; new FLT: {1}' -f $res, [SPIHelper]::GetFLT()

    Note that setting ForegroundLockTimeout to 0 disables Windows' focus-stealing prevention, so this is merely a work-around:

    1. Focus Stealing: Any background application or spawned process can immediately pop to the front and steal keyboard focus while you are typing in another window (rather than flashing on the taskbar).
    2. Misdirected Keystrokes: Typing into an active window (editor, terminal) might accidentally send keystrokes or enter keys into a background window that suddenly takes the foreground.

    For headless machines, CI runners, or dedicated developer/agent VMs, this side effect is harmless; on interactive desktop machines, unexpected pop-ups may interrupt typing.

    @martindroneharmony could you try this work-around and report whether it fixes the token leak for you?

  5. martindroneharmony commented on Oct 21, 2026

    @martindroneharmony

    @dscho thank you, that was it.

    First a clarification: my earlier wording was misleading. Each measurement was one console window running a bash loop of 1,000 /usr/bin/true starts (for i in $(seq 1000); do /usr/bin/true; done), not 1,000 separate windows. So every start was a fork() followed by exec() inside bash.

    The work-around stops the leak here (Windows 11 26H2, build 10.0.26300, Git for Windows 2.55.0.windows.5). Kernel Token objects per 1,000 starts, 3 rounds each:

    ForegroundLockTimeout at its value before (2147483647):

    • runtime 3.6.10 (installed): +1390, +1138, +955
    • runtime 3.6.11 with your fix: +80, +1064, +1146
    • control, cmd.exe: -11, +98, +63
    • idle 60 s: -12, +379, +149

    ForegroundLockTimeout set to 0 with your snippet (run from a foreground window; from a background process the call has no effect):

    • runtime 3.6.10 (installed): -83, +44, +55 and +10, -34, +28
    • runtime 3.6.11 with your fix: -8, +48, -49
    • control, cmd.exe: -7, +4, +55
    • idle 60 s: -50, +18, +51

    Everything is within the idle noise with the timeout at 0. The bash loops also ran faster (about 50 s instead of 85 s per 1,000 starts).

    One thing that may matter: the live value on this machine was 2147483647, while HKCU\Control Panel\Desktop\ForegroundLockTimeout holds the default 200000, so some program raised it at runtime. I don't know yet which one.

    Happy to test more.

    Update: the reason the value is 2147483647. Others report that on Windows 11 the live value is overwritten at every logon by win32kfull!LoadCPUserPreferences, which reads the registry and then stores 0x7FFFFFFF unconditionally, so the registry setting (200000 here, 0 for them) is ignored: bentoner/windows-token-leak#1 (26200.9550 and 26300.9550, with disassembly; the leak itself is traced there to a PsReferencePrimaryToken without a matching dereference in CForegroundLaunch::_CheckAllowForeground). That matches this machine: registry 200000, live value 2147483647.

    So the work-around has to be re-applied after every logon, by a process that may take the foreground (from a background process the call is refused while the value is 2147483647). A non-elevated scheduled task at logon works here: it set the value from 2147483647 to 0 in a test run. Leaked tokens are only freed by a restart (with Fast Startup, "Shut down" keeps them).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions