* [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly
@ 2026-03-13 8:05 Qu Wenruo
2026-03-13 17:12 ` Jean-Christophe Guillain
2026-03-17 10:11 ` David Sterba
0 siblings, 2 replies; 4+ messages in thread
From: Qu Wenruo @ 2026-03-13 8:05 UTC (permalink / raw)
To: linux-btrfs; +Cc: stable, David Sterba, Jean-Christophe Guillain
[BUG]
Since commit 3d74a7556fba ("btrfs: zlib: introduce zlib_compress_bio()
helper"), there are some reports about different crashes in zlib
compression path. One of the symptoms is list corruption like the
following:
list_del corruption. next->prev should be fffffbb340204a08, but was ffff8d6517cb7de0. (next=fffffbb3402d62c8)
------------[ cut here ]------------
kernel BUG at lib/list_debug.c:65!
Oops: invalid opcode: 0000 [#1] SMP NOPTI
CPU: 1 UID: 0 PID: 21436 Comm: kworker/u16:7 Not tainted 7.0.0-rc2-jcg+ #1 PREEMPT
Hardware name: LENOVO 10VGS02P00/3130, BIOS M1XKT57A 02/10/2022
Workqueue: btrfs-delalloc btrfs_work_helper [btrfs]
RIP: 0010:__list_del_entry_valid_or_report+0xec/0xf0
Call Trace:
<TASK>
btrfs_alloc_compr_folio+0xae/0xc0 [btrfs]
zlib_compress_bio+0x39d/0x6a0 [btrfs]
btrfs_compress_bio+0x2e3/0x3d0 [btrfs]
compress_file_range+0x2b0/0x660 [btrfs]
btrfs_work_helper+0xdb/0x3e0 [btrfs]
process_one_work+0x192/0x3d0
worker_thread+0x19a/0x310
kthread+0xdf/0x120
ret_from_fork+0x22e/0x310
ret_from_fork_asm+0x1a/0x30
</TASK>
---[ end trace 0000000000000000 ]---
Other symptoms include VM_BUG_ON() during folio_put() but it's rarer.
David Sterba firstly reported this during his CI runs but unfortunately
I'm unable to hit it.
Meanwhile zstd/lzo doesn't seem to have the same problem.
[CAUSE]
During zlib_compress_bio() every time the output buffer is full, we
queue the full folio into the compressed bio, and allocate a new folio
as the output folio.
After the input has finished, we loop through zlib_deflate() with
Z_FINISH to flush all output.
And when that is done, we still need to check if the last folio has any
content, and if so we still need to queue that part into the compressed
bio.
The problem is in the final folio handling, if the final folio is full
(for x86_64 the folio size is 4K), the length to queue is calculated by
u32 cur_len = offset_in_folio(out_folio, workspace->strm.total_out);
But since total_out is 4K aligned, the resulted @cur_len will be 0, then
we hit the bio_add_folio(), which has a quirk that if bio_add_folio()
got an length 0, it will still queue the folio into the bio, but return
false.
In that case we go to out: tag, which calls btrfs_free_compr_folio() to
release @out_folio, which may put the out folio into the btrfs global
pool list.
On the other hand, that @out_folio is already added to the
compressed bio, and will later be released again by
cleanup_compressed_bio(), which results double release.
And if this time we still need to put the folio into the btrfs global
pool list, it will result a list corruption because it's already in the
list.
[FIX]
Instead of offset_inside_folio(), directly use the difference between
strm.total_out and bi_size.
So that if the last folio is completely full, we can still properly
queue the full folio other than queueing zero byte.
Fixes: 3d74a7556fba ("btrfs: zlib: introduce zlib_compress_bio() helper")
Cc: stable@vger.kernel.org # 7.0+
Reported-by: David Sterba <dsterba@suse.com>
Reported-by: Jean-Christophe Guillain <jean-christophe@guillain.net>
Link: https://bugzilla.kernel.org/show_bug.cgi?id=221176
Signed-off-by: Qu Wenruo <wqu@suse.com>
---
Changelog:
v2:
- Add missing reported-by/link/cc tags
---
fs/btrfs/zlib.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/fs/btrfs/zlib.c b/fs/btrfs/zlib.c
index 1a5093525e32..147c92a4dd04 100644
--- a/fs/btrfs/zlib.c
+++ b/fs/btrfs/zlib.c
@@ -308,7 +308,9 @@ int zlib_compress_bio(struct list_head *ws, struct compressed_bio *cb)
}
/* Queue the remaining part of the folio. */
if (workspace->strm.total_out > bio->bi_iter.bi_size) {
- u32 cur_len = offset_in_folio(out_folio, workspace->strm.total_out);
+ const u32 cur_len = workspace->strm.total_out - bio->bi_iter.bi_size;
+
+ ASSERT(cur_len <= folio_size(out_folio));
if (!bio_add_folio(bio, out_folio, cur_len, 0)) {
ret = -E2BIG;
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly
2026-03-13 8:05 [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly Qu Wenruo
@ 2026-03-13 17:12 ` Jean-Christophe Guillain
2026-03-17 10:11 ` David Sterba
1 sibling, 0 replies; 4+ messages in thread
From: Jean-Christophe Guillain @ 2026-03-13 17:12 UTC (permalink / raw)
To: Qu Wenruo, linux-btrfs; +Cc: stable, David Sterba
On Fri, 2026-03-13 at 18:35 +1030, Qu Wenruo wrote:
> [FIX]
> Instead of offset_inside_folio(), directly use the difference between
> strm.total_out and bi_size.
> So that if the last folio is completely full, we can still properly
> queue the full folio other than queueing zero byte.
>
> Fixes: 3d74a7556fba ("btrfs: zlib: introduce zlib_compress_bio()
> helper")
> Cc: stable@vger.kernel.org # 7.0+
> Reported-by: David Sterba <dsterba@suse.com>
> Reported-by: Jean-Christophe Guillain <jean-christophe@guillain.net>
> Link: https://bugzilla.kernel.org/show_bug.cgi?id=221176
> Signed-off-by: Qu Wenruo <wqu@suse.com>
> ---
> Changelog:
> v2:
> - Add missing reported-by/link/cc tags
This patch (applied on 7.0.0-rc3) fixed the bug for me.
(The uptime of my workstation is more than 5 hours now ; it didn't work
more than 40 minutes before.)
Thank you very much,
Jean-Christophe
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly
2026-03-13 8:05 [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly Qu Wenruo
2026-03-13 17:12 ` Jean-Christophe Guillain
@ 2026-03-17 10:11 ` David Sterba
2026-03-17 21:26 ` Qu Wenruo
1 sibling, 1 reply; 4+ messages in thread
From: David Sterba @ 2026-03-17 10:11 UTC (permalink / raw)
To: Qu Wenruo; +Cc: linux-btrfs, stable, David Sterba, Jean-Christophe Guillain
On Fri, Mar 13, 2026 at 06:35:26PM +1030, Qu Wenruo wrote:
> [BUG]
> Since commit 3d74a7556fba ("btrfs: zlib: introduce zlib_compress_bio()
> helper"), there are some reports about different crashes in zlib
> compression path. One of the symptoms is list corruption like the
> following:
>
> list_del corruption. next->prev should be fffffbb340204a08, but was ffff8d6517cb7de0. (next=fffffbb3402d62c8)
> ------------[ cut here ]------------
> kernel BUG at lib/list_debug.c:65!
> Oops: invalid opcode: 0000 [#1] SMP NOPTI
> CPU: 1 UID: 0 PID: 21436 Comm: kworker/u16:7 Not tainted 7.0.0-rc2-jcg+ #1 PREEMPT
> Hardware name: LENOVO 10VGS02P00/3130, BIOS M1XKT57A 02/10/2022
> Workqueue: btrfs-delalloc btrfs_work_helper [btrfs]
> RIP: 0010:__list_del_entry_valid_or_report+0xec/0xf0
> Call Trace:
> <TASK>
> btrfs_alloc_compr_folio+0xae/0xc0 [btrfs]
> zlib_compress_bio+0x39d/0x6a0 [btrfs]
> btrfs_compress_bio+0x2e3/0x3d0 [btrfs]
> compress_file_range+0x2b0/0x660 [btrfs]
> btrfs_work_helper+0xdb/0x3e0 [btrfs]
> process_one_work+0x192/0x3d0
> worker_thread+0x19a/0x310
> kthread+0xdf/0x120
> ret_from_fork+0x22e/0x310
> ret_from_fork_asm+0x1a/0x30
> </TASK>
> ---[ end trace 0000000000000000 ]---
>
> Other symptoms include VM_BUG_ON() during folio_put() but it's rarer.
>
> David Sterba firstly reported this during his CI runs but unfortunately
> I'm unable to hit it.
>
> Meanwhile zstd/lzo doesn't seem to have the same problem.
>
> [CAUSE]
> During zlib_compress_bio() every time the output buffer is full, we
> queue the full folio into the compressed bio, and allocate a new folio
> as the output folio.
>
> After the input has finished, we loop through zlib_deflate() with
> Z_FINISH to flush all output.
>
> And when that is done, we still need to check if the last folio has any
> content, and if so we still need to queue that part into the compressed
> bio.
>
> The problem is in the final folio handling, if the final folio is full
> (for x86_64 the folio size is 4K), the length to queue is calculated by
>
> u32 cur_len = offset_in_folio(out_folio, workspace->strm.total_out);
>
> But since total_out is 4K aligned, the resulted @cur_len will be 0, then
> we hit the bio_add_folio(), which has a quirk that if bio_add_folio()
> got an length 0, it will still queue the folio into the bio, but return
> false.
>
> In that case we go to out: tag, which calls btrfs_free_compr_folio() to
> release @out_folio, which may put the out folio into the btrfs global
> pool list.
>
> On the other hand, that @out_folio is already added to the
> compressed bio, and will later be released again by
> cleanup_compressed_bio(), which results double release.
>
> And if this time we still need to put the folio into the btrfs global
> pool list, it will result a list corruption because it's already in the
> list.
That's pretty convoluted but makes sense, it's an edge case and
depending on the compression data.
I guess we can drop patch
https://lore.kernel.org/linux-btrfs/30541df912ac4a2dd502796a823558fe1d88baa0.1772065237.git.wqu@suse.com/
the one removing the folio refcount from zlib, this was the first
trigger. Debugging with the assertion dropped allowed the bug to
manifest as the list corruption that I think made it more
understandable.
> [FIX]
> Instead of offset_inside_folio(), directly use the difference between
> strm.total_out and bi_size.
> So that if the last folio is completely full, we can still properly
> queue the full folio other than queueing zero byte.
>
> Fixes: 3d74a7556fba ("btrfs: zlib: introduce zlib_compress_bio() helper")
> Cc: stable@vger.kernel.org # 7.0+
For same development cycle regressions the CC: stable is not necessary.
I'll queue the patch for the next -rc, thanks for fixing it.
> Reported-by: David Sterba <dsterba@suse.com>
> Reported-by: Jean-Christophe Guillain <jean-christophe@guillain.net>
> Link: https://bugzilla.kernel.org/show_bug.cgi?id=221176
> Signed-off-by: Qu Wenruo <wqu@suse.com>
> ---
> Changelog:
> v2:
> - Add missing reported-by/link/cc tags
> ---
> fs/btrfs/zlib.c | 4 +++-
> 1 file changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/fs/btrfs/zlib.c b/fs/btrfs/zlib.c
> index 1a5093525e32..147c92a4dd04 100644
> --- a/fs/btrfs/zlib.c
> +++ b/fs/btrfs/zlib.c
> @@ -308,7 +308,9 @@ int zlib_compress_bio(struct list_head *ws, struct compressed_bio *cb)
> }
> /* Queue the remaining part of the folio. */
Maybe update this comment with brief description of the tricky case.
> if (workspace->strm.total_out > bio->bi_iter.bi_size) {
> - u32 cur_len = offset_in_folio(out_folio, workspace->strm.total_out);
> + const u32 cur_len = workspace->strm.total_out - bio->bi_iter.bi_size;
> +
> + ASSERT(cur_len <= folio_size(out_folio));
>
> if (!bio_add_folio(bio, out_folio, cur_len, 0)) {
> ret = -E2BIG;
> --
> 2.53.0
>
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly
2026-03-17 10:11 ` David Sterba
@ 2026-03-17 21:26 ` Qu Wenruo
0 siblings, 0 replies; 4+ messages in thread
From: Qu Wenruo @ 2026-03-17 21:26 UTC (permalink / raw)
To: dsterba, Qu Wenruo
Cc: linux-btrfs, stable, David Sterba, Jean-Christophe Guillain
在 2026/3/17 20:41, David Sterba 写道:
> On Fri, Mar 13, 2026 at 06:35:26PM +1030, Qu Wenruo wrote:
[...]
>> diff --git a/fs/btrfs/zlib.c b/fs/btrfs/zlib.c
>> index 1a5093525e32..147c92a4dd04 100644
>> --- a/fs/btrfs/zlib.c
>> +++ b/fs/btrfs/zlib.c
>> @@ -308,7 +308,9 @@ int zlib_compress_bio(struct list_head *ws, struct compressed_bio *cb)
>> }
>> /* Queue the remaining part of the folio. */
>
> Maybe update this comment with brief description of the tricky case.
To be honest, the tricky case is not tricky at all.
It's myself considering large folios and bs > ps cases too much.
The new code is in fact more straightforward, if there are new output,
total_out - bi_size is exactly the length of the new output.
And the output length is already limited by folio_size, there is no need
and shouldn't use offset_in_folio().
The offset_in_folio() looks sane on a quick glance, but if you put all
the corner values (0, and folio_size) into it, it only passes the 0 case
but not the folio_size case.
So there is no tricky case in the first place, it's all my fault making
things way too complex and shoot myself in the foot.
Thanks,
Qu
>
>> if (workspace->strm.total_out > bio->bi_iter.bi_size) {
>> - u32 cur_len = offset_in_folio(out_folio, workspace->strm.total_out);
>> + const u32 cur_len = workspace->strm.total_out - bio->bi_iter.bi_size;
>> +
>> + ASSERT(cur_len <= folio_size(out_folio));
>>
>> if (!bio_add_folio(bio, out_folio, cur_len, 0)) {
>> ret = -E2BIG;
>> --
>> 2.53.0
>>
>
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-03-17 21:26 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-13 8:05 [PATCH v2] btrfs: zlib: handle page aligned compressed size correctly Qu Wenruo
2026-03-13 17:12 ` Jean-Christophe Guillain
2026-03-17 10:11 ` David Sterba
2026-03-17 21:26 ` Qu Wenruo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox