# 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.