From 4c435828932bf46f29860e4a0375bcae101b778a Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Sun, 11 Oct 2026 23:35:06 +0800 Subject: [PATCH 01/24] ocfs2/dlm: Protect the recovery list during debugfs reads Recovery participant records must remain alive while debug_state_print() walks reco.node_data and formats their fields. The formatter holds dlm->spinlock, but the recovery participant list is protected by dlm_reco_state_lock. In particular, dlm_destroy_recovery_area() detaches the list under dlm_reco_state_lock and frees the detached records after releasing it. A concurrent debugfs open can therefore read freed memory. Protect the participant traversal and field reads in debug_state_print() with dlm_reco_state_lock, matching the existing list writers. This also serializes the traversal against list_add_tail() during recovery-area initialization. Make the existing lock available to the debug code through dlmcommon.h; the lock remains internal to the OCFS2 DLM module. Release dlm->spinlock after formatting the other domain state and before acquiring dlm_reco_state_lock, so the debug reader does not nest the two locks. Leave recovery list insertion, detachment and freeing unchanged. KASAN report as below: BUG: KASAN: slab-use-after-free in debug_state_open+0x1169/0x12d0 Read of size 4 at addr ffff88810662ef80 by task dlm-state-stres/896 CPU: 1 UID: 0 PID: 896 Comm: dlm-state-stres Not tainted 7.3.0-rc4-next-20260921-pmb-ocfs2-functional-v1+ #1 PREEMPT(lazy) Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 Call Trace: dump_stack_lvl+0x93/0xd0 print_report+0xce/0x630 ? debug_state_open+0x1169/0x12d0 ? srso_alias_return_thunk+0x5/0xfbef5 ? __virt_addr_valid+0x20e/0x420 ? debug_state_open+0x1169/0x12d0 kasan_report+0xe0/0x110 ? debug_state_open+0x1169/0x12d0 debug_state_open+0x1169/0x12d0 ? __pfx_debug_state_open+0x10/0x10 full_proxy_open_regular+0x193/0x310 do_dentry_open+0x595/0x12d0 ? __pfx_full_proxy_open_regular+0x10/0x10 vfs_open_consume+0xd1/0x400 ? srso_alias_return_thunk+0x5/0xfbef5 path_openat+0x18d0/0x2020 ? __pfx_path_openat+0x10/0x10 do_file_open+0x21e/0x470 ? __pfx_do_file_open+0x10/0x10 ? _raw_spin_unlock+0x23/0x40 ? srso_alias_return_thunk+0x5/0xfbef5 ? alloc_fd+0x3a6/0x6b0 do_sys_openat2+0xf4/0x1b0 ? __pfx_do_sys_openat2+0x10/0x10 ? srso_alias_return_thunk+0x5/0xfbef5 ? __fput+0x5b5/0xa60 __x64_sys_openat+0x136/0x1e0 ? __pfx___x64_sys_openat+0x10/0x10 do_syscall_64+0x114/0x620 entry_SYSCALL_64_after_hwframe+0x77/0x7f [Register dump omitted.] Allocated by task 880: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 __kasan_kmalloc+0xaa/0xb0 __kmalloc_cache_noprof+0x28d/0x610 dlm_remaster_locks+0x147/0x1d90 dlm_do_recovery+0xde1/0x1580 dlm_recovery_thread+0x109/0x300 kthread+0x351/0x460 ret_from_fork+0x659/0x940 ret_from_fork_asm+0x1a/0x30 Freed by task 880: kasan_save_stack+0x33/0x60 kasan_save_track+0x14/0x30 kasan_save_free_info+0x3b/0x60 __kasan_slab_free+0x5f/0x80 kfree+0x308/0x580 dlm_destroy_recovery_area+0x285/0x460 dlm_remaster_locks+0x17c4/0x1d90 dlm_do_recovery+0xde1/0x1580 dlm_recovery_thread+0x109/0x300 kthread+0x351/0x460 ret_from_fork+0x659/0x940 ret_from_fork_asm+0x1a/0x30 The buggy address belongs to the object at ffff88810662ef80 which belongs to the cache kmalloc-32 of size 32 The buggy address is located 0 bytes inside of freed 32-byte region [ffff88810662ef80, ffff88810662efa0) [Page and memory-state dumps omitted.] Fixes: 007dce53a29c ("ocfs2/dlm: Dump the dlm state in a debugfs file") Signed-off-by: Cen Zhang Link: https://patch.msgid.link/pm-ocfs2-objects-candidate-0049-v3-184e46c47f5283f06191@gmail.com Reviewed-by: Joseph Qi Signed-off-by: Christian Brauner (Amutable) --- fs/ocfs2/dlm/dlmcommon.h | 2 ++ fs/ocfs2/dlm/dlmdebug.c | 5 ++++- fs/ocfs2/dlm/dlmrecovery.c | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fs/ocfs2/dlm/dlmcommon.h b/fs/ocfs2/dlm/dlmcommon.h index 20f790a47484..58b28dfa8d34 100644 --- a/fs/ocfs2/dlm/dlmcommon.h +++ b/fs/ocfs2/dlm/dlmcommon.h @@ -435,6 +435,8 @@ enum { DLM_DEREF_LOCKRES_DONE = 522, }; +extern spinlock_t dlm_reco_state_lock; + struct dlm_reco_node_data { int state; diff --git a/fs/ocfs2/dlm/dlmdebug.c b/fs/ocfs2/dlm/dlmdebug.c index ce6d301cb942..a91f167ed683 100644 --- a/fs/ocfs2/dlm/dlmdebug.c +++ b/fs/ocfs2/dlm/dlmdebug.c @@ -736,8 +736,11 @@ static int debug_state_print(struct dlm_ctxt *dlm, char *buf, int len) buf + out, len - out); out += scnprintf(buf + out, len - out, "\n"); + spin_unlock(&dlm->spinlock); + /* Recovery Node State: */ out += scnprintf(buf + out, len - out, "Recovery Node State:\n"); + spin_lock(&dlm_reco_state_lock); list_for_each_entry(node, &dlm->reco.node_data, list) { switch (node->state) { case DLM_RECO_NODE_DATA_INIT: @@ -769,7 +772,7 @@ static int debug_state_print(struct dlm_ctxt *dlm, char *buf, int len) node->node_num, state); } - spin_unlock(&dlm->spinlock); + spin_unlock(&dlm_reco_state_lock); return out; } diff --git a/fs/ocfs2/dlm/dlmrecovery.c b/fs/ocfs2/dlm/dlmrecovery.c index 9d4a2695b959..7d519e31fcf1 100644 --- a/fs/ocfs2/dlm/dlmrecovery.c +++ b/fs/ocfs2/dlm/dlmrecovery.c @@ -80,7 +80,7 @@ static int dlm_lockres_master_requery(struct dlm_ctxt *dlm, static u64 dlm_get_next_mig_cookie(void); -static DEFINE_SPINLOCK(dlm_reco_state_lock); +DEFINE_SPINLOCK(dlm_reco_state_lock); static DEFINE_SPINLOCK(dlm_mig_cookie_lock); static u64 dlm_mig_cookie = 1; From f1365f97fb7498d805981102eee22d362ae643f2 Mon Sep 17 00:00:00 2001 From: Sasha Levin Date: Sun, 11 Oct 2026 18:33:42 -0400 Subject: [PATCH 02/24] samples: pidfd: fix userprogs typo The Makefile adds pidfd-metadata to "usertprogs-always-y", which kbuild does not know, so the sample has not been built since the conversion to userprogs, even with CONFIG_SAMPLE_PIDFD=y. Fix the variable name. pidfd-metadata.c builds without warnings with gcc and clang for x86_64 and arm64. Fixes: 60fb0b1239719 ("samples: pidfd: build sample program for target architecture") Signed-off-by: Sasha Levin Link: https://patch.msgid.link/20261011223358.2327625-14-sashal@kernel.org Reviewed-by: Christian Brauner Signed-off-by: Christian Brauner (Amutable) --- samples/pidfd/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/pidfd/Makefile b/samples/pidfd/Makefile index 9754e2d81f70..d82409f7061f 100644 --- a/samples/pidfd/Makefile +++ b/samples/pidfd/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0 -usertprogs-always-y += pidfd-metadata +userprogs-always-y += pidfd-metadata userccflags += -I usr/include From 28d2905d77bc2e7c20260a6bc6b4179bdf95cb06 Mon Sep 17 00:00:00 2001 From: Nickolai Zeldovich Date: Sun, 11 Oct 2026 08:34:39 -0400 Subject: [PATCH 03/24] exit: fix lost-update race on the child_subreaper bitfields is_child_subreaper, has_child_subreaper, autoreap and, with CONFIG_POSIX_TIMERS=y, timer_create_restore_ids are 1-bit fields that share one memory location in struct signal_struct. A store to any of them is a read-modify-write of the whole word, and three of the stores run without a common lock: - PR_SET_CHILD_SUBREAPER writes me->signal->is_child_subreaper with no lock held. - propagate_has_child_subreaper(), run by walk_process_tree() from the same prctl in an ancestor, writes p->signal->has_child_subreaper of every descendant p under read_lock(&tasklist_lock), which does not exclude the other writers. - PR_TIMER_CREATE_RESTORE_IDS writes current->signal->timer_create_restore_ids with no lock held. When a process calls prctl(PR_SET_CHILD_SUBREAPER, 1) while an ancestor's PR_SET_CHILD_SUBREAPER walk visits it, or while another thread of the same process calls prctl(PR_TIMER_CREATE_RESTORE_IDS), the read-modify-write sequences interleave and one of the bits is silently lost: - If has_child_subreaper is lost, the process's orphaned descendants are reparented straight to init when it exits: find_new_reaper() tests father->signal->has_child_subreaper first and never looks for the ancestor subreaper. The service manager that registered as the subreaper never gets SIGCHLD for them and cannot wait() on them. - If is_child_subreaper is lost, the prctl returned 0 but the process is not a subreaper: orphans of its descendants skip it and are reparented to the next subreaper up or to init. PR_GET_CHILD_SUBREAPER reads back 0. - If timer_create_restore_ids is lost, the prctl returned 0 but timer_create() does not restore the requested timer ids (or keeps restoring them after _OFF), and _GET reads back the old value. Documentation/memory-barriers.txt requires all fields of one bitfield to be protected by one lock for exactly this reason. No lock is taken or documented for these flags, and the prctls need no privilege, so any process can hit the race against its own ancestors or descendants, or between two of its own threads. Make is_child_subreaper and has_child_subreaper separate bool members. Each store then has its own memory location and no read-modify-write of a neighbour. The readers and their locking are unchanged. autoreap stays a bit-field and so shares a memory location only with timer_create_restore_ids. That pair is race-free: autoreap is stored exactly once, in copy_process() before the new signal_struct is reachable by any other task, and timer_create_restore_ids is only stored to the signal_struct of an already running process. Fixes: 749860ce2427 ("prctl: propagate has_child_subreaper flag to every descendant") Signed-off-by: Nickolai Zeldovich Link: https://patch.msgid.link/20261011123440.768054-1-nickolai@csail.mit.edu Acked-by: Oleg Nesterov Signed-off-by: Christian Brauner (Amutable) --- include/linux/sched/signal.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/linux/sched/signal.h b/include/linux/sched/signal.h index d45a5476b97d..efa1e2d6c7a6 100644 --- a/include/linux/sched/signal.h +++ b/include/linux/sched/signal.h @@ -130,8 +130,8 @@ struct signal_struct { * process will inherit a flag if they should look for a * child_subreaper process at exit. */ - unsigned int is_child_subreaper:1; - unsigned int has_child_subreaper:1; + bool is_child_subreaper; + bool has_child_subreaper; unsigned int autoreap:1; #ifdef CONFIG_POSIX_TIMERS From c17b44266a1f9fcb49b894d4611d1254bf1aa313 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:51 +0200 Subject: [PATCH 04/24] 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 40bb03d3ea6f6a915218508819ecc4d7a7fa260d Mon Sep 17 00:00:00 2001 From: Kyle Zeng Date: Fri, 2 Oct 2026 06:17:05 +0000 Subject: [PATCH 05/24] pipe: preserve existing fasync readers on writer setup failure F_SETFL and FIOASYNC decide whether to invoke ->fasync() before pipe_fasync() takes pipe->mutex. Two enables of one O_RDWR FIFO can therefore both enter the callback. fasync_helper() allocates before checking for an existing entry. If the first enable installs both entries, a second enable can update the reader entry and then fail the allocation for the writer entry. The error path removes the pre-existing reader entry, clearing FASYNC while the writer entry remains registered. __fput() then skips ->fasync(), so a later pipe notification can dereference the freed file or its f_owner. An unprivileged memory-pressure reproducer triggers a KASAN slab-use-after-free in send_sigio() through this path. Keep the reader helper's result and undo the registration only if this call added it. The pipe mutex serializes changes to both lists, so a failed new registration still removes its reader, while a failed duplicate preserves the existing registrations and FASYNC. Successful duplicate enables continue to update fa_fd as before. Fixes: 76398425bb06 ("Move FASYNC bit handling to f_op->fasync()") Signed-off-by: Kyle Zeng Signed-off-by: Christian Brauner (Amutable) --- fs/pipe.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/pipe.c b/fs/pipe.c index 292425a834dd..61806a013fde 100644 --- a/fs/pipe.c +++ b/fs/pipe.c @@ -838,9 +838,11 @@ pipe_fasync(int fd, struct file *filp, int on) if (filp->f_mode & FMODE_READ) retval = fasync_helper(fd, filp, on, &pipe->fasync_readers); if ((filp->f_mode & FMODE_WRITE) && retval >= 0) { + int read_ret = retval; + retval = fasync_helper(fd, filp, on, &pipe->fasync_writers); - if (retval < 0 && (filp->f_mode & FMODE_READ)) - /* this can happen only if on == T */ + /* Undo only a reader entry added by this call. */ + if (retval < 0 && read_ret > 0) fasync_helper(-1, filp, 0, &pipe->fasync_readers); } mutex_unlock(&pipe->mutex); From 59e483739f2fd14327ece3f60c4995d060ca46b7 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:52 +0200 Subject: [PATCH 06/24] 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 5e531d62f1f8834faf7a4eed0d41105cf6570a8f Mon Sep 17 00:00:00 2001 From: "Cen Zhang (Microsoft)" Date: Fri, 9 Oct 2026 18:47:15 -0400 Subject: [PATCH 07/24] watch_queue: Fix note pool publication race in watch_queue_set_size() There is a race between watch_queue_set_size() and post_one_notification(). watch_queue_set_size() publishes wqueue->notes, notes_bitmap, nr_pages and nr_notes with plain stores, and post_one_notification() reads them with plain loads under a different lock, so nothing orders the four stores against the poster's loads: depending on the build, notes can be stored last (gcc 14 on x86-64 does this). Since keyctl_watch_key() lets a watch attach to a queue before it is sized, post_one_notification() can already be running on another CPU at that moment: it reads nr_notes > 0, passes its bounds check, and then reads notes while it is still NULL, dereferencing notes[0]. An unprivileged user triggers this reliably by watching one of its own keys through a notification pipe and racing ioctl(IOC_WATCH_QUEUE_SET_SIZE) on that pipe against keyctl(KEYCTL_SETPERM) on the key from another CPU, triggering: BUG: kernel NULL pointer dereference, address: 0000000000000000 RIP: 0010:post_one_notification.isra.0+0xa2/0x1e0 Call Trace: __post_watch_notification+0x148/0x180 keyctl_setperm_key+0x119/0x130 do_syscall_64+0x108/0x4c0 entry_SYSCALL_64_after_hwframe+0x77/0x7f Kernel panic - not syncing: Fatal exception in interrupt Fix this by publishing nr_notes last with smp_store_release(), and by reading it once with smp_load_acquire() in post_one_notification() before the bitmap and the page array are touched. A poster that observes a non-zero nr_notes then also observes the pool behind it; one that observes zero drops the notification as before. Fixes: c73be61cede5 ("pipe: Add general notification queue support") Reported-by: Dae R. Jeong Closes: https://lore.kernel.org/all/ZT-S8Q7tyutcvu_q@dragonet/ Reported-by: AutonomousCodeSecurity@microsoft.com Suggested-by: Dae R. Jeong Signed-off-by: Cen Zhang (Microsoft) Link: https://patch.msgid.link/20261009224715.46121-1-cenzhang@linux.microsoft.com Reviewed-by: Bradley Morgan Cc: Signed-off-by: Christian Brauner (Amutable) --- kernel/watch_queue.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/kernel/watch_queue.c b/kernel/watch_queue.c index 538520861e8b..932a88ac1f39 100644 --- a/kernel/watch_queue.c +++ b/kernel/watch_queue.c @@ -101,7 +101,7 @@ static bool post_one_notification(struct watch_queue *wqueue, struct pipe_inode_info *pipe = wqueue->pipe; struct pipe_buffer *buf; struct page *page; - unsigned int head, tail, note, offset, len; + unsigned int head, tail, note, offset, len, nr_notes; bool done = false; spin_lock_irq(&pipe->rd_wait.lock); @@ -111,8 +111,10 @@ static bool post_one_notification(struct watch_queue *wqueue, if (pipe_full(head, tail, pipe->ring_size)) goto lost; - note = find_first_bit(wqueue->notes_bitmap, wqueue->nr_notes); - if (note >= wqueue->nr_notes) + /* Pairs with the smp_store_release() in watch_queue_set_size(). */ + nr_notes = smp_load_acquire(&wqueue->nr_notes); + note = find_first_bit(wqueue->notes_bitmap, nr_notes); + if (note >= nr_notes) goto lost; page = wqueue->notes[note / WATCH_QUEUE_NOTES_PER_PAGE]; @@ -297,7 +299,8 @@ long watch_queue_set_size(struct pipe_inode_info *pipe, unsigned int nr_notes) wqueue->notes = pages; wqueue->notes_bitmap = bitmap; wqueue->nr_pages = nr_pages; - wqueue->nr_notes = nr_notes; + /* Pairs with the smp_load_acquire() in post_one_notification(). */ + smp_store_release(&wqueue->nr_notes, nr_notes); return 0; error_p: From bcec7fc2e224088daf94448d663b02e0d0b9c717 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 9 Oct 2026 23:51:53 +0200 Subject: [PATCH 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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; } From 65676cd4203c0e41a94caf54080a7f0a95434bda Mon Sep 17 00:00:00 2001 From: Sung Byeongchan Date: Thu, 8 Oct 2026 02:35:42 +0900 Subject: [PATCH 14/24] autofs: ignore notification errors from a replaced pipe autofs_notify_daemon() takes a reference to the current notification pipe under wq_mutex, then drops the mutex before writing the request. The write can block while the daemon enters catatonic mode and installs a replacement pipe. If the old pipe's reader is then closed, the delayed write returns -EPIPE and the generic error path enters catatonic mode again. That transition acts on the current superblock state, closes the replacement pipe, and releases the new daemon generation's wait queues. Only enter catatonic mode when the pipe which failed is still the current pipe. Factor the already-locked transition so the identity check and state change are atomic with respect to another replacement. This was reproduced on v7.3-rc5-337-gff47652a4b66c in a local QEMU guest. An unprivileged UID 65534 lookup filled the old packet pipe before a normal CATATONIC/SETPIPEFD restart. On the unmodified kernel, the new pipe received HUP and both a pending victim lookup and a later lookup failed with ENOENT in two out of two fresh-mount runs. With this change, the new pipe remained active, received an intact 304-byte request, and both victim lookups completed in two out of two runs. Normal lookup and restart-without-a-blocked-writer controls passed. No KASAN, oops, refcount, or lock diagnostic was observed. A source reproducer and complete local logs are available privately on request. The demonstrated impact is limited to denial of a shared autofs service during a legitimate daemon restart. No memory corruption, information leak, privilege escalation, or code execution primitive was observed. Fixes: 8d7b48e0bc5fa ("autofs4: add miscellaneous device for ioctls") Cc: stable@vger.kernel.org Signed-off-by: Sung Byeongchan Link: https://patch.msgid.link/20261007173542.593624-1-tjdqudcks0424@naver.com Acked-by: Ian Kent Signed-off-by: Christian Brauner (Amutable) --- fs/autofs/waitq.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/fs/autofs/waitq.c b/fs/autofs/waitq.c index d46241342dfc..92c0bbbf452b 100644 --- a/fs/autofs/waitq.c +++ b/fs/autofs/waitq.c @@ -12,15 +12,13 @@ */ static autofs_wqt_t autofs_next_wait_queue = 1; -void autofs_catatonic_mode(struct autofs_sb_info *sbi) +static void autofs_catatonic_mode_locked(struct autofs_sb_info *sbi) { struct autofs_wait_queue *wq, *nwq; - mutex_lock(&sbi->wq_mutex); - if (sbi->flags & AUTOFS_SBI_CATATONIC) { - mutex_unlock(&sbi->wq_mutex); + lockdep_assert_held(&sbi->wq_mutex); + if (sbi->flags & AUTOFS_SBI_CATATONIC) return; - } pr_debug("entering catatonic mode\n"); @@ -40,6 +38,21 @@ void autofs_catatonic_mode(struct autofs_sb_info *sbi) fput(sbi->pipe); /* Close the pipe */ sbi->pipe = NULL; sbi->pipefd = -1; +} + +void autofs_catatonic_mode(struct autofs_sb_info *sbi) +{ + mutex_lock(&sbi->wq_mutex); + autofs_catatonic_mode_locked(sbi); + mutex_unlock(&sbi->wq_mutex); +} + +static void autofs_catatonic_mode_for_pipe(struct autofs_sb_info *sbi, + struct file *pipe) +{ + mutex_lock(&sbi->wq_mutex); + if (sbi->pipe == pipe) + autofs_catatonic_mode_locked(sbi); mutex_unlock(&sbi->wq_mutex); } @@ -170,7 +183,7 @@ static void autofs_notify_daemon(struct autofs_sb_info *sbi, autofs_wait_release(sbi, wq->wait_queue_token, ret); break; default: - autofs_catatonic_mode(sbi); + autofs_catatonic_mode_for_pipe(sbi, pipe); break; } fput(pipe); From a291a5cf0023ef3a854a85549f2301da9f24d55b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Sun, 11 Oct 2026 17:30:44 +0200 Subject: [PATCH 15/24] fs: drop the smp_mb()s that mnt_get_count() made redundant Both mntput_no_expire_slowpath() and do_umount() have an smp_mb() between taking mount_lock and summing up the reference count. They pair with the smp_mb() in __legitimize_mnt(). A walker in __legitimize_mnt() increments the count and then re-reads the seqcount of mount_lock. The unmounter writes the seqcount when it takes mount_lock and then sums up the count. They must not both miss each other's store. Otherwise the walker keeps a reference to a mount that the unmounter finishes off. That's the store buffering pattern and it needs a full barrier between the store and the load on both sides. In commit 119e1ef80ecf ("fix __legitimize_mnt()/mntput() race") the barrier in the slow path of mntput_no_expire() as added and the one in do_umount() in commit 65781e19dcfc ("do_umount(): add missing barrier before refcount checks in sync case"). Back then mnt_get_count() didn't have a barrier of its own. Since commit 7eb84d54fac5 ("fs: don't let a migrating task hide its reference from do_umount()") it does. It sums up the puts, does an smp_mb() and then sums up the gets. In program order that barrier sits after the seqcount write in lock_mount_hash() and before the loads of the gets. That's all the walker needs. If the sum misses the walker's increment the walker sees the seqcount change and drops its reference under mount_lock where it finds MNT_DOOMED or MNT_SYNC_UMOUNT. The only thing the outer barrier orders on top of that is the seqcount write against the loads of the puts and nothing relies on it: (1) A put from the fast path of mntput_no_expire() never looks at the seqcount. If the sum misses such a put that's what the grace period is for. (2) All other puts happen under mount_lock and are ordered by it. (3) The slow path's own decrement right before the sum is a store on the same cpu and is read back in program order. So the outer barriers don't order anything that the inner one doesn't. That argument is made in the kernel memory model and every architecture has to implement its barriers according to it. herd7 against tools/memory-model agrees. Without the outer smp_mb() the walker against the slow path and against the busy check in do_umount() is "Never". If the inner smp_mb() goes as well it's "Sometimes". The litmus tests for this are in the next patch. Make the comments in __legitimize_mnt() and mnt_get_count() point at each other. Link: https://patch.msgid.link/20261011-work-mount-fixes-5-v1-1-a9b9a8ecf565@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/namespace.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fs/namespace.c b/fs/namespace.c index 3ed3766f3a18..b55cd0e8c865 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -297,7 +297,11 @@ int mnt_get_count(struct mount *mnt) /* puts first, so a put counted here has its get counted below */ for_each_possible_cpu(cpu) puts += per_cpu_ptr(mnt->mnt_pcp, cpu)->mnt_puts; - smp_mb(); /* pairs with the smp_wmb() in mntput_no_expire() */ + /* + * Pairs with the smp_wmb() in mntput_no_expire() and with the + * smp_mb() in __legitimize_mnt(). + */ + smp_mb(); for_each_possible_cpu(cpu) gets += per_cpu_ptr(mnt->mnt_pcp, cpu)->mnt_gets; @@ -782,7 +786,7 @@ int __legitimize_mnt(struct vfsmount *bastard, unsigned seq) return 0; mnt = real_mount(bastard); mnt_inc_count(mnt); - smp_mb(); /* see mntput_no_expire_slowpath() and do_umount() */ + smp_mb(); /* pairs with the smp_mb() in mnt_get_count() */ if (likely(!read_seqretry(&mount_lock, seq))) return 0; lock_mount_hash(); @@ -1419,11 +1423,6 @@ static void noinline mntput_no_expire_slowpath(struct mount *mnt) VFS_BUG_ON(mnt->mnt_ns); lock_mount_hash(); - /* - * make sure that if __legitimize_mnt() has not seen us grab - * mount_lock, we'll see their refcount increment here. - */ - smp_mb(); mnt_dec_count(mnt); count = mnt_get_count(mnt); if (count != 0) { @@ -2006,7 +2005,6 @@ static int do_umount(struct mount *mnt, int flags) umount_tree(mnt, UMOUNT_PROPAGATE); retval = 0; } else { - smp_mb(); // paired with __legitimize_mnt() shrink_submounts(mnt); retval = -EBUSY; if (!propagate_mount_busy(mnt, 2)) { From 81933038f5e273d17ed22792f906889393b204c5 Mon Sep 17 00:00:00 2001 From: Aviv Vaknin Date: Fri, 2 Oct 2026 23:28:20 +0300 Subject: [PATCH 16/24] pid_namespace: make zap_pid_ns_processes() freezable When the init of a pid namespace exits, zap_pid_ns_processes() waits until all other pids in the namespace are freed. A tracer outside the namespace that does not reap its traced zombies keeps those pids allocated, so this wait has no upper bound, and while it lasts every suspend attempt fails: Freezing user space processes failed after 20.006 seconds (2 tasks refusing to freeze, wq_busy=0): Neither wait is freezable. The first loop sleeps in do_wait(), which returns -ERESTARTSYS on the freezer's fake signal; the loop then clears TIF_SIGPENDING and waits again without ever freezing. The final loop sleeps in TASK_INTERRUPTIBLE, and since nothing clears TIF_SIGPENDING there, schedule() returns immediately after the first freeze attempt and the task spins until the wait ends. Sleep in TASK_INTERRUPTIBLE | TASK_FREEZABLE in do_wait(), so that the freezer can freeze tasks waiting there directly, and in TASK_IDLE | TASK_FREEZABLE in the final loop, like coredump_task_exit() does. This was hit with Chromium-based browsers: crashpad ptrace-attaches the threads of a crashed renderer, which is a pid namespace init, and does not reap those killed during the dump. Fixes: 3eb07c8c8adb ("pid namespaces: destroy pid namespace on init's death") Fixes: b9a985db9896 ("pid_ns: Sleep in TASK_INTERRUPTIBLE in zap_pid_ns_processes") Cc: stable@vger.kernel.org # 6.1+ Suggested-by: Oleg Nesterov Signed-off-by: Aviv Vaknin Link: https://patch.msgid.link/20261002202820.3433318-1-vaknins33@gmail.com Acked-by: Oleg Nesterov Reviewed-by: Bradley Morgan Signed-off-by: Christian Brauner (Amutable) --- kernel/exit.c | 2 +- kernel/pid_namespace.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/exit.c b/kernel/exit.c index 282328d2b4cf..ba1469bea8fb 100644 --- a/kernel/exit.c +++ b/kernel/exit.c @@ -1729,7 +1729,7 @@ static long do_wait(struct wait_opts *wo) add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait); do { - set_current_state(TASK_INTERRUPTIBLE); + set_current_state(TASK_INTERRUPTIBLE | TASK_FREEZABLE); retval = __do_wait(wo); if (retval != -ERESTARTSYS) break; diff --git a/kernel/pid_namespace.c b/kernel/pid_namespace.c index d36afc58ee1d..4ade0f37ed54 100644 --- a/kernel/pid_namespace.c +++ b/kernel/pid_namespace.c @@ -268,7 +268,7 @@ void zap_pid_ns_processes(struct pid_namespace *pid_ns) * free_pid() will awaken this task. */ for (;;) { - set_current_state(TASK_INTERRUPTIBLE); + set_current_state(TASK_IDLE | TASK_FREEZABLE); if (pid_ns->pid_allocated == init_pids) break; schedule(); From 36ebddca56b9d8993d6dc984fdecc748ee1f5e06 Mon Sep 17 00:00:00 2001 From: Kyle Zeng Date: Fri, 2 Oct 2026 05:59:02 +0000 Subject: [PATCH 17/24] fsnotify: prevent mark additions during connector teardown fsnotify_destroy_marks() keeps a reference to its current mark while it drops conn->lock to destroy the mark. This stabilizes the forward-walk cursor, but does not prevent a new mark from being inserted before it. For example, an inotify watch re-added after IN_IGNORED sorts before the old, detached mark for the same group. The walk can miss the replacement and then detach the entire connector from the object. The replacement is still on its group's marks_list and, for inotify, in the watch IDR. The group mutex no longer protects the connector's object from disappearing. An unprivileged fdinfo read racing the final detach can consequently call igrab(NULL): BUG: KASAN: null-ptr-deref in igrab+0x24/0x100 Write of size 4 at addr 0000000000000148 Call Trace: igrab+0x24/0x100 inotify_show_fdinfo+0x1dc/0x5e0 seq_show+0x397/0x690 Set a DESTROYING flag under conn->lock before starting the walk, and reject new marks on that connector with -ENOENT. The existing add-mark error paths undo the temporary group-list entry and the backend's watch allocation. Do not wait for teardown while holding the group mutex, which the destroyer needs. Once the old connector is detached, an add can install a fresh connector as before. This closes the insertion race for all object types using the common connector teardown, without changing the mark reference or SRCU lifetime rules. Fixes: 6b3f05d24d35 ("fsnotify: Detach mark from object list when last reference is dropped") Cc: stable@vger.kernel.org Signed-off-by: Kyle Zeng Signed-off-by: Christian Brauner (Amutable) --- fs/notify/mark.c | 11 +++++++++++ include/linux/fsnotify_backend.h | 1 + 2 files changed, 12 insertions(+) diff --git a/fs/notify/mark.c b/fs/notify/mark.c index d17628580a57..cb07f42d3422 100644 --- a/fs/notify/mark.c +++ b/fs/notify/mark.c @@ -929,6 +929,11 @@ restart: goto restart; } + if (conn->flags & FSNOTIFY_CONN_FLAG_DESTROYING) { + err = -ENOENT; + goto out_err; + } + /* is mark the first mark? */ if (hlist_empty(&conn->list)) { hlist_add_head_rcu(&mark->obj_list, &conn->list); @@ -1116,6 +1121,12 @@ void fsnotify_destroy_marks(fsnotify_connp_t *connp) conn = fsnotify_grab_connector(connp); if (!conn) return; + /* + * New marks could be inserted before our cursor and survive the final + * detachment of the connector from the object. Prevent additions to + * this connector before dropping its lock. + */ + conn->flags |= FSNOTIFY_CONN_FLAG_DESTROYING; /* * We have to be careful since we can race with e.g. * fsnotify_clear_marks_by_group() and once we drop the conn->lock, the diff --git a/include/linux/fsnotify_backend.h b/include/linux/fsnotify_backend.h index 618eed4d6d72..a6d5c1a89cff 100644 --- a/include/linux/fsnotify_backend.h +++ b/include/linux/fsnotify_backend.h @@ -566,6 +566,7 @@ struct fsnotify_mark_connector { unsigned char prio; /* Highest priority group */ #define FSNOTIFY_CONN_FLAG_IS_WATCHED 0x01 #define FSNOTIFY_CONN_FLAG_HAS_IREF 0x02 +#define FSNOTIFY_CONN_FLAG_DESTROYING 0x04 unsigned short flags; /* flags [lock] */ union { /* Object pointer [lock] */ From 99ae7dc439249b47a72eda07f78e02290fa1aebc Mon Sep 17 00:00:00 2001 From: Guanglei Zhu Date: Mon, 12 Oct 2026 17:55:38 +0800 Subject: [PATCH 18/24] ovl: reject lowerdir ending with unescaped backslash In ovl_parse_param_split_lowerdirs() a backslash consumes the character that follows it. When the backslash is the last character of the lowerdir value, it consumes the NUL terminator: the loop exits normally and the backslash is kept in the output, so the lowerdir is passed downstream with a trailing backslash and the mount fails later with a confusing ENOENT from path lookup. A trailing unescaped backslash escapes nothing, so reject it with EINVAL. Signed-off-by: Guanglei Zhu Link: https://patch.msgid.link/20261012095538.938789-1-zhugl3@xiaopeng.com Reviewed-by: Amir Goldstein Signed-off-by: Christian Brauner (Amutable) --- fs/overlayfs/params.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/overlayfs/params.c b/fs/overlayfs/params.c index c93fcaa45d4a..5a15e5478a6b 100644 --- a/fs/overlayfs/params.c +++ b/fs/overlayfs/params.c @@ -216,6 +216,9 @@ static ssize_t ovl_parse_param_split_lowerdirs(char *str) if (*s == '\\') { /* keep esc chars in split lowerdir */ *d++ = *s++; + /* A trailing backslash escapes nothing. */ + if (!*s) + return -EINVAL; } else if (*s == ':') { bool next_colon = (*(s + 1) == ':'); From 9cd0df397a119d6b95ad68e2c82b69b4d620694e Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 13 Oct 2026 00:11:06 +0200 Subject: [PATCH 19/24] fs: pass flags to copy_fs() Turn the umh boolean into a flags argument. Idle threads need their own case in copy_fs() as well and that's nicer than a second boolean. No functional changes. Link: https://patch.msgid.link/20261013-work-fs-idle_fs-v1-1-2063d1bcb5aa@kernel.org Signed-off-by: Christian Brauner (Amutable) --- kernel/fork.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kernel/fork.c b/kernel/fork.c index eb198c8dfdc7..947136cdd115 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1619,7 +1619,12 @@ static int copy_exec_state(u64 clone_flags, struct task_struct *tsk) return task_exec_state_copy(tsk); } -static int copy_fs(u64 clone_flags, struct task_struct *tsk, bool umh) +enum copy_fs_flags_t { + COPY_FS_UMH = (1 << 0), +}; + +static int copy_fs(u64 clone_flags, struct task_struct *tsk, + enum copy_fs_flags_t flags) { struct fs_struct *fs; @@ -1629,7 +1634,7 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk, bool umh) * filesystem state, or be started from a non-initial mount * namespace. */ - if (umh) { + if (flags & COPY_FS_UMH) { if (clone_flags & (CLONE_NEWNS | CLONE_FS)) return -EINVAL; if (current->nsproxy->mnt_ns != &init_mnt_ns) @@ -2299,7 +2304,7 @@ __latent_entropy struct task_struct *copy_process( retval = copy_files(clone_flags, p, args->no_files); if (retval) goto bad_fork_cleanup_semundo; - retval = copy_fs(clone_flags, p, args->umh); + retval = copy_fs(clone_flags, p, args->umh ? COPY_FS_UMH : 0); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); From 98b6cf102b35b2bb48d1fd74d5f664eb66fe83eb Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Mon, 12 Oct 2026 14:57:04 +0200 Subject: [PATCH 20/24] namei: test for a negative dentry the way traverse_mounts() does lookup_fast_for_open() only hands an O_CREAT open a cached dentry if it's positive. Otherwise the open goes on to lookup_open(), which takes the directory lock and creates the file. It tests for that with !dentry->d_inode, but traverse_mounts() later decides with the type in d_flags. And when a dentry is instantiated __d_set_inode_and_type() stores ->d_inode first and only then releases the type into ->d_flags. So a refwalk that took its reference to a negative dentry right before another task instantiates it can find ->d_inode already set in lookup_fast_for_open(), go on to step_into(), and still find the negative type in traverse_mounts(). The open then fails with -ENOENT, which an O_CREAT open must never do. rcuwalk doesn't get that far, the d_seq check in step_into() catches the instantiation. Use d_is_negative() so both look at the type. While the type is still negative the open takes lookup_open() and waits for the directory lock like any other O_CREAT open of a negative dentry. On its own the window is a couple of instructions in the creating task, so it takes an interrupt or a preempted vcpu in exactly the right spot. I haven't caught it without help in about 600 million racing O_CREAT opens. The debug patch below stalls the creating task between the two stores for names starting with "ocrace", the way an interrupt landing there would. The reproducer below opens "ocrace" with O_CREAT through symlinks on a strictatime tmpfs: following them updates their atime, which drops the walk out of rcuwalk, so the name is always looked up in refwalk. With echo 20 > /proc/sys/fs/ocrace_instantiate_us ./ocreat_enoent -t 60 on a KASAN kernel open() failed with -ENOENT 43 times in 4.9 million calls. With this patch it never failed in 4.8 million calls. Tracing step_into_slowpath() during such a run, each of the 98 -ENOENTs it returned came right after the same task had entered it with a dentry that was still typed negative. // SPDX-License-Identifier: GPL-2.0 /* * ocreat_enoent: open(O_CREAT) racing the creation of the same name. * * Writers unlink "ocrace" and create it again. Readers open it with * O_CREAT through a symlink of their own on a strictatime tmpfs. * Following the symlink updates its atime, which drops the walk out of * rcu mode, so the name is always looked up in refwalk. An O_CREAT open * must never fail with ENOENT. * * -p arms a cpu-clock perf timer with that period in ns on every writer, * so that interrupts land at random points in it. * * usage: ocreat_enoent [-t secs] [-w writers] [-r readers] [-p ns] */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #define DIR "/tmp/oc" static atomic_int stop; static atomic_long opens, creates, enoent, other; static long period; static void die(const char *what) { fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno)); exit(2); } static void *writer(void *arg) { (void)arg; if (period) { struct perf_event_attr attr = { .type = PERF_TYPE_SOFTWARE, .size = sizeof(attr), .config = PERF_COUNT_SW_CPU_CLOCK, .sample_period = period, }; if (syscall(SYS_perf_event_open, &attr, 0, -1, -1, 0) < 0) die("perf_event_open"); } while (!atomic_load(&stop)) { int fd; unlink(DIR "/ocrace"); fd = open(DIR "/ocrace", O_CREAT | O_RDWR | O_CLOEXEC, 0644); if (fd >= 0) close(fd); atomic_fetch_add(&creates, 1); } return NULL; } static void *reader(void *arg) { char lnk[64]; snprintf(lnk, sizeof(lnk), DIR "/lnk.%ld", (long)arg); while (!atomic_load(&stop)) { int fd = open(lnk, O_CREAT | O_RDWR | O_CLOEXEC, 0644); if (fd >= 0) close(fd); else if (errno == ENOENT) atomic_fetch_add(&enoent, 1); else atomic_fetch_add(&other, 1); atomic_fetch_add(&opens, 1); } return NULL; } int main(int argc, char **argv) { int secs = 60, nw = 4, nr = 12, c, n = 0; pthread_t t[256]; while ((c = getopt(argc, argv, "t:w:r:p:")) != -1) { switch (c) { case 't': secs = atoi(optarg); break; case 'w': nw = atoi(optarg); break; case 'r': nr = atoi(optarg); break; case 'p': period = atol(optarg); break; default: return 2; } } if (nw + nr > 256) return 2; mkdir(DIR, 0755); if (mount("oc", DIR, "tmpfs", MS_STRICTATIME, "mode=0755")) die("tmpfs"); for (long i = 0; i < nr; i++) { char lnk[64]; snprintf(lnk, sizeof(lnk), DIR "/lnk.%ld", i); if (symlink("ocrace", lnk)) die("symlink"); } for (long i = 0; i < nw; i++) if (pthread_create(&t[n++], NULL, writer, NULL)) die("writer"); for (long i = 0; i < nr; i++) if (pthread_create(&t[n++], NULL, reader, (void *)i)) die("reader"); sleep(secs); atomic_store(&stop, 1); for (int i = 0; i < n; i++) pthread_join(t[i], NULL); printf("%ds: %ld opens, %ld creates, ENOENT %ld, other errors %ld\n", secs, atomic_load(&opens), atomic_load(&creates), atomic_load(&enoent), atomic_load(&other)); umount2(DIR, MNT_DETACH); return atomic_load(&enoent) ? 1 : 0; } diff --git a/fs/dcache.c b/fs/dcache.c --- a/fs/dcache.c +++ b/fs/dcache.c @@ -32,9 +32,13 @@ #include #include #include +#include #include "internal.h" #include "mount.h" +/* debug: stall "ocrace*" names between ->d_inode and the type */ +static int sysctl_ocrace_instantiate_us; + #include /* @@ -200,6 +204,13 @@ } static const struct ctl_table fs_dcache_sysctls[] = { + { + .procname = "ocrace_instantiate_us", + .data = &sysctl_ocrace_instantiate_us, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec, + }, { .procname = "dentry-state", .data = &dentry_stat, @@ -405,6 +416,10 @@ unsigned flags; dentry->d_inode = inode; + /* debug: an interrupt landing right here */ + if (unlikely(READ_ONCE(sysctl_ocrace_instantiate_us)) && + !strncmp(dentry->d_name.name, "ocrace", 6)) + udelay(READ_ONCE(sysctl_ocrace_instantiate_us)); flags = READ_ONCE(dentry->d_flags); flags &= ~DCACHE_ENTRY_TYPE; flags |= type_flags; Fixes: e747e15156b7 ("fs: try an opportunistic lookup for O_CREAT opens too") Cc: stable@vger.kernel.org # v6.12+ Link: https://patch.msgid.link/20261012-work-namei-ocreate-v1-1-c7883b17ee28@kernel.org Reviewed-by: Jeff Layton Signed-off-by: Christian Brauner (Amutable) --- fs/namei.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index c87c14ee1a25..dadba8017c04 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -4750,8 +4750,7 @@ static struct dentry *lookup_fast_for_open(struct nameidata *nd, int open_flag) return dentry; if (open_flag & O_CREAT) { - /* Discard negative dentries. Need inode_lock to do the create */ - if (!dentry->d_inode) { + if (d_is_negative(dentry)) { if (!(nd->flags & LOOKUP_RCU)) dput(dentry); dentry = NULL; From 69fbe94f80bc423613fa18dae6656757423732a2 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 14 Oct 2026 21:45:13 +0200 Subject: [PATCH 21/24] namespace: a mount that copy_tree() leaves out can't take over a lock Since commit c62a4766937e ("move_mount: transfer MNT_LOCKED") a mount can be moved beneath a locked mount. The lock moves from the old cover to the new mount which takes over the job of hiding what's under it and the old cover can be unmounted. That works as long as the new mount stays. But the caller picks the new mount and copy_tree() leaves a mount of a mount namespace file out of every copy made without CL_COPY_MNT_NS_FILE, together with everything mounted on it, and never looks at MNT_LOCKED. unshare(CLONE_NEWNS) and mount propagation make such copies. So a task that is root in its own user namespace gets rid of a locked cover on a file without a race and without touching the filesystem below it. It makes a child with a mount namespace of its own, binds the child's /proc//ns/mnt beneath the cover with MOVE_MOUNT_BENEATH, which hands the lock to that bind, and calls unshare(CLONE_NEWNS). The new mount namespace has nothing on the file. That's the usual way to mask a single file, a bind of /dev/null or of an empty file. With a bind of an ordinary file the file stays covered. copy_tree() already refuses to leave out a locked unbindable mount with -EPERM since commit df7342b24018 ("mount: Don't allow copying MNT_UNBINDABLE|MNT_LOCKED mounts"). Do the same for a locked mount of a mount namespace file. That also covers a tree copied with CL_COPY_MNT_NS_FILE and attached into a mount namespace of another user namespace where lock_mnt_tree() locks it. Note that unshare(CLONE_NEWNS) and clone(CLONE_NEWNS) fail with EPERM from such a mount namespace from then on. The mount can't be left out and it can't be unmounted. And refuse to move a mount of a mount namespace file beneath a locked mount in the first place. Otherwise the task ends up with a mount it can't unmount and an unshare() that fails for good. Chris also found a second mount that goes away by itself, a clone of an automounted mount that shrink_submounts() took along with its parent. Commit f78ddcc87224 ("namespace: never expire a locked mount") took care of that one. Chris's reproducer is appended. Mode nsfile reads SECRET after the unshare() on 7.3-rc7, with this patch the move_mount() fails with EINVAL and the file stays covered. nsfile-control uses an ordinary file and reads MASK both ways. // SPDX-License-Identifier: GPL-2.0 /* * Get rid of a locked cover with MOVE_MOUNT_BENEATH. * * The "admin" (root) makes a tmpfs W: * W/D file, holds SECRET, covered by a bind of a file with MASK * W/Dd/inner file, holds SECRET, Dd is covered by a bind of a directory * whose "inner" holds MASK * W/own directory of uid 1000 with the file "mine" and the * directory "minedir", whose "inner" holds MINE * W/dbg debugfs, mode 755. dbg/tracing is an automount point * The "attacker" is uid 1000. It enters a new user namespace with a new mount * namespace, so W and both covers are locked there. * * usage: prog MODE * plain put a bind of own/mine beneath the cover of D, unmount the cover * nsfile put a bind of the mount namespace file of a child beneath the * cover of D, then unshare(CLONE_NEWNS) * nsfile-control the same with a bind of own/mine * shrink put a bind of the automounted tracefs beneath the cover of Dd, * unmount the cover, put a tmpfs beneath W, call umount(W), which fails * shrink-control the same with a bind of own/minedir * The environment variable AUTOMOUNT_DIR names another automount point for "shrink". */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef MOVE_MOUNT_BENEATH #define MOVE_MOUNT_BENEATH 0x00000200 #endif #define W "/run/x2" static void die(const char *what) { fprintf(stderr, "FATAL %s: %s\n", what, strerror(errno)); exit(2); } static void put_file(const char *name, const char *content) { int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd < 0 || write(fd, content, strlen(content)) < 0) die(name); close(fd); } static void show(const char *path) { char buf[32] = ""; int fd = open(path, O_RDONLY); if (fd < 0) { printf("attacker: open(%s) = %s\n", path, strerror(errno)); return; } if (read(fd, buf, sizeof(buf) - 1) < 0) snprintf(buf, sizeof(buf), "<%s>", strerror(errno)); close(fd); printf("attacker: read(%s) = \"%s\"%s\n", path, buf, strncmp(buf, "SECRET", 6) ? "" : " <-- HIT"); } static int say(const char *what, int ret) { printf("attacker: %s: %s\n", what, ret ? strerror(errno) : "ok"); return ret; } static void write_str(const char *path, const char *s) { int fd = open(path, O_WRONLY); if (fd < 0 || write(fd, s, strlen(s)) < 0) die(path); close(fd); } static void mounts(void) { if (system("grep x2 /proc/self/mountinfo")) printf("attacker: no mounts listed\n"); } static void admin_setup(void) { if (unshare(CLONE_NEWNS) || mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) die("unshare"); mkdir(W, 0755); if (mount("W", W, "tmpfs", 0, "mode=755")) die("mount W"); if (mkdir(W "/Dd", 0755) || mkdir(W "/maskdir", 0755) || mkdir(W "/dbg", 0755) || mkdir(W "/own", 0755) || mkdir(W "/own/minedir", 0755)) die("mkdir"); put_file(W "/D", "SECRET"); put_file(W "/Dd/inner", "SECRET"); put_file(W "/mask", "MASK"); put_file(W "/maskdir/inner", "MASK"); put_file(W "/own/mine", "MINE"); put_file(W "/own/minedir/inner", "MINE"); if (mount(W "/mask", W "/D", NULL, MS_BIND, NULL) || mount(W "/maskdir", W "/Dd", NULL, MS_BIND, NULL)) die("covers"); if (mount("dbg", W "/dbg", "debugfs", 0, "mode=755")) die("mount debugfs"); } static void become_attacker(void) { if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1)) die("drop to uid 1000"); if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) die("unshare user and mount namespace"); write_str("/proc/self/setgroups", "deny"); write_str("/proc/self/uid_map", "0 1000 1"); write_str("/proc/self/gid_map", "0 1000 1"); } /* clone the mount at @src and put the clone beneath the top mount at @where */ static int beneath(const char *src, const char *where) { char what[128]; int fd = open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); int ret; snprintf(what, sizeof(what), "open_tree(%s, OPEN_TREE_CLONE)", src); if (say(what, fd < 0)) return -1; ret = move_mount(fd, "", AT_FDCWD, where, MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); snprintf(what, sizeof(what), "move_mount(clone, %s, MOVE_MOUNT_BENEATH)", where); say(what, ret); close(fd); return ret; } static void do_nsfile(int control) { char path[64] = W "/own/mine"; int pfd[2]; pid_t pid; char c; if (pipe(pfd)) die("pipe"); pid = fork(); if (pid < 0) die("fork"); if (!pid) { if (unshare(CLONE_NEWNS) || write(pfd[1], "g", 1) != 1) die("child"); pause(); exit(0); } if (read(pfd[0], &c, 1) != 1) die("sync"); if (!control) snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid); beneath(path, W "/D"); mounts(); say("unshare(CLONE_NEWNS)", unshare(CLONE_NEWNS)); show(W "/D"); mounts(); kill(pid, SIGKILL); waitpid(pid, NULL, 0); } static void do_shrink(int control) { const char *automount = getenv("AUTOMOUNT_DIR"); int fd, mfd; if (!automount) automount = W "/dbg/tracing"; beneath(control ? W "/own/minedir" : automount, W "/Dd"); say("umount2(Dd, 0), the old cover", umount2(W "/Dd", 0)); say("umount2(Dd, 0), the mount that was put beneath", umount2(W "/Dd", 0)); show(W "/Dd/inner"); say("umount2(W, 0)", umount2(W, 0)); fd = fsopen("tmpfs", FSOPEN_CLOEXEC); if (fd < 0 || fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) die("fsopen tmpfs"); mfd = fsmount(fd, FSMOUNT_CLOEXEC, 0); if (mfd < 0) die("fsmount"); say("move_mount(new tmpfs, W, MOVE_MOUNT_BENEATH)", move_mount(mfd, "", AT_FDCWD, W, MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH)); close(mfd); mounts(); say("umount2(W, 0)", umount2(W, 0)); show(W "/Dd/inner"); mounts(); } int main(int argc, char **argv) { const char *mode = argc > 1 ? argv[1] : "plain"; setvbuf(stdout, NULL, _IOLBF, 0); admin_setup(); become_attacker(); show(W "/D"); show(W "/Dd/inner"); say("umount2(D, MNT_DETACH)", umount2(W "/D", MNT_DETACH)); say("umount2(Dd, MNT_DETACH)", umount2(W "/Dd", MNT_DETACH)); if (!strcmp(mode, "plain")) { beneath(W "/own/mine", W "/D"); show(W "/D"); say("umount2(D, MNT_DETACH), the old cover", umount2(W "/D", MNT_DETACH)); show(W "/D"); say("umount2(D, MNT_DETACH), the mount that was put beneath", umount2(W "/D", MNT_DETACH)); show(W "/D"); mounts(); } else if (!strncmp(mode, "nsfile", 6)) { do_nsfile(!strcmp(mode, "nsfile-control")); } else { do_shrink(!strcmp(mode, "shrink-control")); } return 0; } Fixes: c62a4766937e ("move_mount: transfer MNT_LOCKED") Reported-by: Chris Mason Cc: stable@vger.kernel.org # v7.1+ Link: https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-1-24035bd5d8e9@kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/namespace.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/fs/namespace.c b/fs/namespace.c index b55cd0e8c865..c30f063742f4 100644 --- a/fs/namespace.c +++ b/fs/namespace.c @@ -2252,6 +2252,11 @@ struct mount *copy_tree(struct mount *src_root, struct dentry *dentry, } if (!(flag & CL_COPY_MNT_NS_FILE) && is_mnt_ns_file(src_mnt->mnt.mnt_root)) { + if (src_mnt->mnt.mnt_flags & MNT_LOCKED) { + /* Both a mount namespace file and locked. */ + dst_mnt = ERR_PTR(-EPERM); + goto out; + } src_mnt = skip_mnt_tree(src_mnt); continue; } @@ -3647,8 +3652,9 @@ static bool mount_is_ancestor(const struct mount *p1, const struct mount *p2) * @mnt_to: mount under which to mount * @mp: mountpoint of @mnt_to * - * - Make sure that the caller can unmount the topmost mount ensuring - * that the caller could reveal the underlying mountpoint. + * - Make sure that @mnt_from can take over the lock of @mnt_to. A + * mount of a mount namespace file is left out of a copy of the tree + * and can't. * - Ensure that nothing has been mounted on top of @mnt_from before we * grabbed @namespace_sem to avoid creating pointless shadow mounts. * - Prevent mounting beneath a mount if the propagation relationship @@ -3664,6 +3670,15 @@ static int can_move_mount_beneath(const struct mount *mnt_from, { struct mount *parent_mnt_to = mnt_to->mnt_parent; + /* + * @mnt_from takes over the lock of @mnt_to. copy_tree() leaves a + * mount of a mount namespace file out of every copy made without + * CL_COPY_MNT_NS_FILE, together with what is mounted on it, so it + * can't keep anything hidden. + */ + if (IS_MNT_LOCKED(mnt_to) && is_mnt_ns_file(mnt_from->mnt.mnt_root)) + return -EINVAL; + /* Avoid creating shadow mounts during mount propagation. */ if (mnt_from->overmount) return -EINVAL; From d2bdb9b61b42766ed66eef822370ad44435f40fe Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 13 Oct 2026 00:11:07 +0200 Subject: [PATCH 22/24] fs: let idle threads share init_fs With nullfs as the real rootfs userspace can pivot_root() away from the initramfs and unmount it: chdir(rootfs); pivot_root(".", "."); umount2(".", MNT_DETACH); That's what systemd does and the initramfs is supposed to go away at that point together with all the memory it uses. But on every machine with more than one possible cpu the initramfs superblock stays around forever (cf. [1]). With ramfs as rootfs the unpacked initramfs shows up as Unevictable in /proc/meminfo, with tmpfs as Shmem, and it is never freed. In the reports that's between 70MB and 150MB. The idle threads of the secondary cpus are created by fork_idle() from smp_init(). That runs in kernel_init(), i.e., in PID 1, after init_userspace_fs() moved PID 1 from nullfs into the initramfs. fork_idle() doesn't pass CLONE_FS so each idle thread gets a private copy of PID 1's fs_struct and with it a reference to the initramfs as root and as pwd. pivot_root() relies on chroot_fs_refs() to move everyone whose root or pwd is the old root over to the new root. Idle threads aren't on the task list though so chroot_fs_refs() never finds them. They never exit either. So they keep the initramfs mount and with it the superblock alive for good. I overlooked this when I added nullfs. Before that the rootfs could never be unmounted anyway so it didn't matter what pinned it. Idle threads are kernel threads and init_task, the idle thread of the boot cpu, already uses init_fs. So let the idle threads of the other cpus share init_fs as well instead of copying PID 1's filesystem state. Note that kernels before 7.3 need a different backport. There PID 1 still shares init_fs with all kthreads so passing CLONE_FS in fork_idle() is enough. They also have a second pin: devtmpfsd unshares a copy of the initial mount namespace, initramfs mount included. That was fixed in commit 66d2faeccbff ("devtmpfs: create private mount namespace"). Reproducer, as /init in an initramfs together with a 128MB ballast file, booted with root=/dev/sda to get ramfs and with -smp 4: // SPDX-License-Identifier: GPL-2.0 /* initramfs /init: switch root like systemd, check the old root */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include static void meminfo(void) { char line[256]; FILE *f = fopen("/proc/meminfo", "re"); while (f && fgets(line, sizeof(line), f)) if (!strncmp(line, "Unevictable:", 12) || !strncmp(line, "Shmem:", 6)) fputs(line, stdout); if (f) fclose(f); } int main(int argc, char *argv[]) { struct stat st; char dev[32], buf[32]; unsigned int maj, min; int fd, in, out; long r; if (argc > 1) { sleep(2); meminfo(); /* ustat() only finds live superblocks */ r = syscall(SYS_ustat, (unsigned)atoi(argv[1]), buf); printf("initramfs superblock %s\n", r ? "freed" : "pinned"); fflush(stdout); sleep(1); reboot(RB_POWER_OFF); } mount("proc", "/proc", "proc", 0, NULL); mount("devtmpfs", "/dev", "devtmpfs", 0, NULL); /* drop the fds console_on_rootfs() opened in the initramfs */ fd = open("/dev/console", O_RDWR); dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); setvbuf(stdout, NULL, _IOLBF, 0); meminfo(); stat("/", &st); maj = major(st.st_dev); min = minor(st.st_dev); /* new_encode_dev(), what ustat() expects */ snprintf(dev, sizeof(dev), "%u", (min & 0xff) | (maj << 8) | ((min & ~0xffu) << 12)); mkdir("/sysroot", 0755); mount("tmpfs", "/sysroot", "tmpfs", 0, NULL); mkdir("/sysroot/proc", 0755); mkdir("/sysroot/dev", 0755); in = open("/init", O_RDONLY); out = open("/sysroot/init", O_WRONLY | O_CREAT, 0755); fstat(in, &st); sendfile(out, in, NULL, st.st_size); close(in); close(out); mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); mount("/proc", "/sysroot/proc", NULL, MS_BIND | MS_REC, NULL); mount("/dev", "/sysroot/dev", NULL, MS_BIND | MS_REC, NULL); chdir("/sysroot"); if (syscall(SYS_pivot_root, ".", ".") || umount2(".", MNT_DETACH)) perror("pivot_root"); execl("/init", "/init", dev, NULL); return 1; } Before: Unevictable: 131812 kB Shmem: 0 kB Unevictable: 131812 kB Shmem: 792 kB initramfs superblock pinned After: Unevictable: 131812 kB Shmem: 0 kB Unevictable: 0 kB Shmem: 792 kB initramfs superblock freed With -smp 1 the superblock is freed without this patch as well. [1]: https://github.com/systemd/systemd/issues/43911 Fixes: 576ee5dfd459 ("fs: add immutable rootfs") Link: https://patch.msgid.link/20261013-work-fs-idle_fs-v1-2-2063d1bcb5aa@kernel.org Signed-off-by: Christian Brauner (Amutable) --- kernel/fork.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kernel/fork.c b/kernel/fork.c index 947136cdd115..33760ece3bb3 100644 --- a/kernel/fork.c +++ b/kernel/fork.c @@ -1621,6 +1621,7 @@ static int copy_exec_state(u64 clone_flags, struct task_struct *tsk) enum copy_fs_flags_t { COPY_FS_UMH = (1 << 0), + COPY_FS_IDLE = (1 << 1), }; static int copy_fs(u64 clone_flags, struct task_struct *tsk, @@ -1640,13 +1641,14 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk, if (current->nsproxy->mnt_ns != &init_mnt_ns) return -EINVAL; fs = userspace_init_fs; + } else if (flags & COPY_FS_IDLE) { + fs = &init_fs; } else { fs = current->fs; VFS_WARN_ON_ONCE(current->fs != current->real_fs); } if (clone_flags & CLONE_FS) { - /* tsk->fs is already what we want */ read_seqlock_excl(&fs->seq); /* "users" and "in_exec" locked for check_unsafe_exec() */ if (fs->in_exec) { @@ -1655,6 +1657,7 @@ static int copy_fs(u64 clone_flags, struct task_struct *tsk, } fs->users++; read_sequnlock_excl(&fs->seq); + tsk->real_fs = tsk->fs = fs; return 0; } tsk->real_fs = tsk->fs = copy_fs_struct(fs); @@ -2304,7 +2307,9 @@ __latent_entropy struct task_struct *copy_process( retval = copy_files(clone_flags, p, args->no_files); if (retval) goto bad_fork_cleanup_semundo; - retval = copy_fs(clone_flags, p, args->umh ? COPY_FS_UMH : 0); + retval = copy_fs(clone_flags, p, + (args->umh ? COPY_FS_UMH : 0) | + (args->idle ? COPY_FS_IDLE : 0)); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); @@ -2676,7 +2681,7 @@ struct task_struct * __init fork_idle(int cpu) { struct task_struct *task; struct kernel_clone_args args = { - .flags = CLONE_VM, + .flags = CLONE_VM | CLONE_FS, .fn = &idle_dummy, .fn_arg = NULL, .kthread = 1, From eadfe727dd800fc5fca33e45dbfd392ade5bbbd6 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 14 Oct 2026 21:45:14 +0200 Subject: [PATCH 23/24] selftests/filesystems: test that a mntns file can't take over a lock Add a case to the locked mount test: - a bind of the mount namespace file of a grandchild moved beneath a locked cover must fail with EINVAL - a bind of an ordinary file beneath it takes the lock over, the old cover can go and the new one can't - the file is still covered after unshare(CLONE_NEWNS) Link: https://patch.msgid.link/20261014-work-namei-locked-mnt_ns_file-v1-2-24035bd5d8e9@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../umount_propagation/locked_mount_test.c | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c index 0799b2ba545d..e9b2b874dd16 100644 --- a/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c +++ b/tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -27,6 +29,12 @@ #ifndef MOVE_MOUNT_BENEATH #define MOVE_MOUNT_BENEATH 0x00000200 #endif +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif #define DIR_LEN 64 #define PATH_LEN 192 @@ -120,6 +128,35 @@ static bool covered(const char *path) return fd < 0; } +/* What @path reads, "" if it can't be read. */ +static int read_word(const char *path, char *buf, size_t len) +{ + ssize_t n = 0; + int fd; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + n = read(fd, buf, len - 1); + close(fd); + } + buf[n > 0 ? n : 0] = 0; + return fd < 0 ? -1 : 0; +} + +/* Put a clone of the mount at @src beneath the top mount at @where. */ +static int move_beneath(const char *src, const char *where) +{ + int fd, ret; + + fd = syscall(__NR_open_tree, AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (fd < 0) + return -1; + ret = syscall(__NR_move_mount, fd, "", AT_FDCWD, where, + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH); + close(fd); + return ret; +} + FIXTURE(locked_mount) { char base[DIR_LEN]; bool tracing; /* debugfs with the tracefs automount is there */ @@ -429,4 +466,89 @@ TEST_F(locked_mount, propagated_copy_keeps_the_cover_locked) EXPECT_EQ(ret, CHILD_OK); } +/* + * A mount of a mount namespace file is left out of the copy that + * unshare(CLONE_NEWNS) makes, together with everything mounted on it, so + * it can't take over the lock of a cover it is moved beneath. The child + * tries just that with the namespace file of a grandchild and gets EINVAL. + * A bind of an ordinary file takes the lock over, and the file is still + * covered in the next copy of the mount namespace. + */ +static int ns_file_child(const char *base) +{ + char secret[PATH_LEN], mine[PATH_LEN], nsfile[PATH_LEN], buf[32]; + int ret = CHILD_OK, pfd[2]; + pid_t pid; + + snprintf(secret, sizeof(secret), "%s/P/secret", base); + snprintf(mine, sizeof(mine), "%s/mine", base); + + if (enter_userns() || pipe(pfd)) + return CHILD_SETUP; + /* the cover is locked */ + if (!umount2(secret, 0) || errno != EINVAL) + return CHILD_STEP1; + + /* a mount namespace newer than our own, so that a bind of it is allowed */ + pid = fork(); + if (pid < 0) + return CHILD_SETUP; + if (pid == 0) { + if (unshare(CLONE_NEWNS) || send_byte(pfd[1])) + _exit(1); + pause(); + _exit(0); + } + if (wait_byte(pfd[0])) + return CHILD_SETUP; + snprintf(nsfile, sizeof(nsfile), "/proc/%d/ns/mnt", pid); + if (!move_beneath(nsfile, secret) || errno != EINVAL) + ret = CHILD_STEP2; + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + if (ret) + return ret; + + /* an ordinary file beneath the cover takes the lock over */ + if (move_beneath(mine, secret)) + return CHILD_STEP3; + if (umount2(secret, 0)) + return CHILD_STEP3; + if (!umount2(secret, 0) || errno != EINVAL) + return CHILD_STEP3; + /* and it is in the next copy of the namespace */ + if (unshare(CLONE_NEWNS)) + return CHILD_SETUP; + if (read_word(secret, buf, sizeof(buf)) || strcmp(buf, "mine")) + return CHILD_STEP4; + return CHILD_OK; +} + +TEST_F(locked_mount, mount_namespace_file_cant_take_the_lock) +{ + char p[PATH_LEN], q[PATH_LEN]; + pid_t pid; + int ret; + + snprintf(p, sizeof(p), "%s/P", self->base); + ASSERT_EQ(mkdir(p, 0755), 0); + ASSERT_EQ(mount("tmpfs", p, "tmpfs", 0, "mode=0755"), 0); + snprintf(p, sizeof(p), "%s/P/secret", self->base); + ASSERT_EQ(create_file(p, "covered-by-root\n"), 0); + snprintf(q, sizeof(q), "%s/P/empty", self->base); + ASSERT_EQ(create_file(q, ""), 0); + /* the cover: an empty file over the secret */ + ASSERT_EQ(mount(q, p, NULL, MS_BIND, NULL), 0); + snprintf(q, sizeof(q), "%s/mine", self->base); + ASSERT_EQ(create_file(q, "mine"), 0); + + pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) + _exit(ns_file_child(self->base)); + ret = wait_child(pid); + TH_LOG("child exit code %d", ret); + EXPECT_EQ(ret, CHILD_OK); +} + TEST_HARNESS_MAIN From 214897acc8c2e5d6b63189d23ccaed6fa2af45a9 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 16 Oct 2026 10:50:57 +0200 Subject: [PATCH 24/24] Revert "namei: don't reveal overmounted entries in refwalk" This reverts commit 8189ab688f5a2c745dc90b63374da02f213d6994. This bug class is bigger than can be fixed with this simple patch. Back out of it and give us some time to fix this properly. Signed-off-by: Christian Brauner --- fs/namei.c | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index dadba8017c04..e45e30603215 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -2086,33 +2086,6 @@ all_done: // pure jump return NULL; } -/* - * Be careful in case the dentry is unlinked or renamed. Any mounts - * stacked on top of it are going away. We need to make sure that we - * don't reveal the underyling dentry during refwalk. In rcuwalk we - * catch this via d_seq and another lookup for the name. Give the same - * guarantee in refwalk. - */ -static bool unlink_may_reveal(struct nameidata *nd, int flags, - struct path *path, struct dentry *dentry) -{ - /* ".." and LOOKUP_DOWN may land on an unhashed directory */ - if (flags & WALK_NOFOLLOW) - return false; - if (nd->flags & LOOKUP_REVAL) - return false; - /* We crossed onto a mount and the name led us here while it still existed */ - if (path->mnt != nd->path.mnt) - return false; - /* only a name on its way out is flagged */ - if (likely(!cant_mount(dentry))) - return false; - if (!d_unlinked(dentry)) - return false; - dput(no_free_ptr(path->dentry)); - return true; -} - /* * Do we need to follow links? We _really_ want to be able * to do this check without having to look at inode->i_op, @@ -2142,8 +2115,6 @@ static noinline const char *step_into_slowpath(struct nameidata *nd, int flags, if (unlikely(!inode)) return ERR_PTR(-ENOENT); } else { - if (unlikely(unlink_may_reveal(nd, flags, &path, dentry))) - return ERR_PTR(-ESTALE); dput(nd->path.dentry); if (nd->path.mnt != path.mnt) mntput(nd->path.mnt);