Cole Munz

build log · embedded and firmware security

U-Boot never had a btrfs test

2026-08-10

U-Boot can boot from btrfs and has been able to for years. Its filesystem test suite covers fat12, fat16, fat32, exfat, ext4, squashfs and erofs. It has never run a single btrfs test. Grep for btrfs under test/ and nothing comes back.

The gap is not hypothetical. Armbian shipped bootloaders with btrfs zstd decompression broken and users hit it on real boards (armbian/build#9651), while nothing in the U-Boot tree noticed, because nothing in the tree was looking.

I care because the Flipper One boots Debian from btrfs, snapshots and all, so U-Boot has to read a kernel and a devicetree off a btrfs volume before anything else exists. I've been sending fixes into that bring-up for a few weeks. This thread of it started with the missing tests and ended today with Tom Rini applying three of my patches to mainline U-Boot. The middle is the part worth writing down.

Writing the missing suite

The suite itself is not clever. One image per compression algorithm: none, zlib, lzo, zstd. Every file is read back and crc32-compared against the source. The file sizes are picked to hit the separate read paths in fs/btrfs/inode.c: an inline extent, the single sector btrfs leaves uncompressed, one compressed regular extent, one at the 128K compressed maximum, and a two-extent file whose length is not block aligned.

The one deliberate choice: images are built with mkfs.btrfs --rootdir, which needs no root and no loop mount. A filesystem test that needs root is a test that CI quietly skips.

The reproducer nobody had

The zstd bug came with a catch. mkfs.btrfs cannot produce the layout that triggers it, because btrfs-progs writes inline extents whose compressed frame already matches ram_bytes. Only the kernel writes the shape that fails, and getting a kernel-written image means mounting btrfs, which means root, which means the reproducer doesn't fit in the suite.

So the test synthesizes the kernel's layout instead. Take a clean image, rewrite ram_bytes in the extent item and the inode size in place, and recompute the leaf's crc32c. Only fixed-width fields change, so no items move, and btrfs check still reports no errors. The crc32c code proves itself against the checksum btrfs already stored before it's allowed to write anything. Stock U-Boot then fails the readback exactly the way armbian's users saw it:

zstd_decompress: failed to decompress: 70
BTRFS: An error occurred while reading file /inline.bin

Error 70 is dstSize_tooSmall. The fix for that one is in the Flipper One's U-Boot tree and got a mention in Dev Log #8; its mainline trip, together with the suite, is still in my queue.

What the suite caught

U-Boot recently moved btrfs onto its generic directory-listing code (this series). Good change, less custom code. I ran the new suite on top of it and four of the five tests went red.

btrfs_readdir() zeroes the dirent and fills in only the name and the type. dent->size stays 0, so every file on a btrfs volume lists as empty:

=> ls host 0 /
        0   f_192k.bin
        0   small_3k.bin

Reads still worked, because btrfs_read() looks the size up on its own. But every other filesystem in the tree fills dent->size in its readdir: ext4, exfat, erofs, squashfs, fat. And it goes past cosmetics. The EFI layer's dir_read() copies dent->size into both file_size and physical_size, so an EFI application enumerating a btrfs directory sees nothing but zero-byte files.

The fix rides on what the code already had. btrfs_next_dir_entry() has the directory item mapped when it builds the entry, so hand the key it points at back to the caller and use it to reach the inode item and its size. A subvolume entry points at a root item and has no size of its own; it stays 0.

=> ls host 0 /
   196608   f_192k.bin
     3000   small_3k.bin

Review found the second bug

I sent that one fix to the list. Qu Wenruo, who maintains btrfs in the kernel, reviewed the bootloader patch, and while he was in there he pointed at something I had walked right past: U-Boot's copy of btrfs_search_slot() returns on error with the nodes it descended through still attached to the path. The kernel's version releases them. Callers written against the kernel's convention, and btrfs_size() is one, bail out on a failed search and leak references they don't know they hold.

His observation became the second patch, with his Suggested-by on it. Folding the two copies of the same inode-size lookup into one helper became the third. All three carry his Reviewed-by, which is a kernel filesystem maintainer reviewing bootloader code, because the on-disk format doesn't care whose repo you're in. Today the series landed in mainline: the readdir sizes, the path release, the deduplication.

The part that isn't done

The suite that found all of this is not in mainline yet, so U-Boot's btrfs is exactly as untested today as when I started, and the next regression is still free. The three patches are the output; the suite is the machine that produced them, and it's the next thing I send.

What I keep relearning, part two: the fastest way to make a maintainer trust a fix is to hand them the red test that goes green with it. Nobody had to take my word for any of this. They could run it.