Context
This comes from a GPT-5.6-SOL-ULTRA follow-up review of current FastMM5 master. I
reviewed the finding manually and reproduced it locally against the unmodified
revision:
13976edfc84f83c3c55e5731d30fd8686ad51f15
#81 Export FastMM_ProcessAllPendingFrees in BorlndMM.dll
The issue is related to, but distinct from, #72. It does not require concurrent
mode switching, FullDebugMode, or an application callback.
Problem
FastMM_SetMemoryManagerEntryPoints refuses to modify the installed memory
manager if it has been changed externally:
function FastMM_SetMemoryManagerEntryPoints: Boolean;
begin
{Check that the memory manager has not been changed since the last time it was set.}
if FastMM_InstalledMemoryManagerChangedExternally then
Exit(False);
{Select and install the configured entry points...}
end;
The public debug/erasure mode functions modify their nesting counter before this
operation and do not roll the modification back when it returns False.
For example:
function FastMM_BeginEraseFreedBlockContent: Boolean;
begin
SetMemoryManagerLock.Lock;
try
if CurrentInstallationState = mmisInstalled then
begin
Inc(EraseFreedBlockContentCounter);
if EraseFreedBlockContentCounter = 1 then
Result := FastMM_SetMemoryManagerEntryPoints
else
Result := True;
end
else
Result := False;
finally
SetMemoryManagerLock.Unlock;
end;
end;
A single-threaded failure sequence is enough:
- An external memory-manager record is installed after FastMM.
- The first
FastMM_BeginEraseFreedBlockContent increments the counter from 0
to 1.
FastMM_SetMemoryManagerEntryPoints detects the external change and returns
False.
- The counter incorrectly remains 1.
- A second begin call increments it to 2 and returns
True without retrying the
entry-point installation.
- The external
FreeMem entry point remains installed, so freed blocks are not
erased even though the public call reported success.
FastMM_EraseFreedBlockContentActive also reports True, because it only tests
whether the poisoned counter is positive:
function FastMM_EraseFreedBlockContentActive: Boolean;
begin
Result := EraseFreedBlockContentCounter > 0;
end;
The same counter-before-installation pattern appears in all three mode families:
FastMM_EnterDebugMode / FastMM_ExitDebugMode;
FastMM_BeginEraseAllocatedBlockContent /
FastMM_EndEraseAllocatedBlockContent;
FastMM_BeginEraseFreedBlockContent /
FastMM_EndEraseFreedBlockContent.
Both begin/enter and end/exit transitions can leave the counter inconsistent if
the required entry-point update fails.
Reproducer
The reproducer copies FastMM's installed TMemoryManagerEx, changes only its
GetMem entry point to a pass-through wrapper, and installs that record through
the RTL. This safely makes FastMM_InstalledMemoryManagerChangedExternally
return True while preserving ordinary allocation behavior.
It then calls FastMM_BeginEraseFreedBlockContent twice and verifies that the
second call returns True even though the original external FreeMem entry point
is still installed.
Cleanup first restores FastMM's original memory-manager record and calls the end
routine only while the public active-state query remains true. Thus the buggy
implementation drains its two poisoned nesting levels, while a fixed
implementation whose failed begin calls roll back to zero makes no unmatched end
calls.
program ModeFailureCounterRollbackRegression;
{$APPTYPE CONSOLE}
uses
FastMM5;
var
GOriginalMemoryManager: TMemoryManagerEx;
function ForwardGetMem(ASize: NativeInt): Pointer;
begin
Result := GOriginalMemoryManager.GetMem(ASize);
end;
function SameFreeMemEntryPoint(const ALeft, ARight: TMemoryManagerEx): Boolean;
begin
Result := NativeUInt(@ALeft.FreeMem) = NativeUInt(@ARight.FreeMem);
end;
procedure Fail(const AMessage: string);
begin
Writeln(AMessage);
Halt(1);
end;
var
LExternalMemoryManager: TMemoryManagerEx;
LFirstResult: Boolean;
LInstalledAfterSecondCall: TMemoryManagerEx;
LSecondResult: Boolean;
begin
GetMemoryManager(GOriginalMemoryManager);
LExternalMemoryManager := GOriginalMemoryManager;
LExternalMemoryManager.GetMem := ForwardGetMem;
SetMemoryManager(LExternalMemoryManager);
try
LFirstResult := FastMM_BeginEraseFreedBlockContent;
LSecondResult := FastMM_BeginEraseFreedBlockContent;
GetMemoryManager(LInstalledAfterSecondCall);
finally
SetMemoryManager(GOriginalMemoryManager);
while FastMM_EraseFreedBlockContentActive do
begin
if not FastMM_EndEraseFreedBlockContent then
Fail('Cleanup could not restore the erase-before-free nesting state.');
end;
end;
if LFirstResult then
Fail('Unexpected: first begin call succeeded after an external memory-manager change.');
if LSecondResult
and SameFreeMemEntryPoint(LInstalledAfterSecondCall, LExternalMemoryManager) then
Fail('BUG: the second begin call returned True although the erase-before-free entry point was never installed.');
Writeln('OK: failed mode transition did not poison the nesting counter.');
end.
Local validation
Built and run with RAD Studio 37.0 against unmodified 13976ed:
| Build |
Result |
| Default Win32 |
Exit 1: false success reproduced |
| Default Win64 |
Exit 1: false success reproduced |
Both runs print:
BUG: the second begin call returned True although the erase-before-free entry point was never installed.
Both results were reproduced again while preparing this report.
Expected fixed output:
OK: failed mode transition did not poison the nesting counter.
Impact and scope
This violates the Boolean success contract of the public mode functions and makes
the corresponding ...Active state queries unreliable after a failed transition.
The freed-content mode is documented as useful for security purposes, so false
success can also cause an application to believe freed memory is being erased
when its installed FreeMem entry point performs no erasure.
The reproducer deliberately uses an external memory-manager change—the exact
failure condition FastMM detects and reports through False. It does not rely on
invalid allocation/free behavior or a rapid cross-thread transition.
I would rate this as Medium severity for configuration integrity and false
security assurance.
Suggested fix direction
Make each counter transition transactional while SetMemoryManagerLock is held:
- if a 0-to-1 enter/begin transition cannot install its entry points, decrement
the counter back to 0;
- if a 1-to-0 exit/end transition cannot install its entry points, increment the
counter back to 1;
- apply the same rule consistently to debug mode, allocated-content erasure, and
freed-content erasure.
For example, the begin side can retain the existing shape and add rollback only
on the transition that calls FastMM_SetMemoryManagerEntryPoints:
Inc(EraseFreedBlockContentCounter);
if EraseFreedBlockContentCounter = 1 then
begin
Result := FastMM_SetMemoryManagerEntryPoints;
if not Result then
Dec(EraseFreedBlockContentCounter);
end
else
Result := True;
This does not add work to the allocation/free fast paths. The affected public
configuration calls already hold SetMemoryManagerLock.
Prior issue check
Issue #72 reported a concurrent
nested FastMM_EnterDebugMode returning before the first thread completed the
entry-point transition. It noted that the erase routines had a similar transition
shape, and the resulting fix serialized transitions.
This finding is different: it is a failure-path rollback defect after
FastMM_SetMemoryManagerEntryPoints explicitly returns False, it reproduces with
one thread, and it causes a later call to return a false success without retrying.
Context
This comes from a GPT-5.6-SOL-ULTRA follow-up review of current FastMM5 master. I
reviewed the finding manually and reproduced it locally against the unmodified
revision:
The issue is related to, but distinct from, #72. It does not require concurrent
mode switching, FullDebugMode, or an application callback.
Problem
FastMM_SetMemoryManagerEntryPointsrefuses to modify the installed memorymanager if it has been changed externally:
The public debug/erasure mode functions modify their nesting counter before this
operation and do not roll the modification back when it returns
False.For example:
A single-threaded failure sequence is enough:
FastMM_BeginEraseFreedBlockContentincrements the counter from 0to 1.
FastMM_SetMemoryManagerEntryPointsdetects the external change and returnsFalse.Truewithout retrying theentry-point installation.
FreeMementry point remains installed, so freed blocks are noterased even though the public call reported success.
FastMM_EraseFreedBlockContentActivealso reportsTrue, because it only testswhether the poisoned counter is positive:
The same counter-before-installation pattern appears in all three mode families:
FastMM_EnterDebugMode/FastMM_ExitDebugMode;FastMM_BeginEraseAllocatedBlockContent/FastMM_EndEraseAllocatedBlockContent;FastMM_BeginEraseFreedBlockContent/FastMM_EndEraseFreedBlockContent.Both begin/enter and end/exit transitions can leave the counter inconsistent if
the required entry-point update fails.
Reproducer
The reproducer copies FastMM's installed
TMemoryManagerEx, changes only itsGetMementry point to a pass-through wrapper, and installs that record throughthe RTL. This safely makes
FastMM_InstalledMemoryManagerChangedExternallyreturn
Truewhile preserving ordinary allocation behavior.It then calls
FastMM_BeginEraseFreedBlockContenttwice and verifies that thesecond call returns
Trueeven though the original externalFreeMementry pointis still installed.
Cleanup first restores FastMM's original memory-manager record and calls the end
routine only while the public active-state query remains true. Thus the buggy
implementation drains its two poisoned nesting levels, while a fixed
implementation whose failed begin calls roll back to zero makes no unmatched end
calls.
Local validation
Built and run with RAD Studio 37.0 against unmodified
13976ed:Both runs print:
Both results were reproduced again while preparing this report.
Expected fixed output:
Impact and scope
This violates the Boolean success contract of the public mode functions and makes
the corresponding
...Activestate queries unreliable after a failed transition.The freed-content mode is documented as useful for security purposes, so false
success can also cause an application to believe freed memory is being erased
when its installed
FreeMementry point performs no erasure.The reproducer deliberately uses an external memory-manager change—the exact
failure condition FastMM detects and reports through
False. It does not rely oninvalid allocation/free behavior or a rapid cross-thread transition.
I would rate this as Medium severity for configuration integrity and false
security assurance.
Suggested fix direction
Make each counter transition transactional while
SetMemoryManagerLockis held:the counter back to 0;
counter back to 1;
freed-content erasure.
For example, the begin side can retain the existing shape and add rollback only
on the transition that calls
FastMM_SetMemoryManagerEntryPoints:This does not add work to the allocation/free fast paths. The affected public
configuration calls already hold
SetMemoryManagerLock.Prior issue check
Issue #72 reported a concurrent
nested
FastMM_EnterDebugModereturning before the first thread completed theentry-point transition. It noted that the erase routines had a similar transition
shape, and the resulting fix serialized transitions.
This finding is different: it is a failure-path rollback defect after
FastMM_SetMemoryManagerEntryPointsexplicitly returnsFalse, it reproduces withone thread, and it causes a later call to return a false success without retrying.