5

I ran an overnight stress test of my application, and when i came in this morning the software had crashed from a Windows error.

The error occurred because the process was obviously out of GDI handles:

Process Explorer

enter image description here

Task Manager

enter image description here

The next thing is to figure out exactly which kind of GDI resource (e.g. Pen, Brush, Bitmap, Font, Region, DC) i'm leaking. For that i turned to NirSoft's GDIView:

enter image description here

  • Pen: 0
  • Ext Pen: 0
  • Brush: 4
  • Bitmap: 35
  • Font: 19
  • Palette: 1
  • Region: 3
  • DC: 11
  • Metafile DC: 0
  • Enhanced Metafile DC: 0
  • Other GDI: 0
  • GDI Total: 0
  • All GDI: 10,000

What could a GDI handle be, one that is not any known GDI type?

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • 1
    This looks to me like a bug in GDIView, or at least something very unexpected. The "GDI Total" is 0, yet clearly the total should not be 0, since there are brushes, bitmaps, fonts, etc., not to mention the 10,000 handles it is counting under "All GDI". I've never used this application, so I'm not sure how it works or what that might signify. I would just use WinDbg, `dt ntdll!_PEB` to determine the location of the GdiSharedHandleTable in the PEB, and then look through the handles it contains. You might also try [Feng Yuan's GDIObj](http://www.fengyuan.com/download.html). – Cody Gray - on strike Dec 30 '16 at 10:45
  • 2
    According to GDIView's [documentation](http://www.nirsoft.net/utils/gdi_handles.html), "GDI Total" *should be* including counts of all the values above it, but it is clearly not, and I have no explanation for that. However, it does say that the count displayed for "All GDI" is obtained by calling `GetGuiResources`, and more interestingly, *"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)."* – Cody Gray - on strike Dec 30 '16 at 10:47
  • That's a strange comment in the documentation. Icons and cursors are [User Objects](https://msdn.microsoft.com/en-us/library/windows/desktop/ms725486.aspx), not [GDI Objects](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724291.aspx). Or am I missing something here? – IInspectable Dec 30 '16 at 15:22
  • The list mentions Metafile DCs but what about HMETAFILE or HENHMETAFILE objects? – jschroedl Jan 03 '17 at 14:56
  • I would look for mistakes that aren't necessarily leaks. For example, calling DestroyObject on a shared GDI object, or forgetting to select a GDI object back out of a DC before destroying it or releasing/destroying the DC. I would also consider temporarily lowering the GDI object quota in order to be able to repro the problem faster. I've also searched for problems like this by disabling UI features and adding them back in until the problem reappeared. – Adrian McCarthy Mar 24 '17 at 20:47

1 Answers1

8

The answer was GDI HFONT handles.

It is a Windows 8 issue that GDIView cannot show the font handles.

I used hooking to intercept every call to:

  • CreateFont
  • DestroyFont

and logged every handle creation, along with its stack trace of when it was allowed. At the end i created a report of all undeleted HFONTs.

How did i do it?

I used the Detours library for Delphi.

Step 1 - For every GDI function there is that creates something, we ask Detours to intercept the function.

  • We pass the address of our replacement function
  • and it returns the address of the original function (so we can call it)
var
    CreateFontIndirectAOriginal: function (const p1: TLogFontA): HFONT; stdcall = nil;
    DeleteObjectOriginal: function (p1: HGDIOBJ): BOOL; stdcall = nil;

CreateFontIndirectAOriginal := InterceptCreate(@CreateFontIndirectA, @CreateFontIndirectAIntercept);
DeleteObjectOriginal := InterceptCreate(@DeleteObject, @DeleteObjectIntercept);

Step 2 - Declare our versions of the GDI functions:

function CreateFontIndirectAIntercept(const p1: TLogFontA): HFONT; stdcall;
begin
    Result := CreateFontIndirectAOriginal(p1);
end;

function DeleteObjectIntercept(p1: HGDIOBJ): BOOL; stdcall;
begin
    Result := DeleteObjectOriginal(p1);
end;

Step 3 - Add code to track every font created by CreateFont, and every font destruction by DestroyObject

function CreateFontIndirectAIntercept(const p1: TLogFontA): HFONT; stdcall;
begin
    Result := TrampolineCreateFontIndirectA(p1);
    GdiLeakTrackerSvc.AddFont(Result);
end;

function DeleteObjectIntercept(p1: HGDIOBJ): BOOL; stdcall;
var
    objType: DWORD;
begin
    objType := GetObjectType(p1);
    Result := TrampolineDeleteObject(p1);

    case objType of
    OBJ_FONT:  GdiLeakTrackerSvc.RemoveObject(p1);
    end;
end;

And then the GdiLeakTrackerSvc service tracks all font creations, font destructions, and can let us know during program shutdown if anything leaked.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219