On Windows 11, every time I open and close a File Explorer window, it leaks about 140 Key handles to HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore
and some registry keys under CommandStore\shell
, like HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.properties
.
I noticed this issue at the end of last year and reproduced it on both Windows 11 23H2 and 24H2.
The leak source
Thanks to the awesome ETWAnalyzer, finding handle leaks on Windows is so much easier (I was spending hours and hours switching between Handle, x64dbg, IDA Pro, and WPA before when tracking down other handle leaks in Windows). A typical stack trace of the leaked Key handle is the following:
Id: 179797 Object: 0xFFFFE6815DEB10E0 \REGISTRY\MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore Lifetime: 9999.000000 s Type: Key Create+Duplicate-Close: 1+0-0 = 1
10:54:32.794939 13428/22468 HandleCreate 0xBC4C explorer.exe Stack: ntoskrnl.exe!ObpCreateHandle
ntoskrnl.exe!ObOpenObjectByNameEx
ntoskrnl.exe!CmOpenKey
ntoskrnl.exe!NtOpenKeyEx
ntoskrnl.exe!KiSystemServiceCopyEnd
ntdll.dll!ZwOpenKeyEx
KernelBase.dll!LocalBaseRegOpenKey
KernelBase.dll!RegOpenKeyExInternalW
KernelBase.dll!RegOpenKeyExW
SHCore.dll!QuerySourceCreateFromKeyEx
shlwapi.dll!QuerySourceCreateFromKey
windows.storage.dll!AssocElemCreateForKey
windows.storage.dll!RegDataDrivenCommand::SetKeyAndName
windows.storage.dll!GetRegDataDrivenCommandWithAssociation
windows.storage.dll!CCommandStore::GetCanonicalCommand
Windows.UI.FileExplorer.dll!GetCanonicalCommand
Windows.UI.FileExplorer.dll!winrt::WindowsUdk::UI::implementation::FileExplorerMenuItem::CalculateMembers
Windows.UI.FileExplorer.dll!winrt::WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource::GetCommand
Windows.UI.FileExplorer.dll!winrt::impl::produce<winrt::WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource,winrt::WindowsUdk::UI::Shell::IFileExplorerCommandSource>::GetCommand
FileExplorerExtensions.dll!winrt::impl::consume_Windows_Globalization_DateTimeFormatting_IDateTimeFormatterFactory<winrt::Windows::Globalization::DateTimeFormatting::IDateTimeFormatterFactory>::CreateDateTimeFormatter
FileExplorerExtensions.dll!winrt::FileExplorerExtensions::implementation::CommandBarControlVM::GetCommandBarItemVMs
FileExplorerExtensions.dll!winrt::impl::produce<winrt::FileExplorerExtensions::implementation::CommandBarControlVM,winrt::FileExplorerExtensions::ICommandBarControlVM>::GetCommandBarItemVMs
FileExplorerExtensions.dll!winrt::impl::consume_WindowsUdk_Storage_Search_ISearchFolderHelperStatics<winrt::WindowsUdk::Storage::Search::ISearchFolderHelperStatics>::GetSearchScopeFromSearchResultFolder
FileExplorerExtensions.dll!winrt::FileExplorerExtensions::implementation::CommandBarControl::CommandBarControl
FileExplorerExtensions.dll!winrt::make<winrt::FileExplorerExtensions::implementation::CommandBarControl,winrt::WindowsUdk::UI::Shell::FileExplorerCommandBarController const & ptr64>
FileExplorerExtensions.dll!winrt::FileExplorerExtensions::implementation::CommandBarExtension::GetMicrosoftUIXamlView
FileExplorerExtensions.dll!winrt::impl::produce<winrt::FileExplorerExtensions::factory_implementation::CommandBarExtension,winrt::FileExplorerExtensions::ICommandBarExtensionStatics>::GetMicrosoftUIXamlView
Windows.UI.FileExplorer.dll!CommandBarViewAdapter::InitializeCommandBar
Windows.UI.FileExplorer.dll!XamlIslandViewAdapter::Initialize
Windows.UI.FileExplorer.dll!XamlIslandViewAdapter::Create
ExplorerFrame.dll!<lambda_403132f2965820434027b4770b153134>::operator()
ExplorerFrame.dll!CExplorerFrame::_OnCreate
ExplorerFrame.dll!CExplorerFrame::v_WndProc
ExplorerFrame.dll!CImpWndProc::s_WndProc
user32.dll!UserCallWinProcCheckWow
user32.dll!DispatchClientMessage
user32.dll!__fnINLPCREATESTRUCT
ntdll.dll!KiUserCallbackDispatcherContinue
ntoskrnl.exe!KeUserModeCallback
win32kfull.sys!SfnINLPCREATESTRUCT
win32kfull.sys!xxxSendMessageToClient
win32kfull.sys!xxxSendTransformableMessageTimeout
win32kfull.sys!xxxSendMessage
win32kfull.sys!xxxCreateWindowEx
win32kfull.sys!NtUserCreateWindowEx
win32k.sys!NtUserCreateWindowEx
ntoskrnl.exe!KiSystemServiceCopyEnd
win32u.dll!NtUserCreateWindowEx
user32.dll!VerNtUserCreateWindowEx
user32.dll!CreateWindowInternal
user32.dll!CreateWindowExW
ExplorerFrame.dll!SHNoFusionCreateWindowEx
ExplorerFrame.dll!CExplorerFrame::CreateFrameWindow
ExplorerFrame.dll!CExplorerFrame::CreateInstance
ExplorerFrame.dll!BrowserThreadProc
ExplorerFrame.dll!BrowserNewThreadProc
SHCore.dll!_WrapperThreadProc
kernel32.dll!BaseThreadInitThunk
ntdll.dll!RtlUserThreadStart
Finding the handle leak is easy, but what is the root cause of this leak?
Tracking it down
I don’t want to fix this handle leak by simply closing the opened (and leaked) registry key after Windows.UI.FileExplorer.dll!GetCanonicalCommand()
finishes using it.
I am not sure if that fix will work, but it is not addressing the root cause.
The leaked Key handle is associated with a RegDataDrivenCommand
COM object.
When I open a File Explorer window, many RegDataDrivenCommand
COM objects are created but not released when I close the File Explorer window.
If I force-release these COM objects (by manually decreasing their refs in a debugger and then calling ->Release()
on them), explorer.exe
crashes at a later time due to heap memory corruption.
The typical debugging route is tracking the COM object dependency tree from RegDataDrivenCommand
to find two COM objects, CoA
and CoB
, where CoA
is created by CoB
; when I close the File Explorer window, CoB
is released but CoA
is not.
I spent some time understanding how XamlIslandViewAdapter
and CommandBarViewAdapter
work, which turned out unnecessary.
It was actually not so hard before I figured out what is happening.
CoA
, in this case, is FileExplorerMenuItem
.
The expected reference counting
Function WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource::GetCommand()
first creates a FileExplorerMenuItem
by calling ptr = new(304)
then obj = impl::heap_implements<FileExplorerMenuItem>(ptr, ...)
.
Its reference count is 1
right after COM object creation.
Then before the node is added to the tree of FileExplorerMenuItem
, the function calls obj->AddRef()
to increment its reference count.
The reference count is 2
at this point.
Then function call com_ptr<WindowsUdk::UI::implementation::FileExplorerMenuItem>::as<WindowsUdk::UI::MenuItem>()
converts obj
to a WindowsUdk::UI::MenuItem
COM object.
At this point, the reference count of obj
is 3
.
Then it immediately calls com_ptr<CModernSearchBox>::unconditional_release_ref(&obj)
before returning from GetCommand()
.
This is to release the very initial reference that obj
holds right after creation.
The reference count of obj
is 2
right before returning.
At this point, there are two other COM objects referencing obj
:
The FileExplorerMenuItem
tree (let’s call it RefA
), and the MenuItem
COM object (RefB
).
It is worth noting that the MenuItem
object is on the stack of the caller of GetCommand()
, which is WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource::GetCommand
.
The last piece of the puzzle
By setting a hardware breakpoint on the COM reference counter field of obj
(&obj + 0x18
iirc), we can see where it is released.
Once I close the File Explorer window, the reference counter of obj
is only decremented once, and the call stack indicates that the dec-ref operation happens when the corresponding tree node was removed from the tree.
This means the other reference, RefB
, is leaked.
Let’s take a closer look at how RefB
is used.
The pseudocode of WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource::GetCommand
looks like below.
Note that I am not super familiar with how COM objects work, so this is my best-effort decompilation.
unsigned long long winrt::impl::produce<winrt::WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource,winrt::WindowsUdk::UI::Shell::IFileExplorerCommandSource>::GetCommand(
void* this,
winrt::impl *impl,
MenuItem *CoMenuItem)
{
struct MenuItem tmp;
struct winrt::hstring *tmphstring;
winrt::impl *tmp_impl = impl;
*CoMenuItem = NULL;
tmphstring = winrt::WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource::GetCommand(
(winrt::WindowsUdk::UI::Shell::implementation::FileExplorerCommandSource *)(this - 40),
&tmp,
&tmp_impl);
*CoMenuItem = tmphstring;
*tmphstring = NULL;
if (tmp)
winrt::com_ptr<winrt::impl::IGlobalInterfaceTable>::unconditional_release_ref(&tmp);
return NULL;
}
tmp
holds a reference to the FileExplorerMenuItem
(this is the leaked RefB
)
The code looks OK upon my first glance.
Unfortunately, FileExplorerCommandSource::GetCommand()
simply returns its second parameter, which means tmphstring == &tmp
!
Therefore, once *tmphstring
is NULL’ed, the next line if (tmp)
is never satisfied, as such, the unconditional_release_ref()
on tmp
will never execute, leading to the leak of RefB
.
My proposed fix
The fix should probably be something like the following:
...
*CoMenuItem = tmphstring;
// *tmphstring = NULL;
if (tmp)
...
We can patch the process memory by nopping out the line that sets *tmphstring
to NULL:
call ?GetCommand@FileExplorerCommandSource@implementation@Shell@UI@WindowsUdk@winrt@@QEAA?AUMenuItem@456@AEBUhstring@6@@Z
mov rcx, [rax]
and qword ptr [rax], 0 ; nop this instruction
mov [rbx], rcx
After applying this fix, my explorer.exe does not crash and no longer leaks Key handles to HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore
.