VYPR
Unrated severityNVD Advisory· Published May 28, 2026

CVE-2026-46194

CVE-2026-46194

Description

In the Linux kernel, the following vulnerability has been resolved:

f2fs: fix node_cnt race between extent node destroy and writeback

f2fs_destroy_extent_node() does not set FI_NO_EXTENT before clearing extent nodes. When called from f2fs_drop_inode() with I_SYNC set, concurrent kworker writeback can insert new extent nodes into the same extent tree, racing with the destroy and triggering f2fs_bug_on() in __destroy_extent_node(). The scenario is as follows:

drop inode writeback - iput - f2fs_drop_inode // I_SYNC set - f2fs_destroy_extent_node - __destroy_extent_node - while (node_cnt) { write_lock(&et->lock) __free_extent_tree write_unlock(&et->lock) - __writeback_single_inode - f2fs_outplace_write_data - f2fs_update_read_extent_cache - __update_extent_tree_range // FI_NO_EXTENT not set, // insert new extent node } // node_cnt == 0, exit while - f2fs_bug_on(node_cnt) // node_cnt > 0

Additionally, __update_extent_tree_range() only checks FI_NO_EXTENT for EX_READ type, leaving EX_BLOCK_AGE updates completely unprotected.

This patch set FI_NO_EXTENT under et->lock in __destroy_extent_node(), consistent with other callers (__update_extent_tree_range and __drop_extent_tree) and check FI_NO_EXTENT for both EX_READ and EX_BLOCK_AGE tree.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A race condition in Linux kernel f2fs between extent node destroy and writeback can trigger a kernel BUG, fixed by setting FI_NO_EXTENT under lock.

Vulnerability

In the Linux kernel's f2fs filesystem, f2fs_destroy_extent_node() does not set FI_NO_EXTENT before clearing extent nodes. When called from f2fs_drop_inode() with I_SYNC set, a concurrent kworker writeback can insert new extent nodes into the same extent tree, racing with the destroy and triggering f2fs_bug_on() in __destroy_extent_node(). Additionally, __update_extent_tree_range() only checks FI_NO_EXTENT for EX_READ type, leaving EX_BLOCK_AGE updates unprotected. Affected versions are Linux kernel versions prior to the fix commit [1].

Exploitation

An attacker needs to trigger a specific sequence: a drop inode operation (e.g., via iput) that calls f2fs_drop_inode with I_SYNC set, while a concurrent writeback (kworker) is running. The race window exists between the destroy loop and the writeback inserting new extent nodes. No special privileges beyond filesystem access are required.

Impact

Successful exploitation causes a kernel BUG (crash) due to f2fs_bug_on() in __destroy_extent_node(), leading to denial of service. No privilege escalation or data corruption is described in the available references.

Mitigation

The fix is in commit 42dd1c91f993431d0b399502479d00e6ad1bca71 [1], which sets FI_NO_EXTENT under et->lock in __destroy_extent_node() and checks FI_NO_EXTENT for both EX_READ and EX_BLOCK_AGE trees. Users should apply the kernel patch or update to a version containing it. No workaround is mentioned.

AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

10
ab1eaf9d5c99

f2fs: fix node_cnt race between extent node destroy and writeback

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitYongpeng YangFixed in 6.12.88via kernel-cna
1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index e9b60389403fd7..e8119d51fb4b8c 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -86,9 +86,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -601,6 +602,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -636,12 +639,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
b0e4395870eb

f2fs: fix node_cnt race between extent node destroy and writeback

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitYongpeng YangFixed in 6.18.30via kernel-cna
1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
0559a0e962aa

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
ed78aeebef05

f2fs: fix node_cnt race between extent node destroy and writeback

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitYongpeng YangFixed in 7.1-rc1via kernel-cna
1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
42dd1c91f993

f2fs: fix node_cnt race between extent node destroy and writeback

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.gitYongpeng YangFixed in 6.6.140via kernel-cna
1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 847c50e40c22da..09fd0cbd8f7701 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -87,9 +87,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -602,6 +603,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -637,12 +640,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
ab1eaf9d5c99

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index e9b60389403fd7..e8119d51fb4b8c 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -86,9 +86,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -601,6 +602,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -636,12 +639,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
0559a0e962aa

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
b0e4395870eb

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
ed78aeebef05

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 0ed84cc065a7ed..87169fd29d8972 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -119,9 +119,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -644,6 +645,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -688,12 +691,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    
42dd1c91f993

