TrustZone Break-in Vulnerabilities in Ampere UEFI MM Drivers (Arbitrary Out-of-Bounds Write)

sleightofalex
Mon, 05 Jan 2026 15:59:30 -0800

**security-research** Public

# TrustZone Break-in Vulnerabilities in Ampere UEFI MM Drivers (Arbitrary Out-of-Bounds Write)

## Package

## Affected versions

## Patched versions

## Description

#### Summary

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.

#### Analysis

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.

### Timeline

**Date reported**: 2025-09-19

**Date fixed**: 2025-12-18

**Date disclosed**: 2025-12-18