FastMM4-AVX Allocation Size Overflow in FullDebugMode

Security Advisory GHSA-p7fj-xpgj-24qm. Issue #163. Published: August 16, 2026. Fixed in commit b8a22ba and released in FastMM4-AVX v1.0.14 (August 16, 2026).

Summary

In FullDebugMode, adding debug overhead to a near-maximum allocation size could wrap. The underlying allocator then received a small size and could return an undersized block. FastMM4-AVX recorded the original size and wrote the footer outside that block, corrupting the heap before returning. Builds without FullDebugMode were not affected by this defect.

Severity: Medium (CVSS 4.0 Score: 5.8), applying only to builds compiled with FullDebugMode and not reachable when FullDebugMode is disabled. No CVE has been requested for this issue.

Vulnerability Details

GHSA ID GHSA-p7fj-xpgj-24qm
CVE ID None requested
Issue FastMM4-AVX issue #163
Vulnerability Type Integer Overflow or Wraparound (CWE-190) and Integer Overflow to Buffer Overflow (CWE-680), leading to an Out-of-bounds Write (CWE-787)
Maintainer Maxim Masiutin
Product FastMM4-AVX Memory Manager
Affected Component DebugGetMem and DebugReallocMem in FastMM4.pas, reached only when the unit is compiled with FullDebugMode
Affected Versions All releases up to and including v1.0.13, and original FastMM4 code carrying the same expressions
Fixed In v1.0.14 (August 16, 2026), tagged on commit 7b8dc4e. The defect itself was fixed in commit b8a22ba .
Impact Out-of-bounds write of the block footer, allocator metadata corruption, later corruption reports, or process termination. With overflow checking enabled, the process is terminated from inside the allocator instead.

CVSS Score

CVSS Version Score Severity Vector String
CVSS 4.0 5.8 Medium CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N

Every metric is set to what the measured runs demonstrate rather than to what the defect might permit. AV:L: FastMM4-AVX is a library whose only interface is a local allocation API, and no network-reachable path to an allocation size calculation has been demonstrated. AT:P: the target has to have been built with FullDebugMode, a deployment property the attacker neither controls nor can create, which is what Attack Requirements: Present records. The condition does not belong in AC, which describes evading a security measure; there is none to evade here. AC:L: reaching the affected interval needs no special preparation once the target is such a build, since the interval is narrow, 136 bytes on 32-bit and 256 on 64-bit, but deterministic and known. PR:L with UI:N: a local vector without user interaction means an attacker who can already invoke the application. The coherent alternative is PR:N with UI:P, where a crafted input is supplied and a local user opens it. VC:N: the defect is an out-of-bounds write, not a disclosed read, and no confidentiality loss was demonstrated. VI:H and VA:H: allocator memory and metadata are corrupted and the tested process terminates, although arbitrary controlled modification and code execution were not demonstrated. SC:N/SI:N/SA:N: the damage stays inside the process that linked the allocator.

The score is the one GitHub computed from that vector when the advisory was published. The advisory for CVE-2026-27123 carries AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H and scores 9.3; that defect is present in every build and was assessed against an application-layer network input, while this one needs a FullDebugMode build and has only a local reproduction.

Technical Details

Root Cause

DebugGetMem added the debug overhead to the caller's size before any bound was applied:

Result := FastGetMem(ASize + FullDebugBlockOverhead);

When the sum wrapped, FastGetMem could allocate a small block. The footer address, derived from the original size, then pointed outside it. DebugReallocMem had the same wrap in its in-place size test. DebugAllocMem calls DebugGetMem and already handles nil.

For FreePascal, the affected interval is:

ASize > High(ptruint) - FullDebugBlockOverhead

Compilers using a signed parameter must also reject negative sizes and values above High(NativeInt) - FullDebugBlockOverhead.

FullDebugBlockOverhead is 136 bytes on 32-bit and 256 bytes on 64-bit: the 128-byte or 240-byte full-debug header, one NativeUInt trailer, and one trailing free-block pointer.

Impact In Practice

Exposure requires FullDebugMode and a path producing a size in this narrow interval, for example through underflow, signed conversion, or attacker-influenced arithmetic. The allocator cannot detect arithmetic that already wrapped to a small value.

The Original FastMM4 Was Affected As Well

The fork inherited both expressions from original FastMM4 . Upstream master before commit b09f74b and forks carrying those expressions unchanged are affected. A 32-bit FullDebugMode test of the vulnerable upstream code returned an undersized block and then terminated:

=== Test 3: Boundary Conditions ===
Attempting to allocate: $FFFFFFFF bytes (High(NativeUInt))
[FAIL] High(NativeUInt) allocation - VULNERABILITY: got pointer $7FE91940
Runtime error 204 at 00402B88
                    

Two Further Defects Found On The Way

Signed Promotion In AllocateLargeBlock

Looking for a probe that only a checked build would catch turned up a defect on the ordinary path, with nothing to do with FullDebugMode. AllocateLargeBlock pads the caller's size:

LLargeUsedBlockSize := (ASize + LargeBlockHeaderSize + LargeBlockGranularity - 1 + BlockHeaderSize)
  and LargeBlockGranularityMask;
                    

ASize is NativeUInt and the constants are untyped, so FreePascal widens the whole expression to int64. A 64-bit size at or above 2^63 then overflows that signed intermediate. MaxSafeLargeBlockSize does not stop it: at $FFFFFFFFFFE00000 it sits only 2 MB below the top of the range and passes almost the entire upper half through. Unchecked, the mask hid the wrap; with overflow checking on, the process was terminated from inside the allocator instead of receiving nil. Each constant is now added as NativeUInt, so the arithmetic stays in the type the size already has. 32-bit is unaffected, because a 32-bit NativeUInt cannot reach 2^63 inside the widened intermediate.

Truncated Size In The POSIX VirtualAlloc Shim

The POSIX shims for VirtualAlloc and VirtualFree declared dwSize as Cardinal, so a 64-bit size was reduced modulo 2^32 before valloc saw it, and valloc is declared there as taking size_t, which is pointer-sized. An oversized request therefore came back as a small, valid block rather than failing. Both parameters are now NativeUInt. The macOS shim in FastMM_OSXUtil.pas already used SIZE_T, so the non-macOS one was the outlier rather than the intended design.

Reproduction

On unmodified master before the fix, with FreePascal on Windows, place the matching precompiled FullDebugMode DLL beside the test executable and run:

cd Tests/Simple
fpc -B -Sd -Twin64 -Px86_64 -dFullDebugMode -dNoMessageBoxes IntegerOverflowTest.dpr
.\IntegerOverflowTest.exe
                    

For 32-bit, replace -Twin64 -Px86_64 with -Twin32 -Pi386. The top-of-range checks fail on both pointer widths and the run exits 1:

[FAIL] High(NativeUInt) allocation - VULNERABILITY: got pointer $7FF4FDEB8CF0
[FAIL] High(NativeUInt)-1 allocation - VULNERABILITY: got pointer $7FF4FDEB8E10
                    

Without -dNoMessageBoxes the corruption is reported interactively instead, as a modify-after-free during a later GetMem and a corrupted block header during a later FreeMem, naming the same addresses.

Adding -Criot, which turns range and overflow checking on, changes the outcome rather than merely reporting it: the wrap no longer produces a small block quietly, the process is terminated from inside the allocator with runtime error 215.

Fix

The size is refused before the addition rather than after it, in each compiler's own parameter type. That type is unsigned under FreePascal and signed under other supported compilers, where a size that has already wrapped arrives as a negative number and one just below High(NativeInt) would overflow the addition itself. Both are refused:

{$IFDEF FPC}
    LSizeRefused := ASize > (High(ptruint) - FullDebugBlockOverhead);
{$ELSE}
    LSizeRefused := (ASize < 0) or (ASize > (High(NativeInt) - NativeInt(FullDebugBlockOverhead)));
{$ENDIF}
    if LSizeRefused then
      Result := nil
    else
      Result := FastGetMem(ASize + FullDebugBlockOverhead);
                    

Writing the test in the parameter's own type rather than in NativeUInt is what makes the addition below it exact instead of merely harmless, and it is what lets the unit be compiled with overflow checking on: FastMM4 sets no {$Q} or {$R} of its own and inherits whatever the program compiling it asks for.

DebugReallocMem takes the same test as the first operand of its existing comparison. FastMM forces short-circuit Boolean evaluation, so the addition in the last operand is reached only for a size that fits it, and an unsafe size falls through to DebugGetMem, which refuses it and leaves the caller's existing block untouched.

The LSizeRefused flag also keeps the two kinds of nil apart. A size the guard refuses never reaches the allocator, so it says nothing about the address space, and the branch that releases AddressSpaceSlackPtr now runs only when the allocator actually tried and failed. That reserve exists so that a later genuine out-of-memory can still be reported.

Fix Verification

Every configuration was built twice, once from unmodified master and once from the fixed tree, with the allocation probes driven through DebugGetMem and DebugReallocMem directly.

Build Unpatched master Fixed
FreePascal 3.2.2, win64, FullDebugMode exit 1, the top-of-range probe returns a pointer exit 0, all checks pass
FreePascal 3.2.2, win64, FullDebugMode, -Criot exit 215, arithmetic overflow inside the allocator exit 0, all checks pass
FreePascal 3.2.2, win32, FullDebugMode, plain and -Criot exit 1, then exit 215 with checking on exit 0, all checks pass
FreePascal 3.3.1, win64 and win32, FullDebugMode, plain and -Criot exit 1, then exit 215 at the first size in the affected interval exit 0, all checks pass
FreePascal 3.2.2 and 3.3.1, win64, ordinary mode, -Criot exit 215 inside AllocateLargeBlock exit 0, all checks pass

The fixed tree runs 14 FullDebugMode checks and 8 ordinary ones and passes all of them on FreePascal 3.2.2 and 3.3.1, on both pointer widths, plain and with -Criot.

The checked runs are the sharpest evidence and were not predicted when the advisory was first written: with checking on, the wrap does not silently produce a small block, it terminates the process from inside the allocator. Continuous integration had never caught it because IntegerOverflowTest was run neither with FullDebugMode nor with range and overflow checking. Four Windows FullDebugMode jobs now run it, two of them with -Criot, each copying the tracked DLL beside the executable and stopping on its own compiler's exit code.

Workarounds

  • Upgrade to v1.0.14 or later, which is the complete fix, or do not ship a build with FullDebugMode enabled. FullDebugMode is a development aid; an ordinary release build never reaches the affected code.
  • Validate allocation sizes in application code before calling GetMem, ReallocMem or SetLength, rejecting any size above a reasonable application-specific maximum well below High(NativeUInt).
  • Compile with range and overflow checking on. Arithmetic checking does not remove the defect, but it turns a silent undersized allocation into an immediate, diagnosable termination.
  • Upstream FastMM4 master carries the guards in commit b09f74b. Its latest release, 4.993, predates the fix and must be patched or used without FullDebugMode.

CWE Mapping

  • CWE-190: Integer Overflow or Wraparound. The debug overhead is added to the caller's size before the sum is bounded, so a size near the top of the range wraps to a small value.
  • CWE-680: Integer Overflow to Buffer Overflow. The wrapped sum is what the underlying allocator is asked for, so the block returned is far smaller than the size recorded for it.
  • CWE-787: Out-of-bounds Write. The block footer address is computed from the recorded size, so the footer is written outside the block that was allocated.

Timeline

August 15, 2026 Defect found while extending the integer overflow test to FullDebugMode; reported as issue #163
August 16, 2026 Fix measured across FreePascal 3.2.2 and 3.3.1 on both pointer widths; two further defects found and fixed alongside it
August 16, 2026 Pull request 164 merged to master as commit b8a22ba
August 16, 2026 Developer advisory published at masiutin.net
August 16, 2026 FastMM4-AVX v1.0.14 released, carrying the fix and rebuilt FullDebugMode DLLs
August 20, 2026 Original FastMM4 applied the guards directly on master in commit b09f74b; upstream pull request #103 closed without merge and issue #104 closed as completed

Credit

Found and fixed by Maxim Masiutin, maintainer of FastMM4-AVX. The address space slack defect in the first version of the guard, and the truncating POSIX VirtualAlloc shim, were both pointed out by CodeRabbit during review of pull request 164.

Other FastMM4-AVX Security Advisories

GHSA-f6jf-6w84-w2h7 Integer Overflow in Large Block Allocation (CWE-190, CWE-122). Fixed in v1.0.9. CVSS 4.0: 9.3 Critical. The former CVE-2026-27123 identifier was rejected as issued in error. Advisory
GHSA-3x29-6h9j-vcvm FPU Stack Corruption in 32-bit Move Procedures (CWE-908, CWE-703, CWE-754). Fixed in v1.0.10. CVSS 4.0: 5.9 Medium. Advisory
Upstream FastMM4 The original FastMM4 by Pierre le Riche carried the same two unguarded expressions. Upstream master is fixed by commit b09f74b, but no released upstream version contains that commit yet.

References