explorer.exe leaks ~155 Token handles and spawns ~260 thread-pool threads on every \\wsl.localhost (Plan9) access
- Dominant language
- C++
- Stars
- 33.7k
- Forks
- 1.8k
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 116
Description
### Windows Version
Microsoft Windows [Version 10.0.26200.9168]
### WSL Version
2.7.12.0
### Are you using WSL 1 or WSL 2?
- [x] 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\\...`, **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:
```powershell
$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\ | 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.
Contributor guide
Research direction
Start by reproducing the Plan9 access with the supplied PowerShell commands and collect the requested handle and thread-stack diagnostics. Investigate the mentioned Windows integration modules—windows.storage.dll, wslsupport.dll, and p9np.dll—because no repository source file or test is identified. Done means locating and fixing the leak and thread burst, with repeated \\wsl.localhost access leaving explorer.exe without permanent handle growth.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- powershell
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100