**security-research** Public

# TrustZone Break-in Vulnerabilities in Ampere UEFI MM Drivers (Buffer Overflow and Stack Information Leak)

## Package

## Affected versions

## Patched versions

## Description

### Summary

A buffer overflow and stack information leak affecting the ARM Ampere Management Mode (MM) Boot Error Record Table (BERT) driver. 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 BERT driver is used to persist unhandled hardware faults (e.g. errors that stem from memory, CPU, system bus, overheating, failing motherboard, or inadequate power supply) from a previous boot cycle. Non-Secure EL1 (NS-EL1) software communicates with this driver using the Secure Monitor Call (SMC) instruction and a predefined shared buffer for message passing. Upon system reboot NS-EL1 would communicate with the BERT driver to check for prior failures and obtain detailed information from the log.

Values passed from NS-EL1 to S-EL0 defining the `block_size` of `payload_data` lack proper checks. This allows for an Out-of-Bounds (OOB) write on `payload_data` that could be used for privilege escalation from the normal world to the secure world or an OOB read of stack data from the secure world.

Ampere has addressed the vulnerability and posted a security bulletin.

#### Analysis

NS-EL1 software interacts with the MM BERT 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 Driver Execution Environment [DXE] phase as `mNsCommBufferMemRegion` in ArmPkg/Drivers/MmCommunicationDxe/MmCommunication.c) is used to communicate messages between NS-EL1 and the S-EL0 BERT driver.

The handler for the BERT MM driver can be reached using the following GUID using the FFA specification. `BertMmHeader` shows the general layout of the communication buffer. Before the `BertMmHandler` 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 BERT_MM_GUID {0xCB01E6A2, 0xB22C, 0x4955, {0xAB, 0x75, 0xAC, 0x65, 0xEF, 0xFA, 0xE9, 0xD8}} #define BERT_MM_MIN_SIZE 0xe #define BERT_MM_MAX_SIZE 0x1000 typedef struct { UINT8 function; UINT8 unknown_0; UINT32 size; RETURN_STATUS status; UINT8 data[1]; } BertMmHeader;
“`

The `BertMmHeader` structure includes a function field for dispatching to specific sub-handlers and a `UINT32 size` to represent how much data should be returned. `BERT_MM_MAX_SIZE` defines the maximum size that could be returned.

“`
uint8_t *bert_latest; EFI_STATUS BertMmHandler(EFI_HANDLE DispatchHandle,void *RegisterContext, void *CommBuffer, uint64_t *CommBufferSize) { // … BertMmHeader *header; header = (BertMmHeader *)CommBuffer; if (CommBuffer == NULL || CommBufferSize == 0x0) { return 0; } if (*CommBufferSize < BERT_MM_MIN_SIZE) { if ((size & 0xff) == 0) { return EFI_ACCESS_DENIED; } __LINE__ = 0x21; Debug(DEBUG_ERROR,”%a %d Communication buffer size invalid!n”,”BertMmHandler”,__LINE__); return EFI_ACCESS_DENIED; } switch(header->function) { case 1: if (BERT_MM_MAX_SIZE < *CommBufferSize – 0xe) { if ((size & 0xff) == 0) { return EFI_ACCESS_DENIED; } __LINE__ = 0x2d; goto LAB_000025ac; } block_size = header->size; if (block_size == 0) { return EFI_INVALID_PARAMETER; } else { if (mfs == 0x0) { GetProtocol(); } payload_data = &header->data; if ((*bert_latest >> 1 & 1) == 0) { pbVar9 = bert_latest + 8; i = 0; do { if (((bert_latest[i * 0x17] & 1) != 0) && ((bert_latest[i * 0x17] & 4) != 0)) { path = pbVar9; } i = i + 1; pbVar9 = pbVar9 + 0x17; } while (i != 3); result = mfs->open(mfs, &file, path, 1); if (result < 0) { result = -0x7ffffffffffffff9; return result; } mfs->read(mfs, file, 0, payload_data, &block_size); mfs->close(mfs, file); // … } else { block_buffer buffer; buffer.unknown_0 = 1; buffer.size0 = 0x20; buffer.unknown_2 = 1; buffer.unknown_3 = 0; buffer.size1 = 0x20; CopyMem(payload_data, buffer, block_size); // …
“`

When `BertMmHandler` is called, some initial checks are performed on the `CommBuffer` and `CommBufferSize`. When the `header->function` is `1` the `*CommBufferSize` is checked to ensure it’s less than or equal to `BERT_MM_MAX_SIZE` after this the code branches based on a field in the `bert_latest` structure. In both cases though the `block_size` is **never** validated to be less than `BERT_MM_MAX_SIZE`. `payload_data` is either populated from the contents of a file using `mfs->read` or a newly initialized stack based structure using `CopyMem`.

In the case of `CopyMem` the size used is `block_size` and is never checked to be less than or equal to the stack based `block_buffer` structure size or `payload_data` size. When `block_size` is greater than the size of `block_buffer` this results in stack based OOB read with the data being returned to NS-EL1 in `payload_data`. When `block_size` is greater than `payload_data` an OOB write occurs with stack data copied to memory past the end of `payload_data`.

For `mFs->read` `&block_size` is used to determine how many bytes should be written into the `payload_data` buffer. If the file being read and `block_size` are both larger than `payload_data` an OOB write occurs using the contents of the file to write past the end of `payload_data`.

### Timeline

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

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

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