Skip to content

explorer.exe leaks ~155 Token handles and spawns ~260 thread-pool threads on every \\wsl.localhost (Plan9) access #41476

Description

@aploe

Windows Version

Microsoft Windows [Version 10.0.26200.9168]

WSL Version

2.7.12.0

Are you using WSL 1 or WSL 2?

  • WSL 2
  • WSL 1

Kernel Version

6.18.33.2-2

Distro Version

Ubuntu 24.04.4 LTS (default); also registered but stopped: docker-desktop, docker-desktop-data, kali-linux

Other Software

VS Code with Remote-WSL (~30 Code.exe processes connected to the Ubuntu distro); Docker Desktop installed, its distros stopped during all measurements; 64 GB RAM, 16 logical CPUs

Repro Steps

Every time any process opens the Plan9 share \\wsl.localhost\<distro>\..., explorer.exe (the shell process, not the process doing the access) reacts about 15 seconds later with a burst of thread-pool work that leaks handles. Minimal repro from an elevated-or-not PowerShell 7 — the access is done by PowerShell, not by Explorer:

$p = Get-Process explorer | Sort-Object StartTime | Select-Object -First 1   # the shell instance
"before: handles=$($p.HandleCount) threads=$($p.Threads.Count)"

# Any Plan9 access triggers it (an Explorer window on the same path triggers it as well):
Get-ChildItem \\wsl.localhost\Ubuntu\home\<user> | Out-Null

foreach ($i in 1..6) { Start-Sleep 5; $p.Refresh(); "+$($i*5)s: handles=$($p.HandleCount) threads=$($p.Threads.Count)" }

Observed on my machine (three independent runs, identical shape each time):

before:  handles=44024  threads=169
+5s:     handles=44005  threads=170
+10s:    handles=44012  threads=170
+15s:    handles=44033  threads=170
+20s:    handles=46408  threads=389     <-- burst starts ~15-20 s after the access
+25s:    handles=46507  threads=430
+30s:    handles=46507  threads=430

Running wsl.exe -e true (no Plan9 access) does not trigger it. Opening an Explorer window on the WSL path, or reading the path from another process, does.

The extra threads are idle thread-pool workers (ntdll!TppWorkerThread waiting in ZwWaitForWorkViaWorkerFactory) and retire after a few minutes. The extra handles do not go away.

Expected Behavior

Accessing a WSL path should not change the handle count of explorer.exe permanently, and should not create hundreds of threads in the shell process.

Actual Behavior

See above: +155 Token handles (all to explorer.exe's own token) and +~260 threads in explorer.exe per Plan9 access, accumulating until explorer.exe is restarted.

What is leaked (handle analysis)

Using NtQuerySystemInformation(SystemExtendedHandleInformation) + DuplicateHandle + NtQueryObject / GetTokenInformation on the explorer.exe handle table:

  • Per event: +155 handles of type Token (sometimes 2-3x that when several accesses coincide), +~2,500 handles in total (Event / WaitCompletionPacket / Key etc.), the latter mostly transient.
  • After 6 days of uptime: 32,772 Token handles out of 41,000 total handles in explorer.exe (a fresh explorer.exe has a few dozen).
  • 32,763 of the 32,772 Token handles reference one and the same token object: explorer.exe's own primary token (TokenStatistics.TokenId identical for all of them; TokenUser = the interactive user, TokenSource = User32, integrity level Medium, TokenIsAppContainer = false, all in the same logon session).
    -> This is the signature of OpenProcessToken(GetCurrentProcess(), ...) (or OpenThreadToken) being called once per work item without a matching CloseHandle.
  • The count grows in quanta of ~155 per trigger, which suggests one leaked token handle per parallel work item of the burst.

The explorer.exe main thread is idle (NtUserWaitMessage) during the burst; the work happens on thread-pool threads. Modules loaded in explorer.exe that are involved in the WSL integration: windows.storage.dll (Linux shell folder, CLSID {B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}), wslsupport.dll, p9np.dll.

Correlated event-log marker

Each burst is accompanied by a warning in the Application log, provider WSL:

Unknown key 'automount.crossDistro' in /etc/wsl.conf:3

That key comes from the /etc/wsl.conf that Docker Desktop writes into its own distro (see #12727); my default distro's wsl.conf does not contain it. So the shell's refresh apparently enumerates all registered distros (including stopped Docker/Kali ones) and reads their configuration on every trigger. The warning itself is harmless; it is just a convenient timestamp for the trigger.

Real-world impact

  • VS Code Remote-WSL touches \\wsl.localhost paths regularly, so with a normal development session the leak fires roughly every 10-15 minutes without any user action: ~4,500 leaked Token handles per day, ~34,000 after a week, until explorer.exe is restarted.
  • Also triggered by user actions that make the shell resolve a WSL path (e.g. Quick Access / Recent Items entries pointing to \\wsl.localhost\..., or simply opening an Explorer window on a WSL folder).
  • Each burst spawns ~260 thread-pool threads in explorer.exe (thread count 150 -> 430) with ~1-5 s of CPU on the shell process.

Workarounds tried

  • Restarting explorer.exe clears the accumulated handles (the leak then starts over).
  • Hiding the "Linux" node from the navigation pane via
    HKCU\Software\Classes\CLSID\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6} -> System.IsPinnedToNameSpaceTree = 0
    (currently being evaluated; will report back whether it stops the burst).
  • Removing Recent Items / Quick Access entries that point to \\wsl.localhost removes the user-triggered part but not the VS Code / background-triggered part.

Diagnostic Logs

Can provide collect-wsl-logs.ps1 output and the raw handle/thread-stack dumps on request.

Activity

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

    file systemwsl2Issue/feature applies to WSL 2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions