CVE-2026-45892
Description
In the Linux kernel, the following vulnerability has been resolved:
ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
When splitting an unwritten extent in the middle and converting it to initialized in ext4_split_extent() with the EXT4_EXT_MAY_ZEROOUT and EXT4_EXT_DATA_VALID2 flags set, it could leave a stale unwritten extent.
Assume we have an unwritten file and buffered write in the middle of it without dioread_nolock enabled, it will allocate blocks as written extent.
0 A B N [UUUUUUUUUUUU] on-disk extent U: unwritten extent [UUUUUUUUUUUU] extent status tree [--DDDDDDDD--] D: valid data |<- ->| ----> this range needs to be initialized
ext4_split_extent() first try to split this extent at B with EXT4_EXT_DATA_PARTIAL_VALID1 and EXT4_EXT_MAY_ZEROOUT flag set, but ext4_split_extent_at() failed to split this extent due to temporary lack of space. It zeroout B to N and leave the entire extent as unwritten.
0 A B N [UUUUUUUUUUUU] on-disk extent [UUUUUUUUUUUU] extent status tree [--DDDDDDDDZZ] Z: zeroed data
ext4_split_extent() then try to split this extent at A with EXT4_EXT_DATA_VALID2 flag set. This time, it split successfully and leave an written extent from A to N.
0 A B N [UUWWWWWWWWWW] on-disk extent W: written extent [UUUUUUUUUUUU] extent status tree [--DDDDDDDDZZ]
Finally ext4_map_create_blocks() only insert extent A to B to the extent status tree, and leave an stale unwritten extent in the status tree.
0 A B N [UUWWWWWWWWWW] on-disk extent W: written extent [UUWWWWWWWWUU] extent status tree [--DDDDDDDDZZ]
Fix this issue by always cached extent status entry after zeroing out the second part.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A stale unwritten extent persists in the Linux ext4 extent status tree after a partial zeroout during split, risking data corruption.
Vulnerability
In the Linux kernel ext4 filesystem, an issue exists in ext4_split_extent() when handling an unwritten extent that is split in the middle with both EXT4_EXT_MAY_ZEROOUT and EXT4_EXT_DATA_VALID2 flags set. An operation that requires converting a range to initialized can fail temporarily during the first split attempt (at offset B) due to insufficient space, causing a zeroout of the second part (B to N) while leaving the on-disk extent still unwritten. A subsequent successful split at offset A creates a written on-disk extent from A to N, but the extent status tree entry is not properly updated for the zeroed range, leaving a stale unwritten entry from B to N. The issue affects ext4 without dioread_nolock enabled. This was introduced and fixed in mainline and various stable kernel versions as noted in the referenced commit [1].
Exploitation
An attacker would need local access to the filesystem (user space) and be able to perform buffered writes that trigger the specific extent split sequence. No special privileges beyond ordinary file write rights are required. The race or timing condition arises from a temporary memory allocation failure during extent splitting. Successful exploitation relies on creating a file with an unwritten extent and then performing a buffered write that overlaps the extent boundaries such that the split logic exercises the partial zeroout path. The concrete sequence involves initializing data in the middle of an unwritten extent, causing ext4_split_extent() to attempt two splits, with the first failing and the second succeeding, but leaving the stale status tree entry.
Impact
This can lead to a mismatch between the on-disk extent state (written) and the in-memory extent status tree (unwritten). Subsequent reads or operations may interpret the range as unwritten and return stale zeroed data instead of the actual written data, potentially causing data corruption or information disclosure. The impact is local, affecting file data integrity for the user or system processes relying on that data.
Mitigation
The fix is included in the Linux kernel commit f0931a5c17005a0c4fc35bd1a001245effc3354b [1], which ensures the extent status cache entry is correctly cached after zeroing out the second part. Users should update to a kernel containing this commit. No workaround other than applying the kernel patch or upgrading to a fixed version is known. The CVE is not listed on CISA's Known Exploited Vulnerabilities (KEV) as of publication.
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
10f0931a5c1700ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index ed63260d792b1f..2818d297ce464f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3302,8 +3302,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index ed63260d792b1f..2818d297ce464f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3302,8 +3302,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
d8ee559fccdeext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fca3eab419aa54..9ffdf30eb4fef8 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3317,8 +3317,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fca3eab419aa54..9ffdf30eb4fef8 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3317,8 +3317,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
c2ee51d684adext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1f057df0be2ed2..37d02e5bbc936a 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1f057df0be2ed2..37d02e5bbc936a 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
a1b962a821e7ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 20513f37c3ef95..eebb22586163f1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 20513f37c3ef95..eebb22586163f1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
6d882ea3b093ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index be9fd2ab86679e..1094e492345132 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index be9fd2ab86679e..1094e492345132 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
f0931a5c1700ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index ed63260d792b1f..2818d297ce464f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3302,8 +3302,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index ed63260d792b1f..2818d297ce464f 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3302,8 +3302,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
d8ee559fccdeext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fca3eab419aa54..9ffdf30eb4fef8 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3317,8 +3317,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index fca3eab419aa54..9ffdf30eb4fef8 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3317,8 +3317,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
6d882ea3b093ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index be9fd2ab86679e..1094e492345132 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index be9fd2ab86679e..1094e492345132 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
a1b962a821e7ext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 20513f37c3ef95..eebb22586163f1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 20513f37c3ef95..eebb22586163f1 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
c2ee51d684adext4: drop extent cache after doing PARTIAL_VALID1 zeroout
2 files changed · +18 −4
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1f057df0be2ed2..37d02e5bbc936a 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
fs/ext4/extents.c+9 −2 modifieddiff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 1f057df0be2ed2..37d02e5bbc936a 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -3319,8 +3319,16 @@ static struct ext4_ext_path *ext4_split_extent_at(handle_t *handle, * extent length and ext4_split_extent() split will the * first half again. */ - if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) + if (split_flag & EXT4_EXT_DATA_PARTIAL_VALID1) { + /* + * Drop extent cache to prevent stale unwritten + * extents remaining after zeroing out. + */ + ext4_es_remove_extent(inode, + le32_to_cpu(zero_ex.ee_block), + ext4_ext_get_actual_len(&zero_ex)); goto fix_extent_len; + } /* update the extent length and mark as initialized */ ex->ee_len = cpu_to_le16(ee_len); -- cgit 1.3-korg
Vulnerability mechanics
Root cause
"Missing extent cache invalidation after a failed split and zeroout in ext4_split_extent_at() leaves a stale unwritten extent in the extent status tree."
Attack vector
An attacker with the ability to perform buffered writes on an ext4 filesystem (without `dioread_nolock` enabled) can trigger this bug. The attacker writes into the middle of an unwritten extent, causing `ext4_split_extent()` to attempt splitting at two boundaries (B then A). If the first split at B fails due to temporary lack of space, the code zeroes out the range B–N but leaves the extent status tree unchanged. The subsequent successful split at A and insertion by `ext4_map_create_blocks()` only updates the cache for A–B, leaving the B–N range as a stale unwritten entry in the extent status tree [patch_id=2661613].
Affected code
The vulnerability resides in `fs/ext4/extents.c` in the function `ext4_split_extent_at()`. When the `EXT4_EXT_DATA_PARTIAL_VALID1` flag is set and the split fails due to temporary lack of space, the code zeroes out the second part of the extent but does not drop the corresponding extent cache entry, leaving a stale unwritten extent in the extent status tree [patch_id=2661613].
What the fix does
The patch adds a call to `ext4_es_remove_extent()` inside the `EXT4_EXT_DATA_PARTIAL_VALID1` branch, immediately after zeroing out the second part of the extent and before jumping to `fix_extent_len` [patch_id=2661613]. This removes the stale unwritten extent entry from the extent status tree, ensuring the in-memory cache matches the on-disk state after the zeroout. The commit message explains that without this fix, the extent status tree retains an unwritten entry for the zeroed range, causing a mismatch with the on-disk written extent [patch_id=2661604].
Preconditions
- configThe filesystem must be ext4 without the dioread_nolock mount option enabled.
- inputThe attacker must be able to perform buffered writes to an unwritten extent on the filesystem.
- inputA temporary lack of space must occur during the first split attempt at boundary B, causing the split to fail.
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- git.kernel.org/stable/c/6d882ea3b0931b43530d44149b79fcd4ffc13030nvd
- git.kernel.org/stable/c/a1b962a821e7a52d48212ae269b45808b4411267nvd
- git.kernel.org/stable/c/c2ee51d684adca7645e4aa74adca13f6750390bcnvd
- git.kernel.org/stable/c/d8ee559fccdef713f058cfe5f2c03dc9b18be3b1nvd
- git.kernel.org/stable/c/f0931a5c17005a0c4fc35bd1a001245effc3354bnvd
News mentions
0No linked articles in our index yet.