* 7.2-rc1 regression Folio lock leak in writepage_delalloc()
@ 2026-07-21 19:11 Christian Borntraeger
2026-07-21 19:11 ` [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back Christian Borntraeger
2026-07-22 7:21 ` 7.2-rc1 regression Folio lock leak in writepage_delalloc() Qu Wenruo
0 siblings, 2 replies; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-21 19:11 UTC (permalink / raw)
To: linux-btrfs, Qu Wenruo
Cc: borntraeger, David Sterba, Chris Mason, Josef Bacik,
linux-kernel, kvm, linux-s390
We have seen random hangs in our daily CI run where qemu/KVM
processes deadlocks guests with file-backed RAM on btrfs (large data folios)
With the help of claude I think we found the/one problem on an s390
KVM host running 7.2.0-rc3 (KASAN test kernel, but the issue is not
KASAN related). And to be honest here, most of the writeup was created
by claude and I added things where appropriate. Also the patch was
mostly done with the help of claude.
A KVM guest with its RAM backed by a file on btrfs (zstd compression
enabled) locked up together with the host's writeback: two vCPU
threads, an irqfd worker, two flusher workers, khugepaged and a
syncfs caller (dnf) were all stuck in D state for hours. Analysis
of the crash dump shows a leaked folio lock in btrfs'
writepage_delalloc(); a proposed fix is in the reply mail.
I still need to verify that this patches fixes the deadlock in our
CI but wanted some feedback first.
Dump analysis (shortened)
-------------------------
All blocked tasks funnel into one 64-page (256K) large data folio of
the guest RAM file:
folio 0x800083fb000, inode 1881035 (the 1.25G s390.ram file)
flags: PG_locked | PG_waiters | PG_dirty | PG_private | PG_uptodate
(PG_writeback NOT set)
btrfs_folio_state: nr_locked == 0, subpage dirty bitmap empty
still mapped (63/64 PTEs) and on the LRU, no outstanding block I/O
Waiters on that folio lock:
- 2 vCPU threads + 1 irqfd kworker, all in
btrfs_page_mkwrite() -> folio_lock, holding mmap_lock (read)
crash> bt 66448
PID: 66448 TASK: 9e934a00 CPU: 9 COMMAND: "CPU 1/KVM"
#0 [b8b25dbe7d8] __schedule at c0b186d5e78
#1 [b8b25dbe908] schedule at c0b186d7040
#2 [b8b25dbe948] io_schedule at c0b186d723c
#3 [b8b25dbe978] folio_wait_bit_common at c0b167e719c
#4 [b8b25dbeaf0] btrfs_page_mkwrite at c0b172816fc
#5 [b8b25dbec98] do_page_mkwrite at c0b168a4ada
#6 [b8b25dbecf0] do_wp_page at c0b168b2350
#7 [b8b25dbed70] handle_pte_fault at c0b168bfaf4
#8 [b8b25dbee58] __handle_mm_fault at c0b168c003e
#9 [b8b25dbefc0] handle_mm_fault at c0b168c09b6
#10 [b8b25dbf020] __get_user_pages at c0b16899cfc
#11 [b8b25dbf148] get_user_pages_unlocked at c0b1689af1c
#12 [b8b25dbf248] hva_to_pfn at c0a9711e20e [kvm]
#13 [b8b25dbf3f0] __kvm_faultin_pfn at c0a9711ea26 [kvm]
#14 [b8b25dbf4e8] kvm_s390_faultin_gfn at c0a971c092c [kvm]
#15 [b8b25dbf5f8] vcpu_post_run_handle_fault at c0a97148b5e [kvm]
#16 [b8b25dbf6f0] __vcpu_run at c0a9715c1f2 [kvm]
#17 [b8b25dbf808] kvm_arch_vcpu_ioctl_run at c0a9715d3e4 [kvm]
#18 [b8b25dbfbb8] kvm_vcpu_ioctl at c0a97117bd8 [kvm]
#19 [b8b25dbfdd8] __s390x_sys_ioctl at c0b16aa3614
#20 [b8b25dbfe40] __do_syscall at c0b186cdaee
#21 [b8b25dbfe98] system_call at c0b186ebd42
USER-MODE INTERRUPT FRAME; pt_regs at b8b25dbff38:
PSW: 0705000180000000 000003ff8a92662c (user space)
GPRS: 000003ff627faf50 0000000000000036 ffffffffffffffda 000000000000ae80
0000000000000000 000003ff627fc8c0 000002aa1f8a7880 000003ff8a8ad310
000002aa1e1f3c60 0000000000000000 000000000000ae80 000002aa1f8a2f60
000003ff8d3adfa8 000003ff627fc8c0 000003ff627faff0 000003ff627fae88
- flusher: extent_write_cache_pages() -> folio_lock
- delalloc space reclaim worker: same, while holding
fs_info->delalloc_root_mutex (which in turn blocks
btrfs_async_reclaim_metadata_space on the mutex)
Behind those: khugepaged in down_write(mmap_lock), and syncfs.
No task in the system owns the folio lock; nothing references the
folio except the six waiters. The lock was leaked.
Root cause
----------
A folio can carry the folio-level dirty flag with an EMPTY btrfs
subpage dirty bitmap. btrfs data mappings use filemap_dirty_folio(),
so a generic folio_mark_dirty() sets only the folio flag and xarray
tag - no subpage dirty bits, no delalloc reservation. On s390 this
happens all the time: the KVM irq adapter path
(arch/s390/kvm/interrupt.c, adapter_indicators_set()) pins the guest
interrupt indicator page with pin_user_pages_remote(FOLL_WRITE),
sets the indicator bit and calls set_page_dirty_lock(). Once a
previously written folio has gone through one complete writeback
cycle (subpage dirty bitmap empty again), the next adapter interrupt
re-dirties it with only the folio flag.
Writeback then does:
extent_write_cache_pages(): folio_lock(), folio is dirty -> proceed
extent_writepage() -> writepage_delalloc():
- btrfs_copy_subpage_dirty_bitmap() -> submit_bitmap is EMPTY
- the btrfs_folio_set_lock() loop sets nothing (nr_locked stays 0)
- find_lock_delalloc_range() finds nothing -> goto out
- out: bitmap_empty(submit_bitmap) is true -> return 1
The "return 1" path means "all dirty ranges were submitted
asynchronously, the async submission owns the folio unlock" - but
nothing was submitted, so extent_writepage() returns and the folio
stays locked forever. This matches every flag of the dump folio
(locked, dirty, nr_locked == 0, no writeback, still mapped/LRU).
Verifying this in the dump:
- uptodate = 0xffffffffffffffff — all 64 blocks uptodate (consistent with PG_uptodate)
- dirty = 0x0 — the subpage dirty bitmap is EMPTY, exactly as the root cause predicts
- writeback = 0x0 — no writeback in flight (consistent with PG_writeback clear)
Exposure
--------
- Single-block folios are immune: btrfs_copy_subpage_dirty_bitmap()
unconditionally reports bit 0 for blocks_per_folio == 1.
- Subpage setups (e.g. 64K page size with 4K sectorsize) have been
exposed since the submission bitmap rework in v6.12
(bd610c0937aa "btrfs: only unlock the to-be-submitted ranges
inside a folio").
- 4K page size systems became exposed with large data folio support
in v7.2-rc1, which routes every large folio through the subpage
machinery. That is why we only started seeing this now.
Any GUP-style dirtier can trigger it (KVM adapter interrupts on
s390, vfio, RDMA, io_uring fixed buffers, ...) as long as the target
is a multi-block folio of a btrfs data mapping that was clean at the
time of set_page_dirty_lock().
Reproducer outline: KVM guest on s390 with memory-backend-file on
btrfs + virtio devices using irqfd adapter indicators; hangs within
~25 minutes of guest uptime in our setup. A targeted reproducer
should also work on x86: mmap a file on btrfs, write it, fsync, let
writeback finish, then pin_user_pages(FOLL_WRITE) +
set_page_dirty_lock() on a page of a large folio and trigger sync.
Proposed fix
------------
Detect the empty-at-entry bitmap right after it has been copied,
before any range lock is set up, clear the stale folio dirty flag
(nothing can ever be written back for it; all dirty flag setters
serialize on the folio lock we hold) and unlock the folio. Patch
attached below; it survives our compile test and we are preparing a
test run on the affected machine. Comments welcome - especially on
whether clearing the folio dirty flag is the desired semantic here,
versus e.g. routing such folios through the cow fixup worker to
actually persist GUP-written data.
Thanks
Christian
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-21 19:11 7.2-rc1 regression Folio lock leak in writepage_delalloc() Christian Borntraeger
@ 2026-07-21 19:11 ` Christian Borntraeger
2026-07-21 21:07 ` Qu Wenruo
2026-07-22 7:21 ` 7.2-rc1 regression Folio lock leak in writepage_delalloc() Qu Wenruo
1 sibling, 1 reply; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-21 19:11 UTC (permalink / raw)
To: linux-btrfs, Qu Wenruo
Cc: borntraeger, David Sterba, Chris Mason, Josef Bacik,
linux-kernel, kvm, linux-s390
A folio can carry the folio-level dirty flag while its btrfs subpage
dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
so a generic folio_mark_dirty() call sets only the folio flag and the
xarray tag, without setting any subpage dirty bit and without a
delalloc reservation. The typical source is set_page_dirty_lock() on
a GUP pin, e.g. the s390 KVM irq adapter path
(adapter_indicators_set()) which pins guest indicator pages living in
a file-backed guest RAM file, sets a bit and marks the page dirty.
When writeback then picks up such a folio, writepage_delalloc()
copies the empty subpage dirty bitmap into
bio_ctrl->submit_bitmap, sets up no range locks (nr_locked stays 0),
finds no delalloc range, and finally hits
if (bitmap_empty(bio_ctrl->submit_bitmap, blocks_per_folio)) {
wbc->nr_to_write -= delalloc_to_write;
return 1;
}
which is meant for "all dirty ranges were submitted asynchronously,
the async paths own the folio unlock". But nothing was submitted at
all, so extent_writepage() returns without anybody ever unlocking the
folio. The folio stays locked forever and every subsequent locker
(page faults through btrfs_page_mkwrite(), other flushers, delalloc
space reclaim which then parks holding fs_info->delalloc_root_mutex,
syncfs, ...) blocks in D state.
This was debugged from a crash dump of a hung s390 KVM host: a KVM
guest with its RAM backed by a file on btrfs (zstd compression),
where a 64-page (256K) large data folio of the guest RAM file was
found locked and dirty, with an empty subpage dirty bitmap,
nr_locked == 0, no PG_writeback set and no outstanding block I/O,
with two vCPU threads, the irqfd worker, two flusher workers,
khugepaged and syncfs all queued behind it.
Small folios are not affected because
btrfs_copy_subpage_dirty_bitmap() unconditionally reports bit 0 set
for single-block folios. Affected are subpage setups (sectorsize <
PAGE_SIZE, e.g. 64K page size kernels with 4K sectorsize) since the
introduction of the submission bitmap in v6.12, and - much easier to
hit - 4K page size systems since btrfs gained large data folio
support, which makes every large folio take the subpage paths.
Fix it by detecting the empty-at-entry case right after the dirty
bitmap has been copied, before any range lock is set up: there is
nothing that can be submitted for such a folio, so clear the stale
folio-level dirty flag (nothing will ever be written back for it,
and all dirty flag setters serialize on the folio lock we hold, so
this cannot race with a new dirtier) and unlock the folio. Since
folio_clear_dirty_for_io() intentionally leaves PAGECACHE_TAG_DIRTY
in the xarray, also run the same set/clear writeback dance that
extent_writepage_io() uses for the submitted-nothing case, so the
stale tag is dropped and the inode can go clean again.
The data written through the GUP pin is not lost; it sits in the
mapped page cache page. It is simply not persisted until a proper
btrfs write path dirties the folio again - the same long-standing
semantics as any pin_user_pages() write to a file mapping that the
filesystem was not informed about.
Fixes: bd610c0937aa ("btrfs: only unlock the to-be-submitted ranges inside a folio")
Assisted-by: Claude
Signed-off-by: Christian Borntraeger <borntraeger@linux.ibm.com>
---
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index 7d604524e83c3..6a4a00ad43321 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -1492,6 +1492,33 @@ static noinline_for_stack int writepage_delalloc(struct btrfs_inode *inode,
/* Save the dirty bitmap as our submission bitmap will be a subset of it. */
btrfs_copy_subpage_dirty_bitmap(fs_info, folio, bio_ctrl->submit_bitmap);
+ /*
+ * The dirty bitmap can be empty even though the folio is dirty: data
+ * mappings use filemap_dirty_folio(), so a generic folio_mark_dirty()
+ * call (e.g. set_page_dirty_lock() after GUP) only sets the folio
+ * flag, without any subpage dirty bit nor a delalloc reservation.
+ *
+ * There is nothing to submit for such a folio. Bail out now,
+ * otherwise the bitmap_empty() check at the end would mistake it for
+ * "all ranges submitted asynchronously" and return with the folio
+ * lock never released, deadlocking every subsequent locker.
+ *
+ * Also clear the stale dirty flag: with no subpage dirty bits nothing
+ * will ever be written back for it, and leaving the flag would make
+ * writeback rescan the folio forever. All dirty flag setters hold
+ * the folio lock, which we own, so this cannot race with a new
+ * dirtier. As folio_clear_dirty_for_io() keeps PAGECACHE_TAG_DIRTY,
+ * use the same set/clear writeback dance as extent_writepage_io() to
+ * also drop the stale tag, otherwise the inode would never go clean.
+ */
+ if (unlikely(bitmap_empty(bio_ctrl->submit_bitmap, blocks_per_folio))) {
+ folio_clear_dirty_for_io(folio);
+ btrfs_folio_set_writeback(fs_info, folio, page_start, folio_size(folio));
+ btrfs_folio_clear_writeback(fs_info, folio, page_start, folio_size(folio));
+ folio_unlock(folio);
+ return 1;
+ }
+
for_each_set_bitrange(start_bit, end_bit, bio_ctrl->submit_bitmap,
blocks_per_folio) {
u64 start = page_start + (start_bit << fs_info->sectorsize_bits);
--
2.51.0
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-21 19:11 ` [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back Christian Borntraeger
@ 2026-07-21 21:07 ` Qu Wenruo
2026-07-22 8:35 ` Christian Borntraeger
0 siblings, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-21 21:07 UTC (permalink / raw)
To: Christian Borntraeger, linux-btrfs, Qu Wenruo
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/22 04:41, Christian Borntraeger 写道:
> A folio can carry the folio-level dirty flag while its btrfs subpage
> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
> so a generic folio_mark_dirty() call sets only the folio flag and the
> xarray tag, without setting any subpage dirty bit and without a
> delalloc reservation. The typical source is set_page_dirty_lock() on
> a GUP pin,
Shouldn't such folio got its ->page_mkwrite() callback get called first?
> e.g. the s390 KVM irq adapter path
> (adapter_indicators_set()) which pins guest indicator pages living in
> a file-backed guest RAM file, sets a bit and marks the page dirty.
Thus I think this is a bigger problem for S390.
>
> When writeback then picks up such a folio, writepage_delalloc()
> copies the empty subpage dirty bitmap into
> bio_ctrl->submit_bitmap, sets up no range locks (nr_locked stays 0),
> finds no delalloc range, and finally hits
>
> if (bitmap_empty(bio_ctrl->submit_bitmap, blocks_per_folio)) {
> wbc->nr_to_write -= delalloc_to_write;
> return 1;
> }
>
> which is meant for "all dirty ranges were submitted asynchronously,
> the async paths own the folio unlock". But nothing was submitted at
> all, so extent_writepage() returns without anybody ever unlocking the
> folio. The folio stays locked forever and every subsequent locker
> (page faults through btrfs_page_mkwrite(), other flushers, delalloc
> space reclaim which then parks holding fs_info->delalloc_root_mutex,
> syncfs, ...) blocks in D state.
>
> This was debugged from a crash dump of a hung s390 KVM host: a KVM
> guest with its RAM backed by a file on btrfs (zstd compression),
> where a 64-page (256K) large data folio of the guest RAM file was
> found locked and dirty, with an empty subpage dirty bitmap,
> nr_locked == 0, no PG_writeback set and no outstanding block I/O,
> with two vCPU threads, the irqfd worker, two flusher workers,
> khugepaged and syncfs all queued behind it.
>
> Small folios are not affected because
> btrfs_copy_subpage_dirty_bitmap() unconditionally reports bit 0 set
> for single-block folios.
Unfortunately this means btrfs can get a dirty folio that is dirtied
without notifying the fs.
This will trigger a lot of other warnings, e.g. space reservation problems.
Furthermore, such dirtying folio without notifying the fs behavior can
also lead to problems in other fses.
IIRC ext4 will give warning for such cases too.
So I do not think this is the correct fix, but something went totally
wrong in the S390 GUP behavior that allows a fs folio to be marked dirty
without calling page_mkwrite().
Thanks,
Qu
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: 7.2-rc1 regression Folio lock leak in writepage_delalloc()
2026-07-21 19:11 7.2-rc1 regression Folio lock leak in writepage_delalloc() Christian Borntraeger
2026-07-21 19:11 ` [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back Christian Borntraeger
@ 2026-07-22 7:21 ` Qu Wenruo
1 sibling, 0 replies; 17+ messages in thread
From: Qu Wenruo @ 2026-07-22 7:21 UTC (permalink / raw)
To: Christian Borntraeger, linux-btrfs, Qu Wenruo
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/22 04:41, Christian Borntraeger 写道:
> We have seen random hangs in our daily CI run where qemu/KVM
> processes deadlocks guests with file-backed RAM on btrfs (large data folios)
>
> With the help of claude I think we found the/one problem on an s390
> KVM host running 7.2.0-rc3 (KASAN test kernel, but the issue is not
> KASAN related). And to be honest here, most of the writeup was created
> by claude and I added things where appropriate. Also the patch was
> mostly done with the help of claude.
>
> A KVM guest with its RAM backed by a file on btrfs (zstd compression
> enabled) locked up together with the host's writeback: two vCPU
> threads, an irqfd worker, two flusher workers, khugepaged and a
> syncfs caller (dnf) were all stuck in D state for hours. Analysis
> of the crash dump shows a leaked folio lock in btrfs'
> writepage_delalloc(); a proposed fix is in the reply mail.
>
> I still need to verify that this patches fixes the deadlock in our
> CI but wanted some feedback first.
>
> Dump analysis (shortened)
> -------------------------
> All blocked tasks funnel into one 64-page (256K) large data folio of
> the guest RAM file:
>
> folio 0x800083fb000, inode 1881035 (the 1.25G s390.ram file)
> flags: PG_locked | PG_waiters | PG_dirty | PG_private | PG_uptodate
> (PG_writeback NOT set)
> btrfs_folio_state: nr_locked == 0, subpage dirty bitmap empty
> still mapped (63/64 PTEs) and on the LRU, no outstanding block I/O
>
> Waiters on that folio lock:
> - 2 vCPU threads + 1 irqfd kworker, all in
> btrfs_page_mkwrite() -> folio_lock, holding mmap_lock (read)
> crash> bt 66448
> PID: 66448 TASK: 9e934a00 CPU: 9 COMMAND: "CPU 1/KVM"
> #0 [b8b25dbe7d8] __schedule at c0b186d5e78
> #1 [b8b25dbe908] schedule at c0b186d7040
> #2 [b8b25dbe948] io_schedule at c0b186d723c
> #3 [b8b25dbe978] folio_wait_bit_common at c0b167e719c
> #4 [b8b25dbeaf0] btrfs_page_mkwrite at c0b172816fc
> #5 [b8b25dbec98] do_page_mkwrite at c0b168a4ada
> #6 [b8b25dbecf0] do_wp_page at c0b168b2350
> #7 [b8b25dbed70] handle_pte_fault at c0b168bfaf4
> #8 [b8b25dbee58] __handle_mm_fault at c0b168c003e
> #9 [b8b25dbefc0] handle_mm_fault at c0b168c09b6
> #10 [b8b25dbf020] __get_user_pages at c0b16899cfc
> #11 [b8b25dbf148] get_user_pages_unlocked at c0b1689af1c
> #12 [b8b25dbf248] hva_to_pfn at c0a9711e20e [kvm]
> #13 [b8b25dbf3f0] __kvm_faultin_pfn at c0a9711ea26 [kvm]
> #14 [b8b25dbf4e8] kvm_s390_faultin_gfn at c0a971c092c [kvm]
> #15 [b8b25dbf5f8] vcpu_post_run_handle_fault at c0a97148b5e [kvm]
> #16 [b8b25dbf6f0] __vcpu_run at c0a9715c1f2 [kvm]
> #17 [b8b25dbf808] kvm_arch_vcpu_ioctl_run at c0a9715d3e4 [kvm]
> #18 [b8b25dbfbb8] kvm_vcpu_ioctl at c0a97117bd8 [kvm]
> #19 [b8b25dbfdd8] __s390x_sys_ioctl at c0b16aa3614
> #20 [b8b25dbfe40] __do_syscall at c0b186cdaee
> #21 [b8b25dbfe98] system_call at c0b186ebd42
> USER-MODE INTERRUPT FRAME; pt_regs at b8b25dbff38:
> PSW: 0705000180000000 000003ff8a92662c (user space)
> GPRS: 000003ff627faf50 0000000000000036 ffffffffffffffda 000000000000ae80
> 0000000000000000 000003ff627fc8c0 000002aa1f8a7880 000003ff8a8ad310
> 000002aa1e1f3c60 0000000000000000 000000000000ae80 000002aa1f8a2f60
> 000003ff8d3adfa8 000003ff627fc8c0 000003ff627faff0 000003ff627fae88
>
>
> - flusher: extent_write_cache_pages() -> folio_lock
> - delalloc space reclaim worker: same, while holding
> fs_info->delalloc_root_mutex (which in turn blocks
> btrfs_async_reclaim_metadata_space on the mutex)
> Behind those: khugepaged in down_write(mmap_lock), and syncfs.
>
> No task in the system owns the folio lock; nothing references the
> folio except the six waiters. The lock was leaked.
>
> Root cause
> ----------
> A folio can carry the folio-level dirty flag with an EMPTY btrfs
> subpage dirty bitmap. btrfs data mappings use filemap_dirty_folio(),
> so a generic folio_mark_dirty() sets only the folio flag and xarray
> tag - no subpage dirty bits, no delalloc reservation. On s390 this
> happens all the time: the KVM irq adapter path
> (arch/s390/kvm/interrupt.c, adapter_indicators_set()) pins the guest
> interrupt indicator page with pin_user_pages_remote(FOLL_WRITE),
> sets the indicator bit and calls set_page_dirty_lock(). Once a
> previously written folio has gone through one complete writeback
> cycle (subpage dirty bitmap empty again), the next adapter interrupt
> re-dirties it with only the folio flag.
No, that's not how things should work.
I have explained the problem in the RFC patch. I am only going to add
some extra explanation inlined below.
>
> Writeback then does:
>
> extent_write_cache_pages(): folio_lock(), folio is dirty -> proceed
> extent_writepage() -> writepage_delalloc():
> - btrfs_copy_subpage_dirty_bitmap() -> submit_bitmap is EMPTY
> - the btrfs_folio_set_lock() loop sets nothing (nr_locked stays 0)
> - find_lock_delalloc_range() finds nothing -> goto out
> - out: bitmap_empty(submit_bitmap) is true -> return 1
>
> The "return 1" path means "all dirty ranges were submitted
> asynchronously, the async submission owns the folio unlock" - but
> nothing was submitted, so extent_writepage() returns and the folio
> stays locked forever. This matches every flag of the dump folio
> (locked, dirty, nr_locked == 0, no writeback, still mapped/LRU).
>
> Verifying this in the dump:
>
> - uptodate = 0xffffffffffffffff — all 64 blocks uptodate (consistent with PG_uptodate)
> - dirty = 0x0 — the subpage dirty bitmap is EMPTY, exactly as the root cause predicts
> - writeback = 0x0 — no writeback in flight (consistent with PG_writeback clear)
>
>
> Exposure
> --------
> - Single-block folios are immune: btrfs_copy_subpage_dirty_bitmap()
> unconditionally reports bit 0 for blocks_per_folio == 1.
Unfortunately no.
One of the biggest problem is, all the other things, from extent map to
ordered extent are not properly prepared.
E.g. even if btrfs_copy_subpage_dirty_bitmap() returns bit 0 set, later
EXTENT_DELALLOC is not set.
So find_lock_delalloc_range() will return false, and since we found no
delalloc range, @last_delalloc_end is still zero, we goto out label,
without creating any ordered extent/extent map.
Then we go into extent_writepage_io(), which will rely on the extent_map
created by run_delalloc_range() for IO submission.
But since we have no OE/EM created, we will grab one from on-disk
metadata, and if the original on-disk metadata shows there is a hole, we
will trigger the ASSERT() inside submit_one_sector(), about the EM is a
hole.
Before v7.2-rc1, we have a lot of extra handling (folio ordered flag) to
exactly catch such situation.
But since we haven't really hit such case anymore for a while, in
v7.2-rc we also remove the that flag, otherwise it should catch such
problem much earilier.
> - Subpage setups (e.g. 64K page size with 4K sectorsize) have been
> exposed since the submission bitmap rework in v6.12
> (bd610c0937aa "btrfs: only unlock the to-be-submitted ranges
> inside a folio").
> - 4K page size systems became exposed with large data folio support
> in v7.2-rc1, which routes every large folio through the subpage
> machinery. That is why we only started seeing this now.
>
> Any GUP-style dirtier can trigger it (KVM adapter interrupts on
> s390, vfio, RDMA, io_uring fixed buffers, ...) as long as the target
> is a multi-block folio of a btrfs data mapping that was clean at the
> time of set_page_dirty_lock().
>
> Reproducer outline: KVM guest on s390 with memory-backend-file on
> btrfs + virtio devices using irqfd adapter indicators;
I strongly doubt if it's a specific S390 feature breaking the assumption.
As io uring is also heavily tested, and IIRC there is already a huge GUP
work to address the long existing unexpected dirty page behavior in v5.15.
So I strongly doubt if it's some S390x feature not properly following
the existing scheme.
At least on both x86_64 and arm64 (64K page size), since the
introduction of experimental large folios, I haven't seen something
similar like this.
If it's S390X specific, then I do not think it's something we can handle
by ourselves.
Thanks,
Qu
> hangs within
> ~25 minutes of guest uptime in our setup. A targeted reproducer
> should also work on x86: mmap a file on btrfs, write it, fsync, let
> writeback finish, then pin_user_pages(FOLL_WRITE) +
> set_page_dirty_lock() on a page of a large folio and trigger sync.
>
> Proposed fix
> ------------
> Detect the empty-at-entry bitmap right after it has been copied,
> before any range lock is set up, clear the stale folio dirty flag
> (nothing can ever be written back for it; all dirty flag setters
> serialize on the folio lock we hold) and unlock the folio. Patch
> attached below; it survives our compile test and we are preparing a
> test run on the affected machine. Comments welcome - especially on
> whether clearing the folio dirty flag is the desired semantic here,
> versus e.g. routing such folios through the cow fixup worker to
> actually persist GUP-written data.
>
> Thanks
>
> Christian
>
>
>
>
>
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-21 21:07 ` Qu Wenruo
@ 2026-07-22 8:35 ` Christian Borntraeger
2026-07-22 8:59 ` Qu Wenruo
0 siblings, 1 reply; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-22 8:35 UTC (permalink / raw)
To: Qu Wenruo, linux-btrfs, Qu Wenruo
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
Am 21.07.26 um 23:07 schrieb Qu Wenruo:
First, thank you for taking the time to look into this and trying to understand
things and trying to explain things. this is highly appreciated.
I am still trying to fully understand this myself. A question:
> 在 2026/7/22 04:41, Christian Borntraeger 写道:
>> A folio can carry the folio-level dirty flag while its btrfs subpage
>> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
>> so a generic folio_mark_dirty() call sets only the folio flag and the
>> xarray tag, without setting any subpage dirty bit and without a
>> delalloc reservation. The typical source is set_page_dirty_lock() on
>> a GUP pin,
>
> Shouldn't such folio got its ->page_mkwrite() callback get called first?
Isnt that called implicitely at pin time?
At unpin, set_page_dirty_lock() re-dirties the now-clean folio.
From what I can see, this is not an s390 invention but a sanctioned pattern.
But I can certainly not exclude that this is still an s390 specific problem.
Let me dig a bit deeper.
Thanks again for your quick response.
Christian
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 8:35 ` Christian Borntraeger
@ 2026-07-22 8:59 ` Qu Wenruo
2026-07-22 9:29 ` Christian Borntraeger
0 siblings, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-22 8:59 UTC (permalink / raw)
To: Christian Borntraeger, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/22 18:05, Christian Borntraeger 写道:
> Am 21.07.26 um 23:07 schrieb Qu Wenruo:
>
> First, thank you for taking the time to look into this and trying to
> understand
> things and trying to explain things. this is highly appreciated.
> I am still trying to fully understand this myself. A question:
>
>> 在 2026/7/22 04:41, Christian Borntraeger 写道:
>>> A folio can carry the folio-level dirty flag while its btrfs subpage
>>> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
>>> so a generic folio_mark_dirty() call sets only the folio flag and the
>>> xarray tag, without setting any subpage dirty bit and without a
>>> delalloc reservation. The typical source is set_page_dirty_lock() on
>>> a GUP pin,
>>
>> Shouldn't such folio got its ->page_mkwrite() callback get called first?
>
> Isnt that called implicitely at pin time?
I have to admit, I'm not an expert on the MM part, I'm mostly a simple
user of the existing MM interfaces.
AFAIK, the last time I brought this thing up, Christoph mentioned that
dirtying-folio-without-notifying-fs is a bug, and fs should not and is
not able to handle such situation anyway.
And that idea makes a lot of sense to me.
So adding MM list for more help.
Thanks,
Qu
> At unpin, set_page_dirty_lock() re-dirties the now-clean folio.
> From what I can see, this is not an s390 invention but a sanctioned
> pattern.
>
> But I can certainly not exclude that this is still an s390 specific
> problem.
> Let me dig a bit deeper.
>
> Thanks again for your quick response.
>
> Christian
>
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 8:59 ` Qu Wenruo
@ 2026-07-22 9:29 ` Christian Borntraeger
2026-07-22 9:35 ` Qu Wenruo
2026-07-22 12:57 ` Matthew Wilcox
0 siblings, 2 replies; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-22 9:29 UTC (permalink / raw)
To: Qu Wenruo, linux-btrfs, Qu Wenruo, Linux Memory Management List,
linux-fsdevel
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
[-- Attachment #1: Type: text/plain, Size: 10562 bytes --]
Am 22.07.26 um 10:59 schrieb Qu Wenruo:
>
>
> 在 2026/7/22 18:05, Christian Borntraeger 写道:
>> Am 21.07.26 um 23:07 schrieb Qu Wenruo:
>>
>> First, thank you for taking the time to look into this and trying to understand
>> things and trying to explain things. this is highly appreciated.
>> I am still trying to fully understand this myself. A question:
>>
>>> 在 2026/7/22 04:41, Christian Borntraeger 写道:
>>>> A folio can carry the folio-level dirty flag while its btrfs subpage
>>>> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
>>>> so a generic folio_mark_dirty() call sets only the folio flag and the
>>>> xarray tag, without setting any subpage dirty bit and without a
>>>> delalloc reservation. The typical source is set_page_dirty_lock() on
>>>> a GUP pin,
>>>
>>> Shouldn't such folio got its ->page_mkwrite() callback get called first?
>>
>> Isnt that called implicitely at pin time?
>
> I have to admit, I'm not an expert on the MM part, I'm mostly a simple user of the existing MM interfaces.
>
> AFAIK, the last time I brought this thing up, Christoph mentioned that dirtying-folio-without-notifying-fs is a bug, and fs should not and is not able to handle such situation anyway.
>
> And that idea makes a lot of sense to me.
>
> So adding MM list for more help.
So I now have an userspace O_DIRECT reproducer outside of KVM. (attached) which gave me (on an s390 system, though)
[ 189.067731] sysrq: Show Blocked State
[ 189.067872] task:kworker/u1665:5 state:D stack:0 pid:1363 tgid:1363 ppid:2 task_flags:0x4208060 flags:0x00000000
[ 189.067877] Workqueue: writeback wb_workfn (flush-btrfs-1)
[ 189.067884] Call Trace:
[ 189.067886] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.067891] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.067894] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.067896] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.067902] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
[ 189.067907] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
[ 189.067910] [<000003a8b21ad444>] do_writepages+0xd4/0x190
[ 189.067915] [<000003a8b2324590>] __writeback_single_inode+0x50/0x310
[ 189.067917] [<000003a8b2324b1a>] writeback_sb_inodes+0x2ca/0x690
[ 189.067919] [<000003a8b2324f36>] __writeback_inodes_wb+0x56/0x140
[ 189.067921] [<000003a8b2325498>] wb_writeback+0x368/0x460
[ 189.067923] [<000003a8b23258c6>] wb_do_writeback+0x336/0x3d0
[ 189.067925] [<000003a8b2325d2a>] wb_workfn+0x5a/0x1d0
[ 189.067927] [<000003a8b1f0cee2>] process_one_work+0x1d2/0x480
[ 189.067932] [<000003a8b1f0df30>] worker_thread+0x210/0x420
[ 189.067935] [<000003a8b1f190b8>] kthread+0x148/0x170
[ 189.067939] [<000003a8b1e90358>] __ret_from_fork+0x48/0x220
[ 189.067941] [<000003a8b2e2426a>] ret_from_fork+0xa/0x30
[ 189.067994] task:oob-dirty-repro state:D stack:0 pid:8152 tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000800
[ 189.067997] Call Trace:
[ 189.067998] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.068050] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.068052] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.068054] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.068057] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
[ 189.068061] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
[ 189.068063] [<000003a8b21ad444>] do_writepages+0xd4/0x190
[ 189.068066] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
[ 189.068069] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
[ 189.068072] [<000003a8b2637d3c>] btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
[ 189.068074] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
[ 189.068079] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
[ 189.068084] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
[ 189.068086] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
[ 189.068088] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
[ 189.068090] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
[ 189.068094] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
[ 189.068098] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
[ 189.068101] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
[ 189.068106] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
[ 189.068108] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
[ 189.068109] [<000003a8b22d0a70>] vfs_read+0x210/0x370
[ 189.068112] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
[ 189.068114] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
[ 189.068117] [<000003a8b2e24242>] system_call+0x72/0x90
[ 189.068120] task:oob-dirty-repro state:D stack:0 pid:8153 tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
[ 189.068123] Call Trace:
[ 189.068124] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.068126] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.068128] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.068130] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.068133] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
[ 189.068136] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
[ 189.068138] [<000003a8b21ad444>] do_writepages+0xd4/0x190
[ 189.068141] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
[ 189.068144] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
[ 189.068146] [<000003a8b2637d3c>] btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
[ 189.068149] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
[ 189.068152] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
[ 189.068154] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
[ 189.068156] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
[ 189.068158] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
[ 189.068160] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
[ 189.068163] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
[ 189.068165] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
[ 189.068168] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
[ 189.068170] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
[ 189.068172] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
[ 189.068174] [<000003a8b22d0a70>] vfs_read+0x210/0x370
[ 189.068176] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
[ 189.068178] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
[ 189.068181] [<000003a8b2e24242>] system_call+0x72/0x90
[ 189.068184] task:oob-dirty-repro state:D stack:0 pid:8154 tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
[ 189.068187] Call Trace:
[ 189.068188] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.068190] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.068192] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.068193] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.068196] [<000003a8b26260fc>] btrfs_page_mkwrite+0x25c/0x8d0
[ 189.068199] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
[ 189.068202] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
[ 189.068203] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
[ 189.068206] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
[ 189.068208] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
[ 189.068210] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
[ 189.068213] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
[ 189.068215] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
[ 189.068218] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
[ 189.068219] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
[ 189.068221] [<000003a8b22d0a70>] vfs_read+0x210/0x370
[ 189.068223] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
[ 189.068225] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
[ 189.068228] [<000003a8b2e24242>] system_call+0x72/0x90
[ 189.068231] task:oob-dirty-repro state:D stack:0 pid:8155 tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
[ 189.068234] Call Trace:
[ 189.068235] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.068237] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.068239] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.068241] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.068244] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
[ 189.068246] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
[ 189.068249] [<000003a8b21ad444>] do_writepages+0xd4/0x190
[ 189.068252] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
[ 189.068255] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
[ 189.068257] [<000003a8b2637d3c>] btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
[ 189.068260] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
[ 189.068263] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
[ 189.068265] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
[ 189.068267] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
[ 189.068269] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
[ 189.068271] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
[ 189.068274] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
[ 189.068277] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
[ 189.068279] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
[ 189.068281] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
[ 189.068283] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
[ 189.068285] [<000003a8b22d0a70>] vfs_read+0x210/0x370
[ 189.068287] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
[ 189.068289] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
[ 189.068292] [<000003a8b2e24242>] system_call+0x72/0x90
[ 189.068294] task:oob-dirty-repro state:D stack:0 pid:8156 tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
[ 189.068297] Call Trace:
[ 189.068298] [<000003a8b2e19284>] __schedule+0x374/0x900
[ 189.068300] [<000003a8b2e1984c>] schedule+0x3c/0xf0
[ 189.068302] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
[ 189.068304] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
[ 189.068307] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
[ 189.068310] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
[ 189.068313] [<000003a8b21ad444>] do_writepages+0xd4/0x190
[ 189.068315] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
[ 189.068318] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
[ 189.068320] [<000003a8b232bbf2>] sync_file_range+0x122/0x150
[ 189.068323] [<000003a8b232bc84>] ksys_sync_file_range+0x64/0xb0
[ 189.068326] [<000003a8b232bd0a>] __s390x_sys_sync_file_range+0x3a/0x50
[ 189.068328] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
[ 189.068331] [<000003a8b2e24242>] system_call+0x72/0x90
[-- Attachment #2: oob-dirty-repro.c --]
[-- Type: text/x-csrc, Size: 8558 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* oob-dirty-repro.c - reproduce out-of-band dirty folios on btrfs
* (folio lock leak in writepage_delalloc())
*
* Demonstrates that a "folio-level dirty flag set, fs never notified"
* state can be produced on any architecture with sanctioned, in-tree
* APIs - no s390, no KVM required:
*
* 1. A file on btrfs is written with large buffered writes so the
* page cache backs it with large data folios (v7.2-rc1+).
* 2. The file is mmap'ed MAP_SHARED.
* 3. Thread A loops O_DIRECT reads *into* that mapping. The direct
* I/O read path pins the destination pages (GUP; the initial
* fault runs ->page_mkwrite() and reserves delalloc properly)
* and on bio completion re-dirties them via
* bio_check_pages_dirty() -> bio_release_pages(true)
* -> unpin_user_pages_dirty_lock() -> set_page_dirty_lock().
* 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
* Whenever a full clean cycle (clear_page_dirty_for_io(),
* writeback, bits cleared) completes inside thread A's
* submission->completion window, the completion-time
* set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
* sets only the folio flag and the xarray tag - no btrfs subpage
* dirty bit, no delalloc reservation. See the 20-year-old comment
* above bio_set_pages_dirty() in block/bio.c describing exactly
* this ("other code (eg, flusher threads) could clean the pages").
* 5. The next writeback of that folio enters writepage_delalloc()
* with an empty subpage dirty bitmap, takes the
* bitmap_empty() -> return 1 path and leaks the folio lock.
* Every subsequent locker (mkwrite faults, sync, flusher) then
* hangs in D state.
*
* Thread C stores into the mapping (mkwrite traffic, like KVM vCPUs)
* and a watchdog reports when all threads stop making progress.
*
* Usage: gcc -O2 -pthread -Wall -o oob-dirty-repro oob-dirty-repro.c
* ./oob-dirty-repro <dir-on-btrfs> [runtime-seconds]
*
* Expected on 7.2-rc (large data folios enabled): within minutes the
* watchdog reports a stall; the sync/store/io threads are in D state,
* "echo w > /proc/sysrq-trigger" shows them queued on a folio lock
* (folio_wait_bit_common <- btrfs_page_mkwrite / extent_write_cache_pages).
* On kernels without large data folios the leak path is not reachable
* (single-block folios get bit 0 forced in btrfs_copy_subpage_dirty_bitmap());
* the same race then produces the out-of-band state that the (removed)
* COW fixup / "reject out-of-band dirty folios" code used to catch.
*
* WARNING: on affected kernels this wedges writeback of the target
* filesystem (unkillable D-state tasks, syncfs hangs) until reboot.
* Run it on a scratch machine.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <time.h>
#include <unistd.h>
#define BTRFS_SUPER_MAGIC 0x9123683E
#define TARGET_SZ (16UL << 20) /* mmap'ed target file */
#define SRC_SZ (4UL << 20) /* O_DIRECT read source */
#define IO_SZ (2UL << 20) /* per-read size (wide window) */
#define WRITE_CHUNK (1UL << 20) /* buffered init writes (large folios) */
#define STALL_SECS 20
#define NR_IO_THREADS 4
static char *map;
static int target_fd, src_fd;
static atomic_ulong io_iters, sync_iters, store_iters;
static atomic_int stop;
static unsigned long runtime = 600;
static void die(const char *what)
{
perror(what);
exit(1);
}
static void *io_thread(void *arg __attribute__((unused)))
{
/*
* Only the lower half: btrfs verifies read csums in the (pinned)
* destination pages after DMA, so the store thread must not poke
* the same pages, and the NOCOW source avoids csums altogether.
*/
unsigned long nranges = (TARGET_SZ / 2) / IO_SZ;
while (!atomic_load(&stop)) {
off_t off = (random() % nranges) * IO_SZ;
ssize_t r = pread(src_fd, map + off, IO_SZ, 0);
if (r < 0 && errno != EINTR && errno != EIO)
die("O_DIRECT pread into mapping");
atomic_fetch_add(&io_iters, 1);
}
return NULL;
}
static void *sync_thread(void *arg __attribute__((unused)))
{
while (!atomic_load(&stop)) {
if (sync_file_range(target_fd, 0, TARGET_SZ,
SYNC_FILE_RANGE_WAIT_BEFORE |
SYNC_FILE_RANGE_WRITE |
SYNC_FILE_RANGE_WAIT_AFTER) < 0)
die("sync_file_range");
atomic_fetch_add(&sync_iters, 1);
}
return NULL;
}
static void *store_thread(void *arg __attribute__((unused)))
{
struct timespec ts = { 0, 1000000 }; /* 1ms */
while (!atomic_load(&stop)) {
unsigned long off = TARGET_SZ / 2 +
(random() % (TARGET_SZ / 2 / 4096)) * 4096;
map[off] ^= 1; /* mkwrite fault traffic */
atomic_fetch_add(&store_iters, 1);
nanosleep(&ts, NULL);
}
return NULL;
}
int main(int argc, char **argv)
{
char path[4096], *buf;
struct statfs sfs;
pthread_t io[NR_IO_THREADS], sy, st;
unsigned long last_io = 0, last_sync = 0;
unsigned long elapsed = 0, stalled = 0;
setvbuf(stdout, NULL, _IOLBF, 0);
if (argc < 2) {
fprintf(stderr, "usage: %s <dir-on-btrfs> [runtime-secs]\n",
argv[0]);
return 1;
}
if (argc > 2)
runtime = strtoul(argv[2], NULL, 0);
if (statfs(argv[1], &sfs) < 0)
die("statfs");
if ((unsigned int)sfs.f_type != BTRFS_SUPER_MAGIC)
fprintf(stderr, "warning: %s is not on btrfs, "
"the writepage_delalloc() path will not be hit\n",
argv[1]);
/* Target file: large buffered writes -> large data folios. */
snprintf(path, sizeof(path), "%s/oob-target.dat", argv[1]);
target_fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (target_fd < 0)
die("open target");
buf = aligned_alloc(4096, WRITE_CHUNK);
if (!buf)
die("aligned_alloc");
memset(buf, 0x5a, WRITE_CHUNK);
for (unsigned long o = 0; o < TARGET_SZ; o += WRITE_CHUNK)
if (pwrite(target_fd, buf, WRITE_CHUNK, o) != WRITE_CHUNK)
die("pwrite target");
if (fsync(target_fd) < 0)
die("fsync target");
/* Source file for the O_DIRECT reads. */
snprintf(path, sizeof(path), "%s/oob-src.dat", argv[1]);
src_fd = open(path, O_RDWR | O_CREAT | O_TRUNC | O_DIRECT, 0600);
if (src_fd < 0)
die("open source (O_DIRECT)");
{ /* NOCOW/nodatasum: keep csum verification out of the picture */
int attr = 0;
ioctl(src_fd, FS_IOC_GETFLAGS, &attr);
attr |= FS_NOCOW_FL;
ioctl(src_fd, FS_IOC_SETFLAGS, &attr);
}
for (unsigned long o = 0; o < SRC_SZ; o += WRITE_CHUNK)
if (pwrite(src_fd, buf, WRITE_CHUNK, o) != WRITE_CHUNK)
die("pwrite source");
if (fsync(src_fd) < 0)
die("fsync source");
free(buf);
map = mmap(NULL, TARGET_SZ, PROT_READ | PROT_WRITE, MAP_SHARED,
target_fd, 0);
if (map == MAP_FAILED)
die("mmap");
printf("running: O_DIRECT reads into MAP_SHARED btrfs mapping vs. "
"sync_file_range, %lus max\n", runtime);
for (int n = 0; n < NR_IO_THREADS; n++)
pthread_create(&io[n], NULL, io_thread, NULL);
pthread_create(&sy, NULL, sync_thread, NULL);
pthread_create(&st, NULL, store_thread, NULL);
for (elapsed = 0; elapsed < runtime; elapsed += 5) {
unsigned long i, s, w;
sleep(5);
i = atomic_load(&io_iters);
s = atomic_load(&sync_iters);
w = atomic_load(&store_iters);
printf(" t=%4lus io=%lu sync=%lu store=%lu\n",
elapsed + 5, i, s, w);
if (i == last_io && s == last_sync) {
stalled += 5;
if (stalled >= STALL_SECS) {
printf("\n*** io+sync THREADS STALLED for %us - "
"folio lock leak reproduced? ***\n"
"Check: grep -l . /proc/%d/task/*/stack "
"| xargs cat\n"
" echo w > /proc/sysrq-trigger; "
"dmesg | tail -100\n"
"Expect: folio_wait_bit_common <- "
"btrfs_page_mkwrite /\n"
" extent_write_cache_pages in "
"D-state threads.\n",
STALL_SECS, getpid());
/* leave the hung process for inspection */
pause();
}
} else {
stalled = 0;
}
last_io = i;
last_sync = s;
}
atomic_store(&stop, 1);
for (int n = 0; n < NR_IO_THREADS; n++)
pthread_join(io[n], NULL);
pthread_join(sy, NULL);
pthread_join(st, NULL);
printf("finished without stall - not reproduced in %lus "
"(try a longer runtime; the race window is\n"
"submission->completion of the O_DIRECT read vs. one full "
"writeback cycle)\n", runtime);
return 0;
}
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 9:29 ` Christian Borntraeger
@ 2026-07-22 9:35 ` Qu Wenruo
2026-07-22 10:40 ` Christian Borntraeger
2026-07-22 12:57 ` Matthew Wilcox
1 sibling, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-22 9:35 UTC (permalink / raw)
To: Christian Borntraeger, Qu Wenruo, linux-btrfs,
Linux Memory Management List, linux-fsdevel
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/22 18:59, Christian Borntraeger 写道:
> Am 22.07.26 um 10:59 schrieb Qu Wenruo:
>>
>>
>> 在 2026/7/22 18:05, Christian Borntraeger 写道:
>>> Am 21.07.26 um 23:07 schrieb Qu Wenruo:
>>>
>>> First, thank you for taking the time to look into this and trying to
>>> understand
>>> things and trying to explain things. this is highly appreciated.
>>> I am still trying to fully understand this myself. A question:
>>>
>>>> 在 2026/7/22 04:41, Christian Borntraeger 写道:
>>>>> A folio can carry the folio-level dirty flag while its btrfs subpage
>>>>> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
>>>>> so a generic folio_mark_dirty() call sets only the folio flag and the
>>>>> xarray tag, without setting any subpage dirty bit and without a
>>>>> delalloc reservation. The typical source is set_page_dirty_lock() on
>>>>> a GUP pin,
>>>>
>>>> Shouldn't such folio got its ->page_mkwrite() callback get called
>>>> first?
>>>
>>> Isnt that called implicitely at pin time?
>>
>> I have to admit, I'm not an expert on the MM part, I'm mostly a simple
>> user of the existing MM interfaces.
>>
>> AFAIK, the last time I brought this thing up, Christoph mentioned that
>> dirtying-folio-without-notifying-fs is a bug, and fs should not and is
>> not able to handle such situation anyway.
>>
>> And that idea makes a lot of sense to me.
>>
>> So adding MM list for more help.
> So I now have an userspace O_DIRECT reproducer outside of KVM.
> (attached) which gave me (on an s390 system, though)
Thanks a lot, I can also reproduce it on arm64 (64K page size).
This is super bad, as we have just removed a lot of folio ordered
related code to detect such problem.
I'll also check if it's some recent btrfs changes making it worse.
Thanks,
Qu
>
> [ 189.067731] sysrq: Show Blocked State
> [ 189.067872] task:kworker/u1665:5 state:D stack:0 pid:1363
> tgid:1363 ppid:2 task_flags:0x4208060 flags:0x00000000
> [ 189.067877] Workqueue: writeback wb_workfn (flush-btrfs-1)
> [ 189.067884] Call Trace:
> [ 189.067886] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.067891] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.067894] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.067896] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.067902] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
> [ 189.067907] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
> [ 189.067910] [<000003a8b21ad444>] do_writepages+0xd4/0x190
> [ 189.067915] [<000003a8b2324590>] __writeback_single_inode+0x50/0x310
> [ 189.067917] [<000003a8b2324b1a>] writeback_sb_inodes+0x2ca/0x690
> [ 189.067919] [<000003a8b2324f36>] __writeback_inodes_wb+0x56/0x140
> [ 189.067921] [<000003a8b2325498>] wb_writeback+0x368/0x460
> [ 189.067923] [<000003a8b23258c6>] wb_do_writeback+0x336/0x3d0
> [ 189.067925] [<000003a8b2325d2a>] wb_workfn+0x5a/0x1d0
> [ 189.067927] [<000003a8b1f0cee2>] process_one_work+0x1d2/0x480
> [ 189.067932] [<000003a8b1f0df30>] worker_thread+0x210/0x420
> [ 189.067935] [<000003a8b1f190b8>] kthread+0x148/0x170
> [ 189.067939] [<000003a8b1e90358>] __ret_from_fork+0x48/0x220
> [ 189.067941] [<000003a8b2e2426a>] ret_from_fork+0xa/0x30
> [ 189.067994] task:oob-dirty-repro state:D stack:0 pid:8152
> tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000800
> [ 189.067997] Call Trace:
> [ 189.067998] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.068050] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.068052] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.068054] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.068057] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
> [ 189.068061] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
> [ 189.068063] [<000003a8b21ad444>] do_writepages+0xd4/0x190
> [ 189.068066] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
> [ 189.068069] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
> [ 189.068072] [<000003a8b2637d3c>]
> btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
> [ 189.068074] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
> [ 189.068079] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
> [ 189.068084] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
> [ 189.068086] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
> [ 189.068088] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
> [ 189.068090] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
> [ 189.068094] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
> [ 189.068098] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
> [ 189.068101] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
> [ 189.068106] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
> [ 189.068108] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
> [ 189.068109] [<000003a8b22d0a70>] vfs_read+0x210/0x370
> [ 189.068112] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
> [ 189.068114] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
> [ 189.068117] [<000003a8b2e24242>] system_call+0x72/0x90
> [ 189.068120] task:oob-dirty-repro state:D stack:0 pid:8153
> tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
> [ 189.068123] Call Trace:
> [ 189.068124] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.068126] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.068128] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.068130] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.068133] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
> [ 189.068136] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
> [ 189.068138] [<000003a8b21ad444>] do_writepages+0xd4/0x190
> [ 189.068141] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
> [ 189.068144] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
> [ 189.068146] [<000003a8b2637d3c>]
> btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
> [ 189.068149] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
> [ 189.068152] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
> [ 189.068154] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
> [ 189.068156] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
> [ 189.068158] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
> [ 189.068160] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
> [ 189.068163] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
> [ 189.068165] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
> [ 189.068168] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
> [ 189.068170] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
> [ 189.068172] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
> [ 189.068174] [<000003a8b22d0a70>] vfs_read+0x210/0x370
> [ 189.068176] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
> [ 189.068178] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
> [ 189.068181] [<000003a8b2e24242>] system_call+0x72/0x90
> [ 189.068184] task:oob-dirty-repro state:D stack:0 pid:8154
> tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
> [ 189.068187] Call Trace:
> [ 189.068188] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.068190] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.068192] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.068193] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.068196] [<000003a8b26260fc>] btrfs_page_mkwrite+0x25c/0x8d0
> [ 189.068199] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
> [ 189.068202] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
> [ 189.068203] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
> [ 189.068206] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
> [ 189.068208] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
> [ 189.068210] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
> [ 189.068213] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
> [ 189.068215] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
> [ 189.068218] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
> [ 189.068219] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
> [ 189.068221] [<000003a8b22d0a70>] vfs_read+0x210/0x370
> [ 189.068223] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
> [ 189.068225] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
> [ 189.068228] [<000003a8b2e24242>] system_call+0x72/0x90
> [ 189.068231] task:oob-dirty-repro state:D stack:0 pid:8155
> tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
> [ 189.068234] Call Trace:
> [ 189.068235] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.068237] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.068239] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.068241] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.068244] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
> [ 189.068246] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
> [ 189.068249] [<000003a8b21ad444>] do_writepages+0xd4/0x190
> [ 189.068252] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
> [ 189.068255] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
> [ 189.068257] [<000003a8b2637d3c>]
> btrfs_start_ordered_extent_nowriteback+0x18c/0x1d0
> [ 189.068260] [<000003a8b26260ce>] btrfs_page_mkwrite+0x22e/0x8d0
> [ 189.068263] [<000003a8b21f89d0>] do_page_mkwrite+0x60/0xf0
> [ 189.068265] [<000003a8b21ffa64>] do_wp_page+0x134/0x660
> [ 189.068267] [<000003a8b2206584>] __handle_mm_fault+0x1c4/0x5a0
> [ 189.068269] [<000003a8b22069f0>] handle_mm_fault+0x90/0x230
> [ 189.068271] [<000003a8b1ebfbd0>] do_exception+0x190/0x560
> [ 189.068274] [<000003a8b2e136c0>] __do_pgm_check+0x1b0/0x370
> [ 189.068277] [<000003a8b2e243a4>] pgm_check_handler+0x114/0x160
> [ 189.068279] [<000003a8b283135e>] _copy_to_iter+0x6e/0x870
> [ 189.068281] [<000003a8b2831c4a>] copy_page_to_iter+0xea/0x160
> [ 189.068283] [<000003a8b219ff8c>] filemap_read+0x1ec/0x400
> [ 189.068285] [<000003a8b22d0a70>] vfs_read+0x210/0x370
> [ 189.068287] [<000003a8b22d1694>] ksys_pread64+0xa4/0xd0
> [ 189.068289] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
> [ 189.068292] [<000003a8b2e24242>] system_call+0x72/0x90
> [ 189.068294] task:oob-dirty-repro state:D stack:0 pid:8156
> tgid:8151 ppid:7260 task_flags:0x400040 flags:0x00000000
> [ 189.068297] Call Trace:
> [ 189.068298] [<000003a8b2e19284>] __schedule+0x374/0x900
> [ 189.068300] [<000003a8b2e1984c>] schedule+0x3c/0xf0
> [ 189.068302] [<000003a8b2e1996c>] io_schedule+0x2c/0x40
> [ 189.068304] [<000003a8b219d158>] folio_wait_bit_common+0x198/0x3b0
> [ 189.068307] [<000003a8b263e2e8>] extent_write_cache_pages+0x348/0x4d0
> [ 189.068310] [<000003a8b263ea0a>] btrfs_writepages+0x6a/0xd0
> [ 189.068313] [<000003a8b21ad444>] do_writepages+0xd4/0x190
> [ 189.068315] [<000003a8b2199dec>] filemap_writeback+0xbc/0x100
> [ 189.068318] [<000003a8b2199e5a>] filemap_fdatawrite_range+0x2a/0x40
> [ 189.068320] [<000003a8b232bbf2>] sync_file_range+0x122/0x150
> [ 189.068323] [<000003a8b232bc84>] ksys_sync_file_range+0x64/0xb0
> [ 189.068326] [<000003a8b232bd0a>] __s390x_sys_sync_file_range+0x3a/0x50
> [ 189.068328] [<000003a8b2e13b7e>] __do_syscall+0x14e/0x590
> [ 189.068331] [<000003a8b2e24242>] system_call+0x72/0x90
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 9:35 ` Qu Wenruo
@ 2026-07-22 10:40 ` Christian Borntraeger
0 siblings, 0 replies; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-22 10:40 UTC (permalink / raw)
To: Qu Wenruo, Qu Wenruo, linux-btrfs, Linux Memory Management List,
linux-fsdevel
Cc: David Sterba, Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
Am 22.07.26 um 11:35 schrieb Qu Wenruo:
>
>
> 在 2026/7/22 18:59, Christian Borntraeger 写道:
>> Am 22.07.26 um 10:59 schrieb Qu Wenruo:
>>>
>>>
>>> 在 2026/7/22 18:05, Christian Borntraeger 写道:
>>>> Am 21.07.26 um 23:07 schrieb Qu Wenruo:
>>>>
>>>> First, thank you for taking the time to look into this and trying to understand
>>>> things and trying to explain things. this is highly appreciated.
>>>> I am still trying to fully understand this myself. A question:
>>>>
>>>>> 在 2026/7/22 04:41, Christian Borntraeger 写道:
>>>>>> A folio can carry the folio-level dirty flag while its btrfs subpage
>>>>>> dirty bitmap is empty: btrfs data mappings use filemap_dirty_folio(),
>>>>>> so a generic folio_mark_dirty() call sets only the folio flag and the
>>>>>> xarray tag, without setting any subpage dirty bit and without a
>>>>>> delalloc reservation. The typical source is set_page_dirty_lock() on
>>>>>> a GUP pin,
>>>>>
>>>>> Shouldn't such folio got its ->page_mkwrite() callback get called first?
>>>>
>>>> Isnt that called implicitely at pin time?
>>>
>>> I have to admit, I'm not an expert on the MM part, I'm mostly a simple user of the existing MM interfaces.
>>>
>>> AFAIK, the last time I brought this thing up, Christoph mentioned that dirtying-folio-without-notifying-fs is a bug, and fs should not and is not able to handle such situation anyway.
>>>
>>> And that idea makes a lot of sense to me.
>>>
>>> So adding MM list for more help.
>> So I now have an userspace O_DIRECT reproducer outside of KVM. (attached) which gave me (on an s390 system, though)
>
> Thanks a lot, I can also reproduce it on arm64 (64K page size).
>
> This is super bad, as we have just removed a lot of folio ordered related code to detect such problem.
>
> I'll also check if it's some recent btrfs changes making it worse.
The RFC patch from yesterday seems to fix _THIS_ problem, but as you outlined, there might be other issues that break and are not handled by my patch.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 9:29 ` Christian Borntraeger
2026-07-22 9:35 ` Qu Wenruo
@ 2026-07-22 12:57 ` Matthew Wilcox
2026-07-23 0:42 ` Qu Wenruo
1 sibling, 1 reply; 17+ messages in thread
From: Matthew Wilcox @ 2026-07-22 12:57 UTC (permalink / raw)
To: Christian Borntraeger
Cc: Qu Wenruo, linux-btrfs, Qu Wenruo, Linux Memory Management List,
linux-fsdevel, David Sterba, Chris Mason, Josef Bacik,
linux-kernel, kvm, linux-s390
On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
> * Whenever a full clean cycle (clear_page_dirty_for_io(),
> * writeback, bits cleared) completes inside thread A's
> * submission->completion window, the completion-time
> * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
> * sets only the folio flag and the xarray tag - no btrfs subpage
> * dirty bit, no delalloc reservation. See the 20-year-old comment
> * above bio_set_pages_dirty() in block/bio.c describing exactly
> * this ("other code (eg, flusher threads) could clean the pages").
There's your problem. filemap_dirty_folio() documents that btrfs is
doing it wrongly:
* Filesystems which do not use buffer heads should call this function
* from their dirty_folio address space operation. It ignores the
* contents of folio_get_private(), so if the filesystem marks individual
* blocks as dirty, the filesystem should handle that itself.
fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
so btrfs should have its own btrfs_dirty_folio() which does whatever
metadata updates it needs to and then call filemap_dirty_folio() to
take care of the page cache business. See iomap_dirty_folio() as
an example, but many other filesystems also do this.
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-22 12:57 ` Matthew Wilcox
@ 2026-07-23 0:42 ` Qu Wenruo
2026-07-23 12:00 ` Matthew Wilcox
0 siblings, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-23 0:42 UTC (permalink / raw)
To: Matthew Wilcox, Christian Borntraeger
Cc: linux-btrfs, Qu Wenruo, Linux Memory Management List,
linux-fsdevel, David Sterba, Chris Mason, Josef Bacik,
linux-kernel, kvm, linux-s390
在 2026/7/22 22:27, Matthew Wilcox 写道:
> On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
>> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
>> * Whenever a full clean cycle (clear_page_dirty_for_io(),
>> * writeback, bits cleared) completes inside thread A's
>> * submission->completion window, the completion-time
>> * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
>> * sets only the folio flag and the xarray tag - no btrfs subpage
>> * dirty bit, no delalloc reservation. See the 20-year-old comment
>> * above bio_set_pages_dirty() in block/bio.c describing exactly
>> * this ("other code (eg, flusher threads) could clean the pages").
>
> There's your problem. filemap_dirty_folio() documents that btrfs is
> doing it wrongly:
>
> * Filesystems which do not use buffer heads should call this function
> * from their dirty_folio address space operation. It ignores the
> * contents of folio_get_private(), so if the filesystem marks individual
> * blocks as dirty, the filesystem should handle that itself.
>
> fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
>
> so btrfs should have its own btrfs_dirty_folio() which does whatever
> metadata updates it needs to and then call filemap_dirty_folio() to
> take care of the page cache business. See iomap_dirty_folio() as
> an example, but many other filesystems also do this.
Thanks a lot for the advice.
However it looks like the sub-folio dirty block tracking is a little
different between iomap and btrfs.
E.g. iomap will mark the full folio range dirty even if the EOF is
inside the folio, but btrfs will only mark the range inside EOF as dirty.
Another thing is, even if we follow iomap to mark the full folio dirty,
it's still not the end of the story.
We have other supporting mechanisms required to tracking the dirty
range. E.g. EXTENT_DELALLOC flags inside extent-io-tree, indicating we
have already reserved space for the dirty range.
Only with EXTENT_DELALLOC flag set, we will do the real delayed
allocation, allocating the on-disk extents etc.
So even if we always mark the full folio dirty, the writeback path will
not handle them correctly either.
Finally, even without large folios, the reproducer can already cause
problems on btrfs. E.g. on x86_64, with mapping_set_folio_order_range()
disabled.
The symptom there is that, ordered extent accounting underflows, which
may also contribute to the stall observed.
I'll keep digging for this bug.
Thanks,
Qu
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-23 0:42 ` Qu Wenruo
@ 2026-07-23 12:00 ` Matthew Wilcox
2026-07-23 22:40 ` Qu Wenruo
0 siblings, 1 reply; 17+ messages in thread
From: Matthew Wilcox @ 2026-07-23 12:00 UTC (permalink / raw)
To: Qu Wenruo
Cc: Christian Borntraeger, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
>
>
> 在 2026/7/22 22:27, Matthew Wilcox 写道:
> > On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
> > > * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
> > > * Whenever a full clean cycle (clear_page_dirty_for_io(),
> > > * writeback, bits cleared) completes inside thread A's
> > > * submission->completion window, the completion-time
> > > * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
> > > * sets only the folio flag and the xarray tag - no btrfs subpage
> > > * dirty bit, no delalloc reservation. See the 20-year-old comment
> > > * above bio_set_pages_dirty() in block/bio.c describing exactly
> > > * this ("other code (eg, flusher threads) could clean the pages").
> >
> > There's your problem. filemap_dirty_folio() documents that btrfs is
> > doing it wrongly:
> >
> > * Filesystems which do not use buffer heads should call this function
> > * from their dirty_folio address space operation. It ignores the
> > * contents of folio_get_private(), so if the filesystem marks individual
> > * blocks as dirty, the filesystem should handle that itself.
> >
> > fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
> >
> > so btrfs should have its own btrfs_dirty_folio() which does whatever
> > metadata updates it needs to and then call filemap_dirty_folio() to
> > take care of the page cache business. See iomap_dirty_folio() as
> > an example, but many other filesystems also do this.
>
> Thanks a lot for the advice.
>
> However it looks like the sub-folio dirty block tracking is a little
> different between iomap and btrfs.
My point is not that "you should do it the exact same way as iomap".
Rather "the dirty_folio op is the entry point to tell the filesystem
that a folio is being dirtied". And you aren't taking advantage of
that, you're just calling the VFS so the VFS can do its own tracking.
> E.g. iomap will mark the full folio range dirty even if the EOF is inside
> the folio, but btrfs will only mark the range inside EOF as dirty.
>
>
> Another thing is, even if we follow iomap to mark the full folio dirty, it's
> still not the end of the story.
>
> We have other supporting mechanisms required to tracking the dirty range.
> E.g. EXTENT_DELALLOC flags inside extent-io-tree, indicating we have already
> reserved space for the dirty range.
>
> Only with EXTENT_DELALLOC flag set, we will do the real delayed allocation,
> allocating the on-disk extents etc.
>
> So even if we always mark the full folio dirty, the writeback path will not
> handle them correctly either.
>
>
> Finally, even without large folios, the reproducer can already cause
> problems on btrfs. E.g. on x86_64, with mapping_set_folio_order_range()
> disabled.
>
> The symptom there is that, ordered extent accounting underflows, which may
> also contribute to the stall observed.
>
> I'll keep digging for this bug.
>
> Thanks,
> Qu
>
>
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-23 12:00 ` Matthew Wilcox
@ 2026-07-23 22:40 ` Qu Wenruo
2026-07-25 6:26 ` Boris Burkov
0 siblings, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-23 22:40 UTC (permalink / raw)
To: Matthew Wilcox
Cc: Christian Borntraeger, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/23 21:30, Matthew Wilcox 写道:
> On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
>>
>>
>> 在 2026/7/22 22:27, Matthew Wilcox 写道:
>>> On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
>>>> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
>>>> * Whenever a full clean cycle (clear_page_dirty_for_io(),
>>>> * writeback, bits cleared) completes inside thread A's
>>>> * submission->completion window, the completion-time
>>>> * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
>>>> * sets only the folio flag and the xarray tag - no btrfs subpage
>>>> * dirty bit, no delalloc reservation. See the 20-year-old comment
>>>> * above bio_set_pages_dirty() in block/bio.c describing exactly
>>>> * this ("other code (eg, flusher threads) could clean the pages").
>>>
>>> There's your problem. filemap_dirty_folio() documents that btrfs is
>>> doing it wrongly:
>>>
>>> * Filesystems which do not use buffer heads should call this function
>>> * from their dirty_folio address space operation. It ignores the
>>> * contents of folio_get_private(), so if the filesystem marks individual
>>> * blocks as dirty, the filesystem should handle that itself.
>>>
>>> fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
>>>
>>> so btrfs should have its own btrfs_dirty_folio() which does whatever
>>> metadata updates it needs to and then call filemap_dirty_folio() to
>>> take care of the page cache business. See iomap_dirty_folio() as
>>> an example, but many other filesystems also do this.
>>
>> Thanks a lot for the advice.
>>
>> However it looks like the sub-folio dirty block tracking is a little
>> different between iomap and btrfs.
>
> My point is not that "you should do it the exact same way as iomap".
> Rather "the dirty_folio op is the entry point to tell the filesystem
> that a folio is being dirtied".
And since dirty_folio() is not allowed to sleep, we should introduce
some extra mechanism, e.g. page private 2/checked, to notify the fs that
the folio is marked dirty without proper preparation.
Then during writeback, detect such folio and do needed preparation for
it since at writeback we're allowed to sleep.
That sounds feasible, but I haven't seen anyone doing that (including
the older btrfs cow fixup).
Will explore that path. Thanks a lot again for the dirty_folio() help.
Thanks,
Qu
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-23 22:40 ` Qu Wenruo
@ 2026-07-25 6:26 ` Boris Burkov
2026-07-27 8:11 ` Christian Borntraeger
0 siblings, 1 reply; 17+ messages in thread
From: Boris Burkov @ 2026-07-25 6:26 UTC (permalink / raw)
To: Qu Wenruo
Cc: Matthew Wilcox, Christian Borntraeger, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
On Fri, Jul 24, 2026 at 08:10:36AM +0930, Qu Wenruo wrote:
>
>
> 在 2026/7/23 21:30, Matthew Wilcox 写道:
> > On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
> > >
> > >
> > > 在 2026/7/22 22:27, Matthew Wilcox 写道:
> > > > On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
> > > > > * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
> > > > > * Whenever a full clean cycle (clear_page_dirty_for_io(),
> > > > > * writeback, bits cleared) completes inside thread A's
> > > > > * submission->completion window, the completion-time
> > > > > * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
> > > > > * sets only the folio flag and the xarray tag - no btrfs subpage
> > > > > * dirty bit, no delalloc reservation. See the 20-year-old comment
> > > > > * above bio_set_pages_dirty() in block/bio.c describing exactly
> > > > > * this ("other code (eg, flusher threads) could clean the pages").
> > > >
> > > > There's your problem. filemap_dirty_folio() documents that btrfs is
> > > > doing it wrongly:
> > > >
> > > > * Filesystems which do not use buffer heads should call this function
> > > > * from their dirty_folio address space operation. It ignores the
> > > > * contents of folio_get_private(), so if the filesystem marks individual
> > > > * blocks as dirty, the filesystem should handle that itself.
> > > >
> > > > fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
> > > >
> > > > so btrfs should have its own btrfs_dirty_folio() which does whatever
> > > > metadata updates it needs to and then call filemap_dirty_folio() to
> > > > take care of the page cache business. See iomap_dirty_folio() as
> > > > an example, but many other filesystems also do this.
> > >
> > > Thanks a lot for the advice.
> > >
> > > However it looks like the sub-folio dirty block tracking is a little
> > > different between iomap and btrfs.
> >
> > My point is not that "you should do it the exact same way as iomap".
> > Rather "the dirty_folio op is the entry point to tell the filesystem
> > that a folio is being dirtied".
> And since dirty_folio() is not allowed to sleep, we should introduce some
> extra mechanism, e.g. page private 2/checked, to notify the fs that the
> folio is marked dirty without proper preparation.
>
> Then during writeback, detect such folio and do needed preparation for it
> since at writeback we're allowed to sleep.
>
> That sounds feasible, but I haven't seen anyone doing that (including the
> older btrfs cow fixup).
>
> Will explore that path. Thanks a lot again for the dirty_folio() help.
>
> Thanks,
> Qu
Here is my proposal for a candidate fix. It passes the reproducer in
this thread as well as several more intense reproducers (alluded to but
not yet included)
https://lore.kernel.org/linux-btrfs/6758d4f27be0bbdb865cee7dd5adc435c969f4a3.1784960646.git.boris@bur.io/T/#u
Thanks,
Boris
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-25 6:26 ` Boris Burkov
@ 2026-07-27 8:11 ` Christian Borntraeger
2026-07-27 8:41 ` Qu Wenruo
0 siblings, 1 reply; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-27 8:11 UTC (permalink / raw)
To: Boris Burkov, Qu Wenruo
Cc: Matthew Wilcox, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
Am 25.07.26 um 08:26 schrieb Boris Burkov:
> On Fri, Jul 24, 2026 at 08:10:36AM +0930, Qu Wenruo wrote:
>>
>>
>> 在 2026/7/23 21:30, Matthew Wilcox 写道:
>>> On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
>>>>
>>>>
>>>> 在 2026/7/22 22:27, Matthew Wilcox 写道:
>>>>> On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
>>>>>> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
>>>>>> * Whenever a full clean cycle (clear_page_dirty_for_io(),
>>>>>> * writeback, bits cleared) completes inside thread A's
>>>>>> * submission->completion window, the completion-time
>>>>>> * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
>>>>>> * sets only the folio flag and the xarray tag - no btrfs subpage
>>>>>> * dirty bit, no delalloc reservation. See the 20-year-old comment
>>>>>> * above bio_set_pages_dirty() in block/bio.c describing exactly
>>>>>> * this ("other code (eg, flusher threads) could clean the pages").
>>>>>
>>>>> There's your problem. filemap_dirty_folio() documents that btrfs is
>>>>> doing it wrongly:
>>>>>
>>>>> * Filesystems which do not use buffer heads should call this function
>>>>> * from their dirty_folio address space operation. It ignores the
>>>>> * contents of folio_get_private(), so if the filesystem marks individual
>>>>> * blocks as dirty, the filesystem should handle that itself.
>>>>>
>>>>> fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
>>>>>
>>>>> so btrfs should have its own btrfs_dirty_folio() which does whatever
>>>>> metadata updates it needs to and then call filemap_dirty_folio() to
>>>>> take care of the page cache business. See iomap_dirty_folio() as
>>>>> an example, but many other filesystems also do this.
>>>>
>>>> Thanks a lot for the advice.
>>>>
>>>> However it looks like the sub-folio dirty block tracking is a little
>>>> different between iomap and btrfs.
>>>
>>> My point is not that "you should do it the exact same way as iomap".
>>> Rather "the dirty_folio op is the entry point to tell the filesystem
>>> that a folio is being dirtied".
>> And since dirty_folio() is not allowed to sleep, we should introduce some
>> extra mechanism, e.g. page private 2/checked, to notify the fs that the
>> folio is marked dirty without proper preparation.
>>
>> Then during writeback, detect such folio and do needed preparation for it
>> since at writeback we're allowed to sleep.
>>
>> That sounds feasible, but I haven't seen anyone doing that (including the
>> older btrfs cow fixup).
>>
>> Will explore that path. Thanks a lot again for the dirty_folio() help.
>>
>> Thanks,
>> Qu
>
> Here is my proposal for a candidate fix. It passes the reproducer in
> this thread as well as several more intense reproducers (alluded to but
> not yet included)
>
> https://lore.kernel.org/linux-btrfs/6758d4f27be0bbdb865cee7dd5adc435c969f4a3.1784960646.git.boris@bur.io/T/#u
I am willing to give it a try, but that does not seem to apply on 7.2-rc4
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-27 8:11 ` Christian Borntraeger
@ 2026-07-27 8:41 ` Qu Wenruo
2026-07-27 12:59 ` Christian Borntraeger
0 siblings, 1 reply; 17+ messages in thread
From: Qu Wenruo @ 2026-07-27 8:41 UTC (permalink / raw)
To: Christian Borntraeger, Boris Burkov
Cc: Matthew Wilcox, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
在 2026/7/27 17:41, Christian Borntraeger 写道:
> Am 25.07.26 um 08:26 schrieb Boris Burkov:
>> On Fri, Jul 24, 2026 at 08:10:36AM +0930, Qu Wenruo wrote:
>>>
>>>
>>> 在 2026/7/23 21:30, Matthew Wilcox 写道:
>>>> On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
>>>>>
>>>>>
>>>>> 在 2026/7/22 22:27, Matthew Wilcox 写道:
>>>>>> On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger
>>>>>> wrote:
>>>>>>> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the
>>>>>>> target file.
>>>>>>> * Whenever a full clean cycle (clear_page_dirty_for_io(),
>>>>>>> * writeback, bits cleared) completes inside thread A's
>>>>>>> * submission->completion window, the completion-time
>>>>>>> * set_page_dirty_lock() hits a *clean* folio:
>>>>>>> filemap_dirty_folio()
>>>>>>> * sets only the folio flag and the xarray tag - no btrfs
>>>>>>> subpage
>>>>>>> * dirty bit, no delalloc reservation. See the 20-year-
>>>>>>> old comment
>>>>>>> * above bio_set_pages_dirty() in block/bio.c describing
>>>>>>> exactly
>>>>>>> * this ("other code (eg, flusher threads) could clean
>>>>>>> the pages").
>>>>>>
>>>>>> There's your problem. filemap_dirty_folio() documents that btrfs is
>>>>>> doing it wrongly:
>>>>>>
>>>>>> * Filesystems which do not use buffer heads should call this
>>>>>> function
>>>>>> * from their dirty_folio address space operation. It ignores the
>>>>>> * contents of folio_get_private(), so if the filesystem marks
>>>>>> individual
>>>>>> * blocks as dirty, the filesystem should handle that itself.
>>>>>>
>>>>>> fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
>>>>>>
>>>>>> so btrfs should have its own btrfs_dirty_folio() which does whatever
>>>>>> metadata updates it needs to and then call filemap_dirty_folio() to
>>>>>> take care of the page cache business. See iomap_dirty_folio() as
>>>>>> an example, but many other filesystems also do this.
>>>>>
>>>>> Thanks a lot for the advice.
>>>>>
>>>>> However it looks like the sub-folio dirty block tracking is a little
>>>>> different between iomap and btrfs.
>>>>
>>>> My point is not that "you should do it the exact same way as iomap".
>>>> Rather "the dirty_folio op is the entry point to tell the filesystem
>>>> that a folio is being dirtied".
>>> And since dirty_folio() is not allowed to sleep, we should introduce
>>> some
>>> extra mechanism, e.g. page private 2/checked, to notify the fs that the
>>> folio is marked dirty without proper preparation.
>>>
>>> Then during writeback, detect such folio and do needed preparation
>>> for it
>>> since at writeback we're allowed to sleep.
>>>
>>> That sounds feasible, but I haven't seen anyone doing that (including
>>> the
>>> older btrfs cow fixup).
>>>
>>> Will explore that path. Thanks a lot again for the dirty_folio() help.
>>>
>>> Thanks,
>>> Qu
>>
>> Here is my proposal for a candidate fix. It passes the reproducer in
>> this thread as well as several more intense reproducers (alluded to but
>> not yet included)
>>
>> https://lore.kernel.org/linux-
>> btrfs/6758d4f27be0bbdb865cee7dd5adc435c969f4a3.1784960646.git.boris@bur.io/T/#u
> I am willing to give it a try, but that does not seem to apply on 7.2-rc4
>
The latest version is out:
https://lore.kernel.org/linux-btrfs/9f7a81cdd022d9e3f69d347a17c0a25438ccec14.1785131713.git.boris@bur.io/
And you can apply it on the latest btrfs for-next branch without any
conflict:
https://github.com/btrfs/linux.git for-next
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back
2026-07-27 8:41 ` Qu Wenruo
@ 2026-07-27 12:59 ` Christian Borntraeger
0 siblings, 0 replies; 17+ messages in thread
From: Christian Borntraeger @ 2026-07-27 12:59 UTC (permalink / raw)
To: Qu Wenruo, Boris Burkov
Cc: Matthew Wilcox, linux-btrfs, Qu Wenruo,
Linux Memory Management List, linux-fsdevel, David Sterba,
Chris Mason, Josef Bacik, linux-kernel, kvm, linux-s390
Am 27.07.26 um 10:41 schrieb Qu Wenruo:
>
>
> 在 2026/7/27 17:41, Christian Borntraeger 写道:
>> Am 25.07.26 um 08:26 schrieb Boris Burkov:
>>> On Fri, Jul 24, 2026 at 08:10:36AM +0930, Qu Wenruo wrote:
>>>>
>>>>
>>>> 在 2026/7/23 21:30, Matthew Wilcox 写道:
>>>>> On Thu, Jul 23, 2026 at 10:12:27AM +0930, Qu Wenruo wrote:
>>>>>>
>>>>>>
>>>>>> 在 2026/7/22 22:27, Matthew Wilcox 写道:
>>>>>>> On Wed, Jul 22, 2026 at 11:29:36AM +0200, Christian Borntraeger wrote:
>>>>>>>> * 4. Thread B loops sync_file_range(WRITE|WAIT) on the target file.
>>>>>>>> * Whenever a full clean cycle (clear_page_dirty_for_io(),
>>>>>>>> * writeback, bits cleared) completes inside thread A's
>>>>>>>> * submission->completion window, the completion-time
>>>>>>>> * set_page_dirty_lock() hits a *clean* folio: filemap_dirty_folio()
>>>>>>>> * sets only the folio flag and the xarray tag - no btrfs subpage
>>>>>>>> * dirty bit, no delalloc reservation. See the 20-year- old comment
>>>>>>>> * above bio_set_pages_dirty() in block/bio.c describing exactly
>>>>>>>> * this ("other code (eg, flusher threads) could clean the pages").
>>>>>>>
>>>>>>> There's your problem. filemap_dirty_folio() documents that btrfs is
>>>>>>> doing it wrongly:
>>>>>>>
>>>>>>> * Filesystems which do not use buffer heads should call this function
>>>>>>> * from their dirty_folio address space operation. It ignores the
>>>>>>> * contents of folio_get_private(), so if the filesystem marks individual
>>>>>>> * blocks as dirty, the filesystem should handle that itself.
>>>>>>>
>>>>>>> fs/btrfs/inode.c: .dirty_folio = filemap_dirty_folio,
>>>>>>>
>>>>>>> so btrfs should have its own btrfs_dirty_folio() which does whatever
>>>>>>> metadata updates it needs to and then call filemap_dirty_folio() to
>>>>>>> take care of the page cache business. See iomap_dirty_folio() as
>>>>>>> an example, but many other filesystems also do this.
>>>>>>
>>>>>> Thanks a lot for the advice.
>>>>>>
>>>>>> However it looks like the sub-folio dirty block tracking is a little
>>>>>> different between iomap and btrfs.
>>>>>
>>>>> My point is not that "you should do it the exact same way as iomap".
>>>>> Rather "the dirty_folio op is the entry point to tell the filesystem
>>>>> that a folio is being dirtied".
>>>> And since dirty_folio() is not allowed to sleep, we should introduce some
>>>> extra mechanism, e.g. page private 2/checked, to notify the fs that the
>>>> folio is marked dirty without proper preparation.
>>>>
>>>> Then during writeback, detect such folio and do needed preparation for it
>>>> since at writeback we're allowed to sleep.
>>>>
>>>> That sounds feasible, but I haven't seen anyone doing that (including the
>>>> older btrfs cow fixup).
>>>>
>>>> Will explore that path. Thanks a lot again for the dirty_folio() help.
>>>>
>>>> Thanks,
>>>> Qu
>>>
>>> Here is my proposal for a candidate fix. It passes the reproducer in
>>> this thread as well as several more intense reproducers (alluded to but
>>> not yet included)
>>>
>>> https://lore.kernel.org/linux- btrfs/6758d4f27be0bbdb865cee7dd5adc435c969f4a3.1784960646.git.boris@bur.io/T/#u
>> I am willing to give it a try, but that does not seem to apply on 7.2-rc4
>>
>
> The latest version is out:
>
> https://lore.kernel.org/linux-btrfs/9f7a81cdd022d9e3f69d347a17c0a25438ccec14.1785131713.git.boris@bur.io/
>
> And you can apply it on the latest btrfs for-next branch without any conflict:
>
> https://github.com/btrfs/linux.git for-next
>
Ok that seems to work for me avoiding the lockup issues.
I do have another issue (performance) with secure KVM memory on a large btrfs file but I think
I will report this differently when I have something to tell/show
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-07-27 13:16 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-21 19:11 7.2-rc1 regression Folio lock leak in writepage_delalloc() Christian Borntraeger
2026-07-21 19:11 ` [PATCH/RFC] btrfs: fix folio lock leak in writepage_delalloc() for folios dirtied behind btrfs' back Christian Borntraeger
2026-07-21 21:07 ` Qu Wenruo
2026-07-22 8:35 ` Christian Borntraeger
2026-07-22 8:59 ` Qu Wenruo
2026-07-22 9:29 ` Christian Borntraeger
2026-07-22 9:35 ` Qu Wenruo
2026-07-22 10:40 ` Christian Borntraeger
2026-07-22 12:57 ` Matthew Wilcox
2026-07-23 0:42 ` Qu Wenruo
2026-07-23 12:00 ` Matthew Wilcox
2026-07-23 22:40 ` Qu Wenruo
2026-07-25 6:26 ` Boris Burkov
2026-07-27 8:11 ` Christian Borntraeger
2026-07-27 8:41 ` Qu Wenruo
2026-07-27 12:59 ` Christian Borntraeger
2026-07-22 7:21 ` 7.2-rc1 regression Folio lock leak in writepage_delalloc() Qu Wenruo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®