CVE-2026-46002
Description
In the Linux kernel, the following vulnerability has been resolved:
ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
ext2_iget() already rejects inodes with i_nlink == 0 when i_mode is zero or i_dtime is set, treating them as deleted. However, the case of i_nlink == 0 with a non-zero mode and zero dtime slips through. Since ext2 has no orphan list, such a combination can only result from filesystem corruption - a legitimate inode deletion always sets either i_dtime or clears i_mode before freeing the inode.
A crafted image can exploit this gap to present such an inode to the VFS, which then triggers WARN_ON inside drop_nlink() (fs/inode.c) via ext2_unlink(), ext2_rename() and ext2_rmdir():
WARNING: CPU: 3 PID: 609 at fs/inode.c:336 drop_nlink+0xad/0xd0 fs/inode.c:336 CPU: 3 UID: 0 PID: 609 Comm: syz-executor Not tainted 6.12.77+ #1 Call Trace:
inode_dec_link_count include/linux/fs.h:2518 [inline] ext2_unlink+0x26c/0x300 fs/ext2/namei.c:295 vfs_unlink+0x2fc/0x9b0 fs/namei.c:4477 do_unlinkat+0x53e/0x730 fs/namei.c:4541 __x64_sys_unlink+0xc6/0x110 fs/namei.c:4587 do_syscall_64+0xf5/0x220 arch/x86/entry/common.c:78 entry_SYSCALL_64_after_hwframe+0x77/0x7f
WARNING: CPU: 0 PID: 646 at fs/inode.c:336 drop_nlink+0xad/0xd0 fs/inode.c:336 CPU: 0 UID: 0 PID: 646 Comm: syz.0.17 Not tainted 6.12.77+ #1 Call Trace:
inode_dec_link_count include/linux/fs.h:2518 [inline] ext2_rename+0x35e/0x850 fs/ext2/namei.c:374 vfs_rename+0xf2f/0x2060 fs/namei.c:5021 do_renameat2+0xbe2/0xd50 fs/namei.c:5178 __x64_sys_rename+0x7e/0xa0 fs/namei.c:5223 do_syscall_64+0xf5/0x220 arch/x86/entry/common.c:78 entry_SYSCALL_64_after_hwframe+0x77/0x7f
WARNING: CPU: 0 PID: 634 at fs/inode.c:336 drop_nlink+0xad/0xd0 fs/inode.c:336 CPU: 0 UID: 0 PID: 634 Comm: syz-executor Not tainted 6.12.77+ #1 Call Trace:
inode_dec_link_count include/linux/fs.h:2518 [inline] ext2_rmdir+0xca/0x110 fs/ext2/namei.c:311 vfs_rmdir+0x204/0x690 fs/namei.c:4348 do_rmdir+0x372/0x3e0 fs/namei.c:4407 __x64_sys_unlinkat+0xf0/0x130 fs/namei.c:4577 do_syscall_64+0xf5/0x220 arch/x86/entry/common.c:78 entry_SYSCALL_64_after_hwframe+0x77/0x7f
Extend the existing i_nlink == 0 check to also catch this case, reporting the corruption via ext2_error() and returning -EFSCORRUPTED. This rejects the inode at load time and prevents it from reaching any of the namei.c paths.
Found by Linux Verification Center (linuxtesting.org) with Syzkaller.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In ext2 filesystem driver, inodes with zero link count and valid mode bypass deletion checks, causing kernel warnings and potential denial of service.
Vulnerability
In the Linux kernel's ext2 filesystem driver, the ext2_iget() function fails to reject inodes that have i_nlink == 0 but a non-zero i_mode and zero i_dtime. This combination is indicative of filesystem corruption as legitimate deletions always set either i_dtime or clear i_mode. The bug affects all kernel versions prior to the fix commit.
Exploitation
An attacker must provide a crafted ext2 filesystem image containing such an inode. After mounting the image, performing operations like unlink, rename, or rmdir on the malicious inode triggers a kernel WARN_ON in the drop_nlink() function. No special privileges beyond filesystem access are required.
Impact
The primary impact is a denial of service due to kernel warnings that may cause system instability or crash. The bug does not appear to allow privilege escalation or arbitrary code execution.
Mitigation
The vulnerability is fixed in commit 25947cc5b2374cd5bf627fe3141496444260d04f. Users should update their Linux kernel to a version containing this fix. No workaround is known.
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
102dde6377ab2eext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index e10c376843d773..4aac18942eccd9 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index e10c376843d773..4aac18942eccd9 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
470264bbec49ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index dbfe9098a1245d..39d972722f5f84 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index dbfe9098a1245d..39d972722f5f84 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
25947cc5b237ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 18bf1a91dbc24b..73efd3030743d1 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1431,9 +1431,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 18bf1a91dbc24b..73efd3030743d1 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1431,9 +1431,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
32e0b9255726ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 6ff1f8f29a3c62..6eccee03361716 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1432,9 +1432,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 6ff1f8f29a3c62..6eccee03361716 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1432,9 +1432,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
d3af04a43db8ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 177b1f852b63ac..3fcf8191da42a0 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1429,9 +1429,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 177b1f852b63ac..3fcf8191da42a0 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1429,9 +1429,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
d3af04a43db8ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 177b1f852b63ac..3fcf8191da42a0 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1429,9 +1429,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 177b1f852b63ac..3fcf8191da42a0 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1429,9 +1429,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
2dde6377ab2eext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index e10c376843d773..4aac18942eccd9 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index e10c376843d773..4aac18942eccd9 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
470264bbec49ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index dbfe9098a1245d..39d972722f5f84 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index dbfe9098a1245d..39d972722f5f84 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1430,9 +1430,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
32e0b9255726ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 6ff1f8f29a3c62..6eccee03361716 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1432,9 +1432,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 6ff1f8f29a3c62..6eccee03361716 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1432,9 +1432,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
25947cc5b237ext2: reject inodes with zero i_nlink and valid mode in ext2_iget()
2 files changed · +22 −8
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 18bf1a91dbc24b..73efd3030743d1 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1431,9 +1431,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
fs/ext2/inode.c+11 −4 modifieddiff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 18bf1a91dbc24b..73efd3030743d1 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -1431,9 +1431,17 @@ struct inode *ext2_iget (struct super_block *sb, unsigned long ino) * the test is that same one that e2fsck uses * NeilBrown 1999oct15 */ - if (inode->i_nlink == 0 && (inode->i_mode == 0 || ei->i_dtime)) { - /* this inode is deleted */ - ret = -ESTALE; + if (inode->i_nlink == 0) { + if (inode->i_mode == 0 || ei->i_dtime) { + /* this inode is deleted */ + ret = -ESTALE; + } else { + ext2_error(sb, __func__, + "inode %lu has zero i_nlink with mode 0%o and no dtime, " + "filesystem may be corrupt", + ino, inode->i_mode); + ret = -EFSCORRUPTED; + } goto bad_inode; } inode->i_blocks = le32_to_cpu(raw_inode->i_blocks); -- cgit 1.3-korg
Vulnerability mechanics
Root cause
"Missing validation in ext2_iget() for the case where i_nlink is zero but i_mode is non-zero and i_dtime is zero, allowing a corrupted inode to reach the VFS."
Attack vector
An attacker mounts a crafted ext2 filesystem image containing an inode with i_nlink == 0, a non-zero i_mode, and i_dtime == 0. Because the old check in ext2_iget() only rejected i_nlink==0 when i_mode was also zero or i_dtime was set, this corrupted inode passes validation and is loaded into the VFS. When the attacker then performs an unlink(), rename(), or rmdir() syscall on a directory entry referencing that inode, the kernel calls drop_nlink() which triggers a WARN_ON because the link count is already zero [patch_id=2660536].
Affected code
The vulnerable code is in the ext2_iget() function in fs/ext2/inode.c, specifically the existing i_nlink == 0 validation block [patch_id=2660536]. The patch modifies the same function.
What the fix does
The patch restructures the i_nlink == 0 check in ext2_iget() (fs/ext2/inode.c) to handle three cases instead of two. If i_nlink is zero and either i_mode is zero or i_dtime is set, the inode is treated as legitimately deleted (returns -ESTALE). If i_nlink is zero but i_mode is non-zero and i_dtime is zero, the inode is now recognized as corrupt: the patch calls ext2_error() to log the corruption and returns -EFSCORRUPTED instead of allowing the inode to proceed [patch_id=2660536]. This closes the gap that previously let corrupted inodes reach the VFS paths that trigger the WARN_ON in drop_nlink().
Preconditions
- inputAttacker must be able to mount a crafted ext2 filesystem image (requires local access or a mechanism to supply a malicious image).
- inputThe crafted image must contain an inode with i_nlink == 0, non-zero i_mode, and i_dtime == 0.
- authAttacker must be able to perform unlink(), rename(), or rmdir() syscalls on a directory entry referencing that inode.
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/25947cc5b2374cd5bf627fe3141496444260d04fnvd
- git.kernel.org/stable/c/2dde6377ab2e46bb80cf066c659ef016f3ad7a9bnvd
- git.kernel.org/stable/c/32e0b925572686399243834ec99e2a9d85c62eaenvd
- git.kernel.org/stable/c/470264bbec499e276a89a6431144ae58f411ea4dnvd
- git.kernel.org/stable/c/d3af04a43db86379df7438bf8bade71685b8a239nvd
News mentions
0No linked articles in our index yet.