[PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents

Cole Munz <[email protected]>
Newsgroups org.u-boot-project.lists.u-boot,org.kernel.vger.linux-btrfs
Message-ID <f6ac2bbdb9b559041cb0c82efccf89ea7262cb43.1785545440.git.Munzzyy1@proton.me>
The kernel compresses an inline extent as a whole block:
run_delalloc_inline() calls btrfs_compress_bio(inode, 0, blocksize, ...),
so the data is zero-filled past EOF and the resulting zstd frame declares
a content size of one block. The extent item records the unaligned file
size though - __cow_file_range_inline() passes i_size down to
insert_inline_extent(), which stores it as ram_bytes.

btrfs_read_extent_inline() sizes its decompression buffer from ram_bytes,
so for a 1900-byte file the destination is 1900 bytes while the frame
decodes to 4096. Since commit 918adf8e0733 ("btrfs: Use U-Boot API for
decompression") btrfs decompresses through the common U-Boot helper,
which uses the one-shot zstd_decompress_dctx(). That API requires the
destination to cover the whole frame and fails with dstSize_tooSmall,
error 70, otherwise. The streaming ZSTD_decompressStream() path it
replaced stopped once the output buffer was full, so it never hit this.

The kernel side does not notice because fs/btrfs/zstd.c streams into its
own buffer and copies out at most destlen.

Allocate a full block for the decompression buffer and copy only
ram_bytes back to the caller. An inline extent never spans more than one
block, which bounds the allocation.

This shows up on RK3399 and ODROID-N2 as "zstd_decompress: failed to
decompress: 70" (armbian/build#9651, #10208), where it breaks fdt apply
on zstd-compressed overlays. Images built with mkfs.btrfs --rootdir
--compress zstd do not reproduce it, since btrfs-progs writes a frame
whose content size already equals ram_bytes. Only files written at
runtime through the kernel trip it.

Fixes: 918adf8e0733 ("btrfs: Use U-Boot API for decompression")
Signed-off-by: Cole Munz <[email protected]>
---
v2:
 - Move the fix into btrfs_read_extent_inline() and size the decompression
   buffer to a full block, as Qu suggested. decompress_zstd() is left alone.
 - Rewrite the commit message. The failing case is compressed inline
   extents, not sector-padded regular extents: Qu pointed out that
   ram_bytes is always block aligned for regular extents.
 - Drop the zstd_find_frame_compressed_size() input trimming from v1. The
   inline item length is already exact, so it was doing nothing.

v1: https://lore.kernel.org/u-boot/9acf406b5e73fa83001e1951e883143e2afb8b2d.1785528314.git.Munzzyy1@proton.me/

 fs/btrfs/inode.c | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c
index 3998ffc2c819..53f1059e1ef8 100644
--- a/fs/btrfs/inode.c
+++ b/fs/btrfs/inode.c
@@ -362,9 +362,11 @@ int btrfs_read_extent_inline(struct btrfs_path *path,
 			     struct btrfs_file_extent_item *fi, char *dest)
 {
 	struct extent_buffer *leaf = path->nodes[0];
+	struct btrfs_fs_info *fs_info = leaf->fs_info;
 	int slot = path->slots[0];
 	char *cbuf = NULL;
 	char *dbuf = NULL;
+	u32 dbuf_size;
 	u32 csize;
 	u32 dsize;
 	int ret;
@@ -380,8 +382,17 @@ int btrfs_read_extent_inline(struct btrfs_path *path,
 
 	/* Compressed extent, prepare the compressed and data buffer */
 	dsize = btrfs_file_extent_ram_bytes(leaf, fi);
+	/*
+	 * The kernel compresses an inline extent as a whole block, zero-filling
+	 * the tail past EOF, so the stream can decompress to more than
+	 * ram_bytes.  zstd's one-shot API rejects a destination that cannot
+	 * hold the entire frame, so give the decompressor a full block and copy
+	 * only ram_bytes back out.  An inline extent never spans more than one
+	 * block, which bounds the allocation.
+	 */
+	dbuf_size = max_t(u32, dsize, fs_info->sectorsize);
 	cbuf = malloc(csize);
-	dbuf = malloc(dsize);
+	dbuf = malloc(dbuf_size);
 	if (!cbuf || !dbuf) {
 		ret = -ENOMEM;
 		goto out;
@@ -389,7 +400,7 @@ int btrfs_read_extent_inline(struct btrfs_path *path,
 	read_extent_buffer(leaf, cbuf, btrfs_file_extent_inline_start(fi),
 			   csize);
 	ret = btrfs_decompress(btrfs_file_extent_compression(leaf, fi),
-			       cbuf, csize, dbuf, dsize);
+			       cbuf, csize, dbuf, dbuf_size);
 	if (ret < 0) {
 		ret = -EIO;
 		goto out;
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.