In this article, you will see how to trace GDI handle leaks with Windows Debugger and also learn how to fix them.
Introduction
This article is a walkthrough on finding and fixing GDI handle leaks with Windows Debugger. Windows Debugger should be a last resort, firstly do a search for BeginPaint()/EndPaint()
in your entire codebase and examine the GDI code between these two function calls for undeleted handles and delete them.
Using Task Manager, we can add a GDI Objects
column on the Details
tab page to get the count of currently opened GDI handles by each process. One process can have a maximum of 10000 opened GDI handles. Systemwide limit for all processes is 65535. Right-click on the headers to select the columns to be shown.
Check GDI Objects option to add to the Details tab and close the dialog.
In my application, UI language changed 20 times can result in an unresponsive application because it has reached the limit of 10000 opened GDI handles. All the GDI objects created in between BeginPaint()/EndPaint()
are double-checked to be released. Clearly, the leaks are happening elsewhere. To narrow down which type of GDI objects are leaking, we can use GDIView (UI-based) or GDIInquiry.exe (console-based). Given below are snapshots by GDIInquiry.exe before and after a UI language change.
GDIInquiry.exe PID
GDIInquiry.exe needs one argument which is the process ID that can be retrieved from Task Manager.
C:\temp>GDIInquiry.exe 7168
Bitmap:3
Brush:6
DeviceContext:20
Font:5
Palette:0
Pen:0
Region:5
Unknown:0
GDITotal:39
We can see that the font object count increased by 50
with every language change while other GDI objects remained unchanged.
C:\temp>GDIInquiry.exe 7168
Bitmap:3
Brush:6
DeviceContext:20
Font:55
Palette:0
Pen:0
Region:5
Unknown:0
GDITotal:89
After finding a list of the font creation functions on this MSDN page, I attached WinDbg from the file menu to my process with its PID and then put breakpoints with bp
command on those functions. For your information, gdi32
is the name of the DLL without its file extension. Please note GDI handles can also be returned by functions located in the USER32.DLL, be sure to check the MSDN. In my case, I am only concerned with font creation functions in GDI32.DLL.
bp gdi32!createfonta
bp gdi32!createfontw
bp gdi32!createfontindirecta
bp gdi32!createfontindirectw
But WinDbg cannot locate those functions. Then I resort to x
command for a case-insensitive search for the symbolic names beginning with createfont
using the asterisk as a wildcard.
x gdi32!createfont*
The x
command yields the following search results.
00007ffc`481f1210 GDI32!CreateFontWStub (void)
00007ffc`481f1630 GDI32!CreateFontIndirectW (void)
00007ffc`481fcf30 GDI32!CreateFontIndirectAStub (CreateFontIndirectAStub)
00007ffc`481fce50 GDI32!CreateFontAStub (CreateFontAStub)
00007ffc`481f87a0 GDI32!CreateFontIndirectExA (CreateFontIndirectExA)
00007ffc`481f87c0 GDI32!CreateFontIndirectExW (CreateFontIndirectExW)
Then I proceed to put a breakpoint on every single one of them. Additional command can be specified within quotes thereafter, which is executed after the breakpoint is hit. More than 1 command is separated with a semicolon. "kp"
instructs the WinDbg to show call stack and parameters to each function There is another kp
version that ends with an uppercase P letter(kP
): the main difference is each parameter is displayed on its own line.
bp GDI32!CreateFontWStub "kp"
bp GDI32!CreateFontIndirectW "kp"
bp GDI32!CreateFontIndirectAStub "kp"
bp GDI32!CreateFontAStub "kp"
bp GDI32!CreateFontIndirectExA "kp"
bp GDI32!CreateFontIndirectExW "kp"
It could be pretty tiresome to set more than a few breakpoints. Fortunately, a bm
command, to relieve us of this thankless chore, can be used to set multiple breakpoints at once using wildcard.
bm gdi32!createfont* "kp"
After setting the required breakpoints, type g
command in WinDbg to let program execution resume. Type qd
to detach the program and quit WinDbg after debugging is done.
g
Then proceed to do whatever action to produce the GDI leak, in my case, is changing UI language. Note here that you may have to break a few times before you encounter a real leak, meaning font creation without deletion. Soon enough, WinDbg breaks on CreateFontIndirectW
but I am not at liberty to show my call stack publicly, instead, I'll explain the cause of the leak: In my CEdit
derived class, a HFONT
member is created to be set as the current font using SetFont()
without calling DeleteObject()
in its destructor. After fixing this leak, my Font count stabilized but "All GDI" count in GDIView and GDI Object count in Task Manager still grew rapidly at each language change.
The GDIView page states Notice: If you have a problem that the 'All GDI' value is increased, while there is no leak with the other GDI values, it means that you probably have a leak in the creation of icons or cursors (Icons and cursors are created without destroying them later).. So a review of all the icon and cursor creation code is done. The leak is from the LoadImage
which is called multiple times. The chosen fix is not to delete the icon but to load the icon as shared (LR_SHARED
), meaning only one icon instance will be loaded and released at the end of the application run.
WNDCLASSEXW wnd = {0};
....
wnd.hIconSm = (HICON)::LoadImage(thisInstance, MAKEINTRESOURCE(1),
IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
The fix is to add LR_SHARED
to be digital OR
with the last argument.
wnd.hIconSm = (HICON)::LoadImage(thisInstance, MAKEINTRESOURCE(1),
IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR | LR_SHARED);
Why !htrace Command Cannot Be Used?
Readers may be wondering why I did not use !htrace
command which is specifically for tracing handle leaks. !htrace
works only with true kernel handles. You cannot use the !htrace
extension command to track down the source of graphics device interface (GDI) handle leaks.
Conclusion
This article did a walkthrough on finding font and icon leaks. Another common type of GDI leak is bitmap which I did not cover. Make sure bitmap handles returned by GetIconInfo()/GetIconInfoExW()/GetIconInfoExA()
from USER32.DLL are deleted. If you find this article useful, please consider an upvote to encourage me to write more on this subject.
History
- 25th April, 2023: Added a note about systemwide GDI handle limit is 65535.
- 23rd June, 2020: First release