CVE-2026-45985
Description
In the Linux kernel, the following vulnerability has been resolved:
ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
When allocating blocks during within-EOF DIO and writeback with dioread_nolock enabled, EXT4_GET_BLOCKS_PRE_IO was set to split an existing large unwritten extent. However, EXT4_GET_BLOCKS_CONVERT was set when calling ext4_split_convert_extents(), which may potentially result in stale data issues.
Assume we have an unwritten extent, and then DIO writes the second half.
[UUUUUUUUUUUUUUUU] on-disk extent U: unwritten extent [UUUUUUUUUUUUUUUU] extent status tree |<- ->| ----> dio write this range
First, ext4_iomap_alloc() call ext4_map_blocks() with EXT4_GET_BLOCKS_PRE_IO, EXT4_GET_BLOCKS_UNWRIT_EXT and EXT4_GET_BLOCKS_CREATE flags set. ext4_map_blocks() find this extent and call ext4_split_convert_extents() with EXT4_GET_BLOCKS_CONVERT and the above flags set.
Then, ext4_split_convert_extents() calls ext4_split_extent() with EXT4_EXT_MAY_ZEROOUT, EXT4_EXT_MARK_UNWRIT2 and EXT4_EXT_DATA_VALID2 flags set, and it calls ext4_split_extent_at() to split the second half with EXT4_EXT_DATA_VALID2, EXT4_EXT_MARK_UNWRIT1, EXT4_EXT_MAY_ZEROOUT and EXT4_EXT_MARK_UNWRIT2 flags set. However, ext4_split_extent_at() failed to insert extent since a temporary lack -ENOSPC. It zeroes out the first half but convert the entire on-disk extent to written since the EXT4_EXT_DATA_VALID2 flag set, but left the second half as unwritten in the extent status tree.
[0000000000SSSSSS] data S: stale data, 0: zeroed [WWWWWWWWWWWWWWWW] on-disk extent W: written extent [WWWWWWWWWWUUUUUU] extent status tree
Finally, if the DIO failed to write data to the disk, the stale data in the second half will be exposed once the cached extent entry is gone.
Fix this issue by not passing EXT4_GET_BLOCKS_CONVERT when splitting an unwritten extent before submitting I/O, and make ext4_split_convert_extents() to zero out the entire extent range to zero for this case, and also mark the extent in the extent status tree for consistency.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A race condition in ext4's block allocation for within-EOF DIO with dioread_nolock can expose stale data when splitting unwritten extents.
Vulnerability
In the Linux kernel, a race condition exists in the ext4 filesystem when allocating blocks for within-EOF direct I/O (DIO) and writeback with the dioread_nolock mount option enabled [1]. The bug occurs in ext4_iomap_alloc() and ext4_map_blocks() when a large existing unwritten extent must be split. The kernel sets EXT4_GET_BLOCKS_CONVERT alongside EXT4_GET_BLOCKS_PRE_IO, causing ext4_split_convert_extents() to mark the entire on-disk extent as written while only zeroing the first half (from a failed split). Affected kernels include versions with the dioread_nolock feature active; the patch targets the stable tree via commit 67cdb7bd7442 [1].
Exploitation
An attacker needs no special privileges beyond the ability to trigger within-EOF DIO writes on an ext4 filesystem mounted with dioread_nolock [1]. The race window occurs when a temporary lack of space (-ENOSPC) during extent split causes ext4_split_extent_at() to fail insertion. The function zeroes the first half but converts the entire on-disk extent to written (due to EXT4_EXT_DATA_VALID2), leaving the second half as unwritten in the extent status tree. If the DIO write then fails to commit data to disk, the stale data already present in the second half becomes exposed when the cached extent entry is evicted [1].
Impact
Successful exploitation results in disclosure of stale data (kernel memory or previously written file content) from the unwritten second half of the extent [1]. The on-disk extent is incorrectly marked as written, and the extent status tree shows the second half as unwritten, leading to data corruption when the stale data is read. The confidentiality impact is data exposure; integrity is also affected because the file may contain uninitialized or old data where new data was expected [1].
Mitigation
The fix is commit 67cdb7bd7442 ("ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O") in the Linux kernel stable tree [1]. The patch removes EXT4_GET_BLOCKS_CONVERT from the flags passed during split, ensuring ext4_split_convert_extents() zeros the entire extent range instead of converting prematurely. Users should apply the latest kernel updates containing this commit. No workaround is documented; disabling dioread_nolock may avoid the issue but may impact performance. The vulnerability is not listed on the CISA KEV as of publication [1].
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
1477e407967cd8ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index b0e8711fd7fd8b..e5b32d5f634a20 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3705,11 +3705,15 @@ static int ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, ppath, map, split_flag, flags); @@ -3874,7 +3878,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { ret = ext4_split_convert_extents(handle, inode, map, ppath, - flags | EXT4_GET_BLOCKS_CONVERT); + flags); if (ret < 0) { err = ret; goto out2; -- cgit 1.3-korg
37555690f39fext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fd2a096d8ba6a6..54df6fe69d792f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3711,11 +3711,15 @@ static int ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, ppath, map, split_flag, flags); @@ -3880,7 +3884,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { ret = ext4_split_convert_extents(handle, inode, map, ppath, - flags | EXT4_GET_BLOCKS_CONVERT); + flags); if (ret < 0) { err = ret; goto out2; -- cgit 1.3-korg
feaf2a80e78fext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1fee84ea20af1b..91b56de60c9055 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3746,15 +3746,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_SPLIT_NOMERGE; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3930,7 +3934,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_SPLIT_NOMERGE) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
67cdb7bd7442ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 4507e42869854d..ed63260d792b1f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3735,15 +3735,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3920,7 +3924,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
2920ec61c98bext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 7301cf17269038..bd556a3eac1987 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3762,15 +3762,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3949,7 +3953,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
2698731d2582ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 459453e8bb16bc..3ff8dcdd80ce93 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3764,15 +3764,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3951,7 +3955,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
716e7439a5a9ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 8d5ca450aa5d23..1ef23a2a94d535 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3735,15 +3735,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_SPLIT_NOMERGE; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3919,7 +3923,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_SPLIT_NOMERGE) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
77e407967cd8ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index b0e8711fd7fd8b..e5b32d5f634a20 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3705,11 +3705,15 @@ static int ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, ppath, map, split_flag, flags); @@ -3874,7 +3878,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { ret = ext4_split_convert_extents(handle, inode, map, ppath, - flags | EXT4_GET_BLOCKS_CONVERT); + flags); if (ret < 0) { err = ret; goto out2; -- cgit 1.3-korg
37555690f39fext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fd2a096d8ba6a6..54df6fe69d792f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3711,11 +3711,15 @@ static int ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, ppath, map, split_flag, flags); @@ -3880,7 +3884,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { ret = ext4_split_convert_extents(handle, inode, map, ppath, - flags | EXT4_GET_BLOCKS_CONVERT); + flags); if (ret < 0) { err = ret; goto out2; -- cgit 1.3-korg
2698731d2582ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 459453e8bb16bc..3ff8dcdd80ce93 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3764,15 +3764,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3951,7 +3955,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
716e7439a5a9ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 8d5ca450aa5d23..1ef23a2a94d535 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3735,15 +3735,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_SPLIT_NOMERGE; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3919,7 +3923,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_SPLIT_NOMERGE) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
feaf2a80e78fext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1fee84ea20af1b..91b56de60c9055 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3746,15 +3746,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_SPLIT_NOMERGE; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3930,7 +3934,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_SPLIT_NOMERGE) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
67cdb7bd7442ext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 4507e42869854d..ed63260d792b1f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3735,15 +3735,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3920,7 +3924,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
2920ec61c98bext4: don't set EXT4_GET_BLOCKS_CONVERT when splitting before submitting I/O
1 file changed · +8 −5
fs/ext4/extents.c+8 −5 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 7301cf17269038..bd556a3eac1987 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3762,15 +3762,19 @@ static struct ext4_ext_path *ext4_split_convert_extents(handle_t *handle, /* Convert to unwritten */ if (flags & EXT4_GET_BLOCKS_CONVERT_UNWRITTEN) { split_flag |= EXT4_EXT_DATA_ENTIRE_VALID1; - /* Convert to initialized */ - } else if (flags & EXT4_GET_BLOCKS_CONVERT) { + /* Split the existing unwritten extent */ + } else if (flags & (EXT4_GET_BLOCKS_UNWRIT_EXT | + EXT4_GET_BLOCKS_CONVERT)) { /* * It is safe to convert extent to initialized via explicit * zeroout only if extent is fully inside i_size or new_size. */ split_flag |= ee_block + ee_len <= eof_block ? EXT4_EXT_MAY_ZEROOUT : 0; - split_flag |= (EXT4_EXT_MARK_UNWRIT2 | EXT4_EXT_DATA_VALID2); + split_flag |= EXT4_EXT_MARK_UNWRIT2; + /* Convert to initialized */ + if (flags & EXT4_GET_BLOCKS_CONVERT) + split_flag |= EXT4_EXT_DATA_VALID2; } flags |= EXT4_GET_BLOCKS_PRE_IO; return ext4_split_extent(handle, inode, path, map, split_flag, flags, @@ -3949,7 +3953,7 @@ ext4_ext_handle_unwritten_extents(handle_t *handle, struct inode *inode, /* get_block() before submitting IO, split the extent */ if (flags & EXT4_GET_BLOCKS_PRE_IO) { path = ext4_split_convert_extents(handle, inode, map, path, - flags | EXT4_GET_BLOCKS_CONVERT, allocated); + flags, allocated); if (IS_ERR(path)) return path; /* -- cgit 1.3-korg
Vulnerability mechanics
Root cause
"Incorrect flag propagation causes the extent-split error path to convert the entire on-disk extent to written, exposing stale data when the subsequent I/O fails."
Attack vector
An attacker who can trigger within-EOF direct I/O (DIO) or writeback on an ext4 filesystem mounted with `dioread_nolock` can cause a stale-data exposure. The bug is triggered when a DIO write targets the second half of an existing large unwritten extent. During extent splitting, if `ext4_split_extent_at()` fails with `-ENOSPC` (a temporary space shortage), the error path zeroes the first half but converts the entire on-disk extent to written due to the `EXT4_EXT_DATA_VALID2` flag, while the extent status tree still marks the second half as unwritten. If the DIO write subsequently fails to write data to disk, the stale pre-existing data in the second half becomes readable once the cached extent entry is evicted [patch_id=2660699].
Affected code
The vulnerability resides in `fs/ext4/extents.c` in the functions `ext4_split_convert_extents()` and `ext4_ext_handle_unwritten_extents()`. The call site in `ext4_ext_handle_unwritten_extents()` was passing `flags | EXT4_GET_BLOCKS_CONVERT` to `ext4_split_convert_extents()`, and the latter unconditionally set `EXT4_EXT_DATA_VALID2` in the split flags when `EXT4_GET_BLOCKS_CONVERT` was present [patch_id=2660699].
What the fix does
The patch makes two changes in `fs/ext4/extents.c`. First, in `ext4_ext_handle_unwritten_extents()`, the call to `ext4_split_convert_extents()` no longer ORs `EXT4_GET_BLOCKS_CONVERT` into the flags — it passes the original flags directly [patch_id=2660699]. Second, in `ext4_split_convert_extents()`, the condition for setting `EXT4_EXT_DATA_VALID2` is changed: instead of always setting it when `EXT4_GET_BLOCKS_CONVERT` is present, the code now only sets `EXT4_EXT_DATA_VALID2` if `EXT4_GET_BLOCKS_CONVERT` is explicitly set in the flags (separate from `EXT4_GET_BLOCKS_UNWRIT_EXT`). This ensures that when splitting an unwritten extent before I/O submission (the `PRE_IO` path), the `EXT4_EXT_DATA_VALID2` flag is not set, preventing the error path from incorrectly converting the entire on-disk extent to written and leaving stale data exposed [patch_id=2660699].
Preconditions
- configext4 filesystem must be mounted with the 'dioread_nolock' option
- inputA large unwritten extent must exist on disk
- inputA direct I/O (DIO) write must target the second half of that unwritten extent
- inputA temporary ENOSPC condition must occur during extent splitting
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- git.kernel.org/stable/c/2698731d25823267c29190cb578da9296a0c0d7bnvd
- git.kernel.org/stable/c/2920ec61c98b9476781359f05b94da84e80f54d4nvd
- git.kernel.org/stable/c/37555690f39f78ef69af347d9aff897e07445949nvd
- git.kernel.org/stable/c/67cdb7bd7442bd3cdc6d6088bbb2df9be2fe936cnvd
- git.kernel.org/stable/c/716e7439a5a9b18c3ff882c2f8c834b9ced1aaecnvd
- git.kernel.org/stable/c/77e407967cd872cd75d7e4a691908e49c8e6b4d4nvd
- git.kernel.org/stable/c/feaf2a80e78f89ee8a3464126077ba8683b62791nvd
News mentions
0No linked articles in our index yet.