Debugging a registry key handle leak in File Explorer (explorer.exe)

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.