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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions