How Integer Truncation Heap Overflow Happens in C++ UEFI ACPI Parsing and How to Fix It
Introduction
The Mobility.Uefi.Acpi.cpp file handles parsing of ACPI SRAT (System Resource Affinity Table) data to query memory ranges in a UEFI firmware environment. A critical flaw in the MoUefiAcpiQueryMemoryRanges, MoUefiAcpiQueryMergedMemoryRanges, and MoUefiAcpiQueryMemoryHoles functions created a heap buffer overflow vulnerability that could be exploited by an attacker who controls ACPI table contents—a realistic scenario in virtualized environments where the hypervisor provides firmware tables.
The root cause? A single type choice: MO_UINT16 was used to store the result of a size multiplication that can easily exceed 65535. This two-byte integer silently truncates the result, causing MoPlatformHeapAllocate to allocate a buffer far smaller than needed, while the subsequent loop writes data far beyond the allocation boundary.
The Vulnerability Explained
Let's look at the vulnerable code at line 262 (and similar patterns at lines 370 and 433):
MO_UINT16 Size = static_cast<MO_UINT16>(
sizeof(MO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEM) * Count);
PMO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEM Ranges = nullptr;
if (MO_RESULT_SUCCESS_OK != ::MoPlatformHeapAllocate(
reinterpret_cast<PMO_POINTER>(&Ranges),
Size))
Here's the math that makes this dangerous:
MO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEMis at least 16 bytes (likely containing base address, length, and flags for a memory range descriptor)Countis aMO_UINT16, so it can be up to 65535- The multiplication
16 * Countcan produce values up to 1,048,560 (0xFFFF0) - But
MO_UINT16can only hold values up to 65535 (0xFFFF)
When Count exceeds 4095 (for a 16-byte struct), the multiplication overflows the 16-bit Size variable. For example:
| Count | True Size (bytes) | Truncated MO_UINT16 Size | Buffer Shortfall |
|---|---|---|---|
| 4096 | 65,536 | 0 | 65,536 bytes |
| 5000 | 80,000 | 14,464 | 65,536 bytes |
| 65535 | 1,048,560 | 1,048,560 & 0xFFFF = 65,520 | 983,040 bytes |
After the undersized allocation, a loop iterates Count times, writing sizeof(MO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEM) bytes per iteration into the Ranges buffer. This writes potentially hundreds of kilobytes beyond the allocated heap buffer.
Attack Scenario
In a virtualized environment (e.g., running under a malicious or compromised hypervisor), an attacker can craft an ACPI SRAT table with a large number of Memory Affinity Structures. When the guest OS firmware parses this table:
- The SRAT parser counts memory affinity entries →
Count = 5000 Size = (MO_UINT16)(16 * 5000)→Size = 14464(truncated from 80000)MoPlatformHeapAllocateallocates only 14,464 bytes- The filling loop writes 80,000 bytes into a 14,464-byte buffer
- 65,536 bytes overflow into adjacent heap memory
This heap corruption can be leveraged for arbitrary code execution at the firmware level—a particularly devastating outcome since firmware code runs at the highest privilege level, below the operating system.
The Fix
The fix is elegant in its simplicity: change the type of Size from MO_UINT16 to MO_UINTN (the platform-native unsigned integer type, equivalent to size_t). This was applied in three locations:
Before (vulnerable):
MO_UINT16 Size = static_cast<MO_UINT16>(
sizeof(MO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEM) * Count);
After (fixed):
MO_UINTN Size =
sizeof(MO_UEFI_ACPI_SIMPLE_MEMORY_RANGE_ITEM) * Count;
The changes were made at three call sites:
- Line 231 (
MoUefiAcpiQueryMemoryRanges): Allocates buffer for raw memory ranges from SRAT - Line 373 (
MoUefiAcpiQueryMergedMemoryRanges): Allocates buffer for merged/consolidated ranges - Line 436 (
MoUefiAcpiQueryMemoryHoles): Allocates buffer for memory hole descriptors
By using MO_UINTN (which is 32-bit on 32-bit platforms and 64-bit on 64-bit platforms), the multiplication result is preserved without truncation. On a 64-bit UEFI platform, MO_UINTN can hold values up to 2^64-1, making overflow from this multiplication impossible. Even on 32-bit platforms, the maximum result (16 × 65535 = 1,048,560) fits comfortably within a 32-bit unsigned integer.
Notice also that the explicit static_cast<MO_UINT16> was removed. That cast was actually masking a compiler warning about narrowing conversion—it told the compiler "yes, I know this might truncate, do it anyway." Removing the cast and using the correct type eliminates both the vulnerability and the need for the cast.
Prevention & Best Practices
1. Use Appropriate Types for Size Calculations
Always use size_t, MO_UINTN, or equivalent platform-native types for memory size calculations. Never store allocation sizes in types narrower than the platform's addressing width.
// BAD: Narrow type for size
MO_UINT16 size = element_size * count;
// GOOD: Platform-native type
MO_UINTN size = element_size * count;
// BETTER: With explicit overflow check
MO_UINTN size;
if (count > MO_UINTN_MAX / element_size) {
return MO_RESULT_ERROR_ARITHMETIC_OVERFLOW;
}
size = element_size * count;
2. Enable Compiler Warnings
Use -Wconversion and -Wnarrowing (GCC/Clang) or /W4 (MSVC) to catch implicit narrowing conversions. Treat these warnings as errors in security-critical code.
3. Audit static_cast to Narrower Types
Any static_cast that narrows an integer (e.g., static_cast<uint16_t>(...)) in a size calculation context should be treated as a red flag during code review.
4. Validate Counts Before Multiplication
For firmware code processing hardware tables, validate that counts are within reasonable bounds before performing size calculations:
if (Count > MAX_EXPECTED_MEMORY_RANGES) {
return MO_RESULT_ERROR_INVALID_PARAMETER;
}
5. Use Safe Integer Arithmetic Libraries
Consider using safe integer libraries (like SafeInt for C++ or checked arithmetic intrinsics) that detect overflow at runtime.
Key Takeaways
- Never use
MO_UINT16(oruint16_t) to store the result of a size multiplication — even if the count itself fits in 16 bits, the product of count × element_size likely does not. static_castto a narrower type is not validation — it's silent truncation. The explicit cast at line 262 gave a false sense of intentionality while hiding a critical bug.- ACPI table parsing is an attack surface in virtualized environments — hypervisors control the firmware tables presented to guests, making count/size fields attacker-controlled.
- The same bug pattern appeared three times in the same file (
MoUefiAcpiQueryMemoryRanges,MoUefiAcpiQueryMergedMemoryRanges,MoUefiAcpiQueryMemoryHoles), demonstrating how copy-paste propagates vulnerabilities. - Firmware-level heap overflows are uniquely dangerous — they execute below the OS, potentially compromising the entire trust chain from boot through runtime.
How Orbis AppSec Detected This
- Source: The
Countvalue derived from parsing ACPI SRAT Memory Affinity Structures in the firmware table (attacker-controlled in virtualized environments) - Sink:
MoPlatformHeapAllocate(reinterpret_cast<PMO_POINTER>(&Ranges), Size)inMobility.Uefi.Acpi.cpp:262(and lines 373, 436) - Missing control: No use of an appropriately-sized integer type for the size calculation; the
MO_UINT16type silently truncates the multiplication result before it reaches the allocator - CWE: CWE-680 (Integer Overflow to Buffer Overflow)
- Fix: Changed the
Sizevariable type fromMO_UINT16toMO_UINTNin all three affected allocation sites, preventing truncation of the size calculation
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates a deceptively simple but devastating pattern: using an integer type that's too small to hold a computed size. The MO_UINT16 type seemed reasonable at first glance—after all, Count itself is a MO_UINT16—but the multiplication by a struct size pushes the result well beyond 16-bit range. In firmware code that processes hardware tables, this creates a directly exploitable heap overflow.
The fix—changing three variable declarations from MO_UINT16 to MO_UINTN—is minimal in code change but maximal in security impact. It's a reminder that type selection is a security decision, especially when that type determines how much memory gets allocated.