From 937b084ab8f7e59f4056ee19b3f206ae096254aa Mon Sep 17 00:00:00 2001 From: TetzkatLipHoka Date: Mon, 20 Jul 2026 19:20:22 +0200 Subject: [PATCH] Validate the shared memory manager pointer before adopting it (#84) The shared memory manager is discovered through a named file mapping whose name contains only the (public) process ID in the session-scoped Local\ namespace, so a hostile process in the same session can pre-create the mapping and publish an arbitrary pointer. FastMM_AttemptToUseSharedMemoryManager then copies that pointer as a TMemoryManagerEx record (InstalledMemoryManager := LPMemoryManagerEx^), faulting on a planted/garbage pointer. Before the record is adopted it is now validated: the whole record must lie in committed, readable memory, and its three mandatory entry points (GetMem/FreeMem/ReallocMem) must point into committed, executable memory. An invalid pointer is treated as "no shared manager found" instead of being dereferenced, turning the attacker-triggered access violation into a graceful no-op. This is deliberately validation only - it does not add any structure, version or provenance field to the shared record, so it does not change the wire format and remains compatible with the memory managers of other FastMM versions and the built-in memory manager: a genuine provider's record (readable memory, executable entry points) passes unchanged. Verified with a same-process reproducer that plants the mapping for its own PID (indistinguishable from a hostile process's mapping from FastMM's point of view): before the change the target faults (runtime error 216); after it, a non-readable pointer and a readable-but-non-executable buffer are both rejected without a crash, while a valid record with real executable entry points is still adopted. Delphi 13.1, Win32 and Win64. --- FastMM5.pas | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/FastMM5.pas b/FastMM5.pas index c8ecba4..d0cce7f 100644 --- a/FastMM5.pas +++ b/FastMM5.pas @@ -9609,6 +9609,65 @@ procedure FastMM_BuildFileMappingObjectName; end; end; +{Returns True if the memory region [APStart, APStart + ASize) is entirely committed and readable.} +function SharedMemoryManager_RegionIsReadable(APStart: Pointer; ASize: NativeUInt): Boolean; +var + LRegionInfo: TMemoryRegionInfo; + LPCurrent, LPEnd, LPNextRegion: Pointer; +begin + Result := False; + if (APStart = nil) or (ASize = 0) then + Exit; + LPCurrent := APStart; + LPEnd := Pointer(PAnsiChar(APStart) + ASize); + while NativeUInt(LPCurrent) < NativeUInt(LPEnd) do + begin + OS_GetVirtualMemoryRegionInfo(LPCurrent, LRegionInfo); + if (LRegionInfo.RegionState <> mrsAllocated) or (not (marRead in LRegionInfo.AccessRights)) then + Exit; + {Advance to the start of the next region. Bail out if there is no forward progress (a defensive guard against a + zero-sized or backward region result).} + LPNextRegion := Pointer(PAnsiChar(LRegionInfo.RegionStartAddress) + LRegionInfo.RegionSize); + if NativeUInt(LPNextRegion) <= NativeUInt(LPCurrent) then + Exit; + LPCurrent := LPNextRegion; + end; + Result := True; +end; + +{Returns True if APCodeAddress points into committed, executable memory.} +function SharedMemoryManager_AddressIsExecutable(APCodeAddress: Pointer): Boolean; +var + LRegionInfo: TMemoryRegionInfo; +begin + Result := False; + if APCodeAddress = nil then + Exit; + OS_GetVirtualMemoryRegionInfo(APCodeAddress, LRegionInfo); + Result := (LRegionInfo.RegionState = mrsAllocated) and (marExecute in LRegionInfo.AccessRights); +end; + +{The shared memory manager is discovered through a named file mapping whose name contains only the (public) process ID +in a session-scoped namespace, so a hostile process in the same session can pre-create the mapping and publish an +arbitrary pointer. Before the record is adopted (and dereferenced) it is therefore validated: the whole record must +lie in committed, readable memory, and its three mandatory entry points must point into committed, executable memory. +This turns a planted/garbage pointer into a graceful "no shared manager found" instead of an access violation. It does +not change the shared record format, so it remains compatible with the memory managers of other FastMM versions and the +built-in memory manager - a genuine provider's record passes these checks.} +function SharedMemoryManager_RecordIsValid(APMemoryManagerEx: PMemoryManagerEx): Boolean; +var + LPEntryPoints: PPointerArray; +begin + Result := False; + if not SharedMemoryManager_RegionIsReadable(APMemoryManagerEx, SizeOf(TMemoryManagerEx)) then + Exit; + {The mandatory GetMem, FreeMem and ReallocMem fields are the first three pointers in the record.} + LPEntryPoints := PPointerArray(APMemoryManagerEx); + Result := SharedMemoryManager_AddressIsExecutable(LPEntryPoints[0]) + and SharedMemoryManager_AddressIsExecutable(LPEntryPoints[1]) + and SharedMemoryManager_AddressIsExecutable(LPEntryPoints[2]); +end; + {Searches the current process for a shared memory manager} function FastMM_FindSharedMemoryManager: PMemoryManagerEx; var @@ -9633,6 +9692,11 @@ function FastMM_FindSharedMemoryManager: PMemoryManagerEx; end; Winapi.Windows.CloseHandle(LLocalMappingObjectHandle); end; + + {The pointer comes from a mapping that an untrusted process could have created, so validate it before it is returned + (and subsequently dereferenced) - see SharedMemoryManager_RecordIsValid.} + if (Result <> nil) and (not SharedMemoryManager_RecordIsValid(Result)) then + Result := nil; end; {Searches the current process for a shared memory manager. If no memory has been allocated using this memory manager