[BUG] Windows MSIX Desktop: every process a session spawns leaks kernel nonpaged pool (NtFC) when it opens a path through a junction under AppData (bindflt.sys bug)

Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 4 comments · opened Sep 24, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

On Windows 11 with the MSIX Claude Desktop, every process spawned by a Claude Code session leaks NtFC nonpaged pool whenever it opens a path through a junction or symlink located under %APPDATA% or %LOCALAPPDATA%. That includes bash, pwsh, git, node, python and hooks. It takes nothing unusual: a uv-managed Python, nvm-windows or pnpm globals put such junctions on the hottest paths.

On a 16 GB machine this reached 2.7 GB in 9 hours of normal agent work. It is freed only by a reboot.

The root cause is a Windows bug in bindflt.sys, the filter behind MSIX AppData virtualisation, reported to Microsoft as Feedback Hub https://aka.ms/AA13ld3q. It stays invisible until Claude Desktop runs every agent child process inside that virtualisation.

Related:

  • #95489 is the same tag and the same MSIX AppData redirect, with a different trigger (a retry loop).
  • #94198 and #55361 are NtFC growth with Cowork.
  • #96722 has FMfn, File and IoFE.

Environment

  • Windows 11 Home 25H2, build 26200.9457 (ntfs.sys 10.0.26100.9444)
  • Claude Desktop MSIX 2.7032.0.0 (Claude_pzs8sxrjxfjjc), Claude Code 2.1.280
  • Only Microsoft file-system minifilters loaded

What it costs in practice (measured per operation, in-session)

  • uv-managed Python. uv's minor-version junction %APPDATA%\uv\python\cpython-3.12-windows-x86_64-none is used by the ~\.local\bin trampolines and by venvs whose home points there. Every stdlib import crosses it: 65 blocks (22.9 KB) per python -c pass, and about 1,250 blocks (440 KB) per start of a Flask/psycopg app.
  • The security-guidance plugin's hooks start python3.12 through that junction: about 600 blocks (200 KB) per hook run. A Bash tool command containing $(...) fires 7 of them, about 1.5 MB.
  • PATH entries that are AppData junctions (nvm-windows .nodejs, Codex bin): 2 × 160 B per command lookup that reaches them.
  • pnpm globals (%LOCALAPPDATA%\pnpm\global, a symlink farm): about 25 blocks per sfw/socket run.
  • It is a true leak, not a cache. 20,000 opens added exactly +20,000 outstanding blocks, and nothing was freed after 10 minutes, on process exit, or on deleting the fixture.

Root cause (WPR pool trace with stacks, tag NtFC)

  • The leaking helper. bindflt.sys (TimeDateStamp 0x4E2AE6D0) has an IRP_MJ_CREATE pre-operation callback that calls a probe helper (RVA 0x2390, return address bindflt+0x2477). The helper re-opens the name in the host silo via FltCreateFileEx2(FILE_READ_ATTRIBUTES, FILE_OPEN, IO_STOP_ON_SYMLINK).
  • What it drops. On STATUS_STOPPED_ON_SYMLINK it returns success without freeing the REPARSE_DATA_BUFFER that NTFS returned in its stack-local IoStatusBlock.Information. NTFS allocates that buffer at Ntfs+0x144BFD.
  • The sibling path is correct. The re-issue path in the same callback (bindflt+0x2BBD) hands its buffer on as TagData with STATUS_REPARSE, and IopParseDevice frees it.
  • The trace counts. 2,902 of 2,902 unfreed nonpaged NtFC blocks carry the probe frame, and 0 of 11,098 freed blocks do. No events were lost: the kernel's own NtFC counters moved by exactly the traced totals.
  • Not involved. NTFS, FltMgr and the I/O manager behave correctly, and UnionFS, wcifs and WdFilter appear on no NtFC stack. Out of context, the same open is a plain top-level create with no bindflt frames.

Workarounds that measured 0

  • uv: set UV_PYTHON_INSTALL_DIR outside AppData (e.g. %USERPROFILE%\.uv\python), reinstall the Pythons and rebuild the venvs and uv tools. Python startup went from 65 blocks to 0, and the hook from about 600 to 0.
  • pnpm: set PNPM_HOME outside AppData and reinstall the globals. Note that pnpm's global config stays in %LOCALAPPDATA%\pnpm\config.
  • PATH: replace AppData junction entries with their targets, e.g. nvm-windows .nodejs → .shim. For the Codex CLI, set CODEX_INSTALL_DIR outside AppData, otherwise every codex update re-prepends its AppData bin junction.
  • Anything launched outside Claude Desktop's package context (a normal terminal) is unaffected.

