From c17b44266a1f9fcb49b894d4611d1254bf1aa313 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:51 +0200 Subject: [PATCH 1/8] ext4: free the sbi after a grace period ext4_get_link() follows a symlink that keeps its target in a data block in rcu pathwalk mode with ext4_getblk(EXT4_GET_BLOCKS_CACHED_NOWAIT). That reads s_mount_state in the ASSERT() of ext4_getblk() and again in ext4_es_lookup_extent(), and the lookup bumps the extent status hit or miss counter, all through sb->s_fs_info. ext4_put_super() destroys those counters, clears sb->s_fs_info and frees the sbi without waiting for a grace period. A pathwalk that is still in ext4_get_link() when the last reference to the mount goes away then dereferences a NULL s_fs_info, the freed sbi or the destroyed percpu counters. With a pathwalk held up in ext4_get_link() while the last reference goes away: KASAN: null-ptr-deref in range [0x00000000000000a8-0x00000000000000af] RIP: 0010:ext4_getblk+0xf0/0x640 Call Trace: ext4_get_link+0x26c/0x410 pick_link+0xf96/0x1780 step_into_slowpath+0x9b8/0x1250 path_lookupat+0x484/0xb00 filename_lookup+0x1f1/0x5b0 vfs_statx+0xd7/0x3b0 vfs_fstatat+0x4d/0x90 __do_sys_newfstatat+0xa0/0x100 do_syscall_64+0x10e/0x5e0 entry_SYSCALL_64_after_hwframe+0x77/0x7f So far that has been hard to hit. umount(2) waits for a grace period in namespace_unlock() before it drops the mount, so only a mount whose last reference goes away elsewhere, like the close of a file or a chdir out of a lazily unmounted directory, can race a walker. Keep sb->s_fs_info and free the sbi together with the extent status counters after a grace period. Split the counters out of ext4_es_unregister_shrinker() for that. The shrinker still gets wasted in ext4_put_super(), the counters go with the sbi. Fixes: 6493792d3299 ("ext4: convert symlink external data block mapping to bdev") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-1-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/ext4/ext4.h | 2 ++ fs/ext4/extents-test.c | 2 ++ fs/ext4/extents.c | 1 + fs/ext4/extents_status.c | 7 ++++++- fs/ext4/extents_status.h | 1 + fs/ext4/super.c | 13 +++++++++++-- 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 724a27e8be61..af70bb26bb54 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -1774,6 +1774,8 @@ struct ext4_sb_info { struct list_head s_es_list; /* List of inodes with reclaimable extents */ long s_es_nr_inode; struct ext4_es_stats s_es_stats; + /* ext4_get_link() reads the sbi in rcu pathwalk */ + struct rcu_head s_rcu; struct mb_cache *s_ea_block_cache; struct mb_cache *s_ea_inode_cache; diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c index c3836ecb89f9..2e997f228330 100644 --- a/fs/ext4/extents-test.c +++ b/fs/ext4/extents-test.c @@ -151,6 +151,7 @@ static void extents_kunit_exit(struct kunit *test) sbi = k_ctx.k_ei->vfs_inode.i_sb->s_fs_info; ext4_es_unregister_shrinker(sbi); + ext4_es_destroy_stats(sbi); deactivate_super(sbi->s_sb); kfree(sbi); kfree(k_ctx.k_ei); @@ -340,6 +341,7 @@ out: k_ctx.k_data = NULL; ext4_es_unregister_shrinker(sbi); + ext4_es_destroy_stats(sbi); out_deactivate: deactivate_locked_super(sb); kfree(sbi); diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 76038b6c3655..3bf0a6cdbec2 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -6409,6 +6409,7 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_ext_dirty); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_zeroout); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_register_shrinker); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_unregister_shrinker); +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_destroy_stats); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_create_blocks); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_init_tree); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_lookup_extent); diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c index 6e4a191e8219..018b6146b9d8 100644 --- a/fs/ext4/extents_status.c +++ b/fs/ext4/extents_status.c @@ -1881,12 +1881,17 @@ err1: } void ext4_es_unregister_shrinker(struct ext4_sb_info *sbi) +{ + shrinker_free(sbi->s_es_shrinker); +} + +/* ext4_get_link() bumps the hit and miss counters in rcu pathwalk */ +void ext4_es_destroy_stats(struct ext4_sb_info *sbi) { percpu_counter_destroy(&sbi->s_es_stats.es_stats_cache_hits); percpu_counter_destroy(&sbi->s_es_stats.es_stats_cache_misses); percpu_counter_destroy(&sbi->s_es_stats.es_stats_all_cnt); percpu_counter_destroy(&sbi->s_es_stats.es_stats_shk_cnt); - shrinker_free(sbi->s_es_shrinker); } /* diff --git a/fs/ext4/extents_status.h b/fs/ext4/extents_status.h index f3396cf32b44..01cddcaaa68e 100644 --- a/fs/ext4/extents_status.h +++ b/fs/ext4/extents_status.h @@ -238,6 +238,7 @@ static inline void ext4_es_store_pblock_status(struct extent_status *es, extern int ext4_es_register_shrinker(struct ext4_sb_info *sbi); extern void ext4_es_unregister_shrinker(struct ext4_sb_info *sbi); +extern void ext4_es_destroy_stats(struct ext4_sb_info *sbi); extern int ext4_seq_es_shrinker_info_show(struct seq_file *seq, void *v); diff --git a/fs/ext4/super.c b/fs/ext4/super.c index d636b89090a2..3ace06b4f9e3 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -1279,6 +1279,14 @@ static void ext4_flex_groups_free(struct ext4_sb_info *sbi) } } +static void ext4_put_sbi_rcu(struct rcu_head *head) +{ + struct ext4_sb_info *sbi = container_of(head, struct ext4_sb_info, s_rcu); + + ext4_es_destroy_stats(sbi); + kfree(sbi); +} + static void ext4_put_super(struct super_block *sb) { struct ext4_sb_info *sbi = EXT4_SB(sb); @@ -1374,7 +1382,6 @@ static void ext4_put_super(struct super_block *sb) ext4_stop_mmpd(sbi); brelse(sbi->s_sbh); - sb->s_fs_info = NULL; /* * Now that we are completely done shutting down the * superblock, we need to actually destroy the kobject. @@ -1387,7 +1394,8 @@ static void ext4_put_super(struct super_block *sb) #if IS_ENABLED(CONFIG_UNICODE) utf8_unload(sb->s_encoding); #endif - kfree(sbi); + /* ext4_get_link() reads the sbi and the counters in rcu pathwalk */ + call_rcu(&sbi->s_rcu, ext4_put_sbi_rcu); } static struct kmem_cache *ext4_inode_cachep; @@ -5806,6 +5814,7 @@ failed_mount3a: /* Drain deferred EA inode iputs from journal replay */ flush_delayed_work(&sbi->s_ea_inode_work); ext4_es_unregister_shrinker(sbi); + ext4_es_destroy_stats(sbi); failed_mount3: /* flush s_sb_upd_work before sbi destroy */ flush_work(&sbi->s_sb_upd_work); From 59e483739f2fd14327ece3f60c4995d060ca46b7 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:52 +0200 Subject: [PATCH 2/8] afs: free the super info after a grace period afs_atcell_get_link() resolves the @cell and .@cell symlinks of a dynamic root in rcu pathwalk mode. Before it looks at the dentry it gets the afs_net through afs_i2net(), which reads ->net_ns from the afs_super_info in sb->s_fs_info. The @cell dentries are kept around when unused and the dynamic root has no ->d_revalidate(), so a lookup that finds @cell in the dcache gets to ->get_link() in rcu mode. afs_kill_super() frees the afs_super_info with a plain kfree() in afs_destroy_sbi(). Right now a pathwalk can't still be in afs_atcell_get_link() at that point, but only by accident. umount(2) waits for a grace period in namespace_unlock() before it drops the mount. And a mount whose last reference goes away elsewhere, like the chdir out of a lazily unmounted directory, still gets a grace period from bdi_unregister(). afs_fill_super() sets up a private bdi, so generic_shutdown_super() unregisters it from kill_anon_super() and bdi_remove_from_list() waits in synchronize_rcu_expedited() before afs_destroy_sbi() runs. That grace period protects the rcu readers of bdi_list in the writeback code. It has nothing to do with pathwalk. Don't rely on it and free the afs_super_info after a grace period. Fixes: 30bca65bbbae ("afs: Make /afs/@cell and /afs/.@cell symlinks") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-2-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/afs/internal.h | 2 ++ fs/afs/super.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/afs/internal.h b/fs/afs/internal.h index 330654ed16ec..3282d5d6e1c0 100644 --- a/fs/afs/internal.h +++ b/fs/afs/internal.h @@ -251,6 +251,8 @@ struct afs_super_info { struct afs_volume *volume; /* volume record */ enum afs_flock_mode flock_mode:8; /* File locking emulation mode */ bool dyn_root; /* True if dynamic root */ + /* afs_atcell_get_link() reads ->net_ns in rcu pathwalk */ + struct rcu_head rcu; }; static inline struct afs_super_info *AFS_FS_S(struct super_block *sb) diff --git a/fs/afs/super.c b/fs/afs/super.c index 82bb713825a0..33b195821e36 100644 --- a/fs/afs/super.c +++ b/fs/afs/super.c @@ -523,7 +523,7 @@ static void afs_destroy_sbi(struct afs_super_info *as) afs_put_volume(as->volume, afs_volume_trace_put_destroy_sbi); afs_unuse_cell(as->cell, afs_cell_trace_unuse_sbi); put_net(as->net_ns); - kfree(as); + kfree_rcu(as, rcu); } } From bcec7fc2e224088daf94448d663b02e0d0b9c717 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:53 +0200 Subject: [PATCH 3/8] 9p: free the session after a grace period v9fs_vfs_get_link_dotl() reads ->cache from the v9fs_session_info in sb->s_fs_info to decide whether the symlink target sits in the page cache. It does that before it looks at the dentry, so it also does it in rcu pathwalk mode. Without loose or metadata caching the dentries have no ->d_revalidate() that would make the walk leave rcu mode before it gets there. v9fs_kill_super() frees the session and clears sb->s_fs_info without waiting for a grace period. Right now a pathwalk can't still be in v9fs_vfs_get_link_dotl() at that point, but only by accident. umount(2) waits for a grace period in namespace_unlock() before it drops the mount. And a mount whose last reference goes away elsewhere, like the chdir out of a lazily unmounted directory, still gets a grace period from bdi_unregister(). v9fs_fill_super() sets up a private bdi, so generic_shutdown_super() unregisters it from kill_anon_super() and bdi_remove_from_list() waits in synchronize_rcu_expedited() before v9fs_kill_super() frees the session. That grace period protects the rcu readers of bdi_list in the writeback code. It has nothing to do with pathwalk. Don't rely on it. Keep sb->s_fs_info and free the session after a grace period. Fixes: 712da38d134b ("9p: Enable symlink caching in page cache") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-3-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/9p/v9fs.h | 1 + fs/9p/vfs_super.c | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/9p/v9fs.h b/fs/9p/v9fs.h index a462bcbfc7da..2ac863dc8446 100644 --- a/fs/9p/v9fs.h +++ b/fs/9p/v9fs.h @@ -137,6 +137,7 @@ struct v9fs_session_info { struct list_head slist; /* list of sessions registered with v9fs */ struct rw_semaphore rename_sem; long session_lock_timeout; /* retry interval for blocking locks */ + struct rcu_head rcu; /* v9fs_vfs_get_link_dotl() reads ->cache in rcu pathwalk */ }; #define NDENTRY_TIMEOUT_NEVER (-1U) diff --git a/fs/9p/vfs_super.c b/fs/9p/vfs_super.c index 94d6b02c221b..79526044685c 100644 --- a/fs/9p/vfs_super.c +++ b/fs/9p/vfs_super.c @@ -173,8 +173,8 @@ static void v9fs_kill_super(struct super_block *s) v9fs_session_cancel(v9ses); v9fs_session_close(v9ses); - kfree(v9ses); - s->s_fs_info = NULL; + /* v9fs_vfs_get_link_dotl() may still read ->cache in rcu pathwalk */ + kfree_rcu(v9ses, rcu); p9_debug(P9_DEBUG_VFS, "exiting kill_super\n"); } From 6e6928b396685574010244654e3412060b9d8471 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:54 +0200 Subject: [PATCH 4/8] eventfs: free the eventfs_inode after an rcu grace period too tracefs_d_revalidate() reads ->is_freed of the eventfs_inode that dentry->d_fsdata points to, and ->d_revalidate() is called in rcu pathwalk mode with nothing but rcu_read_lock() held. The dentry itself stays around for such a walker, but tracefs_d_release() drops its reference to the eventfs_inode when the dentry is killed, and release_ei() then frees it after a grace period of eventfs_srcu. An SRCU grace period doesn't wait for rcu_read_lock() sections, so the walker can read an eventfs_inode that is already gone. With a pathwalk held up in tracefs_d_revalidate() while the dentry goes away: BUG: KASAN: slab-use-after-free in tracefs_d_revalidate+0x76/0x90 Read of size 1 at addr ff1100010152d8d0 by task walker/97 Call Trace: tracefs_d_revalidate+0x76/0x90 lookup_fast+0x320/0x640 path_lookupat+0x160/0xb00 filename_lookup+0x1f1/0x5b0 vfs_statx+0xd7/0x3b0 vfs_fstatat+0x4d/0x90 __do_sys_newfstatat+0xa0/0x100 do_syscall_64+0x10e/0x5e0 entry_SYSCALL_64_after_hwframe+0x77/0x7f [...] Allocated by task 1: __kmalloc_cache_noprof+0x249/0x560 eventfs_create_dir+0x7a/0x590 event_create_dir+0x2fb/0xcf0 trace_add_event_call+0xbf/0x150 [...] Freed by task 11: kfree+0x1db/0x5b0 srcu_invoke_callbacks+0x3a3/0x620 process_one_work+0x937/0x1810 worker_thread+0x4f1/0xd60 Before commit 8e556432477e ("eventfs: Use SRCU for freeing eventfs_inodes") release_ei() used call_rcu(), which covered the walker, while eventfs_iterate() needs the SRCU grace period. Wait for both and chain call_rcu() from the SRCU callback. Fixes: 8e556432477e ("eventfs: Use SRCU for freeing eventfs_inodes") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-4-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/tracefs/event_inode.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/tracefs/event_inode.c b/fs/tracefs/event_inode.c index 6e3513b13cfa..090aca946717 100644 --- a/fs/tracefs/event_inode.c +++ b/fs/tracefs/event_inode.c @@ -91,6 +91,14 @@ static void free_ei_rcu(struct rcu_head *rcu) } } +/* read under eventfs_srcu and, by tracefs_d_revalidate(), in rcu pathwalk */ +static void free_ei_srcu(struct rcu_head *rcu) +{ + struct eventfs_inode *ei = container_of(rcu, struct eventfs_inode, rcu); + + call_rcu(&ei->rcu, free_ei_rcu); +} + /* * eventfs_inode reference count management. * @@ -112,7 +120,7 @@ static void release_ei(struct kref *ref) entry->release(entry->name, ei->data); } - call_srcu(&eventfs_srcu, &ei->rcu, free_ei_rcu); + call_srcu(&eventfs_srcu, &ei->rcu, free_ei_srcu); } static inline void put_ei(struct eventfs_inode *ei) From f0e48172a59e517e613b73f73cc35f24fd123aeb Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:55 +0200 Subject: [PATCH 5/8] ipc: drop the mqueue mount with kern_unmount() on setup failure mq_init_ns() creates the mqueue mount with fc_mount_longterm(), so its ->mnt_ns is MNT_NS_INTERNAL. When setting up the mqueue or ipc sysctls or msg_init_ns() fails afterwards, create_ipc_ns() drops it with a plain mntput(). With ->mnt_ns set that takes the fast path of mntput_no_expire(). The count drops to zero and nothing ever kills the mount, so the mount and the mqueue superblock are leaked. Commit dc55e35f9e81 ("ipc: Store mqueue sysctls in the ipc namespace") added the first failure path after mq_init_ns() and never dropped the mount. Commit 7229d74e5e8c ("ipc: create_ipc_ns: drop mqueue mount on sysctl setup failure") tried to fix that with a mntput() but that doesn't help for a long-term mount. Use kern_unmount(). free_ipc() open-codes the same thing for a namespace that made it. Fixes: dc55e35f9e81 ("ipc: Store mqueue sysctls in the ipc namespace") Fixes: 7229d74e5e8c ("ipc: create_ipc_ns: drop mqueue mount on sysctl setup failure") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-5-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- ipc/namespace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipc/namespace.c b/ipc/namespace.c index 1e71353bdb4a..75aa7b469348 100644 --- a/ipc/namespace.c +++ b/ipc/namespace.c @@ -96,7 +96,7 @@ fail_ipc: fail_mq_sysctls: retire_mq_sysctls(ns); fail_mq_mount: - mntput(ns->mq_mnt); + kern_unmount(ns->mq_mnt); fail_put: put_user_ns(ns->user_ns); ns_common_free(ns); From a96c5299bd7305eb984377681eb3222a57376405 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:56 +0200 Subject: [PATCH 6/8] unicode: free the map after a grace period generic_ci_d_hash() and generic_ci_d_compare() read sb->s_encoding and dereference the unicode map while a pathwalk in rcu mode holds nothing but rcu_read_lock(). The filesystems that use them, ext4, f2fs and tmpfs, drop the map with utf8_unload() from ->put_super(), which runs when the last reference to the mount goes away. A lookup that is still inside such a filesystem at that point reads a freed map. With a pathwalk held up in utf8_casefold_hash() while the last reference to a casefolded tmpfs goes away: BUG: KASAN: slab-use-after-free in utf8nlookup+0x74c/0x950 Read of size 8 at addr ff110001030fe658 by task walker/98 Call Trace: utf8nlookup+0x74c/0x950 utf8byte+0x160/0x1560 utf8_casefold_hash+0x167/0x230 generic_ci_d_hash+0xdd/0x1c0 link_path_walk+0x11ad/0x1d30 path_lookupat+0x63/0xb00 filename_lookup+0x1f1/0x5b0 vfs_statx+0xd7/0x3b0 [...] Allocated by task 94: __kmalloc_cache_noprof+0x249/0x560 utf8_load+0x4e/0x570 shmem_parse_opt_casefold.isra.0+0x92/0x270 [...] Freed by task 97: kfree+0x1db/0x5b0 shmem_put_super+0x72/0x170 generic_shutdown_super+0x14c/0x530 kill_anon_super+0x3a/0x60 deactivate_locked_super+0x9c/0xf0 cleanup_mnt+0x9f/0x130 So far that has been hard to hit. umount(2) waits for a grace period in namespace_unlock() before it drops the mount, so only a mount whose last reference goes away elsewhere, like the close of a file or a chdir out of a lazily unmounted directory, can race a walker. So it is reachable. Delay the free until a grace period has passed. The map and the data table it holds a reference to then outlive any pathwalk that found the superblock before the mount went away. The reference on the data table module is dropped from the rcu callback as well. utf8-core is built in whenever CONFIG_UNICODE is set, so there is no module exit that would have to wait for the callbacks. Overlayfs points the s_encoding of an overlay at the map of its first layer without taking a reference of its own. That stays correct. The layer is held by the overlay, so its map is dropped only after the overlay is gone. Fixes: b886ee3e778e ("ext4: Support case-insensitive file name lookups") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-6-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/unicode/utf8-core.c | 21 +++++++++++++++++---- include/linux/unicode.h | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/fs/unicode/utf8-core.c b/fs/unicode/utf8-core.c index f313532f5e80..74a74a48dd8b 100644 --- a/fs/unicode/utf8-core.c +++ b/fs/unicode/utf8-core.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -183,12 +184,24 @@ out_free_um: } EXPORT_SYMBOL(utf8_load); +static void utf8_unload_rcu(struct rcu_head *head) +{ + struct unicode_map *um = container_of(head, struct unicode_map, rcu); + + symbol_put(utf8_data_table); + kfree(um); +} + +/* + * A pathwalk in rcu mode may still be in ->d_hash() or ->d_compare() of a + * filesystem that is being torn down and read the map through its + * superblock, so the map and the tables it points to have to outlive the + * grace period. + */ void utf8_unload(struct unicode_map *um) { - if (um) { - symbol_put(utf8_data_table); - kfree(um); - } + if (um) + call_rcu(&um->rcu, utf8_unload_rcu); } EXPORT_SYMBOL(utf8_unload); diff --git a/include/linux/unicode.h b/include/linux/unicode.h index 64fa44fe180c..3a8cb603eb93 100644 --- a/include/linux/unicode.h +++ b/include/linux/unicode.h @@ -53,6 +53,7 @@ struct unicode_map { unsigned int version; const struct utf8data *ntab[UTF8_NMAX]; const struct utf8data_table *tables; + struct rcu_head rcu; }; int utf8_validate(const struct unicode_map *um, const struct qstr *str); From c01b091b06412d3b1b53d8c2925f6de586c8a2fc Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:57 +0200 Subject: [PATCH 7/8] ntfs3: free the sbi after a grace period If the "nocase" mount option ntfs3 uses custom hash and dentry compare functions. Both access ntfs3 superblock private information in rcu pathwalk. ntfs_put_super() and ntfs3_kill_sb() drop the info without any rcu delay. A lookup that's still inside the filesystem when the last reference to the mount is dropped causes a UAF. With a pathwalk held up in ntfs_d_hash() while the last reference to the mount goes away: BUG: KASAN: slab-use-after-free in ntfs_nls_to_utf16+0x752/0x940 Read of size 8 at addr ff1100010453c850 by task walker/99 Call Trace: ntfs_nls_to_utf16+0x752/0x940 ntfs_d_hash+0x22b/0x500 link_path_walk+0x11ad/0x1d30 path_lookupat+0x63/0xb00 filename_lookup+0x1f1/0x5b0 vfs_statx+0xd7/0x3b0 [...] Allocated by task 95: __kmalloc_cache_noprof+0x249/0x560 ntfs_init_fs_context+0x2db/0x5a0 alloc_fs_context+0x52e/0xc70 [...] Freed by task 98: kfree+0x1db/0x5b0 deactivate_locked_super+0x9c/0xf0 cleanup_mnt+0x9f/0x130 task_work_run+0x131/0x250 The bug is currently masked by the grace period in namespace_unlock() for umount(2). Keep the necessary information until the filesystem private info is freed and free it with an rcu delay. ntfs3_free_sbi() now runs from an rcu callback and drops the upcase table via ntfs_put_shared(). So switch s_shared_lock over to spin_lock_bh(). Otherwise the callback can deadlock against a task holding the lock on the same cpu. Fixes: a3a956c78efa ("fs/ntfs3: Add option "nocase"") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-7-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/ntfs3/ntfs_fs.h | 1 + fs/ntfs3/super.c | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 5811d89d67b3..bbcd2e02bf63 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -211,6 +211,7 @@ struct ntfs_index { /* Ntfs file system in-core superblock data. */ struct ntfs_sb_info { struct super_block *sb; + struct rcu_head rcu; u32 discard_granularity; u64 discard_granularity_mask_inv; // ~(discard_granularity_mask_inv-1) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index f4a42a0c73a4..841f9068d82f 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -196,7 +196,7 @@ void *ntfs_set_shared(void *ptr, u32 bytes) void *ret = NULL; int i, j = -1; - spin_lock(&s_shared_lock); + spin_lock_bh(&s_shared_lock); for (i = 0; i < ARRAY_SIZE(s_shared); i++) { if (!s_shared[i].cnt) { j = i; @@ -214,7 +214,7 @@ void *ntfs_set_shared(void *ptr, u32 bytes) s_shared[j].cnt = 1; ret = ptr; } - spin_unlock(&s_shared_lock); + spin_unlock_bh(&s_shared_lock); return ret; } @@ -231,7 +231,7 @@ void *ntfs_put_shared(void *ptr) void *ret = ptr; int i; - spin_lock(&s_shared_lock); + spin_lock_bh(&s_shared_lock); for (i = 0; i < ARRAY_SIZE(s_shared); i++) { if (s_shared[i].cnt && s_shared[i].ptr == ptr) { if (--s_shared[i].cnt) @@ -239,7 +239,7 @@ void *ntfs_put_shared(void *ptr) break; } } - spin_unlock(&s_shared_lock); + spin_unlock_bh(&s_shared_lock); return ret; } @@ -708,6 +708,8 @@ static noinline void ntfs3_put_sbi(struct ntfs_sb_info *sbi) static void ntfs3_free_sbi(struct ntfs_sb_info *sbi) { + if (sbi->options) + put_mount_options(sbi->options); kfree(sbi->new_rec); kvfree(ntfs_put_shared(sbi->upcase)); kvfree(sbi->def_table); @@ -719,6 +721,11 @@ static void ntfs3_free_sbi(struct ntfs_sb_info *sbi) kfree(sbi); } +static void ntfs3_free_sbi_rcu(struct rcu_head *head) +{ + ntfs3_free_sbi(container_of(head, struct ntfs_sb_info, rcu)); +} + static void ntfs_put_super(struct super_block *sb) { struct ntfs_sb_info *sbi = sb->s_fs_info; @@ -728,11 +735,11 @@ static void ntfs_put_super(struct super_block *sb) /* Mark rw ntfs as clear, if possible. */ ntfs_set_state(sbi, NTFS_DIRTY_CLEAR); - if (sbi->options) { - put_mount_options(sbi->options); - sbi->options = NULL; - } - + /* + * The mount options stay until the sbi is freed: ->d_hash() and + * ->d_compare() of a pathwalk in rcu mode read the nls table through + * them. + */ ntfs3_put_sbi(sbi); } @@ -1939,9 +1946,11 @@ static void ntfs3_kill_sb(struct super_block *sb) kill_block_super(sb); - if (sbi->options) - put_mount_options(sbi->options); - ntfs3_free_sbi(sbi); + /* + * A pathwalk in rcu mode may still be in ->d_hash() or ->d_compare() + * and read the upcase table and the nls table through the sbi. + */ + call_rcu(&sbi->rcu, ntfs3_free_sbi_rcu); } // clang-format off From 56c3824de56b5275c02ff6b6aa27ddbacfbb9b25 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:58 +0200 Subject: [PATCH 8/8] ntfs3: free replaced mount options after a grace period ntfs_fs_reconfigure() replaces the old mount options with new mount options in sbi->options. The old mount options land in fs_context and are freed as soon as remount returns. With a pathwalk held up in ntfs_nls_to_utf16() between loading sbi->options and dereferencing it while a remount replaces them: BUG: KASAN: slab-use-after-free in ntfs_nls_to_utf16+0x7ae/0x7c0 Read of size 8 at addr ff110001044fd448 by task walker/98 Call Trace: ntfs_nls_to_utf16+0x7ae/0x7c0 ntfs_d_hash+0x22b/0x500 link_path_walk+0x11ad/0x1d30 path_lookupat+0x63/0xb00 filename_lookup+0x1f1/0x5b0 vfs_statx+0xd7/0x3b0 [...] Allocated by task 95: __kmalloc_node_track_caller_noprof+0x2a2/0x730 kmemdup_noprof+0x25/0x50 ntfs_fill_super+0x1e5/0x4550 [...] Freed by task 97: kfree+0x1db/0x5b0 put_fs_context+0x13c/0x7d0 path_mount+0x54a/0x2130 __x64_sys_mount+0x20c/0x270 That info is needed in rcu lookup in the dentry hash and dentry compary methods for ntfs3 and causes a UAF today. Delay freeing with an rcu grace period. ntfs_nls_to_utf16() reads ->nls and ->ads once under rcu_read_lock() so the hooks pair with the release in ntfs_fs_reconfigure(). That also covers callers outside of rcu pathwalk such as ntfs_lookup(). They don't hold rcu_read_lock() themselves and wouldn't be protected by the grace period otherwise. Remount refuses to change the iocharset so ->nls stays valid after rcu_read_unlock(). Fixes: a3a956c78efa ("fs/ntfs3: Add option "nocase"") Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20261009-work-fixes-rcu-v1-8-7446d2d335e6@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/ntfs3/dir.c | 13 +++++++++++-- fs/ntfs3/ntfs_fs.h | 2 ++ fs/ntfs3/super.c | 16 +++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index eb9152e9fa22..14ca54e54578 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -186,11 +186,20 @@ int ntfs_nls_to_utf16(struct ntfs_sb_info *sbi, const u8 *name, u32 name_len, { int ret, slen, i; const u8 *end; - struct nls_table *nls = sbi->options->nls; + struct ntfs_mount_options *opts; + struct nls_table *nls; + bool ads; u16 *uname = uni->name; static_assert(sizeof(wchar_t) == sizeof(u16)); + /* ntfs_fs_reconfigure() may replace the options, but not ->nls */ + rcu_read_lock(); + opts = READ_ONCE(sbi->options); + nls = opts->nls; + ads = opts->ads; + rcu_read_unlock(); + if (!nls) { /* utf8 -> utf16 */ ret = _utf8s_to_utf16s(name, name_len, endian, uname, max_ulen); @@ -230,7 +239,7 @@ int ntfs_nls_to_utf16(struct ntfs_sb_info *sbi, const u8 *name, u32 name_len, uni->len = ret; uni->ads_len = 0; - if (ret > 0 && sbi->options->ads) { + if (ret > 0 && ads) { uname = uni->name; /* Find delimiter in range [1 : ret-2). */ for (i = 1; i + 1 < ret; i++) { diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index bbcd2e02bf63..44d59b444648 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -91,6 +91,8 @@ enum utf16_endian; struct ntfs_mount_options { char *nls_name; struct nls_table *nls; + /* ->d_hash() and ->d_compare() read the options in rcu pathwalk */ + struct rcu_head rcu; kuid_t fs_uid; kgid_t fs_gid; diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 841f9068d82f..58ef6be3286b 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -251,6 +251,11 @@ static inline void put_mount_options(struct ntfs_mount_options *options) kfree(options); } +static void put_mount_options_rcu(struct rcu_head *head) +{ + put_mount_options(container_of(head, struct ntfs_mount_options, rcu)); +} + enum Opt { Opt_uid, Opt_gid, @@ -444,6 +449,7 @@ static int ntfs_fs_reconfigure(struct fs_context *fc) struct super_block *sb = fc->root->d_sb; struct ntfs_sb_info *sbi = sb->s_fs_info; struct ntfs_mount_options *new_opts = fc->fs_private; + struct ntfs_mount_options *old_opts; int ro_rw; ro_rw = sb_rdonly(sb) && !(fc->sb_flags & SB_RDONLY); @@ -473,7 +479,15 @@ static int ntfs_fs_reconfigure(struct fs_context *fc) } sync_filesystem(sb); - swap(sbi->options, fc->fs_private); + old_opts = sbi->options; + /* pairs with the READ_ONCE() in ntfs_nls_to_utf16() */ + smp_store_release(&sbi->options, new_opts); + fc->fs_private = NULL; + /* + * ntfs_d_hash() and ntfs_d_compare() of a pathwalk in rcu mode may + * still read the old options through the sbi. + */ + call_rcu(&old_opts->rcu, put_mount_options_rcu); return 0; }