* [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents
@ 2026-07-31 20:07 Cole Munz
2026-07-31 21:38 ` Qu Wenruo
2026-08-01 0:55 ` [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents Cole Munz
0 siblings, 2 replies; 6+ messages in thread
From: Cole Munz @ 2026-07-31 20:07 UTC (permalink / raw)
To: Marek Behún, Tom Rini; +Cc: Qu Wenruo, Simon Glass, u-boot, linux-btrfs
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. 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.
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.
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".
Fixes: 918adf8e0733 ("btrfs: Use U-Boot API for decompression")
Signed-off-by: Cole Munz <Munzzyy1@proton.me>
---
fs/btrfs/compression.c | 76 +++++++++++++++++++++++++++++++++++++++---
1 file changed, 71 insertions(+), 5 deletions(-)
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 @@
*/
#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 clen, u8 *dbuf, u32 dlen)
static u32 decompress_zstd(const u8 *cbuf, u32 clen, u8 *dbuf, u32 dlen)
{
- struct abuf in, out;
+ zstd_dctx *ctx;
+ zstd_frame_header fh;
+ size_t wsize, len, dst_capacity = dlen;
+ void *workspace;
+ u8 *dst = dbuf, *bounce = NULL;
+ u32 ret;
+
+ wsize = zstd_dctx_workspace_bound();
+ workspace = malloc(wsize);
+ if (!workspace)
+ return -1;
+
+ ctx = zstd_init_dctx(workspace, wsize);
+ if (!ctx) {
+ ret = -1;
+ goto out;
+ }
- 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 = zstd_find_frame_compressed_size(cbuf, clen);
+ if (zstd_is_error(len)) {
+ ret = -1;
+ goto out;
+ }
- 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 != ZSTD_CONTENTSIZE_UNKNOWN &&
+ fh.frameContentSize > dlen) {
+ if (fh.frameContentSize > SIZE_MAX) {
+ ret = -1;
+ goto out;
+ }
+ bounce = malloc(fh.frameContentSize);
+ if (!bounce) {
+ ret = -1;
+ goto out;
+ }
+ dst = bounce;
+ dst_capacity = fh.frameContentSize;
+ }
+
+ len = zstd_decompress_dctx(ctx, dst, dst_capacity, cbuf, len);
+ if (zstd_is_error(len)) {
+ ret = -1;
+ goto out;
+ }
+
+ if (bounce) {
+ memcpy(dbuf, bounce, dlen);
+ ret = dlen;
+ } else {
+ ret = len;
+ }
+out:
+ free(bounce);
+ free(workspace);
+ return ret;
}
u32 btrfs_decompress(u8 type, const char *c, u32 clen, char *d, u32 dlen)
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents
2026-07-31 20:07 [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents Cole Munz
@ 2026-07-31 21:38 ` Qu Wenruo
2026-07-31 22:26 ` Cole Munz
2026-08-01 0:55 ` [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents Cole Munz
1 sibling, 1 reply; 6+ messages in thread
From: Qu Wenruo @ 2026-07-31 21:38 UTC (permalink / raw)
To: Cole Munz, Marek Behún, Tom Rini
Cc: Qu Wenruo, Simon Glass, u-boot, linux-btrfs
在 2026/8/1 05:37, Cole Munz 写道:
> 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
all other major fses are block device based, all IO including
compression is done at fs block level, so is the compressed data, which
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,
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
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.
>
> 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.
>
> 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".
>
> Fixes: 918adf8e0733 ("btrfs: Use U-Boot API for decompression")
> Signed-off-by: Cole Munz <Munzzyy1@proton.me>
> ---
> fs/btrfs/compression.c | 76 +++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 71 insertions(+), 5 deletions(-)
>
> 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 @@
> */
>
> #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 clen, u8 *dbuf, u32 dlen)
>
> static u32 decompress_zstd(const u8 *cbuf, u32 clen, u8 *dbuf, u32 dlen)
> {
> - struct abuf in, out;
> + zstd_dctx *ctx;
> + zstd_frame_header fh;
> + size_t wsize, len, dst_capacity = dlen;
> + void *workspace;
> + u8 *dst = dbuf, *bounce = NULL;
> + u32 ret;
> +
> + wsize = zstd_dctx_workspace_bound();
> + workspace = malloc(wsize);
> + if (!workspace)
> + return -1;
> +
> + ctx = zstd_init_dctx(workspace, wsize);
> + if (!ctx) {
> + ret = -1;
> + goto out;
> + }
>
> - 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 = zstd_find_frame_compressed_size(cbuf, clen);
> + if (zstd_is_error(len)) {
> + ret = -1;
> + goto out;
> + }
>
> - 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 != ZSTD_CONTENTSIZE_UNKNOWN &&
> + fh.frameContentSize > dlen) {
> + if (fh.frameContentSize > SIZE_MAX) {
> + ret = -1;
> + goto out;
> + }
> + bounce = malloc(fh.frameContentSize);
> + if (!bounce) {
> + ret = -1;
> + goto out;
> + }
> + dst = bounce;
> + dst_capacity = fh.frameContentSize;
> + }
> +
> + len = zstd_decompress_dctx(ctx, dst, dst_capacity, cbuf, len);
> + if (zstd_is_error(len)) {
> + ret = -1;
> + goto out;
> + }
> +
> + if (bounce) {
> + memcpy(dbuf, bounce, dlen);
> + ret = dlen;
> + } else {
> + ret = len;
> + }
> +out:
> + free(bounce);
> + free(workspace);
> + return ret;
> }
>
> u32 btrfs_decompress(u8 type, const char *c, u32 clen, char *d, u32 dlen)
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents
2026-07-31 21:38 ` Qu Wenruo
@ 2026-07-31 22:26 ` Cole Munz
2026-07-31 22:37 ` Qu Wenruo
0 siblings, 1 reply; 6+ messages in thread
From: Cole Munz @ 2026-07-31 22:26 UTC (permalink / raw)
To: Qu Wenruo; +Cc: Marek Behún, Tom Rini, Simon Glass, u-boot, linux-btrfs
Hi Qu,
> The ram_bytes described the decompressed size of a compressed extent,
> 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".
You're right, and the commit message overstates it. For regular extents
ram_bytes is block aligned and matches the frame's content size, exactly as
you describe. The case that actually fails is the one you carved out:
compressed inline extents.
For the inline path, current mainline compresses the whole first block:
run_delalloc_inline():
cb = btrfs_compress_bio(inode, 0, blocksize, compress_type, ...);
so the page is zero-padded past EOF and the resulting zstd frame declares
frameContentSize = blocksize. But the extent item gets the unaligned size:
__cow_file_range_inline(inode, i_size, compressed_size, ...) ->
insert_inline_extent() -> btrfs_set_file_extent_ram_bytes(leaf, ei, size);
So a 1900-byte file stored as a compressed inline extent has ram_bytes 1900
while the frame decodes to 4096. The kernel side never notices because
fs/btrfs/zstd.c zstd_decompress() streams into out_buf and copies out at most
destlen. U-Boot's decompress_zstd() is the one-shot zstd_decompress_dctx(),
and btrfs_read_extent_inline() sizes the destination with
dsize = btrfs_file_extent_ram_bytes(), so the frame fails the whole-frame
capacity check with dstSize_tooSmall - error code 70, which matches the
"failed to decompress: 70" in the Armbian reports.
One data point from testing while chasing this: an image built with
mkfs.btrfs --rootdir --compress zstd (btrfs-progs 7.1) does NOT reproduce.
For a 1900-byte file, progs writes an inline extent with ram_bytes 1900 whose
frame also decodes to exactly 1900:
item 8 key (257 EXTENT_DATA 0) itemoff 15316 itemsize 423
generation 6 type 0 (inline)
inline extent data size 402 ram_bytes 1900 compression 3 (zstd)
so mkfs-built images boot fine, and the failure only shows up on files
(re)written at runtime through the kernel - which fits the Armbian pattern of
/boot scripts and overlays breaking after a package update touched them.
I'll send a v2 with the commit message rewritten to name compressed inline
extents as the failing case instead of the hand-wave about sector padding. If
you'd like the on-disk evidence too, I can loop-mount a scratch fs with
compress=zstd, write an unaligned file, and include the dump-tree output plus
the frame header read from the leaf in the v2 cover.
The fix itself stays in btrfs's decompress_zstd() rather than lib/zstd
because for FIT/ximg/ubifs an undersized destination really does mean corrupt
input; only btrfs hands the decompressor a destination smaller than the frame
on purpose. If you'd rather see it shaped differently - say, only engaging
the bounce path for inline extents - happy to do that in v2 as well.
Thanks,
Cole
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents
2026-07-31 22:26 ` Cole Munz
@ 2026-07-31 22:37 ` Qu Wenruo
0 siblings, 0 replies; 6+ messages in thread
From: Qu Wenruo @ 2026-07-31 22:37 UTC (permalink / raw)
To: Cole Munz; +Cc: Marek Behún, Tom Rini, Simon Glass, u-boot, linux-btrfs
在 2026/8/1 07:56, Cole Munz 写道:
> Hi Qu,
>
>> The ram_bytes described the decompressed size of a compressed extent,
>> 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".
>
> You're right, and the commit message overstates it. For regular extents
> ram_bytes is block aligned and matches the frame's content size, exactly as
> you describe. The case that actually fails is the one you carved out:
> compressed inline extents.
>
> For the inline path, current mainline compresses the whole first block:
>
> run_delalloc_inline():
> cb = btrfs_compress_bio(inode, 0, blocksize, compress_type, ...);
>
> so the page is zero-padded past EOF and the resulting zstd frame declares
> frameContentSize = blocksize. But the extent item gets the unaligned size:
>
> __cow_file_range_inline(inode, i_size, compressed_size, ...) ->
> insert_inline_extent() -> btrfs_set_file_extent_ram_bytes(leaf, ei, size);
>
> So a 1900-byte file stored as a compressed inline extent has ram_bytes 1900
> while the frame decodes to 4096. The kernel side never notices because
> fs/btrfs/zstd.c zstd_decompress() streams into out_buf and copies out at most
> destlen. U-Boot's decompress_zstd() is the one-shot zstd_decompress_dctx(),
> and btrfs_read_extent_inline() sizes the destination with
> dsize = btrfs_file_extent_ram_bytes(), so the frame fails the whole-frame
> capacity check with dstSize_tooSmall - error code 70, which matches the
> "failed to decompress: 70" in the Armbian reports.
Thanks a lot! Now I see where the problem is.
>
> One data point from testing while chasing this: an image built with
> mkfs.btrfs --rootdir --compress zstd (btrfs-progs 7.1) does NOT reproduce.
> For a 1900-byte file, progs writes an inline extent with ram_bytes 1900 whose
> frame also decodes to exactly 1900:
>
> item 8 key (257 EXTENT_DATA 0) itemoff 15316 itemsize 423
> generation 6 type 0 (inline)
> inline extent data size 402 ram_bytes 1900 compression 3 (zstd)
>
> so mkfs-built images boot fine, and the failure only shows up on files
> (re)written at runtime through the kernel - which fits the Armbian pattern of
> /boot scripts and overlays breaking after a package update touched them.
>
> I'll send a v2 with the commit message rewritten to name compressed inline
> extents as the failing case instead of the hand-wave about sector padding. If
> you'd like the on-disk evidence too, I can loop-mount a scratch fs with
> compress=zstd, write an unaligned file, and include the dump-tree output plus
> the frame header read from the leaf in the v2 cover.
Another thing is, since this bug only affects inlined extents, I'd
prefer to have the fix located inside btrfs_read_extent_inline().
So that we allocate a full block for decompression, then only copy
ram_bytes back to the destination.
With a mention about the limit during dbuf allocation inside
btrfs_read_extent_inline().
Thanks a lot of the explanation and fix,
Qu
>
> The fix itself stays in btrfs's decompress_zstd() rather than lib/zstd
> because for FIT/ximg/ubifs an undersized destination really does mean corrupt
> input; only btrfs hands the decompressor a destination smaller than the frame
> on purpose. If you'd rather see it shaped differently - say, only engaging
> the bounce path for inline extents - happy to do that in v2 as well.
>
> Thanks,
> Cole
>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents
2026-07-31 20:07 [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents Cole Munz
2026-07-31 21:38 ` Qu Wenruo
@ 2026-08-01 0:55 ` Cole Munz
2026-08-01 1:13 ` Qu Wenruo
1 sibling, 1 reply; 6+ messages in thread
From: Cole Munz @ 2026-08-01 0:55 UTC (permalink / raw)
To: kabel, trini; +Cc: quwenruo.btrfs, sjg, u-boot, linux-btrfs
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 <Munzzyy1@proton.me>
---
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
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents
2026-08-01 0:55 ` [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents Cole Munz
@ 2026-08-01 1:13 ` Qu Wenruo
0 siblings, 0 replies; 6+ messages in thread
From: Qu Wenruo @ 2026-08-01 1:13 UTC (permalink / raw)
To: Cole Munz, kabel, trini; +Cc: sjg, u-boot, linux-btrfs
在 2026/8/1 10:25, Cole Munz 写道:
> 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 <Munzzyy1@proton.me>
Reviewed-by: Qu Wenruo <wqu@suse.com>
Now the fix looks much simpler.
Thanks,
Qu
> ---
> 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;
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-08-01 1:13 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 20:07 [PATCH] fs: btrfs: fix zstd decompression of sector-padded extents Cole Munz
2026-07-31 21:38 ` Qu Wenruo
2026-07-31 22:26 ` Cole Munz
2026-07-31 22:37 ` Qu Wenruo
2026-08-01 0:55 ` [PATCH v2] fs: btrfs: fix zstd decompression of compressed inline extents Cole Munz
2026-08-01 1:13 ` Qu Wenruo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox