mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* 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®