Re: [Linux Kernel Bug] WARNING in nilfs_copy_back_pages

From: Ryusuke Konishi

Date: Tue Sep 01 2026 - 12:36:44 EST


On Tue, Sep 1, 2026 at 9:58 PM Jiaming Zhang wrote:
>
> Dear Linux kernel developers and maintainers,
>
> We are writing to report an issue discovered in the NILFS2 subsystem. The
> issue is reproducible on the latest version of linux (v7.3-rc1, commit
> cee9395acd8043be0644b25c34bfa86623f2b935). Below is the kernel report:
>
> NILFS (loop0): error -2 preparing GC: cannot delete virtual blocks from DAT file
> ------------[ cut here ]------------
> folio_test_dirty(dfolio)
> WARNING: fs/nilfs2/page.c:331 at nilfs_copy_back_pages+0x639/0x810
> fs/nilfs2/page.c:331, CPU#0: syz-executor887/9521
> Modules linked in:
> CPU: 0 UID: 0 PID: 9521 Comm: syz-executor887 Not tainted 7.3.0-rc1
> #37 PREEMPT(full)
> Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix,
> 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> RIP: 0010:nilfs_copy_back_pages+0x639/0x810 fs/nilfs2/page.c:331
> Code: 44 89 fe e8 39 8e 12 fe 45 84 f6 74 25 e8 9f 8a 12 fe 48 89 df
> e8 67 9a 55 fe 4c 8b 7c 24 10 e9 e5 fe ff ff e8 88 8a 12 fe 90 <0f> 0b
> 90 e9 1d fe ff ff e8 7a 8a 12 fe 4c 8b 7c 24 10 e9 c8 fe ff
> RSP: 0018:ffffc9000854f4c0 EFLAGS: 00010293
> RAX: ffffffff83a8e928 RBX: ffffea00013cf3c0 RCX: ffff888026619f80
> RDX: 0000000000000000 RSI: 0000000000000010 RDI: 0000000000000000
> RBP: ffffc9000854f6e8 R08: ffffea00012db607 R09: 1ffffd400025b6c0
> R10: dffffc0000000000 R11: fffff9400025b6c1 R12: ffffc9000854f548
> R13: ffffea00012db600 R14: 0000000000000010 R15: 1ffffd4000279e79
> FS: 000055558d08a380(0000) GS:ffff8880985d9000(0000) knlGS:0000000000000000
> CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 000055558d08aca0 CR3: 0000000044f02000 CR4: 0000000000752ef0
> PKRU: 55555554
> Call Trace:
> <TASK>
> nilfs_mdt_restore_from_shadow_map+0x127/0x260 fs/nilfs2/mdt.c:652
> nilfs_clean_segments+0xab8/0xba0 fs/nilfs2/segment.c:2552
> nilfs_ioctl_clean_segments fs/nilfs2/ioctl.c:938 [inline]
> nilfs_ioctl+0x242a/0x2590 fs/nilfs2/ioctl.c:1368
> vfs_ioctl fs/ioctl.c:51 [inline]
> __do_sys_ioctl fs/ioctl.c:597 [inline]
> __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
> do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
> do_syscall_64+0x170/0x540 arch/x86/entry/syscall_64.c:84
> entry_SYSCALL_64_after_hwframe+0x77/0x7f
> RIP: 0033:0x7fa00bf4d42d
> Code: b3 66 2e 0f 1f 84 00 00 00 00 00 66 90 f3 0f 1e fa 48 89 f8 48
> 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
> 01 f0 ff ff 73 01 c3 48 c7 c1 c0 ff ff ff f7 d8 64 89 01 48
> RSP: 002b:00007ffd36ab1678 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
> RAX: ffffffffffffffda RBX: 0000000000000003 RCX: 00007fa00bf4d42d
> RDX: 0000200000000300 RSI: 0000000040786e88 RDI: 0000000000000004
> RBP: 0000000000000000 R08: 0000000000000000 R09: 00007ffd36ab16c0
> R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffd36ab169c
> R13: 00007ffd36ab16e0 R14: 000000000000000c R15: 431bde82d7b634db
> </TASK>
>
> Following is the root cause analysis for this issue, note that the
> analysis is performed with the assistance of LLM, but we try our best to
> ensure the accuracy.
>
> The WARNING triggers because a folio of the DAT metadata file is still
> dirty where nilfs_copy_back_pages() expects a clean one. That function
> runs when garbage collection fails, which the reproducer causes by asking
> it to free a virtual block number the DAT file does not map. nilfs2 then
> restores the DAT page cache to a snapshot taken before the collection
> started: it first clears the dirty state of the folios in that cache, then
> copies the snapshot back over them, assuming the first step left nothing
> dirty.
>
> The clearing step can skip a folio. Since commit ca76bb226bf4 ("nilfs2: do
> not force clear folio if buffer is referenced") nilfs_clear_folio_dirty()
> returns without clearing if any buffer head of the folio is busy, to avoid
> clobbering the state of a buffer somebody else is using. A buffer head is
> busy here: reading a metadata block goes through nilfs_mdt_read_block(),
> which also submits read-ahead for the blocks that follow and waits only for
> the first. When the block size is smaller than the page size, several
> metadata blocks share one folio, so a locked read-ahead buffer can be
> adjacent to a dirty buffer and keep the whole folio dirty.
>
> nilfs_copy_folio() then copies the snapshot over that folio and clears
> BH_Dirty on all of its buffer heads, leaving the folio dirty with no dirty
> buffer under it, which no later segment construction cleans up. On a
> kernel booted with panic_on_warn, the WARNING can lead to a system crash.
>
> To fix this issue, the dirty flag of the destination folio should be
> cleared before the folio is overwritten:
>
> diff --git a/fs/nilfs2/page.c b/fs/nilfs2/page.c
> index cf4f1c6798f5..b26d9c3bda6d 100644
> --- a/fs/nilfs2/page.c
> +++ b/fs/nilfs2/page.c
> @@ -328,7 +328,8 @@ void nilfs_copy_back_pages(struct address_space *dmap,
> dfolio = filemap_lock_folio(dmap, index);
> if (!IS_ERR(dfolio)) {
> /* overwrite existing folio in the destination cache */
> - WARN_ON(folio_test_dirty(dfolio));
> + if (unlikely(folio_test_dirty(dfolio)))
> + __nilfs_clear_folio_dirty(dfolio);
> nilfs_copy_folio(dfolio, folio, false);
> folio_unlock(dfolio);
> folio_put(dfolio);
>
> After applying the above patch, the reproducer no longer triggers the issue
> on our machine. If this solution is acceptable, we are happy to submit a
> formal patch.
>
> The kernel console output, kernel config, syzkaller reproducer, and C
> reproducer are available at google drive:
> https://drive.google.com/drive/folders/1R6EqqLPh5vgylKRPvO2ji5Q8Tsf0x8QY?usp=sharing
>
> Please let us know if any further information is required.

Thanks for the report, Jiaming Zhang!

I think the issue is that commit ca76bb226bf4 changed the expected
behavior of nilfs_clear_dirty_pages(), which is called before
nilfs_copy_back_pages().

Originally, nilfs_clear_dirty_pages() was implemented for
nilfs_mdt_restore_from_shadow_map() to restore the pre-GC state from
the DAT shadow page cache. It was designed to forcefully clear dirty
pages/folios in the DAT page cache, assuming complete synchronization
with the log writer.

However, over time, it started being called to resolve page/folio
dirty states when the filesystem degrades to read-only during a
page/folio writeback request.

Since this page/folio dirty state clearing was asynchronous with the
log writer, it caused unpredictable bugs in various places after the
filesystem fell back to read-only. Commit ca76bb226bf4 is one of the
essential workarounds introduced to avoid this.

The root cause is that the same helper function is being used for
calls with two entirely different premises. I believe the solution is
to restore the original semantics of nilfs_clear_dirty_pages() only
when it is called from nilfs_mdt_restore_from_shadow_map().

Specifically, how about adding a bool force flag argument to
nilfs_clear_dirty_pages() and nilfs_clear_folio_dirty(), and having
the call from nilfs_mdt_restore_from_shadow_map() set this flag to
clear the dirty state regardless of the buffer head's reference state,
just like the original implementation?

Would it be possible for you to propose a fix using this approach?

Alternatively, since their premises differ, we could separate the
implementations entirely, but I expect that adding a boolean option to
switch modes would be simpler.

Adjusting the WARN_ON in nilfs_copy_back_pages() would break the
original design (premise) and is therefore undesirable, in my view.

Thanks,
Ryusuke Konishi

>
> Best Regards,
> Jiaming Zhang