Multiple arbitrary Out-of-Bounds (OOB) '\0' byte write vulnerabilities affecting the ARM Ampere Management Mode (MM) PCIe driver were discovered. This code is bundled into the ARM Unified Extensible Firmware Interface (UEFI) firmware and runs in the Secure world at Exception Level 0 (S-EL0). The PCIe driver is used to initialize the root complex, underlying controllers, logging facilities, and perform self tests during UEFI Driver Execution Environment (DXE) phase. After initialization a lock is used to limit the available interfaces to a few functions, which are accessible from Non-Secure EL1 (NS-EL1) using the Secure Monitor Call (SMC) instruction with a predefined shared buffer for message passing.
It was found that the post initialization lock was never set leaving per-initialization interfaces available and multiple handlers use a UIN64 size from an NS-EL1 supplied buffer without proper bounds checking to index an array and writing a '\0' byte.
Ampere has addressed the vulnerability and posted a security bulletin.
NS-EL1 software interacts with the MM PCIe driver using the Firmware Framework for A-Profile (FFA) specification through the SMC instruction. The SMC instruction is used to switch to and from the Non-Secure and Secure worlds and FFA is used for dispatching to specific drivers and services. A shared buffer (initialized during the UEFI DXE phase as mNsCommBufferMemRegion in ArmPkg/Drivers/MmCommunicationDxe/MmCommunication.c) is used to communicate messages between NS-EL1 and the S-EL0 PCIe driver.
The handler for the PCIe MM driver can be reached using the following GUID using the FFA specification. PCIeMmHeader and PCIeMmBuffer show the general layout of the communication buffer. Before the PCIeMmHandler dispatch routine is called the MM subsystem copies the shared message buffer to a private memory region to prevent Time-of-Check to Time-of-Use (TOCTOU) issues. As drivers uniquely define the underlying structures in the communicated messages each must perform validation before using the supplied data.
The code snippets included were created through analysis with Ghidra using names contained in string references from the UEFI image. Known functions and variables from the EDK2 repository were also applied.
#define PCIE_MM_GUID \
{0xe49f1b7a, 0xd3c9, 0x44f4, { 0x9b, 0xc4, 0xd3, 0xb2, 0x9a, 0xcb, 0xb3, 0x20 }};
#define MAX_PCIE_MM_MAX_SIZE 0x10000
typedef struct {
UINT64 FuncId;
UINT8 data[1];
} PcieMmHeader;
// ...
typedef struct {
UINT64 unknown_0;
UINT64 size;
UINT64 unknown_1;
UINT8 data[1];
} PcieMmBuffer;
The PCIeMmBuffer structure includes a FuncId field for dispatching to specific sub-handlers and both 103 and 104 , shown in the code snippet below as case 2 and case 3, include a UINT64 size to represent the size of a NS-EL1 supplied buffer.
EFI_STATUS PcieMmHandler(int DispatchHandle,void *RegisterContext,void *CommBuffer, uint64_t *CommBufferSize) {
// ...
PcieMmHeader *header;
PcieMmBuffer *buffer;
ASSERT(CommBuffer);
ASSERT(CommBufferSize);
if ((DebugLevel() & 0xff) != 0) {
Debug(0x40,"%a \n","PcieMmHandler");
Debug(0x400000, "PcieMm Handler: CommBuffer - 0x%p, CommBufferSize - 0x%x\n", CommBuffer, *CommBufferSize);
Debug(0x400000, "PcieMm Handler: FuncId - %d\n", CommBuffer->FuncId);
}
header = (PCIeMmHeader *)CommBuffer;
FuncId = header->FuncId;
buffer = header->data;
if (pcie_mm_lock == '\0') {
if (106 < function) {
//...
goto switchD_000077a8_caseD_4;
}
if (FuncId < 100) goto LAB_00007760;
if (FuncId - 101 < 6) {
switch(FuncId - 101 & 0xffffffff) {
// ...
case 1: // initialization lock function
pcie_mm_lock = '\x01';
break;
case 2:
if (CommBuffer->size == 0) {
data = (char *)0x0;
}
else {
data = &CommBuffer->data;
i = CommBuffer->size + -1;
if (data[i] != '\0') {
data[i] = '\0';
}
}
// ...
case 3:
if (CommBuffer->size == 0) {
data = (char *)0x0;
}
else {
data = &CommBuffer->data;
i = CommBuffer->size + -1;
if (data[i] != '\0') {
data[i] = '\0';
}
}
// ...
// ...
When PCIeMmHandler is called some initial checks are performed on the CommBuffer and CommBufferSize. FuncId extracted from the CommBuffer is used for further dispatching with case values greater than 99 only being allowed before pcie_mm_lock is set, which is meant to perform during initialization in the UEFI DXE boot phase. Initialization code is responsible for calling FuncId 102 once completed. After this only the FuncIds with a value less than or equal to 99 are allowed.
Each dispatch case is independently responsible for validating the PCIeMmBuffer because the underlying structure is unique to the specific handler. For function 103 and 104 no validation is performed. When either of these routines are called they check buffer->size and if not zero proceed to index data with size and assign a '\0' byte to the dereferenced location. As size is a UINT64 this provides the ability to write a '\0' byte to any location in the S-EL0 driver's address space.
Additionally, it was found that with pcie_mm_lock not being set the HotPlug MM driver left additional interfaces exposed to NS-EL1 at runtime. The HotPlugMmHandler could be used to call function 107 with an attacker controlled buffer. The data structures specific to the HotPlugMmHandler are excluded because the conversion is similar to the PCIeMmHandler above.
#define NUM_TABLE_ENTRIES 25
HotPlugTableEntry TableEntries[NUM_TABLE_ENTRIES];
// ...
UINT32 TableIndex = 0;
// ...
EFI_STATUS HotPlugMmHandler(int DispatchHandle,void *RegisterContext,void *CommBuffer, uint64_t *CommBufferSize) {
// ...
HotPlugHeader *header;
HotPlugBuffer *buffer;
header = (HotPlugHeader *)CommBuffer;
function = header->function;
index = header->index;
data header->data;
// ...
if (function == 107) {
// ...
if (index < 25) {
ZeroMem(&TableEntries[TableIndex], 8);
CopyMem(&TableEntries[TableIndex], data, 8);
//...
}
else {
if (index != 255) goto LAB_000068f4;
ZeroMem(&TableEntries[TableIndex], 8);
CopyMem(&TableEntries[TableIndex], data, 8);
// ...
}
// ...
}
The buffer is copied into a global table as long as an attacker supplied UINT32 index is less than 25 or if the index equals 255. TableIndex is never checked to ensure it’s within the bounds of the table resulting in other global data structures to be overwritten when called enough times.
Date reported: 2025-09-19
Date fixed: 2025-12-18
Date disclosed: 2025-12-18