From 937c262d4f55e472f5bd66cf8a293f20da2616f4 Mon Sep 17 00:00:00 2001 From: Adarsh Das Date: Fri, 6 Feb 2026 18:26:38 +0530 Subject: [PATCH 1/9] fs: udf: avoid assignment in condition when selecting allocation goal Avoid assignment inside an if condition when choosing the block allocation goal in inode_getblk(), and make the priority order explicit. No functional change. [JK: Fixup conditions to really not change functionality] Signed-off-by: Adarsh Das Link: https://patch.msgid.link/20260206125638.94194-1-adarshdas950@gmail.com Signed-off-by: Jan Kara --- fs/udf/inode.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index 7fae8002344a..c766a805f9b9 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -734,7 +734,7 @@ static int inode_getblk(struct inode *inode, struct udf_map_rq *map) sector_t offset = 0; int8_t etype, tmpetype; struct udf_inode_info *iinfo = UDF_I(inode); - udf_pblk_t goal = 0, pgoal = iinfo->i_location.logicalBlockNum; + udf_pblk_t goal = 0, pgoal = 0; int lastblock = 0; bool isBeyondEOF = false; int ret = 0; @@ -893,11 +893,10 @@ static int inode_getblk(struct inode *inode, struct udf_map_rq *map) else { /* otherwise, allocate a new block */ if (iinfo->i_next_alloc_block == map->lblk) goal = iinfo->i_next_alloc_goal; - - if (!goal) { - if (!(goal = pgoal)) /* XXX: what was intended here? */ - goal = iinfo->i_location.logicalBlockNum + 1; - } + if (!goal) + goal = pgoal; + if (!goal) + goal = iinfo->i_location.logicalBlockNum + 1; newblocknum = udf_new_block(inode->i_sb, inode, iinfo->i_location.partitionReferenceNum, From 6d942c874f6fc8cea801981b6f2cfd9829a641d4 Mon Sep 17 00:00:00 2001 From: Milos Nikic Date: Fri, 6 Feb 2026 16:29:08 -0800 Subject: [PATCH 2/9] ext2: remove stale TODO about kmap The TODO comment in the file header asking to get rid of kmap() is outdated. The code has already been converted to use the folio API (specifically kmap_local_folio). Remove the stale comment to reflect the current state of the code. Signed-off-by: Milos Nikic Link: https://patch.msgid.link/20260207002908.176933-1-nikic.milos@gmail.com Signed-off-by: Jan Kara --- fs/ext2/namei.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/fs/ext2/namei.c b/fs/ext2/namei.c index bde617a66cec..3ab23de558fb 100644 --- a/fs/ext2/namei.c +++ b/fs/ext2/namei.c @@ -14,8 +14,6 @@ * * The only non-static object here is ext2_dir_inode_operations. * - * TODO: get rid of kmap() use, add readahead. - * * Copyright (C) 1992, 1993, 1994, 1995 * Remy Card (card@masi.ibp.fr) * Laboratoire MASI - Institut Blaise Pascal From 0cf9c58bf654d0f27abe18005281dbf9890de401 Mon Sep 17 00:00:00 2001 From: Milos Nikic Date: Fri, 6 Feb 2026 17:06:17 -0800 Subject: [PATCH 3/9] ext2: replace BUG_ON with WARN_ON_ONCE in ext2_get_blocks If ext2_get_blocks() is called with maxblocks == 0, it currently triggers a BUG_ON(), causing a kernel panic. While this condition implies a logic error in the caller, a filesystem should not crash the system due to invalid arguments. Replace the BUG_ON() with a WARN_ON_ONCE() to provide a stack trace for debugging, and return -EINVAL to handle the error gracefully. Signed-off-by: Milos Nikic Link: https://patch.msgid.link/20260207010617.216675-1-nikic.milos@gmail.com Signed-off-by: Jan Kara --- fs/ext2/inode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index dbfe9098a124..18bf1a91dbc2 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -638,7 +638,8 @@ static int ext2_get_blocks(struct inode *inode, int count = 0; ext2_fsblk_t first_block = 0; - BUG_ON(maxblocks == 0); + if (WARN_ON_ONCE(maxblocks == 0)) + return -EINVAL; depth = ext2_block_to_path(inode,iblock,offsets,&blocks_to_boundary); From ad0e9663f0f5b0ed8e27d3690c5ac9de72243fba Mon Sep 17 00:00:00 2001 From: Milos Nikic Date: Fri, 6 Feb 2026 18:29:20 -0800 Subject: [PATCH 4/9] ext2: guard reservation window dump with EXT2FS_DEBUG The function __rsv_window_dump() is a heavyweight debug tool that walks the reservation red-black tree. It is currently guarded by #if 1, forcing it to be compiled into all kernels, even production ones. Match the rest of the file by guarding it with #ifdef EXT2FS_DEBUG, so it is only included when explicit debugging is enabled. This removes the unused function code from standard builds. Signed-off-by: Milos Nikic Link: https://patch.msgid.link/20260207022920.258247-1-nikic.milos@gmail.com Signed-off-by: Jan Kara --- fs/ext2/balloc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext2/balloc.c b/fs/ext2/balloc.c index 007eee794bd1..adf0f31fbddd 100644 --- a/fs/ext2/balloc.c +++ b/fs/ext2/balloc.c @@ -201,7 +201,7 @@ static void group_adjust_blocks(struct super_block *sb, int group_no, * windows(start, end). Otherwise, it will only print out the "bad" windows, * those windows that overlap with their immediate neighbors. */ -#if 1 +#ifdef EXT2FS_DEBUG static void __rsv_window_dump(struct rb_root *root, int verbose, const char *fn) { @@ -248,7 +248,7 @@ static void __rsv_window_dump(struct rb_root *root, int verbose, __rsv_window_dump((root), (verbose), __func__) #else #define rsv_window_dump(root, verbose) do {} while (0) -#endif +#endif /* EXT2FS_DEBUG */ /** * goal_in_my_reservation() From 19134a133184fcc49c41cf42797cb2e7fef76065 Mon Sep 17 00:00:00 2001 From: Ziyi Guo Date: Wed, 11 Feb 2026 02:20:52 +0000 Subject: [PATCH 5/9] ext2: avoid drop_nlink() during unlink of zero-nlink inode in ext2_unlink() ext2_unlink() calls inode_dec_link_count() unconditionally, which invokes drop_nlink(). If the inode was loaded from a corrupted disk image with i_links_count == 0, drop_nlink() triggers WARN_ON(inode->i_nlink == 0) Follow the ext4 pattern from __ext4_unlink(): check i_nlink before decrementing. If already zero, skip the decrement. Signed-off-by: Ziyi Guo Link: https://patch.msgid.link/20260211022052.973114-1-n7l8m4@u.northwestern.edu Signed-off-by: Jan Kara --- fs/ext2/namei.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/ext2/namei.c b/fs/ext2/namei.c index 3ab23de558fb..0d09d22fe708 100644 --- a/fs/ext2/namei.c +++ b/fs/ext2/namei.c @@ -291,7 +291,10 @@ static int ext2_unlink(struct inode *dir, struct dentry *dentry) goto out; inode_set_ctime_to_ts(inode, inode_get_ctime(dir)); - inode_dec_link_count(inode); + + if (inode->i_nlink) + inode_dec_link_count(inode); + err = 0; out: return err; From 08841b06fa64d8edbd1a21ca6e613420c90cc4b8 Mon Sep 17 00:00:00 2001 From: Seohyeon Maeng Date: Tue, 10 Mar 2026 17:16:52 +0900 Subject: [PATCH 6/9] udf: fix partition descriptor append bookkeeping Mounting a crafted UDF image with repeated partition descriptors can trigger a heap out-of-bounds write in part_descs_loc[]. handle_partition_descriptor() deduplicates entries by partition number, but appended slots never record partnum. As a result duplicate Partition Descriptors are appended repeatedly and num_part_descs keeps growing. Once the table is full, the growth path still sizes the allocation from partnum even though inserts are indexed by num_part_descs. If partnum is already aligned to PART_DESC_ALLOC_STEP, ALIGN(partnum, step) can keep the old capacity and the next append writes past the end of the table. Store partnum in the appended slot and size growth from the next append count so deduplication and capacity tracking follow the same model. Fixes: ee4af50ca94f ("udf: Fix mounting of Win7 created UDF filesystems") Cc: stable@vger.kernel.org Signed-off-by: Seohyeon Maeng Link: https://patch.msgid.link/20260310081652.21220-1-bioloidgp@gmail.com Signed-off-by: Jan Kara --- fs/udf/super.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/udf/super.c b/fs/udf/super.c index 27f463fd1d89..df2b62eddfc0 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1694,8 +1694,9 @@ static struct udf_vds_record *handle_partition_descriptor( return &(data->part_descs_loc[i].rec); if (data->num_part_descs >= data->size_part_descs) { struct part_desc_seq_scan_data *new_loc; - unsigned int new_size = ALIGN(partnum, PART_DESC_ALLOC_STEP); + unsigned int new_size; + new_size = data->num_part_descs + PART_DESC_ALLOC_STEP; new_loc = kzalloc_objs(*new_loc, new_size); if (!new_loc) return ERR_PTR(-ENOMEM); @@ -1705,6 +1706,7 @@ static struct udf_vds_record *handle_partition_descriptor( data->part_descs_loc = new_loc; data->size_part_descs = new_size; } + data->part_descs_loc[data->num_part_descs].partnum = partnum; return &(data->part_descs_loc[data->num_part_descs++].rec); } From e93ab401da4b2e2c1b8ef2424de2f238d51c8b2d Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Fri, 27 Feb 2026 14:22:16 +0100 Subject: [PATCH 7/9] quota: Fix race of dquot_scan_active() with quota deactivation dquot_scan_active() can race with quota deactivation in quota_release_workfn() like: CPU0 (quota_release_workfn) CPU1 (dquot_scan_active) ============================== ============================== spin_lock(&dq_list_lock); list_replace_init( &releasing_dquots, &rls_head); /* dquot X on rls_head, dq_count == 0, DQ_ACTIVE_B still set */ spin_unlock(&dq_list_lock); synchronize_srcu(&dquot_srcu); spin_lock(&dq_list_lock); list_for_each_entry(dquot, &inuse_list, dq_inuse) { /* finds dquot X */ dquot_active(X) -> true atomic_inc(&X->dq_count); } spin_unlock(&dq_list_lock); spin_lock(&dq_list_lock); dquot = list_first_entry(&rls_head); WARN_ON_ONCE(atomic_read(&dquot->dq_count)); The problem is not only a cosmetic one as under memory pressure the caller of dquot_scan_active() can end up working on freed dquot. Fix the problem by making sure the dquot is removed from releasing list when we acquire a reference to it. Fixes: 869b6ea1609f ("quota: Fix slow quotaoff") Reported-by: Sam Sun Link: https://lore.kernel.org/all/CAEkJfYPTt3uP1vAYnQ5V2ZWn5O9PLhhGi5HbOcAzyP9vbXyjeg@mail.gmail.com Signed-off-by: Jan Kara --- fs/quota/dquot.c | 38 ++++++++++++++++++++++++++++++-------- include/linux/quotaops.h | 9 +-------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c index 376739f6420e..64cf42721496 100644 --- a/fs/quota/dquot.c +++ b/fs/quota/dquot.c @@ -363,6 +363,31 @@ static inline int dquot_active(struct dquot *dquot) return test_bit(DQ_ACTIVE_B, &dquot->dq_flags); } +static struct dquot *__dqgrab(struct dquot *dquot) +{ + lockdep_assert_held(&dq_list_lock); + if (!atomic_read(&dquot->dq_count)) + remove_free_dquot(dquot); + atomic_inc(&dquot->dq_count); + return dquot; +} + +/* + * Get reference to dquot when we got pointer to it by some other means. The + * dquot has to be active and the caller has to make sure it cannot get + * deactivated under our hands. + */ +struct dquot *dqgrab(struct dquot *dquot) +{ + spin_lock(&dq_list_lock); + WARN_ON_ONCE(!dquot_active(dquot)); + dquot = __dqgrab(dquot); + spin_unlock(&dq_list_lock); + + return dquot; +} +EXPORT_SYMBOL_GPL(dqgrab); + static inline int dquot_dirty(struct dquot *dquot) { return test_bit(DQ_MOD_B, &dquot->dq_flags); @@ -641,15 +666,14 @@ int dquot_scan_active(struct super_block *sb, continue; if (dquot->dq_sb != sb) continue; - /* Now we have active dquot so we can just increase use count */ - atomic_inc(&dquot->dq_count); + __dqgrab(dquot); spin_unlock(&dq_list_lock); dqput(old_dquot); old_dquot = dquot; /* * ->release_dquot() can be racing with us. Our reference - * protects us from new calls to it so just wait for any - * outstanding call and recheck the DQ_ACTIVE_B after that. + * protects us from dquot_release() proceeding so just wait for + * any outstanding call and recheck the DQ_ACTIVE_B after that. */ wait_on_dquot(dquot); if (dquot_active(dquot)) { @@ -717,7 +741,7 @@ int dquot_writeback_dquots(struct super_block *sb, int type) /* Now we have active dquot from which someone is * holding reference so we can safely just increase * use count */ - dqgrab(dquot); + __dqgrab(dquot); spin_unlock(&dq_list_lock); err = dquot_write_dquot(dquot); if (err && !ret) @@ -963,9 +987,7 @@ struct dquot *dqget(struct super_block *sb, struct kqid qid) spin_unlock(&dq_list_lock); dqstats_inc(DQST_LOOKUPS); } else { - if (!atomic_read(&dquot->dq_count)) - remove_free_dquot(dquot); - atomic_inc(&dquot->dq_count); + __dqgrab(dquot); spin_unlock(&dq_list_lock); dqstats_inc(DQST_CACHE_HITS); dqstats_inc(DQST_LOOKUPS); diff --git a/include/linux/quotaops.h b/include/linux/quotaops.h index c334f82ed385..f9c0f9d7c9d9 100644 --- a/include/linux/quotaops.h +++ b/include/linux/quotaops.h @@ -44,14 +44,7 @@ int dquot_initialize(struct inode *inode); bool dquot_initialize_needed(struct inode *inode); void dquot_drop(struct inode *inode); struct dquot *dqget(struct super_block *sb, struct kqid qid); -static inline struct dquot *dqgrab(struct dquot *dquot) -{ - /* Make sure someone else has active reference to dquot */ - WARN_ON_ONCE(!atomic_read(&dquot->dq_count)); - WARN_ON_ONCE(!test_bit(DQ_ACTIVE_B, &dquot->dq_flags)); - atomic_inc(&dquot->dq_count); - return dquot; -} +struct dquot *dqgrab(struct dquot *dquot); static inline bool dquot_is_busy(struct dquot *dquot) { From 982999538269d5adbd7574098bd12e2c506bacfe Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 5 Apr 2026 16:47:17 +0100 Subject: [PATCH 8/9] ext2: use get_random_u32() where appropriate Use the typed random integer helpers instead of get_random_bytes() when filling a single integer variable. The helpers return the value directly, require no pointer or size argument, and better express intent. Signed-off-by: David Carlier Link: https://patch.msgid.link/20260405154717.4705-1-devnexen@gmail.com Signed-off-by: Jan Kara --- fs/ext2/super.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ext2/super.c b/fs/ext2/super.c index 603f2641fe10..e4136490c883 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -1151,7 +1151,7 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) goto failed_mount2; } sbi->s_gdb_count = db_count; - get_random_bytes(&sbi->s_next_generation, sizeof(u32)); + sbi->s_next_generation = get_random_u32(); spin_lock_init(&sbi->s_next_gen_lock); /* per filesystem reservation list head & lock */ From 25947cc5b2374cd5bf627fe3141496444260d04f Mon Sep 17 00:00:00 2001 From: Vasiliy Kovalev Date: Sat, 4 Apr 2026 18:20:11 +0300 Subject: [PATCH 9/9] 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. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Signed-off-by: Vasiliy Kovalev Link: https://patch.msgid.link/20260404152011.2590197-1-kovalev@altlinux.org Signed-off-by: Jan Kara --- fs/ext2/inode.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index 18bf1a91dbc2..73efd3030743 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);