Windows Version
Microsoft Windows [Version 10.0.26200.9168]
WSL Version
2.7.12.0
Are you using WSL 1 or WSL 2?
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.
Windows Version
Microsoft Windows [Version 10.0.26200.9168]
WSL Version
2.7.12.0
Are you using WSL 1 or WSL 2?
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:Observed on my machine (three independent runs, identical shape each time):
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!TppWorkerThreadwaiting inZwWaitForWorkViaWorkerFactory) 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/GetTokenInformationon the explorer.exe handle table:Token(sometimes 2-3x that when several accesses coincide), +~2,500 handles in total (Event / WaitCompletionPacket / Key etc.), the latter mostly transient.TokenStatistics.TokenIdidentical 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(), ...)(orOpenThreadToken) being called once per work item without a matchingCloseHandle.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:That key comes from the
/etc/wsl.confthat Docker Desktop writes into its own distro (see #12727); my default distro'swsl.confdoes 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
\\wsl.localhostpaths 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.\\wsl.localhost\..., or simply opening an Explorer window on a WSL folder).Workarounds tried
HKCU\Software\Classes\CLSID\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}->System.IsPinnedToNameSpaceTree= 0(currently being evaluated; will report back whether it stops the burst).
\\wsl.localhostremoves the user-triggered part but not the VS Code / background-triggered part.Diagnostic Logs
Can provide
collect-wsl-logs.ps1output and the raw handle/thread-stack dumps on request.