After these changes, NtFC stayed at 0 outstanding blocks through normal use.

Possible fixes

  1. Consider launching Claude Code sessions outside the package's AppData virtualisation. One route is desktop6:FileSystemWriteVirtualization / flexible virtualisation opt-outs. Another is starting the engine through a broker outside the package context. Either would also end the split-AppData surprises, where config written by agent tools, such as a GnuPG home, lands only in Packages\Claude_…\LocalCache.
  2. Until then, document the leak and the workarounds for Windows users of Claude Desktop, especially the uv one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

What Should Happen?

No memory leak: NtFC staying at 0 outstanding blocks through normal use.

Error Messages/Logs

Steps to Reproduce

Repro (unelevated, about 1 minute, creates nothing)

The script opens the OS-created junction %LOCALAPPDATA%\Microsoft\Windows\Temporary Internet Files (→ INetCache), which every Windows profile has, 2,000 times. It compares that with opening the target directly, reading the NtFC counters via NtQuerySystemInformation(SystemPoolTagInformation). Run it once from a Claude Code session in Claude Desktop (PowerShell tool, or powershell -File from the Bash tool) and once from a normal PowerShell window.

<details>
<summary>ntfc-appdata-junction-repro.ps1</summary>

# NtFC nonpaged-pool leak repro: opens through a junction under %LOCALAPPDATA% from an MSIX-packaged app's
# child process (e.g. a Claude Code session in Claude Desktop) leak one NTFS reparse buffer each (bindflt.sys).
# Uses the OS-created junction "%LOCALAPPDATA%\Microsoft\Windows\Temporary Internet Files" -> INetCache, which
# every Windows profile has, so nothing is created or deleted. Unelevated; Windows PowerShell 5.1 or pwsh 7.
# Run it once inside a Claude Desktop session and once from a normal PowerShell window, and compare.
# Each in-session run leaks roughly 0.5 MB of nonpaged pool until reboot.
param([int] $N = 2000)

Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public static class NtfcRepro {
    [DllImport("ntdll.dll")]
    static extern int NtQuerySystemInformation(int infoClass, IntPtr buffer, int length, out int returned);
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr sa, uint disposition, uint flags, IntPtr template);

    // SystemPoolTagInformation (22): ULONG Count, then 40-byte SYSTEM_POOLTAG entries from offset 8 on x64.
    public static long[] Counters() {           // { NonPagedAllocs, NonPagedFrees, NonPagedUsed } for tag "NtFC"
        int length = 1 << 20, returned, status;
        while (true) {
            IntPtr buffer = Marshal.AllocHGlobal(length);
            try {
                status = NtQuerySystemInformation(22, buffer, length, out returned);
                if (status == unchecked((int)0xC0000004)) { length = Math.Max(length * 2, returned + 65536); continue; }
                if (status != 0) throw new Exception("NtQuerySystemInformation: 0x" + status.ToString("X8"));
                int count = Marshal.ReadInt32(buffer);
                for (int i = 0; i < count; i++) {
                    IntPtr entry = IntPtr.Add(buffer, 8 + i * 40);
                    if (Marshal.ReadInt32(entry) == 0x4346744E)   // 'N','t','F','C' little-endian, case-sensitive
                        return new long[] { (uint)Marshal.ReadInt32(entry, 24), (uint)Marshal.ReadInt32(entry, 28), Marshal.ReadInt64(entry, 32) };
                }
                return new long[] { 0, 0, 0 };
            } finally { Marshal.FreeHGlobal(buffer); }
        }
    }

    public static int Open(string path, int n) {  // FILE_READ_ATTRIBUTES, share all, OPEN_EXISTING, BACKUP_SEMANTICS
        int failures = 0;
        for (int i = 0; i < n; i++)
            using (SafeFileHandle h = CreateFileW(path, 0x80, 7, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero))
                if (h.IsInvalid) failures++;
        return failures;
    }
}
'@

