Re: [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents

Qu Wenruo <[email protected]> Sat, 1 Aug 2026 07:08:29 +0930
Newsgroups org.kernel.vger.linux-btrfs,org.u-boot-project.lists.u-boot
Message-ID <[email protected]>

=E5=9C=A8 2026/8/1 05:37, Cole Munz =E5=86=99=E9=81=93:
> Btrfs pads compressed extents up to a sector boundary, so a file whose
> size isn't sector-aligned still gets compressed as a frame whose
> declared content size is the sector-rounded length, larger than the
> extent's ram_bytes.

The padding at plain text level is completely common, since btrfs like=20
all other major fses are block device based, all IO including=20
compression is done at fs block level, so is the compressed data, which=20
still needs to be block aligned.

But your "larger than the extent's ram_bytes" is where I do not get.

The ram_bytes described the decompressed size of a compressed extent,=20
except inlined extents, the ram_bytes should always be fs block aligned.

I didn't see how things can go "larger than the extent's ram_bytes".

Can you provide more info about this, or better, provide the dump-tree=20
output for the involved files?

Thanks,
Qu

> zstd_decompress_dctx()'s one-shot API requires the
> destination buffer to cover the whole frame and fails with
> ZSTD_error_dstSize_tooSmall otherwise, even though
> btrfs_read_extent_reg() and btrfs_read_extent_inline() already
> zero-fill any short tail. The file then reads back truncated or the
> read fails outright, which is what breaks fdt apply on zstd-compressed
> overlays.
>=20
> Decompress into a bounce buffer sized to the frame when the declared
> content size exceeds ram_bytes, and copy out only what the caller
> asked for. Also trim the input with zstd_find_frame_compressed_size()
> first, since the on-disk extent may carry trailing sector padding
> after the frame.
>=20
> The fix lives here rather than in the shared lib/zstd/zstd.c wrapper
> on purpose: for FIT images, ximg and ubifs an undersized destination
> really does mean a corrupt input and should keep failing hard. Armbian
> carries the same caller-side fix for this exact failure on RK3399 and
> ODROID-N2 boards (armbian/build#9651, #10208), where it showed up as
> "zstd_decompress: failed to decompress: 70".
>=20
> Fixes: 918adf8e0733 ("btrfs: Use U-Boot API for decompression")
> Signed-off-by: Cole Munz <[email protected]>
> ---
>   fs/btrfs/compression.c | 76 +++++++++++++++++++++++++++++++++++++++---
>   1 file changed, 71 insertions(+), 5 deletions(-)
>=20
> diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c
> index c69524d38ecc..017e61242a68 100644
> --- a/fs/btrfs/compression.c
> +++ b/fs/btrfs/compression.c
> @@ -6,7 +6,7 @@
>    */
>  =20
>   #include "btrfs.h"
> -#include <abuf.h>
> +#include <limits.h>
>   #include <log.h>
>   #include <malloc.h>
>   #include <linux/lzo.h>
> @@ -137,12 +137,78 @@ static u32 decompress_zlib(const u8 *_cbuf, u32 cl=
en, u8 *dbuf, u32 dlen)
>  =20
>   static u32 decompress_zstd(const u8 *cbuf, u32 clen, u8 *dbuf, u32 dle=
n)
>   {
> -	struct abuf in, out;
> +	zstd_dctx *ctx;
> +	zstd_frame_header fh;
> +	size_t wsize, len, dst_capacity =3D dlen;
> +	void *workspace;
> +	u8 *dst =3D dbuf, *bounce =3D NULL;
> +	u32 ret;
> +
> +	wsize =3D zstd_dctx_workspace_bound();
> +	workspace =3D malloc(wsize);
> +	if (!workspace)
> +		return -1;
> +
> +	ctx =3D zstd_init_dctx(workspace, wsize);
> +	if (!ctx) {
> +		ret =3D -1;
> +		goto out;
> +	}
>  =20
> -	abuf_init_set(&in, (u8 *)cbuf, clen);
> -	abuf_init_set(&out, dbuf, dlen);
> +	/*
> +	 * Compressed extents are padded up to a sector boundary, so clen may
> +	 * include trailing junk after the actual zstd frame.
> +	 */
> +	len =3D zstd_find_frame_compressed_size(cbuf, clen);
> +	if (zstd_is_error(len)) {
> +		ret =3D -1;
> +		goto out;
> +	}
>  =20
> -	return zstd_decompress(&in, &out);
> +	/*
> +	 * Btrfs compresses whole sectors, so a file whose size is not a
> +	 * multiple of the sector size still yields a frame whose content
> +	 * size is the sector-rounded length, which can be larger than dlen
> +	 * (ram_bytes from the extent item). zstd_decompress_dctx() requires
> +	 * its output buffer to cover the whole frame and would otherwise
> +	 * fail with ZSTD_error_dstSize_tooSmall, even though the extra bytes
> +	 * are just padding the caller is going to discard: both
> +	 * btrfs_read_extent_reg() and btrfs_read_extent_inline() already
> +	 * zero-fill any tail beyond what we return here. Decompress into a
> +	 * bounce buffer sized to the frame when that happens.
> +	 */
> +	if (!zstd_get_frame_header(&fh, cbuf, len) &&
> +	    fh.frameContentSize !=3D ZSTD_CONTENTSIZE_UNKNOWN &&
> +	    fh.frameContentSize > dlen) {
> +		if (fh.frameContentSize > SIZE_MAX) {
> +			ret =3D -1;
> +			goto out;
> +		}
> +		bounce =3D malloc(fh.frameContentSize);
> +		if (!bounce) {
> +			ret =3D -1;
> +			goto out;
> +		}
> +		dst =3D bounce;
> +		dst_capacity =3D fh.frameContentSize;
> +	}
> +
> +	len =3D zstd_decompress_dctx(ctx, dst, dst_capacity, cbuf, len);
> +	if (zstd_is_error(len)) {
> +		ret =3D -1;
> +		goto out;
> +	}
> +
> +	if (bounce) {
> +		memcpy(dbuf, bounce, dlen);
> +		ret =3D dlen;
> +	} else {
> +		ret =3D len;
> +	}
> +out:
> +	free(bounce);
> +	free(workspace);
> +	return ret;
>   }
>  =20
>   u32 btrfs_decompress(u8 type, const char *c, u32 clen, char *d, u32 dl=
en)