[PATCH] Fix GDI bitmap handle leak in Explorer context menus
TortoiseSVN-dev <[email protected]>
| Newsgroups | gmane.comp.version-control.subversion.tortoisesvn.devel |
|---|---|
| Message-ID | <CA+_7EL2VC3G_n465HL6+yGj98-kqYiea4VsvNKZg3nfP9NCYXQ@mail.gmail.com> |
TortoiseSVN creates a new `HBITMAP` for each classic Explorer context-menu icon via `IconBitmapUtils::IconToBitmapPARGB32()`. The handles are assigned to `MENUITEMINFO::hbmpItem`, but they are not owned by the menu and are never released by either `IconBitmapUtils` or `CShellExt`. The same context-menu code path in TortoiseGit was reproduced at runtime with ETW and Explorer crash dumps: - Explorer terminated in `explorer!CTray::CheckGDIHandleLimit` with exception `0xc0000409`. - The final dump contained 8,945 GDI handles, including 8,347 bitmap/SURFACE handles. - Across 40 classic context-menu opens, the Explorer GDI count increased by 169. - 160 of 177 outstanding type-5 bitmap creation stacks included `TortoiseGit.dll`. - Disabling context-menu icons stopped the growth. TortoiseGit tracking issue with the reproduction details: https://gitlab.com/tortoisegit/tortoisegit/-/work_items/4278 TortoiseSVN 1.14.9 and current trunk r29822 contain the equivalent allocation and ownership pattern. I have not installed TortoiseSVN on the affected machine, so the TortoiseSVN-specific runtime reproduction is not claimed here; the patch addresses the directly observable handle-lifetime defect in the source. The attached patch: - tracks successfully created menu-item `HBITMAP` handles on `CShellExt`; - releases them with `DeleteObject()` when the shell-extension object is destroyed; - avoids creating classic-menu bitmaps when no `HMENU` is supplied for the modern Explorer command path; - avoids setting `MIIM_BITMAP` when no bitmap is used. Verification performed: - based on TortoiseSVN trunk r29822; - changed lines formatted with the repository `.clang-format` using clang-format 15; - unified diff checked by reversing and reapplying it against a clean sparse SVN working copy; - full Windows/MSVC build not run because the available isolated Docker environment is Linux and cannot build the Windows ATL/MFC shell extension. Microsoft's menu-item bitmap example explicitly frees application-created bitmap resources with `DeleteObject()`: https://learn.microsoft.com/en-us/windows/win32/menurc/using-menus Please review the ownership lifetime and, if possible, run the existing context-menu GDI-handle test against a TortoiseSVN build with this patch. -- You received this message because you are subscribed to the Google Groups "TortoiseSVN-dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/tortoisesvn-dev/CA%2B_7EL2VC3G_n465HL6%2ByGj98-kqYiea4VsvNKZg3nfP9NCYXQ%40mail.gmail.com.
TortoiseSVN-GDI-menu-bitmap-leak-r29822.patch
(text/plain, 5.3 KB)
Index: src/TortoiseShell/ContextMenu.cpp
===================================================================
--- src/TortoiseShell/ContextMenu.cpp (revision 29822)
+++ src/TortoiseShell/ContextMenu.cpp (working copy)
@@ -770,6 +770,14 @@
return S_OK;
}
+HBITMAP CShellExt::CreateMenuBitmap(UINT icon)
+{
+ auto bitmap = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, icon);
+ if (bitmap)
+ m_menuBitmaps.push_back(bitmap);
+ return bitmap;
+}
+
void CShellExt::InsertSVNMenu(BOOL isTop, HMENU menu, UINT pos, UINT_PTR id, UINT stringId, UINT icon, UINT idCmdFirst, SVNCommands com, const std::wstring& verb)
{
wchar_t menuTextBuffer[255] = {0};
@@ -805,11 +813,14 @@
MENUITEMINFO menuItemInfo = {0};
menuItemInfo.cbSize = sizeof(menuItemInfo);
- menuItemInfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_BITMAP | MIIM_STRING;
+ menuItemInfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_STRING;
menuItemInfo.fType = MFT_STRING;
menuItemInfo.dwTypeData = menuTextBuffer;
- if (icon)
- menuItemInfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, icon);
+ if (icon && menu)
+ {
+ menuItemInfo.fMask |= MIIM_BITMAP;
+ menuItemInfo.hbmpItem = CreateMenuBitmap(icon);
+ }
menuItemInfo.wID = static_cast<UINT>(id);
if (menu)
InsertMenuItem(menu, pos, TRUE, &menuItemInfo);
@@ -1276,15 +1287,14 @@
myIDMap[idCmd - idCmdFirst] = ShellSubMenu;
myIDMap[idCmd] = ShellSubMenu;
}
- HBITMAP bmp = nullptr;
-
- menuItemInfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_BITMAP | MIIM_STRING;
- if (bShowIcons)
- menuItemInfo.hbmpItem = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, uIcon);
- menuItemInfo.hbmpChecked = bmp;
- menuItemInfo.hbmpUnchecked = bmp;
- menuItemInfo.hSubMenu = subMenu;
- menuItemInfo.wID = idCmd++;
+ menuItemInfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_STRING;
+ if (uIcon && hMenu)
+ {
+ menuItemInfo.fMask |= MIIM_BITMAP;
+ menuItemInfo.hbmpItem = CreateMenuBitmap(uIcon);
+ }
+ menuItemInfo.hSubMenu = subMenu;
+ menuItemInfo.wID = idCmd++;
if (hMenu)
{
InsertMenuItem(hMenu, indexMenu++, TRUE, &menuItemInfo);
@@ -2531,15 +2541,15 @@
{
MENUITEMINFO menuiteminfo = {0};
menuiteminfo.cbSize = sizeof(menuiteminfo);
- menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_BITMAP | MIIM_STRING;
+ menuiteminfo.fMask = MIIM_FTYPE | MIIM_ID | MIIM_SUBMENU | MIIM_DATA | MIIM_STRING;
menuiteminfo.fType = MFT_STRING;
- HBITMAP bmp = m_iconBitmapUtils.IconToBitmapPARGB32(g_hResInst, icon);
- if (icon)
- menuiteminfo.hbmpItem = bmp;
- menuiteminfo.hbmpChecked = bmp;
- menuiteminfo.hbmpUnchecked = bmp;
- menuiteminfo.hSubMenu = ignoreSubMenu;
- menuiteminfo.wID = idCmd;
+ if (icon && hMenu)
+ {
+ menuiteminfo.fMask |= MIIM_BITMAP;
+ menuiteminfo.hbmpItem = CreateMenuBitmap(icon);
+ }
+ menuiteminfo.hSubMenu = ignoreSubMenu;
+ menuiteminfo.wID = idCmd;
SecureZeroMemory(stringTableBuffer, sizeof(stringTableBuffer));
if (itemStates & ITEMIS_IGNORED)
GetMenuTextFromResource(ShellMenuUnIgnoreSub);
Index: src/TortoiseShell/ShellExt.cpp
===================================================================
--- src/TortoiseShell/ShellExt.cpp (revision 29822)
+++ src/TortoiseShell/ShellExt.cpp (working copy)
@@ -58,6 +58,10 @@
CShellExt::~CShellExt()
{
+ // Destroying a menu does not release the bitmaps assigned to its items.
+ for (const auto bitmap : m_menuBitmaps)
+ ::DeleteObject(bitmap);
+
AutoLocker lock(g_csGlobalComGuard);
InterlockedDecrement(&g_cRefThisDll);
g_shellObjects.Erase(this);
Index: src/TortoiseShell/ShellExt.h
===================================================================
--- src/TortoiseShell/ShellExt.h (revision 29822)
+++ src/TortoiseShell/ShellExt.h (working copy)
@@ -225,9 +225,11 @@
CString columnFolder; ///< current folder of ColumnProvider
std::vector<std::pair<std::wstring, std::string>> columnUserProps; ///< user properties of ColumnProvider
std::vector<Microsoft::WRL::ComPtr<CExplorerCommand>> m_explorerCommands;
+ std::vector<HBITMAP> m_menuBitmaps;
#define MAKESTRING(ID) LoadStringEx(g_hResInst, ID, stringTableBuffer, _countof(stringTableBuffer), (WORD)CRegStdDWORD(L"Software\\TortoiseSVN\\LanguageID", MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)))
private:
+ HBITMAP CreateMenuBitmap(UINT icon);
void InsertSVNMenu(BOOL isTop, HMENU menu, UINT pos, UINT_PTR id, UINT stringId, UINT icon, UINT idCmdFirst, SVNCommands com, const std::wstring& verb);
void InsertIgnoreSubmenus(UINT& idCmd, UINT idCmdFirst, HMENU hMenu, HMENU subMenu, UINT& indexMenu, int& indexSubMenu, unsigned __int64 topMenu, bool bShowIcons);
static std::wstring WriteFileListToTempFile(const std::vector<std::wstring>& files, const std::wstring folder);