CVE-2026-92501: ext4: drain in-flight DIO before buffered write fallback
In the Linux kernel, the following vulnerability has been resolved:
ext4: drain in-flight DIO before buffered write fallback
generic/746 started failing intermittently on ext3 (no-extent inodes). The test triggers 'Page cache invalidation failure on direct I/O' warnings and subsequent fsync returns -EIO. Adding a 50ms delay between ext4bufferedwriteiter() and filemapwriteandwaitrange() in ext4diowriteiter() makes the race almost always reproducible.
On no-extent inodes, DIO writes to holes cannot use unwritten extents, so ext4iomapalloc() leaves mflags=0 and ext4mapblocks() returns 0. The iomap layer then returns -ENOTBLK, causing fallback to buffered I/O.
The fallback path in ext4diowriteiter() calls ext4bufferedwriteiter() which dirties pages, then does flush and invalidate. However, there's an unprotected window between ext4bufferedwriteiter() returning (with inode lock released) and the subsequent flush+invalidate.
Concurrent async DIO completions from other threads can run kiocbinvalidatepostdirectwrite() during this window. If pages have been re-dirtied, post-invalidation finds dirty pages and triggers the warning, setting -EIO in the error sequence.
Consider a file with two 4k extents: [hole][written]. Thread A does DIO to the written extent, while thread B does DIO spanning both:
kworker A (4k DIO, allocated block) kworker B (8k DIO, fallback) ----------------------------------- ---------------------------- inodelockshared() inodelockshared() iomapdiorw(): iomapdiorw(): kiocbinvalidatepages -> clean iomapbegin -> -ENOTBLK submitbio (async) dio->size = 0 inodeunlockshared() inodeunlockshared()
[bio pending in block layer] / fallback: lock released / ext4bufferedwriteiter() inodelock(exclusive) genericperformwrite() -> dirty pages [0, 8k] inodeunlock(exclusive)
/ pages dirty, no lock / [bio completes] filemapwriteandwaitrange() iomapdiocomplete() -> flush dirty pages kiocbinvalidatepostdirectwrite() invalidatemappingpages() invalidateinodepages2range() -> finds dirty page! -> diowarnstalepagecache() -> errseqset(-EIO)
This issue can be triggered through normal I/O paths, not just intentionally overlapping DIO writes from userspace. For example, generic/746 uses a loop device where multiple kworkers issue concurrent I/O to the backing file. Additionally, when blocksize < foliosize, non-overlapping DIO writes that share a large folio can also trigger the race.
Add inodediowait() in ext4bufferedwriteiter() before ext4writechecks() to drain all in-flight DIO. This ensures that all DIO clears existing pages before submitting IO (via kiocbinvalidatepages()), all BIO waits for all DIO to complete (via inodediowait()), and ext4writechecks() observes the inode size after all completed DIO so that ext4blockzeroeof() does not race with in-flight DIO, thus eliminating the race.
Affected Software
Event History
Frequently Asked Questions
Which systems are most likely to encounter this issue?
The described failure occurs on ext3-style no-extent inodes when direct I/O writes target holes and fall back to buffered I/O. It requires concurrency with asynchronous direct-I/O completions from other threads during the fallback path.
What operational symptoms indicate that the issue may be occurring?
Kernel logs may contain “Page cache invalidation failure on direct I/O” warnings. Subsequent fsync operations can return -EIO after the error is recorded in the inode error sequence.
Does a direct I/O write always trigger the affected fallback path?
No. The fallback described occurs when a no-extent inode cannot allocate an unwritten extent for a direct-I/O write to a hole, causing the iomap layer to return -ENOTBLK and use buffered I/O instead.