syzbot
WARNING in ext4_journalled_invalidate_folio
Status:
upstream: reported C repro on 2026/11/12 04:57
Subsystems:
ext4
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+f111db4cb2acd44eb2f4@syzkaller.appspotmail.com
First crash: 1d08h, last: 16h28m
✨ 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:
------------[ 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.17/5572
Modules linked in:
CPU: 0 UID: 0 PID: 5572 Comm: syz.0.17 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:ffffc900021f7240 EFLAGS: 00010293
RAX: ffffffff829ec759 RBX: 00000000fffffff0 RCX: ffff888033d3cb00
RDX: 0000000000000000 RSI: 00000000fffffff0 RDI: 0000000000000000
RBP: 000000000006d300 R08: ffff888013b5a467 R09: 1ffff1100276b48c
R10: dffffc0000000000 R11: ffffed100276b48d R12: ffffea00011f6798
R13: dffffc0000000000 R14: 0000000000000300 R15: ffffea00011f6780
FS: 00007f91140b36c0(0000) GS:ffff88808a9f9000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000076000 CR3: 000000004039c000 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
aio_write+0x5cc/0x870 fs/aio.c:1697
__io_submit_one fs/aio.c:-1 [inline]
io_submit_one+0x798/0x13d0 fs/aio.c:2121
__do_sys_io_submit fs/aio.c:2180 [inline]
__se_sys_io_submit+0x195/0x340 fs/aio.c:2150
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:0x7f91131a8fd9
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:00007f91140b3028 EFLAGS: 00000246 ORIG_RAX: 00000000000000d1
RAX: ffffffffffffffda RBX: 00007f9113425fa0 RCX: 00007f91131a8fd9
RDX: 0000200000008c80 RSI: 000000000000003b RDI: 00007f911408a000
RBP: 00007f9113237fed R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f9113426038 R14: 00007f9113425fa0 R15: 00007ffe99a17598
</TASK>
* Struck through repros no longer work on HEAD.