f2fs: fix node_cnt race between extent node destroy and writeback

1 file changed · +10 8
  • fs/f2fs/extent_cache.c+10 8 modified
    diff --git a/fs/f2fs/extent_cache.c b/fs/f2fs/extent_cache.c
    index 847c50e40c22da..09fd0cbd8f7701 100644
    --- a/fs/f2fs/extent_cache.c
    +++ b/fs/f2fs/extent_cache.c
    @@ -87,9 +87,10 @@ static bool __may_extent_tree(struct inode *inode, enum extent_type type)
     	if (!__init_may_extent_tree(inode, type))
     		return false;
     
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT))
    +		return false;
    +
     	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT))
    -			return false;
     		if (is_inode_flag_set(inode, FI_COMPRESSED_FILE) &&
     				 !f2fs_sb_has_readonly(F2FS_I_SB(inode)))
     			return false;
    @@ -602,6 +603,8 @@ static unsigned int __destroy_extent_node(struct inode *inode,
     
     	while (atomic_read(&et->node_cnt)) {
     		write_lock(&et->lock);
    +		if (!is_inode_flag_set(inode, FI_NO_EXTENT))
    +			set_inode_flag(inode, FI_NO_EXTENT);
     		node_cnt += __free_extent_tree(sbi, et, nr_shrink);
     		write_unlock(&et->lock);
     	}
    @@ -637,12 +640,12 @@ static void __update_extent_tree_range(struct inode *inode,
     
     	write_lock(&et->lock);
     
    -	if (type == EX_READ) {
    -		if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    -			write_unlock(&et->lock);
    -			return;
    -		}
    +	if (is_inode_flag_set(inode, FI_NO_EXTENT)) {
    +		write_unlock(&et->lock);
    +		return;
    +	}
     
    +	if (type == EX_READ) {
     		prev = et->largest;
     		dei.len = 0;
     
    -- 
    cgit 1.3-korg
    
    
    

Vulnerability mechanics

Root cause

"Missing FI_NO_EXTENT flag check in __destroy_extent_node() and incomplete FI_NO_EXTENT guard in __update_extent_tree_range() allows a concurrent writeback to insert new extent nodes while the extent tree is being destroyed, causing a node-count mismatch that triggers f2fs_bug_on()."

Attack vector

An attacker who can trigger inode eviction (iput) while concurrent writeback is in progress can exploit a race condition in the f2fs extent cache. When f2fs_drop_inode() is called with I_SYNC set, it invokes f2fs_destroy_extent_node() which clears the extent tree without first setting the FI_NO_EXTENT flag. A concurrent kworker writeback path — __writeback_single_inode → f2fs_outplace_write_data → f2fs_update_read_extent_cache → __update_extent_tree_range — can insert new extent nodes into the same tree during the destroy loop. After the while loop exits (node_cnt appears zero), the newly inserted nodes cause the subsequent f2fs_bug_on(node_cnt) assertion to fail, crashing the kernel. Additionally, the FI_NO_EXTENT check in __update_extent_tree_range() was only applied for EX_READ type, leaving EX_BLOCK_AGE updates unprotected against the same race.

Affected code

The vulnerability is in fs/f2fs/extent_cache.c, specifically in the functions __destroy_extent_node(), __update_extent_tree_range(), and __may_extent_tree(). The destroy function lacked setting FI_NO_EXTENT under the extent tree lock, and the update function only checked FI_NO_EXTENT for EX_READ type, leaving EX_BLOCK_AGE unprotected.

What the fix does

The patch makes three coordinated changes in fs/f2fs/extent_cache.c [patch_id=2897874]. First, in __destroy_extent_node(), the FI_NO_EXTENT flag is now set under the extent tree write lock (et->lock) before freeing nodes, preventing concurrent writeback from inserting new nodes during the destroy loop. Second, in __update_extent_tree_range(), the FI_NO_EXTENT check is moved outside the type == EX_READ conditional so it guards both EX_READ and EX_BLOCK_AGE tree updates. Third, in __may_extent_tree(), the FI_NO_EXTENT check is hoisted before the type-specific checks, ensuring the flag is evaluated for all extent types consistently. These changes close the race window and align the flag usage with other callers like __drop_extent_tree.

Preconditions

  • inputThe attacker must be able to trigger inode eviction (iput) on an f2fs inode while concurrent writeback (kworker) is active on the same inode.
  • configThe inode must have I_SYNC set during f2fs_drop_inode() to reach the vulnerable code path.

Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.