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: 8h28m, last: 11m
✨ 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:
* Struck through repros no longer work on HEAD.