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.
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.
Date reported: 2025-09-19
Date fixed: 2025-12-18
Date disclosed: 2025-12-18