From a61fb39759d719a4f4966742b4d28387e33d81c4 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Mon, 31 Aug 2026 11:17:14 +0200 Subject: [PATCH 01/26] udf: Make sure there's enough space for LVID implementation use field udf_lvid_valid() verifies that the LVID has enough space for all the tables and the reported length of ImplementationUse field. However it doesn't make sure the reported length is large enough to contain logicalVolIntegrityDescImpUse structure which OSTA specification requires. This can thus lead to accesses beyond the end of LVID buffer for suitably corrupted UDF filesystem. Add the length check. Reported-by: Yunpeng Tian Reported-by: Gongming Wang Reported-by: Mingda Zhang Reported-by: Qinrun Dai Cc: stable@vger.kernel.org Fixes: 781d2a9a2fc7 ("udf: Check LVID earlier") Signed-off-by: Jan Kara --- fs/udf/super.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/udf/super.c b/fs/udf/super.c index 2ba5973ef4dd..5351755aca3e 100644 --- a/fs/udf/super.c +++ b/fs/udf/super.c @@ -1617,7 +1617,10 @@ static bool udf_lvid_valid(struct super_block *sb, parts = le32_to_cpu(lvid->numOfPartitions); impuselen = le32_to_cpu(lvid->lengthOfImpUse); - if (parts >= sb->s_blocksize || impuselen >= sb->s_blocksize || + if (sizeof(struct logicalVolIntegrityDescImpUse) > impuselen || + impuselen >= sb->s_blocksize) + return false; + if (parts >= sb->s_blocksize || sizeof(struct logicalVolIntegrityDesc) + impuselen + 2 * parts * sizeof(u32) > sb->s_blocksize) return false; From 9a2390959685ac35a58c80c8016120849a2babf6 Mon Sep 17 00:00:00 2001 From: Yunpeng Tian Date: Mon, 31 Aug 2026 17:31:16 +0200 Subject: [PATCH 02/26] udf: validate the device specification EA before using it udf_get_extendedattr() bounds an extended attribute only against struct genericFormat, which is 12 bytes. A type-12 device specification is struct deviceSpec, which is 24 bytes plus a flexible impUse array. Thus a corrupted filesystem can contain extended attribute that is accepted by udf_get_extendedattr() but udf_write_inode() or udf_read_inode() may access beyond the end of provided buffer. Provide a udf_device_spec_valid() check to validate correctness of extended attribute that is a deviceSpec. While here, check the result of udf_add_extendedattr() before dereferencing it as it can return NULL in case of error. [JK: Removed duplicated validity checks] Reported-by: Yunpeng Tian Reported-by: Gongming Wang Reported-by: Mingda Zhang Reported-by: Qinrun Dai Cc: stable@vger.kernel.org Signed-off-by: Yunpeng Tian Signed-off-by: Jan Kara --- fs/udf/inode.c | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index e45e546a739a..baf9845ac06b 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -1336,6 +1336,26 @@ int udf_setsize(struct inode *inode, loff_t newsize) return err; } +/* + * Verify validity of struct deviceSpec on disk. udf_get_extendedattr() has + * already verified the generic header and made sure attribute fits in the + * inode so we just have to make sure attribute space is large enough for + * deviceSpec struct and required impUse information. + */ +static bool udf_device_spec_valid(struct deviceSpec *dsea) +{ + u32 attr_length, imp_use_length; + + attr_length = le32_to_cpu(dsea->attrLength); + imp_use_length = le32_to_cpu(dsea->impUseLength); + if (attr_length < sizeof(struct deviceSpec) || + imp_use_length < sizeof(struct regid) || + imp_use_length > attr_length - sizeof(struct deviceSpec)) + return false; + + return true; +} + /* * Maximum length of linked list formed by ICB hierarchy. The chosen number is * arbitrary - just that we hopefully don't limit any real use of rewritten @@ -1654,13 +1674,12 @@ static int udf_read_inode(struct inode *inode, bool hidden_inode) if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) { struct deviceSpec *dsea = (struct deviceSpec *)udf_get_extendedattr(inode, 12, 1); - if (dsea) { - init_special_inode(inode, inode->i_mode, + + if (!dsea || !udf_device_spec_valid(dsea)) + goto out; + init_special_inode(inode, inode->i_mode, MKDEV(le32_to_cpu(dsea->majorDeviceIdent), le32_to_cpu(dsea->minorDeviceIdent))); - /* Developer ID ??? */ - } else - goto out; } ret = 0; out: @@ -1757,6 +1776,7 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) struct udf_sb_info *sbi = UDF_SB(inode->i_sb); unsigned char blocksize_bits = inode->i_sb->s_blocksize_bits; struct udf_inode_info *iinfo = UDF_I(inode); + int err; bh = sb_getblk(inode->i_sb, udf_get_lb_pblock(inode->i_sb, &iinfo->i_location, 0)); @@ -1821,6 +1841,10 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) udf_add_extendedattr(inode, sizeof(struct deviceSpec) + sizeof(struct regid), 12, 0x3); + if (!dsea) { + err = -ENOSPC; + goto out_unlock; + } dsea->attrType = cpu_to_le32(12); dsea->attrSubtype = 1; dsea->attrLength = cpu_to_le32( @@ -1828,6 +1852,10 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) sizeof(struct regid)); dsea->impUseLength = cpu_to_le32(sizeof(struct regid)); } + if (!udf_device_spec_valid(dsea)) { + err = -EFSCORRUPTED; + goto out_unlock; + } eid = (struct regid *)dsea->impUse; memset(eid, 0, sizeof(*eid)); strcpy(eid->ident, UDF_ID_DEVELOPER); @@ -1962,6 +1990,11 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) set_inode_metadata_writeback(inode); return 0; + +out_unlock: + unlock_buffer(bh); + brelse(bh); + return err; } struct inode *__udf_iget(struct super_block *sb, struct kernel_lb_addr *ino, From 127b0d87649f946198b80f05ae62123dd87bc6f4 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Tue, 1 Sep 2026 10:55:38 +0200 Subject: [PATCH 03/26] udf: Avoid confusion of missing and corrupted extended attributes udf_get_extendedattr() returns NULL either when extended attribute was not found or when some corrupted attribute was found. Similarly udf_add_extendedattr() returns NULL when extended attributes are corrupted or when there's no space to insert the extended attribute. Make both functions return proper error codes to discern these cases and handle them in the callers. Signed-off-by: Jan Kara --- fs/udf/inode.c | 23 ++++++++++++++++------- fs/udf/misc.c | 15 ++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index baf9845ac06b..8babc2fbce9a 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -1675,8 +1675,15 @@ static int udf_read_inode(struct inode *inode, bool hidden_inode) struct deviceSpec *dsea = (struct deviceSpec *)udf_get_extendedattr(inode, 12, 1); - if (!dsea || !udf_device_spec_valid(dsea)) + if (IS_ERR(dsea)) { + ret = PTR_ERR(dsea); goto out; + } + /* Device inodes must have a device spec attribute */ + if (!dsea || !udf_device_spec_valid(dsea)) { + ret = -EFSCORRUPTED; + goto out; + } init_special_inode(inode, inode->i_mode, MKDEV(le32_to_cpu(dsea->majorDeviceIdent), le32_to_cpu(dsea->minorDeviceIdent))); @@ -1836,13 +1843,19 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) struct regid *eid; struct deviceSpec *dsea = (struct deviceSpec *)udf_get_extendedattr(inode, 12, 1); + + /* Validity of extended attrs was checked on load */ + if (WARN_ON_ONCE(IS_ERR(dsea))) { + err = PTR_ERR(dsea); + goto out_unlock; + } if (!dsea) { dsea = (struct deviceSpec *) udf_add_extendedattr(inode, sizeof(struct deviceSpec) + sizeof(struct regid), 12, 0x3); - if (!dsea) { - err = -ENOSPC; + if (IS_ERR(dsea)) { + err = PTR_ERR(dsea); goto out_unlock; } dsea->attrType = cpu_to_le32(12); @@ -1852,10 +1865,6 @@ int udf_write_inode(struct inode *inode, struct writeback_control *wbc) sizeof(struct regid)); dsea->impUseLength = cpu_to_le32(sizeof(struct regid)); } - if (!udf_device_spec_valid(dsea)) { - err = -EFSCORRUPTED; - goto out_unlock; - } eid = (struct regid *)dsea->impUse; memset(eid, 0, sizeof(*eid)); strcpy(eid->ident, UDF_ID_DEVELOPER); diff --git a/fs/udf/misc.c b/fs/udf/misc.c index 6928e378fbbd..a2084dfbfbd6 100644 --- a/fs/udf/misc.c +++ b/fs/udf/misc.c @@ -58,7 +58,7 @@ struct genericFormat *udf_add_extendedattr(struct inode *inode, uint32_t size, cpu_to_le16(TAG_IDENT_EAHD) || le32_to_cpu(eahd->descTag.tagLocation) != iinfo->i_location.logicalBlockNum) - return NULL; + return ERR_PTR(-EFSCORRUPTED); } else { struct udf_sb_info *sbi = UDF_SB(inode->i_sb); @@ -122,7 +122,7 @@ struct genericFormat *udf_add_extendedattr(struct inode *inode, uint32_t size, return (struct genericFormat *)&ea[offset]; } - return NULL; + return ERR_PTR(-ENOSPC); } struct genericFormat *udf_get_extendedattr(struct inode *inode, uint32_t type, @@ -144,7 +144,7 @@ struct genericFormat *udf_get_extendedattr(struct inode *inode, uint32_t type, cpu_to_le16(TAG_IDENT_EAHD) || le32_to_cpu(eahd->descTag.tagLocation) != iinfo->i_location.logicalBlockNum) - return NULL; + return ERR_PTR(-EFSCORRUPTED); if (type < 2048) offset = sizeof(struct extendedAttrHeaderDesc); @@ -153,16 +153,17 @@ struct genericFormat *udf_get_extendedattr(struct inode *inode, uint32_t type, else offset = le32_to_cpu(eahd->appAttrLocation); - while (offset + sizeof(*gaf) < iinfo->i_lenEAttr) { + while (offset < + iinfo->i_lenEAttr - sizeof(struct genericFormat)) { uint32_t attrLength; gaf = (struct genericFormat *)&ea[offset]; attrLength = le32_to_cpu(gaf->attrLength); /* Detect undersized elements and buffer overflows */ - if ((attrLength < sizeof(*gaf)) || - (attrLength > (iinfo->i_lenEAttr - offset))) - break; + if (attrLength < sizeof(struct genericFormat) || + attrLength > iinfo->i_lenEAttr - offset) + return ERR_PTR(-EFSCORRUPTED); if (le32_to_cpu(gaf->attrType) == type && gaf->attrSubtype == subtype) From e76ed86ee88f68f19a3ada75de2192d7fbb3ac99 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:29 -0400 Subject: [PATCH 04/26] ext2: mark s_next_generation as guarded by s_next_gen_lock s_next_generation is only ever modified while holding s_next_gen_lock (in ext2_new_inode()), so annotate it with __guarded_by() for Clang's context analysis. The only other write is the initialisation in ext2_fill_super(), which runs before the superblock is live. No concurrent access should be possible. Convert the spinlock initialization to use scoped_guard(spinlock_init, ...) and place the write under the guard to prevent a warning. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-2-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/ext2.h | 2 +- fs/ext2/super.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 5642451bf191..690452808d24 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -93,7 +93,7 @@ struct ext2_sb_info { int s_inode_size; int s_first_ino; spinlock_t s_next_gen_lock; - u32 s_next_generation; + u32 s_next_generation __guarded_by(&s_next_gen_lock); unsigned long s_dir_count; u8 *s_debts; struct percpu_counter s_freeblocks_counter; diff --git a/fs/ext2/super.c b/fs/ext2/super.c index a40f530872a4..3d64b28745d3 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -1126,8 +1126,9 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) goto failed_mount2; } sbi->s_gdb_count = db_count; - sbi->s_next_generation = get_random_u32(); spin_lock_init(&sbi->s_next_gen_lock); + scoped_guard(spinlock, &sbi->s_next_gen_lock) + sbi->s_next_generation = get_random_u32(); /* per filesystem reservation list head & lock */ spin_lock_init(&sbi->s_rsv_window_lock); From eb34d143711ac8607ab6abdc6f0b7f13fd8f8287 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:30 -0400 Subject: [PATCH 05/26] ext2: annotate ext2_update_dynamic_rev() as requiring s_lock ext2_update_dynamic_rev() modifies several fields within 'struct ext2_super_block' and is already documented as requiring s_lock. Express that with __must_hold() for Clang's context analysis. Both callers ext2_xattr_update_super_block() and __ext2_write_inode() already hold s_lock. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-3-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/ext2.h | 3 ++- fs/ext2/super.c | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 690452808d24..2ace014ed43c 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -761,7 +761,8 @@ extern __printf(3, 4) void ext2_error(struct super_block *, const char *, const char *, ...); extern __printf(3, 4) void ext2_msg(struct super_block *, const char *, const char *, ...); -extern void ext2_update_dynamic_rev (struct super_block *sb); +extern void ext2_update_dynamic_rev(struct super_block *sb) + __must_hold(&EXT2_SB(sb)->s_lock); extern void ext2_sync_super(struct super_block *sb, struct ext2_super_block *es, int wait); diff --git a/fs/ext2/super.c b/fs/ext2/super.c index 3d64b28745d3..27f32637d1b8 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -127,6 +127,7 @@ void ext2_msg(struct super_block *sb, const char *prefix, * This must be called with sbi->s_lock held. */ void ext2_update_dynamic_rev(struct super_block *sb) + __must_hold(&EXT2_SB(sb)->s_lock) { struct ext2_super_block *es = EXT2_SB(sb)->s_es; From 3d0c4287b70c880ab15196dfe4e4f9f9eb1dc9b3 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:31 -0400 Subject: [PATCH 06/26] ext2: mark statfs overhead cache as guarded by s_lock s_overhead_last and s_blocks_last cache the filesystem overhead computation and are only ever accessed in ext2_statfs() under s_lock. Annotate them with __guarded_by() for Clang's context analysis. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-4-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/ext2.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 2ace014ed43c..67cf556aaf44 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -77,8 +77,8 @@ struct ext2_sb_info { unsigned long s_gdb_count; /* Number of group descriptor blocks */ unsigned long s_desc_per_block; /* Number of group descriptors per block */ unsigned long s_groups_count; /* Number of groups in the fs */ - unsigned long s_overhead_last; /* Last calculated overhead */ - unsigned long s_blocks_last; /* Last seen block count */ + unsigned long s_overhead_last __guarded_by(&s_lock); /* Last calculated overhead */ + unsigned long s_blocks_last __guarded_by(&s_lock); /* Last seen block count */ struct buffer_head * s_sbh; /* Buffer containing the super block */ struct ext2_super_block * s_es; /* Pointer to the super block in the buffer */ struct buffer_head ** s_group_desc; From 023051c2d0f1d782da1bd0b09092f31592b46f3a Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:32 -0400 Subject: [PATCH 07/26] ext2: mark s_mount_state as guarded by s_lock s_mount_state tracks the filesystem's mount/error state and is protected by s_lock everywhere it is accessed. Annotate it with __guarded_by() for Clang's context analysis. ext2_setup_super() reads s_mount_state and is called with s_lock held from the remount path, so annotate it with __must_hold(). There are two accesses (the initial read in ext2_fill_super() and the setup_super() call) in the mount flow. This is before the superblock is live, so no concurrent access should be possible. Convert the spinlock initialization to use guard(spinlock_init) to prevent warnings on the writes later in initialization. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-5-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/ext2.h | 2 +- fs/ext2/super.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 67cf556aaf44..800200b342c4 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -86,7 +86,7 @@ struct ext2_sb_info { unsigned long s_sb_block; kuid_t s_resuid; kgid_t s_resgid; - unsigned short s_mount_state; + unsigned short s_mount_state __guarded_by(&s_lock); unsigned short s_pad; int s_addr_per_block_bits; int s_desc_per_block_bits; diff --git a/fs/ext2/super.c b/fs/ext2/super.c index 27f32637d1b8..4d9e5b43f59a 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -634,6 +634,7 @@ static int ext2_parse_param(struct fs_context *fc, struct fs_parameter *param) static int ext2_setup_super (struct super_block * sb, struct ext2_super_block * es, int read_only) + __must_hold(&EXT2_SB(sb)->s_lock) { int res = 0; struct ext2_sb_info *sbi = EXT2_SB(sb); @@ -895,7 +896,7 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) sb->s_fs_info = sbi; sbi->s_sb_block = sb_block; - spin_lock_init(&sbi->s_lock); + guard(spinlock_init)(&EXT2_SB(sb)->s_lock); ret = -EINVAL; /* From 7aa90b3c8ef8f936f7db310bc9ec60eba26cd726 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:33 -0400 Subject: [PATCH 08/26] ext2: annotate ext2_init_block_alloc_info() as requiring truncate_mutex ext2_init_block_alloc_info() sets up the inode's block reservation info and is already documented as needing truncate_mutex protection. Express that with __must_hold() for Clang's context analysis. Both callers (ext2_get_blocks() and ext2_ioctl()) already hold truncate_mutex. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-6-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/balloc.c | 1 + fs/ext2/ext2.h | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/ext2/balloc.c b/fs/ext2/balloc.c index adf0f31fbddd..53c91cb38bde 100644 --- a/fs/ext2/balloc.c +++ b/fs/ext2/balloc.c @@ -414,6 +414,7 @@ static inline int rsv_is_empty(struct ext2_reserve_window *rsv) * Needs truncate_mutex protection prior to calling this function. */ void ext2_init_block_alloc_info(struct inode *inode) + __must_hold(&EXT2_I(inode)->truncate_mutex) { struct ext2_inode_info *ei = EXT2_I(inode); struct ext2_block_alloc_info *block_i; diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 800200b342c4..4b1622eec2af 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -710,7 +710,8 @@ extern struct ext2_group_desc * ext2_get_group_desc(struct super_block * sb, struct buffer_head ** bh); extern void ext2_discard_reservation (struct inode *); extern int ext2_should_retry_alloc(struct super_block *sb, int *retries); -extern void ext2_init_block_alloc_info(struct inode *); +extern void ext2_init_block_alloc_info(struct inode *inode) + __must_hold(&EXT2_I(inode)->truncate_mutex); extern void ext2_rsv_window_add(struct super_block *sb, struct ext2_reserve_window_node *rsv); /* dir.c */ From d8c906454fde090a45edaf136f93aa2af720e5fa Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:34 -0400 Subject: [PATCH 09/26] ext2: annotate block-mapping helpers as requiring truncate_mutex The indirect-block mapping helpers in inode.c all run under the inode's truncate_mutex. ext2_find_goal(), ext2_alloc_blocks(), ext2_alloc_branch() and ext2_splice_branch() are reached only from ext2_get_blocks() while it holds the mutex. ext2_find_shared(), ext2_free_data() and ext2_free_branches() are called from __ext2_truncate_blocks() while it holds the mutex. Add __must_hold() annotations for the Clang's context analysis to reflect this. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-7-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/inode.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/ext2/inode.c b/fs/ext2/inode.c index a9245f0cda4d..1a1ea1fd485b 100644 --- a/fs/ext2/inode.c +++ b/fs/ext2/inode.c @@ -327,6 +327,7 @@ static ext2_fsblk_t ext2_find_near(struct inode *inode, Indirect *ind) static inline ext2_fsblk_t ext2_find_goal(struct inode *inode, long block, Indirect *partial) + __must_hold(&EXT2_I(inode)->truncate_mutex) { struct ext2_block_alloc_info *block_i; @@ -397,6 +398,7 @@ ext2_blks_to_allocate(Indirect * branch, int k, unsigned long blks, static int ext2_alloc_blocks(struct inode *inode, ext2_fsblk_t goal, int indirect_blks, int blks, ext2_fsblk_t new_blocks[4], int *err) + __must_hold(&EXT2_I(inode)->truncate_mutex) { int target, i; unsigned long count = 0; @@ -477,6 +479,7 @@ static int ext2_alloc_blocks(struct inode *inode, static int ext2_alloc_branch(struct inode *inode, int indirect_blks, int *blks, ext2_fsblk_t goal, int *offsets, Indirect *branch) + __must_hold(&EXT2_I(inode)->truncate_mutex) { int blocksize = inode->i_sb->s_blocksize; int i, n = 0; @@ -558,6 +561,7 @@ static int ext2_alloc_branch(struct inode *inode, */ static void ext2_splice_branch(struct inode *inode, long block, Indirect *where, int num, int blks) + __must_hold(&EXT2_I(inode)->truncate_mutex) { int i; struct ext2_block_alloc_info *block_i; @@ -1002,6 +1006,7 @@ static Indirect *ext2_find_shared(struct inode *inode, int offsets[4], Indirect chain[4], __le32 *top) + __must_hold(&EXT2_I(inode)->truncate_mutex) { Indirect *partial, *p; int k, err; @@ -1057,6 +1062,7 @@ static Indirect *ext2_find_shared(struct inode *inode, * appropriately. */ static inline void ext2_free_data(struct inode *inode, __le32 *p, __le32 *q) + __must_hold(&EXT2_I(inode)->truncate_mutex) { ext2_fsblk_t block_to_free = 0, count = 0; ext2_fsblk_t nr; @@ -1097,6 +1103,7 @@ static inline void ext2_free_data(struct inode *inode, __le32 *p, __le32 *q) * appropriately. */ static void ext2_free_branches(struct inode *inode, __le32 *p, __le32 *q, int depth) + __must_hold(&EXT2_I(inode)->truncate_mutex) { struct buffer_head * bh; ext2_fsblk_t nr; From c23746524fafe992e594d84f432534dabf4daf20 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:35 -0400 Subject: [PATCH 10/26] ext2: annotate s_rsv_window_root as requiring s_rsv_window_lock The per-filesystem reservation window rb-tree (s_rsv_window_root) is protected by s_rsv_window_lock. Mark the s_rsv_window_root field with __guarded_by() for Clang's context analysis. The helpers (ext2_rsv_window_add, rsv_window_remove, and find_next_reservable_window) that mutate or walk the tree are all called with the s_rsv_window_lock held. Annotate these helpers with __must_hold(). The accesses in ext2_fill_super() are before the superblock is live. Since no concurrent access should be possible, s_rsv_window_lock is not taken. Convert the spinlock initialization to use scoped_guard(spinlock_init, ...) and place the writes under the guard to prevent warnings. Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-8-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/balloc.c | 3 +++ fs/ext2/ext2.h | 5 +++-- fs/ext2/super.c | 26 ++++++++++++++------------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/fs/ext2/balloc.c b/fs/ext2/balloc.c index 53c91cb38bde..80acc1e19387 100644 --- a/fs/ext2/balloc.c +++ b/fs/ext2/balloc.c @@ -334,6 +334,7 @@ search_reserve_window(struct rb_root *root, ext2_fsblk_t goal) */ void ext2_rsv_window_add(struct super_block *sb, struct ext2_reserve_window_node *rsv) + __must_hold(&EXT2_SB(sb)->s_rsv_window_lock) { struct rb_root *root = &EXT2_SB(sb)->s_rsv_window_root; struct rb_node *node = &rsv->rsv_node; @@ -373,6 +374,7 @@ void ext2_rsv_window_add(struct super_block *sb, */ static void rsv_window_remove(struct super_block *sb, struct ext2_reserve_window_node *rsv) + __must_hold(&EXT2_SB(sb)->s_rsv_window_lock) { rsv->rsv_start = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; rsv->rsv_end = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; @@ -759,6 +761,7 @@ static int find_next_reservable_window( struct super_block * sb, ext2_fsblk_t start_block, ext2_fsblk_t last_block) + __must_hold(&EXT2_SB(sb)->s_rsv_window_lock) { struct rb_node *next; struct ext2_reserve_window_node *rsv, *prev; diff --git a/fs/ext2/ext2.h b/fs/ext2/ext2.h index 4b1622eec2af..7aeb7cfb0ceb 100644 --- a/fs/ext2/ext2.h +++ b/fs/ext2/ext2.h @@ -102,7 +102,7 @@ struct ext2_sb_info { struct blockgroup_lock *s_blockgroup_lock; /* root of the per fs reservation window tree */ spinlock_t s_rsv_window_lock; - struct rb_root s_rsv_window_root; + struct rb_root s_rsv_window_root __guarded_by(&s_rsv_window_lock); struct ext2_reserve_window_node s_rsv_window_head; /* * s_lock protects against concurrent modifications of s_mount_state, @@ -712,7 +712,8 @@ extern void ext2_discard_reservation (struct inode *); extern int ext2_should_retry_alloc(struct super_block *sb, int *retries); extern void ext2_init_block_alloc_info(struct inode *inode) __must_hold(&EXT2_I(inode)->truncate_mutex); -extern void ext2_rsv_window_add(struct super_block *sb, struct ext2_reserve_window_node *rsv); +extern void ext2_rsv_window_add(struct super_block *sb, struct ext2_reserve_window_node *rsv) + __must_hold(&EXT2_SB(sb)->s_rsv_window_lock); /* dir.c */ int ext2_add_link(struct dentry *, struct inode *); diff --git a/fs/ext2/super.c b/fs/ext2/super.c index 4d9e5b43f59a..fb217ef2d69f 100644 --- a/fs/ext2/super.c +++ b/fs/ext2/super.c @@ -1134,18 +1134,20 @@ static int ext2_fill_super(struct super_block *sb, struct fs_context *fc) /* per filesystem reservation list head & lock */ spin_lock_init(&sbi->s_rsv_window_lock); - sbi->s_rsv_window_root = RB_ROOT; - /* - * Add a single, static dummy reservation to the start of the - * reservation window list --- it gives us a placeholder for - * append-at-start-of-list which makes the allocation logic - * _much_ simpler. - */ - sbi->s_rsv_window_head.rsv_start = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; - sbi->s_rsv_window_head.rsv_end = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; - sbi->s_rsv_window_head.rsv_alloc_hit = 0; - sbi->s_rsv_window_head.rsv_goal_size = 0; - ext2_rsv_window_add(sb, &sbi->s_rsv_window_head); + scoped_guard(spinlock, &EXT2_SB(sb)->s_rsv_window_lock) { + sbi->s_rsv_window_root = RB_ROOT; + /* + * Add a single, static dummy reservation to the start of the + * reservation window list --- it gives us a placeholder for + * append-at-start-of-list which makes the allocation logic + * _much_ simpler. + */ + sbi->s_rsv_window_head.rsv_start = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; + sbi->s_rsv_window_head.rsv_end = EXT2_RESERVE_WINDOW_NOT_ALLOCATED; + sbi->s_rsv_window_head.rsv_alloc_hit = 0; + sbi->s_rsv_window_head.rsv_goal_size = 0; + ext2_rsv_window_add(sb, &sbi->s_rsv_window_head); + } err = percpu_counter_init(&sbi->s_freeblocks_counter, ext2_count_free_blocks(sb), GFP_KERNEL); From 9708538ba204d20aa9a5936530862254dcbc6f70 Mon Sep 17 00:00:00 2001 From: Timothy Day Date: Tue, 11 Aug 2026 12:03:36 -0400 Subject: [PATCH 11/26] ext2: enable context analysis support for ext2 filesystem Update ext2 Makefile to support context analysis [1]. [1] https://docs.kernel.org/dev-tools/context-analysis.html Signed-off-by: Timothy Day Acked-by: Marco Elver Link: https://patch.msgid.link/20260811160336.782342-9-timday@thelustrecollective.com Signed-off-by: Jan Kara --- fs/ext2/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/ext2/Makefile b/fs/ext2/Makefile index 8860948ef9ca..33db2e9dc908 100644 --- a/fs/ext2/Makefile +++ b/fs/ext2/Makefile @@ -3,6 +3,8 @@ # Makefile for the linux ext2-filesystem routines. # +CONTEXT_ANALYSIS := y + obj-$(CONFIG_EXT2_FS) += ext2.o ext2-y := balloc.o dir.o file.o ialloc.o inode.o \ From 6a35b82f8ef70e0129344ece5bbe5bca76acd886 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Thu, 3 Sep 2026 11:05:04 +0200 Subject: [PATCH 12/26] ocfs2: Refuse to enable ocfs2 format for another filesystem It is possible to issue Q_QUOTAON quotactl(2) e.g. for ext4 filesystem requesting to enable quota using ocfs2 quota format. With suitable passed arguments this can get upto ocfs2_local_check_quota_file() where the passed superblock is from the ext4 filesystem and that can cause all sorts of interesting things, most likely crashes. As ocfs2 quotas are specific to ocfs2 filesystem, verify we are indeed enabling quotas there. Reviewed-by: Joseph Qi Link: https://patch.msgid.link/20260903090508.1360863-3-jack@suse.cz Signed-off-by: Jan Kara --- fs/ocfs2/quota_local.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/ocfs2/quota_local.c b/fs/ocfs2/quota_local.c index f55810c59b1b..a2b276bcd0a8 100644 --- a/fs/ocfs2/quota_local.c +++ b/fs/ocfs2/quota_local.c @@ -171,6 +171,10 @@ static int ocfs2_local_check_quota_file(struct super_block *sb, int type) struct ocfs2_disk_dqheader *dqhead; int status, ret = 0; + /* OCFS2 quota format is supported only for OCFS2 filesystems */ + if (sb->s_magic != OCFS2_SUPER_MAGIC) + goto out_err; + /* First check whether we understand local quota file */ status = ocfs2_read_quota_block(linode, 0, &bh); if (status) { From 9e1483cf2d712012f9f35a13e51dbc58b6c90606 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Thu, 3 Sep 2026 11:05:05 +0200 Subject: [PATCH 13/26] shmem: Refuse to enable tmpfs format for another filesystem It is possible to issue Q_QUOTAON quotactl(2) e.g. for ext4 filesystem requesting to enable quota using tmpfs quota format. With suitable passed arguments this can actually succeed in enabling tmpfs quota format on top of ext4 filesystem which leads to crashes later in dquot_acquire(). As tmpfs quota format is intended for tmpfs filesystems only, verify we are indeed enabling quotas there. Reported-by: Farhad Alemi Acked-by: Hugh Dickins Link: https://patch.msgid.link/20260903090508.1360863-4-jack@suse.cz Signed-off-by: Jan Kara --- mm/shmem_quota.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mm/shmem_quota.c b/mm/shmem_quota.c index d0b92d6da50f..a3fa4bd93e75 100644 --- a/mm/shmem_quota.c +++ b/mm/shmem_quota.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,10 @@ struct quota_id { static int shmem_check_quota_file(struct super_block *sb, int type) { + /* Verify enabling happens on tmpfs superblock */ + if (sb->s_magic != TMPFS_MAGIC) + return 0; + /* There is no real quota file, nothing to do */ return 1; } From 43de050fa4a31f7e7a73cf8974dcf6037e161a39 Mon Sep 17 00:00:00 2001 From: Baolin Liu Date: Mon, 7 Sep 2026 16:26:02 +0800 Subject: [PATCH 14/26] isofs: fix Rock Ridge CE extent validation on multisession media Commit a36d990f5913 ("isofs: validate Rock Ridge CE continuation extent against volume size") compares the CE extent directly with s_nzones. The extent is an absolute block number, while s_nzones is the number of blocks relative to the selected ISO session. The comparison is therefore wrong when a multisession disc starts at a non-zero block. isofs_get_last_session() selects the last session on multisession media, and its volume descriptors describe a volume beginning at that session's LBA. For example, a session beginning at LBA 45447 with 64 blocks can contain a valid CE at absolute LBA 45467. The existing check rejects that CE, so the ER continuation record is not read, Rock Ridge is disabled, and the mount falls back to Joliet names. Save the selected session start in filesystem-block units and validate the CE extent against the half-open interval [session_start, session_end). Scale the session length to the same block units and retain a separate block-device limit. The lower bound is intentional: accepting arbitrary blocks before the selected session could make a CE read data from a previous session or another filesystem on the device. Only apply the bounds check when cont_extent is non-zero. A zero extent is the in-memory sentinel indicating that no CE continuation was found, rather than a request to read block zero. For a single-session image, session_start is zero and the effective volume boundary remains unchanged. Build-tested with: make CONFIG_RUST= CONFIG_RUST_DRIVERS= fs/isofs/ Fixes: a36d990f5913 ("isofs: validate Rock Ridge CE continuation extent against volume size") Cc: stable@vger.kernel.org Signed-off-by: Baolin Liu Link: https://patch.msgid.link/20260907082602.3777551-1-liubaolin12138@163.com Signed-off-by: Jan Kara --- fs/isofs/inode.c | 2 ++ fs/isofs/isofs.h | 2 ++ fs/isofs/rock.c | 11 +++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c index 337836a0a170..b766b5c9c593 100644 --- a/fs/isofs/inode.c +++ b/fs/isofs/inode.c @@ -821,6 +821,8 @@ static int isofs_fill_super(struct super_block *s, struct fs_context *fc) if (!sb_set_blocksize(s, orig_zonesize)) goto out_freesbi; + sbi->s_session_start = (sector_t)vol_desc_start << + (ISOFS_BLOCK_BITS - s->s_blocksize_bits); sbi->s_nls_iocharset = NULL; #ifdef CONFIG_JOLIET diff --git a/fs/isofs/isofs.h b/fs/isofs/isofs.h index dacb9cdae4fd..79ca0256843a 100644 --- a/fs/isofs/isofs.h +++ b/fs/isofs/isofs.h @@ -35,6 +35,8 @@ struct isofs_sb_info { unsigned long s_firstdatazone; unsigned long s_log_zone_size; unsigned long s_max_size; + /* Session start in filesystem block units. */ + sector_t s_session_start; int s_rock_offset; /* offset of SUSP fields within SU area */ s32 s_sbsector; diff --git a/fs/isofs/rock.c b/fs/isofs/rock.c index 2628f31bd3a5..84e0d764c210 100644 --- a/fs/isofs/rock.c +++ b/fs/isofs/rock.c @@ -9,6 +9,7 @@ #include #include +#include #include "isofs.h" #include "rock.h" @@ -84,6 +85,10 @@ static void init_rock_state(struct rock_state *rs, struct inode *inode) */ static int rock_continue(struct rock_state *rs) { + struct super_block *sb = rs->inode->i_sb; + struct isofs_sb_info *sbi = ISOFS_SB(sb); + sector_t session_end = sbi->s_session_start + + ((sector_t)sbi->s_nzones << (ISOFS_BLOCK_BITS - sb->s_blocksize_bits)); int ret = 1; int blocksize = 1 << rs->inode->i_blkbits; const int min_de_size = offsetof(struct rock_ridge, u); @@ -101,11 +106,13 @@ static int rock_continue(struct rock_state *rs) goto out; } - if ((unsigned)rs->cont_extent >= ISOFS_SB(rs->inode->i_sb)->s_nzones) { + if (rs->cont_extent && + (rs->cont_extent < sbi->s_session_start || + rs->cont_extent >= session_end)) { printk(KERN_NOTICE "rock: corrupted directory entry. " "extent=%u out of volume (nzones=%lu)\n", (unsigned)rs->cont_extent, - ISOFS_SB(rs->inode->i_sb)->s_nzones); + sbi->s_nzones); ret = -EIO; goto out; } From 12255a04ec331a9da0bf049ed48c92af57dfafae Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 11:18:38 -0400 Subject: [PATCH 15/26] isofs: convert the zisofs read path to use folios Store folios in the array that zisofs_read_folio() passes to zisofs_fill_pages() and zisofs_uncompress_block(), use the folio APIs on them, and grab the folios with __filemap_get_folio() instead of grab_cache_page_nowait(). This removes six compound_head() calls, from page_offset(), the three SetPageUptodate() calls, unlock_page() and put_page(), and the last struct page usage in isofs. isofs only uses order-0 folios, so folio_size() is PAGE_SIZE and the logic is unchanged. grab_cache_page_nowait() passed FGP_NOFS as well, but FGP_NOWAIT already prevents reclaim entirely, so leave it out, as FGP_NOFS is on its way out [1]. Change poffset to be unsigned int rather than just unsigned while at it. [1] https://lore.kernel.org/linux-mm/20260830041901.2668-9-willy@infradead.org/ Signed-off-by: Tal Zussman Link: https://patch.msgid.link/20260907-isofs-zisofs-folio-v2-1-c2e430abfcbc@columbia.edu Signed-off-by: Jan Kara --- fs/isofs/compress.c | 77 +++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/fs/isofs/compress.c b/fs/isofs/compress.c index f9869d62b850..2d23abaeb874 100644 --- a/fs/isofs/compress.c +++ b/fs/isofs/compress.c @@ -39,7 +39,7 @@ static DEFINE_MUTEX(zisofs_zlib_lock); */ static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, loff_t block_end, int pcount, - struct page **pages, unsigned poffset, + struct folio **folios, unsigned int poffset, int *errp) { unsigned int zisofs_block_shift = ISOFS_I(inode)->i_format_parm[1]; @@ -66,11 +66,12 @@ static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, if (block_size == 0) { for ( i = 0 ; i < pcount ; i++ ) { unsigned int off = i ? 0 : poffset; + struct folio *folio = folios[i]; - if (!pages[i]) + if (!folio) continue; - memzero_page(pages[i], off, PAGE_SIZE - off); - SetPageUptodate(pages[i]); + folio_zero_range(folio, off, folio_size(folio) - off); + folio_mark_uptodate(folio); } return (((loff_t)pcount) << PAGE_SHIFT) - poffset; } @@ -119,11 +120,12 @@ static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, while (curpage < pcount && curbh < haveblocks && zerr != Z_STREAM_END) { + struct folio *folio = folios[curpage]; + if (!stream.avail_out) { - if (pages[curpage]) { - stream.next_out = kmap_local_page(pages[curpage]) - + poffset; - stream.avail_out = PAGE_SIZE - poffset; + if (folio) { + stream.next_out = kmap_local_folio(folio, poffset); + stream.avail_out = folio_size(folio) - poffset; poffset = 0; } else { stream.next_out = (void *)&zisofs_sink_page; @@ -173,9 +175,9 @@ static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, if (!stream.avail_out) { /* This page completed */ - if (pages[curpage]) { - flush_dcache_page(pages[curpage]); - SetPageUptodate(pages[curpage]); + if (folio) { + flush_dcache_folio(folio); + folio_mark_uptodate(folio); } if (stream.next_out != (unsigned char *)zisofs_sink_page) { kunmap_local(stream.next_out); @@ -206,7 +208,7 @@ static loff_t zisofs_uncompress_block(struct inode *inode, loff_t block_start, * fills in other pages if we have data for them. */ static int zisofs_fill_pages(struct inode *inode, int full_page, int pcount, - struct page **pages) + struct folio **folios) { loff_t start_off, end_off; loff_t block_start, block_end; @@ -221,14 +223,14 @@ static int zisofs_fill_pages(struct inode *inode, int full_page, int pcount, int err; loff_t ret; - BUG_ON(!pages[full_page]); + BUG_ON(!folios[full_page]); /* * We want to read at least 'full_page' page. Because we have to * uncompress the whole compression block anyway, fill the surrounding * pages with the data we have anyway... */ - start_off = page_offset(pages[full_page]); + start_off = folio_pos(folios[full_page]); end_off = min_t(loff_t, start_off + PAGE_SIZE, inode->i_size); cstart_block = start_off >> zisofs_block_shift; @@ -267,9 +269,9 @@ static int zisofs_fill_pages(struct inode *inode, int full_page, int pcount, } err = 0; ret = zisofs_uncompress_block(inode, block_start, block_end, - pcount, pages, poffset, &err); + pcount, folios, poffset, &err); poffset += ret; - pages += poffset >> PAGE_SHIFT; + folios += poffset >> PAGE_SHIFT; pcount -= poffset >> PAGE_SHIFT; full_page -= poffset >> PAGE_SHIFT; poffset &= ~PAGE_MASK; @@ -289,9 +291,11 @@ static int zisofs_fill_pages(struct inode *inode, int full_page, int pcount, cstart_block++; } - if (poffset && *pages) { - memzero_page(*pages, poffset, PAGE_SIZE - poffset); - SetPageUptodate(*pages); + if (poffset && *folios) { + struct folio *folio = *folios; + + folio_zero_range(folio, poffset, folio_size(folio) - poffset); + folio_mark_uptodate(folio); } brelse(bh); return 0; @@ -312,7 +316,7 @@ static int zisofs_read_folio(struct file *file, struct folio *folio) unsigned int zisofs_pages_per_cblock = PAGE_SHIFT <= zisofs_block_shift ? (1 << (zisofs_block_shift - PAGE_SHIFT)) : 0; - struct page **pages; + struct folio **folios; pgoff_t index = folio->index, end_index; end_index = (inode->i_size + PAGE_SIZE - 1) >> PAGE_SHIFT; @@ -336,33 +340,38 @@ static int zisofs_read_folio(struct file *file, struct folio *folio) full_page = 0; pcount = 1; } - pages = kzalloc_objs(*pages, - max_t(unsigned int, zisofs_pages_per_cblock, 1)); - if (!pages) { + folios = kzalloc_objs(*folios, + max_t(unsigned int, zisofs_pages_per_cblock, 1)); + if (!folios) { folio_unlock(folio); return -ENOMEM; } - pages[full_page] = &folio->page; + folios[full_page] = folio; for (i = 0; i < pcount; i++, index++) { - if (i != full_page) - pages[i] = grab_cache_page_nowait(mapping, index); + if (i == full_page) + continue; + folios[i] = __filemap_get_folio(mapping, index, + FGP_LOCK | FGP_CREAT | FGP_NOWAIT, + mapping_gfp_mask(mapping)); + if (IS_ERR(folios[i])) + folios[i] = NULL; } - err = zisofs_fill_pages(inode, full_page, pcount, pages); + err = zisofs_fill_pages(inode, full_page, pcount, folios); - /* Release any residual pages, do not SetPageUptodate */ + /* Release any residual folios, do not mark them uptodate */ for (i = 0; i < pcount; i++) { - if (pages[i]) { - flush_dcache_page(pages[i]); - unlock_page(pages[i]); + if (folios[i]) { + flush_dcache_folio(folios[i]); + folio_unlock(folios[i]); if (i != full_page) - put_page(pages[i]); + folio_put(folios[i]); } - } + } /* At this point, err contains 0 or -EIO depending on the "critical" page */ - kfree(pages); + kfree(folios); return err; } From 0bb666d5f5a2339a5692afb312c2161df9620503 Mon Sep 17 00:00:00 2001 From: Marco Elver Date: Thu, 3 Sep 2026 10:16:38 +0000 Subject: [PATCH 16/26] once_lite: Simplify condition handling and fix context analysis When WARN_ON_ONCE() wraps a conditional lock acquisition (such as down_write_trylock()) on architectures relying on DO_ONCE_LITE_IF() (e.g. arm), Clang's context analysis (Thread Safety Analysis) failed with a false positive: fs/ext2/xattr.c:825:6: error: rw_semaphore 'EXT2_I().xattr_sem' is not held on every path through here [-Werror,-Wthread-safety-analysis] 825 | if (WARN_ON_ONCE(!down_write_trylock(&EXT2_I(inode)->xattr_sem))) | ^ This happens because DO_ONCE_LITE_IF() branches on __ONCE_LITE_IF()'s return value (__ret_once), creating an intermediate branch merge point where the lock may or may not be held depending on whether the once-flag (__already_done) was already set. Because the merge branch condition is __ret_once rather than the trylock predicate (__ret_do_once), Clang cannot reconcile the lockset at the branch merge points. Fix it by refactoring __ONCE_LITE_IF() into an unconditional __ONCE_LITE() primitive and redefining __ONCE_LITE_IF(condition) as: (unlikely(condition) && __ONCE_LITE()) This simplifies the implementation, short-circuits evaluation so that __ONCE_LITE() is not called when the condition is false, and ensures that DO_ONCE_LITE_IF() only enters __ONCE_LITE() when __ret_do_once is true. Reported-by: Nathan Chancellor Link: https://lore.kernel.org/all/20260903072759.GA1750084@ax162/ Signed-off-by: Marco Elver Tested-by: Nathan Chancellor # build Link: https://patch.msgid.link/20260903101843.3462767-1-elver@google.com Signed-off-by: Nathan Chancellor --- include/linux/once_lite.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/include/linux/once_lite.h b/include/linux/once_lite.h index 236592c4eeb1..5e2b67039aaf 100644 --- a/include/linux/once_lite.h +++ b/include/linux/once_lite.h @@ -10,19 +10,21 @@ #define DO_ONCE_LITE(func, ...) \ DO_ONCE_LITE_IF(true, func, ##__VA_ARGS__) -#define __ONCE_LITE_IF(condition) \ +#define __ONCE_LITE() \ ({ \ static bool __section(".data..once") __already_done; \ - bool __ret_cond = !!(condition); \ bool __ret_once = false; \ \ - if (unlikely(__ret_cond) && unlikely(!__already_done)) {\ + if (unlikely(!__already_done)) { \ __already_done = true; \ __ret_once = true; \ } \ unlikely(__ret_once); \ }) +#define __ONCE_LITE_IF(condition) \ + (unlikely(condition) && __ONCE_LITE()) + #define DO_ONCE_LITE_IF(condition, func, ...) \ ({ \ bool __ret_do_once = !!(condition); \ From 6e69419ca6a4bfc3f3445ab139fd07a0e9cf74be Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Wed, 23 Sep 2026 23:32:20 +0800 Subject: [PATCH 17/26] isofs: validate directory records in isofs_read_level3_size() isofs_read_level3_size() walks the multi-extent directory records of a file and dereferences de->size and de->flags for each one without ever checking the record's length byte. A record with a short length placed near the end of a block makes those fixed-field reads run past the record, and for a record at the end of the last block of a page, past the buffer. readdir, lookup and the NFS get_parent path have validated every record with isofs_dir_record_valid() since commit e2ee4078ec58 ("isofs: validate directory records consistently"). Do the same here, after moving on to the next block when the previous record ended exactly at the end of the block or the rest of the block is zero padding, as commit 3c01d9263683 ("isofs: Fix handling of directories with tight blocks") does for the other two walkers. The check rejects a record that would run past its block, so the code that reassembled such a record from two blocks can no longer run. ECMA-119 does not allow directory records to straddle sector boundaries, which is what commit b2eb2e288604 ("isofs: Drop support of directory entries straddling blocks") relied on when it removed the same code from readdir and lookup. Remove it here too, along with its temporary record buffer. Found by fuzzing fs/isofs in a userspace harness with ASan (heap-buffer-overflow reads in de->flags). Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/20260923153220.777907-1-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/inode.c | 44 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c index b766b5c9c593..184350d2e6ad 100644 --- a/fs/isofs/inode.c +++ b/fs/isofs/inode.c @@ -1176,7 +1176,6 @@ static int isofs_read_level3_size(struct inode *inode) unsigned long block, offset, block_saved, offset_saved; int i = 0; int more_entries = 0; - struct iso_directory_record *tmpde = NULL; struct iso_inode_info *ei = ISOFS_I(inode); inode->i_size = 0; @@ -1200,9 +1199,12 @@ static int isofs_read_level3_size(struct inode *inode) goto out_noread; } de = (struct iso_directory_record *) (bh->b_data + offset); - de_len = *(unsigned char *) de; - if (de_len == 0) { + /* + * If we are at the end of a block (or at its zero-padded + * tail), move on to the next block. + */ + if (offset >= bufsize || de->length[0] == 0) { brelse(bh); bh = NULL; ++block; @@ -1210,32 +1212,18 @@ static int isofs_read_level3_size(struct inode *inode) continue; } + if (!isofs_dir_record_valid(de, offset, bufsize)) { + printk(KERN_NOTICE "iso9660: Corrupted directory entry in block %lu of inode %llu\n", + block, inode->i_ino); + brelse(bh); + return -EIO; + } + + de_len = de->length[0]; block_saved = block; offset_saved = offset; offset += de_len; - /* Make sure we have a full directory entry */ - if (offset >= bufsize) { - int slop = bufsize - offset + de_len; - if (!tmpde) { - tmpde = kmalloc(256, GFP_KERNEL); - if (!tmpde) - goto out_nomem; - } - memcpy(tmpde, de, slop); - offset &= bufsize - 1; - block++; - brelse(bh); - bh = NULL; - if (offset) { - bh = sb_bread(inode->i_sb, block); - if (!bh) - goto out_noread; - memcpy((void *)tmpde+slop, bh->b_data, offset); - } - de = tmpde; - } - inode->i_size += isonum_733(de->size); if (i == 1) { ei->i_next_section_block = block_saved; @@ -1249,17 +1237,11 @@ static int isofs_read_level3_size(struct inode *inode) goto out_toomany; } while (more_entries); out: - kfree(tmpde); brelse(bh); return 0; -out_nomem: - brelse(bh); - return -ENOMEM; - out_noread: printk(KERN_INFO "ISOFS: unable to read i-node block %lu\n", block); - kfree(tmpde); return -EIO; out_toomany: From 1e2ef9f221e142ed19d874a71b2cae7fabbaf138 Mon Sep 17 00:00:00 2001 From: Jiale Yao Date: Thu, 24 Sep 2026 18:51:23 +0800 Subject: [PATCH 18/26] ext2: drain in-flight DIO before buffered write fallback An asynchronous direct write can remain in flight after the inode lock is released. If another direct write falls back to buffered I/O while the first write is still pending, generic_perform_write() can dirty pages before the first write completes its post-I/O page cache invalidation. The invalidation then finds dirty pages, reports a page cache invalidation failure, and records -EIO in the mapping error sequence. A later fsync() therefore returns -EIO. Commit 15cdefd0c0522f9d5e12d947fa04f4c11649b699 ("ext4: drain in-flight DIO before buffered write fallback") fixed the same race in ext4. Ext2 has an equivalent fallback after iomap_dio_rw() returns -ENOTBLK or a short write, but does not drain other in-flight DIO before dirtying the page cache. Wait for in-flight DIO before calling generic_perform_write() in the fallback path. A reproducer using concurrent AIO direct writes and buffered fallback triggered the following warning and made a subsequent fsync() return -EIO: Page cache invalidation failure on direct I/O. Possible data corruption due to collision with buffered I/O! Fixes: fb5de4358e1a ("ext2: Move direct-io to use iomap") Link: https://lore.kernel.org/r/20260629113827.4074335-3-libaokun@linux.alibaba.com Signed-off-by: Jiale Yao Link: https://patch.msgid.link/20260924105125.577714-2-yaojiale02@163.com Signed-off-by: Jan Kara --- fs/ext2/file.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/ext2/file.c b/fs/ext2/file.c index b9020df7d89e..67fe423c3828 100644 --- a/fs/ext2/file.c +++ b/fs/ext2/file.c @@ -135,6 +135,13 @@ static ssize_t ext2_dio_write_iter(struct kiocb *iocb, struct iov_iter *from) int ret2; iocb->ki_flags &= ~IOCB_DIRECT; + + /* + * Prevent concurrent direct I/O and buffered I/O to the same file + * range. Wait for in-flight DIO to finish before dirtying pages. + */ + inode_dio_wait(inode); + pos = iocb->ki_pos; status = generic_perform_write(iocb, from); if (unlikely(status < 0)) { From 8b1534b6c47b6f85d3eaa1f169d2e298db19de24 Mon Sep 17 00:00:00 2001 From: Qiliang Yuan Date: Mon, 28 Sep 2026 20:54:40 +0800 Subject: [PATCH 19/26] quota: fix shrinker scan budget accounting in dqcache_shrink_scan() The dquot cache shrinker's count_objects() callback, dqcache_shrink_count(), reports the number of unused dquot objects via the DQST_FREE_DQUOTS percpu counter (through vfs_pressure_ratio()). do_shrink_slab() reads this count exactly once per invocation and derives a one-shot scan budget (total_scan) from it, then repeatedly calls scan_objects() in fixed-size batches until that budget is exhausted. dqcache_shrink_scan() never updates sc->nr_scanned, even though include/linux/shrinker.h documents that "the callee should track its actual progress" in that field. The function already decrements sc->nr_to_scan in place as it walks free_dquots, one per dquot destroyed, and its while loop exits immediately without touching sc->nr_to_scan at all when free_dquots is already empty. Because sc->nr_scanned defaults to sc->nr_to_scan before every call, do_shrink_slab() always believes a full batch was examined regardless of what actually happened, so it has no way to tell "there was nothing to do this pass" from "a full batch was scanned and none of it was freeable", and keeps calling scan_objects() until the original, one-shot total_scan budget computed from the percpu count is drained, even though free_dquots emptied out long before that budget was used up. Compute sc->nr_scanned from the difference between the nr_to_scan value on entry and the value left behind by the reclaim loop, and return SHRINK_STOP once that difference comes back as zero, since that only happens when free_dquots was already empty and no further calls in this reclaim pass can make progress. Tested on a loop-mounted ext4 image with the quota feature enabled: chown()ing several hundred files to distinct uid:gid pairs to populate allocated dquots, then triggering "echo 2 > /proc/sys/vm/drop_caches" to let them drop to zero references and become reclaimable, while using an ad hoc kprobe pair on dqcache_shrink_scan() (this shrinker has no dedicated tracepoints) to record sc->nr_to_scan on entry and the return value on exit: total scan_objects() calls with calls freed == 0 before this patch 12 5 (42%) after this patch 5 0 The last call in the "after" run returns SHRINK_STOP (0xffffffffffffffff) instead of a fifth zero-progress attempt. This is the same missing sc->nr_scanned accounting already fixed in the analogous ext4 extents_status shrinker and the jbd2 checkpoint shrinker; dqcache_shrink_scan() was found by auditing every shrinker in the tree whose count_objects() reads a percpu_counter (the precondition for count_objects() to report a stale, still-positive value after the reclaimable list has actually drained), while ruling out the much larger set of shrinkers built on the exact list_lru atomic counters, which cannot observe this kind of staleness. Signed-off-by: Qiliang Yuan Link: https://patch.msgid.link/20260928-fix-dquot-shrink-scan-nr-scanned-v1-1-789c661c9dd6@gmail.com Signed-off-by: Jan Kara --- fs/quota/dquot.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c index 204afc5e984b..ae631a28d319 100644 --- a/fs/quota/dquot.c +++ b/fs/quota/dquot.c @@ -809,6 +809,7 @@ static unsigned long dqcache_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) { struct dquot *dquot; + unsigned long orig_nr_to_scan = sc->nr_to_scan; unsigned long freed = 0; spin_lock(&dq_list_lock); @@ -822,6 +823,21 @@ dqcache_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) freed++; } spin_unlock(&dq_list_lock); + + sc->nr_scanned = orig_nr_to_scan - sc->nr_to_scan; + + /* + * DQST_FREE_DQUOTS is a percpu counter, so count_objects() can + * report a stale/approximate value that is still positive even + * though free_dquots is actually empty by the time we get here. + * When that happens sc->nr_scanned comes back 0 and we have + * nothing further to offer this reclaim pass, so tell + * do_shrink_slab() to stop calling us instead of letting it burn + * through its one-shot scan budget against an empty list. + */ + if (sc->nr_scanned == 0) + return SHRINK_STOP; + return freed; } From a7378b68f816103c68406e6fde71a1afab7224bd Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 16:35:17 +0800 Subject: [PATCH 20/26] isofs: bound empty directory blocks in isofs_read_level3_size() isofs_read_level3_size() walks the sections of a level-3 (multi-extent) directory record and, on a zero length byte, moves on to the next block with no limit. A crafted image can put a long run of empty blocks between two sections of a multi-extent file, and the walk reads every one of them before it gives up, all the way to the end of the device if it has to. Real discs do have trailing empty directory blocks, written by tools such as Easy CD Creator and Nero, but always at the end of the directory, never between two sections of a multi-extent record, so they do not exercise this path [1]. Jan Kara suggested treating an empty block like a section: count it towards the existing 100-section limit rather than adding a separate one [2]. Do that: both the section count and the empty-block count are checked against their combined total, so neither one alone can reach 100 while the other keeps growing. Only a zero length byte at the start of a block counts as an empty block; the same byte later in a block still just ends that block's records, as it does today, and is not counted. Hui Peng's earlier patch for this function added its own, separate limit on the number of empty blocks [3]; this uses the combined limit Jan suggested instead. Suggested-by: Jan Kara Signed-off-by: Matthias Goergens [1] https://lore.kernel.org/all/20260925043804.2091174-1-matthias.goergens@gmail.com/ [2] https://lore.kernel.org/all/cmlro2xzle2aa7ebflvxhbcv74mea7p6qvxhin6egpdvyzyuxh@s45tpgxdal3n/ [3] https://lore.kernel.org/all/20260919222553.3792320-1-benquike@gmail.com/ Link: https://patch.msgid.link/20260926083517.556576-1-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/inode.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c index 184350d2e6ad..e884618c0c53 100644 --- a/fs/isofs/inode.c +++ b/fs/isofs/inode.c @@ -1175,6 +1175,7 @@ static int isofs_read_level3_size(struct inode *inode) struct buffer_head *bh = NULL; unsigned long block, offset, block_saved, offset_saved; int i = 0; + int empty_blocks = 0; int more_entries = 0; struct iso_inode_info *ei = ISOFS_I(inode); @@ -1202,9 +1203,14 @@ static int isofs_read_level3_size(struct inode *inode) /* * If we are at the end of a block (or at its zero-padded - * tail), move on to the next block. + * tail), move on to the next block. A zero length byte at + * the start of a block means the whole block is empty; + * count that towards the same limit as sections below, or a + * chain of empty blocks could be walked without bound. */ if (offset >= bufsize || de->length[0] == 0) { + if (offset == 0 && ++empty_blocks + i > 100) + goto out_toomany; brelse(bh); bh = NULL; ++block; @@ -1233,7 +1239,7 @@ static int isofs_read_level3_size(struct inode *inode) more_entries = de->flags[-high_sierra] & 0x80; i++; - if (i > 100) + if (i + empty_blocks > 100) goto out_toomany; } while (more_entries); out: @@ -1245,7 +1251,7 @@ static int isofs_read_level3_size(struct inode *inode) return -EIO; out_toomany: - printk(KERN_INFO "%s: More than 100 file sections ?!?, aborting...\n" + printk(KERN_INFO "%s: More than 100 file sections/empty blocks ?!?, aborting...\n" "isofs_read_level3_size: inode=%llu\n", __func__, inode->i_ino); goto out; From 595373206d0c0cfc4e65f233e36d7b3e110b0709 Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Wed, 30 Sep 2026 12:28:56 +0200 Subject: [PATCH 21/26] isofs: Simplify isofs_read_level3_size() After recent changes it is possible to further simplify isofs_read_level3_size() by reorganizing the loop a bit and removing empty_blocks, block_saved, offset_saved variables. Reviewed-by: Matthias Goergens Tested-by: Matthias Goergens Signed-off-by: Jan Kara --- fs/isofs/inode.c | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c index e884618c0c53..c8f0f8803a7b 100644 --- a/fs/isofs/inode.c +++ b/fs/isofs/inode.c @@ -1173,9 +1173,8 @@ static int isofs_read_level3_size(struct inode *inode) unsigned long bufsize = ISOFS_BUFFER_SIZE(inode); int high_sierra = ISOFS_SB(inode->i_sb)->s_high_sierra; struct buffer_head *bh = NULL; - unsigned long block, offset, block_saved, offset_saved; + unsigned long block, offset; int i = 0; - int empty_blocks = 0; int more_entries = 0; struct iso_inode_info *ei = ISOFS_I(inode); @@ -1209,7 +1208,7 @@ static int isofs_read_level3_size(struct inode *inode) * chain of empty blocks could be walked without bound. */ if (offset >= bufsize || de->length[0] == 0) { - if (offset == 0 && ++empty_blocks + i > 100) + if (offset == 0 && ++i > 100) goto out_toomany; brelse(bh); bh = NULL; @@ -1225,21 +1224,17 @@ static int isofs_read_level3_size(struct inode *inode) return -EIO; } - de_len = de->length[0]; - block_saved = block; - offset_saved = offset; - offset += de_len; - - inode->i_size += isonum_733(de->size); - if (i == 1) { - ei->i_next_section_block = block_saved; - ei->i_next_section_offset = offset_saved; + /* Save the first continuation directory entry in the inode */ + if (more_entries && !ei->i_next_section_block) { + ei->i_next_section_block = block; + ei->i_next_section_offset = offset; } - + de_len = de->length[0]; + offset += de_len; + inode->i_size += isonum_733(de->size); more_entries = de->flags[-high_sierra] & 0x80; - i++; - if (i + empty_blocks > 100) + if (++i > 100) goto out_toomany; } while (more_entries); out: From b8253bcf92d6365b20eea6465f5a93c9fc4aa029 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 12:29:13 +0800 Subject: [PATCH 22/26] isofs: return long Joliet names whole get_joliet_filename() writes into a buffer allocated by its caller but is not told how big that buffer is. It hardcodes PAGE_SIZE as the output limit for utf16s_to_utf8s() and gives uni16_to_x8() no limit at all. PAGE_SIZE was never the right bound. Before commit b2eb2e288604 ("isofs: Drop support of directory entries straddling blocks") both callers allocated a page, but only its first 1024 bytes were for the name, the rest holding a copy of a directory record that straddled a block. It could not overflow only because a directory record holds at most 222 bytes of name (255 bytes, 33 of them fixed), which no converter expands beyond 1024 bytes. It also keeps the converted length in an unsigned char. 222 bytes of name are 111 UTF-16 units, and the UTF-8 converter, used for iocharset=utf8 and when CONFIG_NLS_DEFAULT is "utf8", needs three bytes for each CJK, Thai or Devanagari character, so such a name can take up to 333 bytes. Past 255 the length wraps. readdir then reports a short name that usually ends in the middle of a UTF-8 sequence, and lookup finds the file only under that name, never under its real one; a name that wraps to exactly 0 bytes is not listed at all. Such names are out of spec, since Joliet allows 64 units, but common tools write them: PowerISO 9.5 and UltraISO 9.76 both keep up to 110 units by default. Windows, for which Joliet was made, returns them whole, including 111-unit names in records that leave out the padding byte. Return them whole here too. Pass the buffer size down and have both converters respect it. utf16s_to_utf8s() stops before a character that does not fit, and uni16_to_x8() now hands uni2char() the space that actually remains and stops on -ENAMETOOLONG, as fs/hfsplus/unicode.c does. Both callers pass JOLIET_NAME_MAX + 1, room for 111 units at three bytes each plus the terminator; no character set needs more than three bytes for a UTF-16 unit, and isofs_dir_record_valid() already rejects a record whose name_len claims more than the record holds, so no name is cut. Make the length an int, which is what both converters return. These names are longer than NAME_MAX. POSIX lets the limit vary by filesystem, reported by pathconf(_PC_NAME_MAX), and the VFS limits a name only by PATH_MAX (verify_dirent_name() in fs/readdir.c). vfat, exfat, hfsplus and ntfs3 already return names of up to 255 UTF-16 units, 765 bytes of UTF-8, and such names break the same things there: copying the file to a filesystem with a 255-byte limit, such as ext4 or tmpfs, fails with ENAMETOOLONG, and cp, tar, rsync and Python's shutil report that file and carry on with the rest, so the copy is incomplete but says so. glibc's readdir() returns the names, but the deprecated readdir_r() skips them and fails with ENAMETOOLONG, and an inotify reader with the buffer size inotify(7) suggests gets EINVAL. In the kernel, fanotify reports events on such a file without its name and warns once in fanotify_info_copy_name(), and a directory with such a name cannot be reconnected from a file handle, because the generic get_name() in fs/exportfs, which isofs uses, skips names longer than NAME_MAX. Cutting at NAME_MAX would avoid these, but would list names that Windows does not show and that can coincide within a directory. Every name of at most 255 bytes is returned exactly as before. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/20260926042916.3277409-2-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/dir.c | 4 +++- fs/isofs/isofs.h | 11 ++++++++++- fs/isofs/joliet.c | 27 ++++++++++++++++++++------- fs/isofs/namei.c | 3 ++- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index c7ca7603e97a..f4243fcbb697 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -199,7 +199,9 @@ static int do_isofs_readdir(struct inode *inode, struct file *file, if (map) { #ifdef CONFIG_JOLIET if (sbi->s_joliet_level) { - len = get_joliet_filename(de, tmpname, inode); + len = get_joliet_filename(de, tmpname, + JOLIET_NAME_MAX + 1, + inode); p = tmpname; } else #endif diff --git a/fs/isofs/isofs.h b/fs/isofs/isofs.h index 79ca0256843a..47c43a3c6a61 100644 --- a/fs/isofs/isofs.h +++ b/fs/isofs/isofs.h @@ -121,7 +121,16 @@ bool isofs_dir_record_valid(struct iso_directory_record *de, unsigned long offset, unsigned long bufsize); -int get_joliet_filename(struct iso_directory_record *, unsigned char *, struct inode *); +/* + * The longest name the Joliet converter returns, in bytes. A directory + * record is at most 255 bytes long, which leaves room for 111 UTF-16 units + * of name, and no character set needs more than three bytes for one unit. + */ +#define JOLIET_NAME_MAX \ + ((255 - sizeof(struct iso_directory_record)) / 2 * 3) + +int get_joliet_filename(struct iso_directory_record *de, unsigned char *outname, + int outsize, struct inode *inode); int get_acorn_filename(struct iso_directory_record *, char *, struct inode *); extern struct dentry *isofs_lookup(struct inode *, struct dentry *, unsigned int flags); diff --git a/fs/isofs/joliet.c b/fs/isofs/joliet.c index c0f04a1e7f69..d37e67c5e36f 100644 --- a/fs/isofs/joliet.c +++ b/fs/isofs/joliet.c @@ -15,19 +15,26 @@ * Convert Unicode 16 to UTF-8 or ASCII. */ static int -uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls) +uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls, + int outsize) { __be16 *ip, ch; - unsigned char *op; + unsigned char *op, *end; ip = uni; op = ascii; + end = ascii + outsize - 1; /* leave room for the terminator */ while ((ch = get_unaligned(ip)) && len) { int llen; - llen = nls->uni2char(be16_to_cpu(ch), op, NLS_MAX_CHARSET_SIZE); + + if (op >= end) + break; + llen = nls->uni2char(be16_to_cpu(ch), op, end - op); if (llen > 0) op += llen; + else if (llen == -ENAMETOOLONG) + break; else *op++ = '?'; ip++; @@ -38,21 +45,27 @@ uni16_to_x8(unsigned char *ascii, __be16 *uni, int len, struct nls_table *nls) return (op - ascii); } +/* + * Convert the Joliet name of @de into @outname, a buffer of @outsize bytes. + * The result is at most @outsize - 1 bytes long; a longer name is cut at a + * character boundary. + */ int -get_joliet_filename(struct iso_directory_record * de, unsigned char *outname, struct inode * inode) +get_joliet_filename(struct iso_directory_record *de, unsigned char *outname, + int outsize, struct inode *inode) { struct nls_table *nls; - unsigned char len = 0; + int len = 0; nls = ISOFS_SB(inode->i_sb)->s_nls_iocharset; if (!nls) { len = utf16s_to_utf8s((const wchar_t *) de->name, de->name_len[0] >> 1, UTF16_BIG_ENDIAN, - outname, PAGE_SIZE); + outname, outsize - 1); } else { len = uni16_to_x8(outname, (__be16 *) de->name, - de->name_len[0] >> 1, nls); + de->name_len[0] >> 1, nls, outsize); } if ((len > 2) && (outname[len-2] == ';') && (outname[len-1] == '1')) len -= 2; diff --git a/fs/isofs/namei.c b/fs/isofs/namei.c index 010682f5901a..ccf6f01cb3bb 100644 --- a/fs/isofs/namei.c +++ b/fs/isofs/namei.c @@ -107,7 +107,8 @@ isofs_find_entry(struct inode *dir, struct dentry *dentry, dpnt = tmpname; #ifdef CONFIG_JOLIET } else if (sbi->s_joliet_level) { - dlen = get_joliet_filename(de, tmpname, dir); + dlen = get_joliet_filename(de, tmpname, + JOLIET_NAME_MAX + 1, dir); dpnt = tmpname; #endif } else if (sbi->s_mapping == 'a') { From 1885d252b3a944997701db0a4df89761741ed43f Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 12:29:14 +0800 Subject: [PATCH 23/26] isofs: report the Joliet name length in statfs isofs_statfs() reports NAME_MAX as the longest name, but a Joliet name can now be up to JOLIET_NAME_MAX bytes long. glibc answers pathconf(_PC_NAME_MAX) from f_namelen, so a program sizing a buffer from it should get room for every name readdir returns. vfat has reported 255 times NLS_MAX_CHARSET_SIZE, 1530, since commit f68e542f3478 ("fat: Fix statfs->f_namelen"), which followed a report of readdir_r() overflowing a buffer sized that way, and exfat reports the same. Windows reports a maximum component length of 110 for CDFS. Report JOLIET_NAME_MAX on Joliet mounts. Rock Ridge and plain ISO 9660 mounts keep NAME_MAX. Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/20260926042916.3277409-3-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/inode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/isofs/inode.c b/fs/isofs/inode.c index c8f0f8803a7b..4824e42995cc 100644 --- a/fs/isofs/inode.c +++ b/fs/isofs/inode.c @@ -1012,7 +1012,8 @@ static int isofs_statfs (struct dentry *dentry, struct kstatfs *buf) buf->f_files = ISOFS_SB(sb)->s_ninodes; buf->f_ffree = 0; buf->f_fsid = u64_to_fsid(id); - buf->f_namelen = NAME_MAX; + buf->f_namelen = ISOFS_SB(sb)->s_joliet_level ? + JOLIET_NAME_MAX : NAME_MAX; return 0; } From 9f3d3b4b5300124416d87e71280a8fea6a60f6c9 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 12:29:15 +0800 Subject: [PATCH 24/26] isofs: pass the name buffer size to get_rock_ridge_filename() get_rock_ridge_filename() assembles a name from one or more NM entries into a buffer allocated by its caller. It is not told how big that buffer is, and instead stops adding NM entries once the name would exceed NAME_MAX, relying on the callers' buffer being larger. Pass the size in and check against it. Both callers pass NAME_MAX + 1, so the behaviour is unchanged: an NM entry that would take the name past 255 bytes is dropped, together with any that follow, and the entries before it are returned. Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/20260926042916.3277409-4-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/dir.c | 3 ++- fs/isofs/isofs.h | 3 ++- fs/isofs/namei.c | 3 ++- fs/isofs/rock.c | 8 ++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index f4243fcbb697..ed8a8dc41fb2 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -190,7 +190,8 @@ static int do_isofs_readdir(struct inode *inode, struct file *file, map = 1; if (sbi->s_rock) { - len = get_rock_ridge_filename(de, tmpname, inode); + len = get_rock_ridge_filename(de, tmpname, NAME_MAX + 1, + inode); if (len != 0) { /* may be -1 */ p = tmpname; map = 0; diff --git a/fs/isofs/isofs.h b/fs/isofs/isofs.h index 47c43a3c6a61..a2d28a23e892 100644 --- a/fs/isofs/isofs.h +++ b/fs/isofs/isofs.h @@ -115,7 +115,8 @@ struct timespec64 iso_date(u8 *p, int flags); struct inode; /* To make gcc happy */ extern int parse_rock_ridge_inode(struct iso_directory_record *, struct inode *, int relocated); -extern int get_rock_ridge_filename(struct iso_directory_record *, char *, struct inode *); +int get_rock_ridge_filename(struct iso_directory_record *de, char *retname, + int retnamesize, struct inode *inode); extern int isofs_name_translate(struct iso_directory_record *, char *, struct inode *); bool isofs_dir_record_valid(struct iso_directory_record *de, unsigned long offset, diff --git a/fs/isofs/namei.c b/fs/isofs/namei.c index ccf6f01cb3bb..7ee1a2e1e38b 100644 --- a/fs/isofs/namei.c +++ b/fs/isofs/namei.c @@ -102,7 +102,8 @@ isofs_find_entry(struct inode *dir, struct dentry *dentry, dpnt = de->name; if (sbi->s_rock && - ((i = get_rock_ridge_filename(de, tmpname, dir)))) { + ((i = get_rock_ridge_filename(de, tmpname, NAME_MAX + 1, + dir)))) { dlen = i; /* possibly -1 */ dpnt = tmpname; #ifdef CONFIG_JOLIET diff --git a/fs/isofs/rock.c b/fs/isofs/rock.c index 84e0d764c210..5a5984b72224 100644 --- a/fs/isofs/rock.c +++ b/fs/isofs/rock.c @@ -209,10 +209,14 @@ static int rock_check_overflow(struct rock_state *rs, int sig) } /* + * Build the Rock Ridge name of @de in @retname, a buffer of @retnamesize + * bytes. From the first NM entry that does not fit along with the + * terminator, the rest of the name is dropped. + * * return length of name field; 0: not found, -1: to be ignored */ int get_rock_ridge_filename(struct iso_directory_record *de, - char *retname, struct inode *inode) + char *retname, int retnamesize, struct inode *inode) { struct rock_state rs; struct rock_ridge *rr; @@ -287,7 +291,7 @@ int get_rock_ridge_filename(struct iso_directory_record *de, break; } len = rr->len - 5; - if (retnamlen + len > NAME_MAX) { + if (retnamlen + len >= retnamesize) { truncate = 1; break; } From e646566b4ad537286ebee65a55de460003a7bf90 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 12:29:16 +0800 Subject: [PATCH 25/26] isofs: shrink the name conversion buffer isofs_readdir() and isofs_lookup() allocate 1024 bytes for the name converters. Now that get_rock_ridge_filename() and get_joliet_filename() are told the buffer size, nothing writes beyond JOLIET_NAME_MAX + 1 bytes: get_joliet_filename() is passed that size, get_rock_ridge_filename() is passed NAME_MAX + 1, isofs_name_translate() copies at most the 222 bytes of name a directory record can hold, and get_acorn_filename() appends at most five bytes to that. Allocate JOLIET_NAME_MAX + 1 bytes, matching what the Joliet callers pass. No functional change. Signed-off-by: Matthias Goergens Link: https://patch.msgid.link/20260926042916.3277409-5-matthias.goergens@gmail.com Signed-off-by: Jan Kara --- fs/isofs/dir.c | 7 ++++++- fs/isofs/namei.c | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fs/isofs/dir.c b/fs/isofs/dir.c index ed8a8dc41fb2..310b74b08b5e 100644 --- a/fs/isofs/dir.c +++ b/fs/isofs/dir.c @@ -240,7 +240,12 @@ static int isofs_readdir(struct file *file, struct dir_context *ctx) char *tmpname; struct inode *inode = file_inode(file); - tmpname = kmalloc(1024, GFP_KERNEL); + /* + * Rockridge can produce names of NAME_MAX size, Acorn extensions at + * most 227 chars. + */ + BUILD_BUG_ON(JOLIET_NAME_MAX < NAME_MAX || JOLIET_NAME_MAX < 227); + tmpname = kmalloc(JOLIET_NAME_MAX + 1, GFP_KERNEL); if (tmpname == NULL) return -ENOMEM; diff --git a/fs/isofs/namei.c b/fs/isofs/namei.c index 7ee1a2e1e38b..19128de0c3a7 100644 --- a/fs/isofs/namei.c +++ b/fs/isofs/namei.c @@ -155,7 +155,7 @@ struct dentry *isofs_lookup(struct inode *dir, struct dentry *dentry, unsigned i struct inode *inode; char *tmpname; - tmpname = kmalloc(1024, GFP_USER); + tmpname = kmalloc(JOLIET_NAME_MAX + 1, GFP_USER); if (!tmpname) return ERR_PTR(-ENOMEM); From 0f242ca0878896df30292f4922700a32e8fdd039 Mon Sep 17 00:00:00 2001 From: Giorgi Kobakhia Date: Mon, 28 Sep 2026 13:04:45 -0700 Subject: [PATCH 26/26] udf: fix return value for non-recorded-allocated extents in udf_map_block() When someone creates a corrupted UDF filesystem where symlink contains other extent type than EXT_RECORDED_ALLOCATED, it can leads to a crash like: Oops: general protection fault KASAN: maybe wild-memory-access RIP: 0010:page_get_link (fs/namei.c:6503) Call Trace: vfs_readlink (fs/namei.c:6419) do_readlinkat (fs/stat.c:583) __x64_sys_readlink (fs/stat.c:605 fs/stat.c:602 fs/stat.c:602) This is because in this case udf_map_block() fails to convert return value of inode_bmap() from 1 to 0. Returned '1' propagates through udf_bread() all the way up to __page_get_link() where it ends up being treated as a folio pointer with obvious results. Move the reset to 0 line outside the if, so udf_map_block() returns either 0 or -errno, even if the extent type is not EXT_RECORDED_ALLOCATED. Fixes: c226964ec786 ("udf: refactor inode_bmap() to handle error") Cc: stable@vger.kernel.org Assisted-by: LLM Tested-by: Xiang Mei Signed-off-by: Giorgi Kobakhia Link: https://patch.msgid.link/20260928200445.18722-1-gkobakhi@asu.edu Signed-off-by: Jan Kara --- fs/udf/inode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/udf/inode.c b/fs/udf/inode.c index 8babc2fbce9a..bacf2a86161e 100644 --- a/fs/udf/inode.c +++ b/fs/udf/inode.c @@ -372,8 +372,8 @@ static int udf_map_block(struct inode *inode, struct udf_map_rq *map) map->pblk = udf_get_lb_pblock(inode->i_sb, &eloc, offset); map->oflags |= UDF_BLK_MAPPED; - ret = 0; } + ret = 0; out_read: up_read(&iinfo->i_data_sem); brelse(epos.bh);