syzbot
WARNING in ext4_journalled_invalidate_folio
Status:
moderation: reported on 2026/11/11 22:05
Subsystems:
ext4
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+c3d6438a7c434037c610@syzkaller.appspotmail.com
First crash: 1h34m, last: 1h28m
✨ AI-generated root cause summary:
When a buffered write fails partially (e.g. short copy due to user page fault) on an ext4 inode in data=journal mode with blocksize < PAGE_SIZE, ext4_journalled_write_end() invokes ext4_truncate_failed_write() to discard blocks allocated past inode->i_size. In data=journal mode, buffers attached to a folio straddling inode->i_size that are undergoing transaction commit cannot be invalidated immediately by jbd2_journal_invalidate_folio() and return -EBUSY. While the normal truncate path (ext4_truncate_down()) calls ext4_wait_for_tail_page_commit() to wait for the committing transaction before truncating the page cache, ext4_truncate_failed_write() directly calls truncate_inode_pages(), causing ext4_journalled_invalidate_folio() to encounter -EBUSY and trigger a WARN_ON.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Relevant Functions, Structs, and Files
- Files:
fs/ext4/truncate.h: Defines ext4_truncate_failed_write().
fs/ext4/inode.c: Defines ext4_journalled_write_end(), ext4_journalled_invalidate_folio(), __ext4_journalled_invalidate_folio(), and ext4_wait_for_tail_page_commit().
fs/jbd2/transaction.c: Defines jbd2_journal_invalidate_folio() and journal_unmap_buffer().
mm/truncate.c: Implements truncate_inode_pages_range(), truncate_inode_partial_folio(), and folio_invalidate().
- Structures and Fields:
struct folio: Folio straddling EOF / inode->i_size.
struct buffer_head & struct journal_head: Buffer and journal metadata heads attached to the folio.
journal_t: JBD2 journal (journal->j_committing_transaction, journal->j_running_transaction).
transaction_t: Current committing transaction (jh->b_transaction == journal->j_committing_transaction).
struct address_space_operations: ext4_journalled_aops.invalidate_folio.
2. Triggering Mechanism and Invariant Violation
-
Triggering Sequence:
- A file on an ext4 filesystem configured in
data=journal mode (ext4_should_journal_data(inode) == true) with a sub-page block size (blocksize < PAGE_SIZE, e.g., 1KB blocks with 4KB pages) undergoes an extending buffered write (pwritev -> generic_perform_write()).
ext4_write_begin() instantiates and allocates new disk blocks beyond the existing inode->i_size.
- The user-space copy operation then experiences a fault or short copy (
copied < len, e.g., page fault at CR2 = 0x20000000d000).
- Execution reaches
ext4_journalled_write_end() (fs/ext4/inode.c):
- The journalled buffers are zeroed and marked dirty via
write_end_fn() -> ext4_dirty_journalled_data().
- The inode size is updated to
pos + copied via ext4_update_inode_size(), leaving inode->i_size unaligned with the page boundary (straddling the folio).
- The inode is added to the orphan list (
ext4_orphan_add()), and the transaction handle is stopped via ext4_journal_stop(handle).
- Because blocks were allocated past the new
inode->i_size (pos + len > inode->i_size), ext4_journalled_write_end() calls ext4_truncate_failed_write(inode) to clean up unwritten blocks.
-
The Invariant Violation:
- In JBD2, when invalidating a partial folio (
partial_page == (offset || length < folio_size(folio))), any buffer_head undergoing commit (jh->b_transaction == journal->j_committing_transaction) cannot be discarded or unmapped immediately because its contents are actively being written to the journal.
journal_unmap_buffer() (fs/jbd2/transaction.c:2406-2415) explicitly enforces this rule:
if (partial_page) {
...
return -EBUSY;
}
Callers invalidating partial folios in data=journal mode must wait for the committing transaction to complete and then retry.
- Because VFS
a_ops->invalidate_folio() returns void, the page cache truncation code (truncate_inode_partial_folio()) cannot handle or retry -EBUSY.
- In the standard truncate path (
ext4_truncate_down(), fs/ext4/inode.c:6974), ext4 explicitly satisfies this requirement before calling truncate_pagecache():
if (ext4_should_journal_data(inode))
ext4_wait_for_tail_page_commit(inode);
- However,
ext4_truncate_failed_write() (fs/ext4/truncate.h:12-24) lacks this check:
static inline void ext4_truncate_failed_write(struct inode *inode)
{
struct address_space *mapping = inode->i_mapping;
filemap_invalidate_lock(mapping);
truncate_inode_pages(mapping, inode->i_size);
ext4_truncate(inode);
filemap_invalidate_unlock(mapping);
}
It directly calls truncate_inode_pages(mapping, inode->i_size) without calling ext4_wait_for_tail_page_commit(inode).
-
Manifestation:
Sample crash report:
loop0: detected capacity change from 0 to 16384
EXT4-fs: Ignoring removed i_version option
EXT4-fs (loop0): Using encoding defined by superblock: utf8-12.1.0 with flags 0x0
EXT4-fs: The data=journal mount option is deprecated and support will be removed in January 2028
EXT4-fs: Warning: mounting with data=journal disables delayed allocation, dioread_nolock, O_DIRECT and fast_commit support!
EXT4-fs (loop0): encrypted files will use data=ordered instead of data journaling mode
EXT4-fs (loop0): mounted filesystem 00000000-0000-0000-0000-000000000000 r/w with journalled data mode. Quota mode: journalled.
------------[ cut here ]------------
__ext4_journalled_invalidate_folio(folio, offset, length) < 0
WARNING: fs/ext4/inode.c:3561 at ext4_journalled_invalidate_folio+0x4a/0x50 fs/ext4/inode.c:3561, CPU#0: syz.0.0/5341
Modules linked in:
CPU: 0 UID: 0 PID: 5341 Comm: syz.0.0 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:ext4_journalled_invalidate_folio+0x4a/0x50 fs/ext4/inode.c:3561
Code: e8 3b 06 00 00 89 c3 31 ff 89 c6 e8 00 70 2a ff 85 db 78 10 e8 e7 6a 2a ff 5b 41 5e 41 5f e9 0d 23 6d 09 cc e8 d7 6a 2a ff 90 <0f> 0b 90 eb ea 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
RSP: 0018:ffffc90000ed7320 EFLAGS: 00010293
RAX: ffffffff829ec759 RBX: 00000000fffffff0 RCX: ffff888036fd4b00
RDX: 0000000000000000 RSI: 00000000fffffff0 RDI: 0000000000000000
RBP: 000000000000e3c0 R08: ffff888045dc7467 R09: 1ffff11008bb8e8c
R10: dffffc0000000000 R11: ffffed1008bb8e8d R12: ffffea00015366d8
R13: dffffc0000000000 R14: 00000000000003c0 R15: ffffea00015366c0
FS: 00007f741f3c56c0(0000) GS:ffff88808a9f9000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000020000000f000 CR3: 0000000045a65000 CR4: 0000000000352ef0
Call Trace:
<TASK>
folio_invalidate mm/truncate.c:140 [inline]
truncate_inode_partial_folio+0x70b/0xec0 mm/truncate.c:261
truncate_inode_pages_range+0x5f0/0xc10 mm/truncate.c:460
ext4_truncate_failed_write fs/ext4/truncate.h:21 [inline]
ext4_journalled_write_end+0x99f/0xd20 fs/ext4/inode.c:1726
generic_perform_write+0x612/0x8e0 mm/filemap.c:4413
ext4_buffered_write_iter+0x139/0x540 fs/ext4/file.c:372
ext4_file_write_iter+0xae4/0x1cd0 fs/ext4/file.c:-1
do_iter_readv_writev+0x612/0x8c0 fs/read_write.c:-1
vfs_writev+0x33d/0x9a0 fs/read_write.c:1038
do_pwritev+0x1a9/0x2d0 fs/read_write.c:1133
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x16a/0x530 arch/x86/entry/syscall_64.c:89
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f741e5a8f19
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 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 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f741f3c4fe8 EFLAGS: 00000246 ORIG_RAX: 0000000000000148
RAX: ffffffffffffffda RBX: 00007f741e825fa0 RCX: 00007f741e5a8f19
RDX: 0000000000000001 RSI: 0000200000008400 RDI: 0000000000000004
RBP: 00007f741e637f6d R08: 0000000000000000 R09: 0000000000000003
R10: 0000000000007800 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f741e826038 R14: 00007f741e825fa0 R15: 00007ffdac68b738
</TASK>
* Struck through repros no longer work on HEAD.