Last week I found out that I had some Windows ATOM issues before, but this beats them easily was still a draft in stead if in the blog queue.
I got reminded to it by someone asking on Telegram about
“Do I need to use
GarbageCollectAtoms
in Delphi? I used it in delphi 7, but I dont know what is benefit.”.
The short answer is: yes, if your Delphi application does terminate in a way that the Controls unit cannot cleanly unload (and cannot free the Windows atoms) or leaks Windows atoms in a different way. I have been in that situation and that’s why I wrote the above blog post that got published in 2016.
The longer answer is likely no, both the Windows atom and registered Windows message table share a heap and that registered VCL Windows message leaking bug got fixed some 10 years ago in Delphi XE2, see:
- [Wayback] Archive.is: QualityCentral Report #: 90511, Status: Closed, Resource leak caused by RM_GetObjectInstance message
The only place the VCL uses the
RM_GetObjectInstance
message is
inside theObjectFromHWnd()
function, which validates theHWND
belongs to the calling process before issuing the message, so the
registered message ID does not need to be unique across multiple VCL
processes. A statically defined name should suffice so that only 1
message ID need be allocated during Windows’ lifetime - [Wayback] Archive.is: QualityCentral Resolution Entries for Report #90511
Note that there is still a ton of applications built in older versions without Andreas Hausladen’s fix for those older versions (the fix used to be at Wayback: andy.jgknet.de, but is now at Archive.is Downloads – Andy’s Blog and Tools as [Wayback] ControlsAtomFix1.7z).
Let’s explain the long answer
Back in 2016, I already hinted I would like to give more information in Some notes and links: when a filled ATOM table is not caused by your Delphi app.
For non-Delphi applications, it was excessive registration of Windows messages through RegisterWindowMessage
or leaking atoms by not deleting them.
Both make use of the atom tables (atoms directly, registered Windows messages indirectly, just like clipboard formats also use them indirectly) as per [Wayback/Archive.is] About Atom Tables – Win32 apps | Microsoft Docs; look for “Atom table size“.
For Delphi applications, the most common problem was an excessive registration in the global Windows message table by the InitControls
method of the Controls
(for older Delphi versions), or Vcl.Controls
(for newer) unit
: each Delphi application instance would register a new message there.
Before Delphi XE2, the relevant portion of the code used to be this:
var WindowAtom: TAtom; ControlAtom: TAtom; WindowAtomString: string; ControlAtomString: string; RM_GetObjectInstance: DWORD; // registered window message ... procedure InitControls; ... WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]); WindowAtom := GlobalAddAtom(PChar(WindowAtomString)); ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]); ControlAtom := GlobalAddAtom(PChar(ControlAtomString)); RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString)); ...
As of Delphi XE2, it is this:
var WindowAtom: TAtom; ControlAtom: TAtom; WindowAtomString: string; ControlAtomString: string; RM_GetObjectInstance: DWORD; // registered window message ... procedure InitControls; ... WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]); WindowAtom := GlobalAddAtom(PChar(WindowAtomString)); ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]); ControlAtom := GlobalAddAtom(PChar(ControlAtomString)); RM_GetObjectInstance := RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance')); // Do not localize ...
The bold italic bits are different, and this is why:
- Though atoms and registered Windows messages share the same ID space, and
GlobalAddAtom
has an inverseGlobalDeleteAtom
, there is no inverse ofRegisterWindowMessage
. The actual Windows API function names of these are:-
- [Wayback/Archive.is]
GlobalAddAtomA
function (winbase.h) – Win32 apps | Microsoft Docs (for non-unicode Delphi up to Delphi 2009) - [Wayback/Archive.is]
GlobalAddAtomW
function (winbase.h) – Win32 apps | Microsoft Docs (for unicode Delphi starting at Delphi 2009) - [Wayback/Archive.is]
GlobalDeleteAtom
function (winbase.h) – Win32 apps | Microsoft Docs (for any Delphi version)
- [Wayback/Archive.is]
Great articles about this are [Wayback/Archive.is] Sure, we have RegisterWindowMessage and RegisterClipboardFormat, but where are DeregisterWindowMessage and DeregisterClipboardFormat? – The Old New Thing and [Wayback/Archive.is] Which message numbers belong to whom? – The Old New Thing (which has a table that clearly shows the ID space of atoms and Windows messages is shared, but each is in a different range of that space). -
- The old code assumed that the Windows message ID for calling
RM_GetObjectInstance
had to be unique for each process. It doesn’t: the Windows message ID can be shared across Delphi application instances as each of them will handled it individually because the only place it is used is the implementation of this method:
function ObjectFromHWnd(Handle: HWnd): TWinControl;
The result of that function is only valid within the current process, so there is no need for the
RM_GetObjectInstance
to be different for each Delphi process.
What does the fix in ControlsAtomFix1.7z
do?
That 7z file contains unit ControlsAtomFix
which for Delphi versions before XE2 needs to be in the uses list before the Controls
unit and supports Delphi versions 6 through XE.
It hooks the RegisterWindowMessage
Windows API method (actually [Wayback/Archive.is] RegisterWindowMessageA
function (winuser.h) – Win32 apps | Microsoft Docs for non-unicode Delphi before Delphi 2009 and [Wayback/Archive.is] RegisterWindowMessageW
function (winuser.h) – Win32 apps | Microsoft Docs for Delphi 2009 and up).
Inside the hooked method, it checks if the calling parameters are consistent with RegisterWindowMessage(PChar(ControlAtomString))
. If so, it returns the result for RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance'))
, otherwise it calls the original method.
That way, Delphi versions 6 through XE will behave like Delphi XE2 and up.
What does GarbageCollectAtoms
do?
Back to “GarbageCollectAtoms in Delphi? I used it in delphi 7, but I dont know what is benefit. ”. It is part of the answers to question [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow of which each has a distinctive edge:
- [Wayback/Archive.is] about the
GlobalDeleteAtom
method where [Wayback/Archive.is] Christian explains the workaround of manually modifying the Controls unit withRM_GetObjectInstance := RegisterWindowMessage('RM_GetObjectInstance');
..and explaining the working of his
GlobalDeleteAtom
method. - [Wayback/Archive.is] https://stackoverflow.com/users/62123/steve-black where user [Wayback/Archive.is] Steve Black explains how to change the second number in the
SharedSection
parameter ofcsrss.exe
(the Windows Client/Server Runtime Subsystem) inside theWindows
value data of theHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems
registry key. It is further documented in these Microsoft articles:- [Wayback/Archive.is] Microsoft KB Archive/126962 – BetaArchive Wiki covering Windows NT 3.1, NT 3.5/3.51, NT 4.0, 2000, XP and 2003 Server:
The first
SharedSection
value (1024
) defines the heap size common to all desktops. This includes the global handle table (Window handles are unique machine wide) and shared system settings (such as SystemMetrics). It is unlikely you would ever need to change this value.The second
SharedSection
value (3072
) controls the size of the desktop heap (used for Windows objects). This static value is used to prevent ill- behaved applications from consuming too many resources. Because the desktop heap is mapped into each process’ address space, this value should not be set to an arbitrarily high value (as it would decrease performance), but should only be increased sufficiently to allow all the desired applications to run. - [Wayback/Archive.is] User32.dll or Kernel32.dll does not initialize – Application Developer | Microsoft Docs (formerly KB184802) covering Windows Vista through Windows Server 2012 R2:
The first
SharedSection
value (1024
) is the shared heap size common to all desktops. This includes the global handle table. This table holds handles to windows, menus, icons, cursors, and so on, and shared system settings. It is unlikely that you would ever have to change this value.The second
SharedSection
value is the size of the desktop heap for each desktop that is associated with the interactive window stationWinSta0
. User objects such as hooks, menus, strings, and windows consume memory in this desktop heap. It is unlikely that you would ever have to change this value. - [Wayback/Archive.is] Desktop heap limitation causes out of memory error – Windows Server | Microsoft Docs (formerly KB947246) covering Windows 7 and Windows Server 2012 R2:
- The second value of the
SharedSection
registry entry is the size of the desktop heap for each desktop that is associated with an interactive window station. The heap is required for each desktop that is created in the interactive window station (WinSta0
). The value is in kilobytes (KB). - The third
SharedSection
value is the size of the desktop heap for each desktop that is associated with a non-interactive window station. The value is in kilobytes (KB). - We don’t recommend that you set a value that is over
20480
KB for the secondSharedSection
value.
- The second value of the
- [Wayback/Archive.is] CreateDesktopExA function (winuser.h) – Win32 apps | Microsoft Docs
- The first
SharedSection
value is the size of the shared heap common to all desktops, in kilobytes. - The second
SharedSection
value is the size of the desktop heap needed for each desktop that is created in the interactive window station,WinSta0
, in kilobytes. - The third
SharedSection
value is the size of the desktop heap needed for each desktop that is created in a noninteractive window station, in kilobytes.
- The first
The second
SharedSection
value has grown over time (with one step back in Windows 3.5):- 512 (Windows NT 3.5)
- 3072 (Windows NT 3.1, NT 4.0, 2000, XP and Vista)
- 12288 (x86 editions of Windows Vista SP1, 7, 8, 8.1, and newer x86 Windows versions)
- 20480 (x64 editions of Windows Vista, 7, 8, 8.1, Server 2008, Server 2008 R2, Server 2012, and Server 2012 R2, and newer x64 Windows versions)
- [Wayback/Archive.is] Microsoft KB Archive/126962 – BetaArchive Wiki covering Windows NT 3.1, NT 3.5/3.51, NT 4.0, 2000, XP and 2003 Server:
- [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow where [Wayback/Archive.is] Jordi Corbilla explains the bugs filed in QC (which have now been fixed), the structure of the atom and message names registered by the
Controls
unit, and his [Wayback/Archive.is] Global Atom Monitor GUI application on GitHub that allows you to visually watch the global atom table. TheGlobalDeleteAtom
method is based on this code. I amended that post with the fix for Delphi 6-XE by Andreas Hausladen.
I forked the Global Atom Monitor into [Wayback/Archive] jpluimers/atom-table-monitor: Automatically exported from code.google.com/p/atom-table-monitor back in 2015 because the original repository did not show any activity for a long time.
There I added a console application which displays all the User Atoms including their names, see the trick in [Wayback/Archive] atom-table-monitor/ATOMScannerConsoleApplicationUnit.pas at master · jpluimers/atom-table-monitor of [Wayback/Archive] atom-table-monitor/ATOMScannerConsole at master · jpluimers/atom-table-monitor commit [Wayback/Archive] First try at console version of ATOM scanner; Delphi 2007 version of … · jpluimers/atom-table-monitor@bd48b5b:
constructor TRegisteredWindowsMessageInformation.Create(const AIndex: Integer); var LLength: Integer; LName: string; LWindowsMessageNameChars: array [Byte] of Char; begin .. LLength := GetClipboardFormatName(AIndex, LWindowsMessageNameChars, High(Byte)); if LLength = 0 then inherited Create(AIndex, '') else begin LName := StrPas(LWindowsMessageNameChars); SetLength(LName, LLength); inherited Create(AIndex, LName); end; end;
In retrospect, I made a wordblindness error (
WindowsMessage
in stead ofWindowMessage
), so I will likely post a commit there some day: [Wayback/Archive] Fix `WindowsMessage` into `WindowMessage` in various identifiers. · Issue #2 · jpluimers/atom-table-monitor
This lists any User Atom nam be it registered with RegisterClass
, RegisterWindowMessage
, or RegisterClipboardFormat
(as they share the same User Atom table)
The trick is explained in the Microsoft documentation linked further below.
More about Windows messages
Windows messages can be a mess, so two years ago, I put a lot of related links in Some odd Windows Messages for my research list (Windows 10 with a very basic Delphi application).
Edit 20221021: more on Windows Atoms
Seems not all of you are dinosaurs like me, so below are some links on Windows Atoms (most via [Wayback/Archive] microsoft windows atoms – Google Search).
Windows Atoms are basically dictionaries having an index of 16-bit SHORT
integers and storing corresponding null terminated strings. This structure type helped preserve memory in the 16-bit Windows days. That way you can communicate using Windows messages (see below) where SendMessage
and PostMessage
had 16-bit WPARM
and LPARAM
typed parameters.
Each process could have their own atom tables (local atom tables), but there are also global atom tables shared by all running Windows processes, for instance the user atom table (API calls like RegisterClass
, RegisterWindowMessage
, or RegisterClipboardFormat
add entries here and the last two calls are persistent over your Windows Station session until you logoff or reboot) and the global atom table (often used for DDE – Dynamic Data Exchange – and OLE – Object Linking and Embedding).
Being 16-bit in index size, there can be only 65536 entries in an atom table, of which only 16383 can be used for string entries, so they can fill up. In the 16-bit Windows era, that was usually not much of a problem because processors were mainly single-core, memory was limited and therefore few processes ran at a time. Fast forward today, with ubiquitous memory and CPU resources, especially global atom tables can fill up faster than you might expect so keep an eye on that when adding entries to them.
- [Wayback/Archive] About Atom Tables – Win32 apps | Microsoft Learn (dense, but very well coverage of both basics and side effects including the below debugging remark using Clipboard Format names; that’s what I did in the above Delphi ).
Another way to dump the contents of the user atom table is by calling GetClipboardFormatName over the range of possible atoms from
0xC000
to0xFFFF
. If the total atom count steadily goes up while the application is running or does not return to baseline when the app is closed, there is a problem. - [Wayback/Archive] atom (WinDbg) – Windows drivers | Microsoft Learn (atom support in WinDbg can be really convenient to track down Windows Atom table related issues)
- [Wayback/Archive] Atom Functions – Win32 apps | Microsoft Learn
- [Wayback/Archive] Window Stations – Win32 apps | Microsoft Learn
- [Wayback/Archive] RegisterClassA function (winuser.h) – Win32 apps | Microsoft Learn and[Wayback/Archive] RegisterClassExA function (winuser.h) – Win32 apps | Microsoft Learn
- Both can be reversed (and free up their corresponding User Atom table entry) via [Wayback/Archive] UnregisterClassA function (winuser.h) – Win32 apps | Microsoft Learn
- [Wayback/Archive] RegisterWindowMessageA function (winuser.h) – Win32 apps | Microsoft Learn
- this Windows Message name registration cannot be undone, which means any registered unique Window Message name will full up the atom table; registering the same Window Message name twice will return back the same 16-bit User Atom table ID
- [Wayback/Archive] RegisterClipboardFormatA function (winuser.h) – Win32 apps | Microsoft Learn
- this Clipboard Format Name registration cannot be undone, which means any registered unique Window Message name will full up the atom table; registering the same Window Message name twice will return back the same 16-bit User Atom table ID
- [Wayback/Archive] GetClipboardFormatNameA function (winuser.h) – Win32 apps | Microsoft Learn (which can obtain any User Atom table name, as this table is shared for
ClassName
,WindowMessage
andClipboardFormat
names) - [Wayback/Archive] security – So, just what are Windows Atom tables for? – Stack Overflow (thanks [Wayback/Archive] Stone True, [Wayback/Archive] Hans Passant and [Wayback/Archive] Anders):
C
They are a simple
Dictionary<int, string>
. One of the many tricks Microsoft used to shoe-horn a GUI operating system and its apps into 640 KB of RAM. Carrying around a 16-bit int is a lot cheaper than having to use a string literal. It doesn’t care that it is actually a string at all, any blob of bytes will do. It is still just plain data. Exploiting it doesn’t just require already having control over the process, you’d still need to turn that data into code. A fallacy that Raymond Chen likes to [Wayback/Archive] make fun of.A
An atom table lets you associate a string with a 16-bit number. You give Windows your string and it gives you back a number. You can then retrieve the string again just by knowing the assigned number.
Every normal process has its own local atom table but it is usually empty and is not a security issue.
There are multiple “global” atom tables that are shared by all processes in the same window station. 1 of them is documented and it is called the global atom table. MSDN is also nice enough to tell us that
RegisterClipboardFormat
andRegisterClass
also use their own atom tables internally in their current implementation. Other functions likeSetProp
also use atoms but we are only interested in the atom table used by the exploit and atoms are added to that table with theGlobalAddAtom
function.The main purpose of this atom table is to act as a simple storage location so that different processes can communicate with each other in a protocol called DDE. When a process wants to send a message to a window in a different process you cannot send more than 8 bytes (2 parameters, 4 bytes each) and this is not enough space to transfer a filesystem path or a URL.
To work around this limitation the application stores the string/path/URL in the public global atom table by calling
GlobalAddAtom
.GlobalAddAtom
returns a number that the application can send to the other process. When the other process receives the DDE message it just passes the number to theGlobalGetAtomName
function to retrieve the string. - The “make fun of” link above is dead due to link rot, but is now at [Wayback/Archive] It rather involved being on the other side of this airtight hatchway | The Old New Thing.
- [Wayback/Archive] AtomBombing: Brand New Code Injection for Windows – Breaking Malware /[Wayback/Archive] BreakingMalwareResearch/atom-bombing: Brand New Code Injection for Windows
Windows Messaging:
- [Wayback/Archive] Using Messages and Message Queues – Win32 apps | Microsoft Learn
- [Wayback/Archive]
PostMessage
function (winuser.h) – Win32 apps | Microsoft Learn - [Wayback/Archive]
SendMessage
function (winuser.h) – Win32 apps | Microsoft Learn
Related blog posts:
- I had some Windows ATOM issues before, but this beats them easily
- Some notes and links: when a filled ATOM table is not caused by your Delphi app
–jeroen