function Measure-Opens([string] $Label, [string] $Path) {
    [void] [NtfcRepro]::Open($Path, 50)                          # warm-up
    $before = [NtfcRepro]::Counters()
    $failures = [NtfcRepro]::Open($Path, $N)
    $after = [NtfcRepro]::Counters()
    $allocs = $after[0] - $before[0]; $frees = $after[1] - $before[1]; $bytes = $after[2] - $before[2]
    '{0,-34} {1} opens ({2} failed): NtFC allocs/op {3:N2}  frees/op {4:N2}  leaked {5:N2} blocks/op, {6:N0} bytes total' -f `
        $Label, $N, $failures, ($allocs / $N), ($frees / $N), (($allocs - $frees) / $N), $bytes
}

$junction = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\Temporary Internet Files'
$target   = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\INetCache'
Measure-Opens 'through the AppData junction' $junction
Measure-Opens 'junction target directly (control)' $target

</details>

Result on my machine:

| Where it ran | Through the AppData junction | Target directly (control) |
|---|---|---|
| Claude Code session inside Claude Desktop | 2.00 allocs / 1.00 frees per open → 1 block (272 B) leaked per open, 544,000 B per run | 0 / 0 |
| Normal PowerShell window | 1.00 / 1.00 per open → 0 leaked | 0 / 0 |

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

Claude Desktop 2.7032.0 (6c468a)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

_No response_

View original on GitHub ↗

3 Comments

mgrm112112-prog · 20 days ago

Hi @peterlau03, I analyzed the critical issue regarding the kernel nonpaged pool leak (NtFC) when processes spawn in MSIX Desktop and traverse junctions under AppData due to the bindflt.sys bug. I've prepared a robust diagnostic and path-sanitization Python script that automatically detects problematic junction links, monitors handle leaks, and provides workaround isolation wrappers. This tool helps pinpoint the exact paths triggering the memory leak and cleans up local environment configurations. You can integrate it directly into your pipeline to prevent memory exhaustion. Let me know if you need further customization!

konrad-rzonca · 17 days ago

Same leak on a second machine, with a few findings that narrow it down.

Environment: Windows 11 Pro 26200.9457, ntfs.sys 10.0.26100.8875, bindflt.sys 10.0.26100.9278, Claude Desktop MSIX 2.9939.2.0, Claude Code engine 2.1.281. Microsoft Defender only.

1. The engines have no package identity and still leak. The Desktop starts each Claude Code engine (%APPDATA%\Claude\claude-code\<version>\claude.exe) without package identity: GetCurrentPackageFullName returns APPMODEL_ERROR_NO_PACKAGE for the engine and every shell under it. Their opens still leak. Here are 5,000 failed opens (File.Exists on a missing file) through %APPDATA%\uv\python\cpython-3.11-windows-x86_64-none, counting NtFC nonpaged allocations minus frees:

| Started from | Blocks kept |
|---|---|
| a shell under a Desktop Code session | 5,003, then 5,000 |
| Win32_Process.Create (WMI) | 3 |
| a pwsh tab opened by wt.exe | 3 |
| Explorer | 0 |

Windows Terminal is an MSIX package too, so the leak follows the Claude package's virtualization, not packaging in general.

2. The manifest leaves file-system write virtualization on. AppxManifest.xml sets desktop6:RegistryWriteVirtualization to disabled, but virtualization:FileSystemWriteVirtualization stays on, with four ExcludedDirectories (Microsoft\Office\16.0\WEF, Claude-3p, Claude-Data, Claude\logs). Agents' tools therefore write %LOCALAPPDATA% into Packages\Claude_pzs8sxrjxfjjc\LocalCache, and processes outside the Desktop see a different AppData. Here, a tool's machine-wide registry file exists twice for that reason.

3. On this build %TEMP% is not affected. Failed opens through a new junction directly under %TEMP%, or under %TEMP%\claude\...\scratchpad, kept 0 blocks. New junctions in %LOCALAPPDATA% and in %APPDATA% kept 2 blocks (320 B) per open. #95489 saw Temp\claude redirected on 26200.9106, where that folder was itself a junction.

4. The block size follows the link's reparse data. That fits the unfreed reparse buffer described above:

  • uv junction, reparse data 0xAE: 208 B blocks
  • legacy %LOCALAPPDATA%\Historia junction, 0xF0: 272 B
  • Temporary Internet Files junction, 0xF8: 272 B
  • a relative pnpm pkg symlink: 2 blocks of 112 B

A successful attributes query on an existing file through the uv junction kept nothing.

5. Impact here. Over 15 hours of multi-agent work the pool grew about 7.6 MB an hour, with bursts up to 4.4 MB a minute. An earlier boot reached 1.33 GB after 3.6 days. Each uv run python -c pass keeps about 73 blocks. With Fast Startup on, only a Restart frees it.

Possible fix on the Desktop side: take the engine process tree out of file-system write virtualization, the way the registry already is (desktop6:FileSystemWriteVirtualization = disabled with the unvirtualizedResources capability the manifest already declares), or start the engines outside the package's virtualization.

raimondasl · 6 days ago

Same leak, reproduced on a newer build: Windows 11 Home 26H2, build 10.0.26300.9550 (ntfs.sys, bindflt.sys and wcifs.sys all 10.0.26100.9549). So it is not fixed in 26H2.

Showing cached comments. Read the full discussion on GitHub ↗