fs/kernfs/mount.c | 4 ++-- fs/super.c | 39 +++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 22 deletions(-)
From: Christian Brauner <brauner@kernel.org>
iterate_supers_type() drops sb_lock while invoking the callback and keeps
only a passive reference to the current superblock. That reference keeps
the object allocated, but does not keep its s_instances node linked.
After the iterator releases s_umount, final teardown can unlink the current
s_instances node. The iterator then advances through a reinitialized node.
With the current hlist it stops without visiting the remaining superblocks.
The unlink moved from generic_shutdown_super() to kill_super_notify(), but
the cursor lifetime has been unsafe since the helper was introduced.
The CIFS DFS lookup can consequently miss a matching superblock and return
-EINVAL.
Move removal from fs_supers to put_super(), alongside removal from
super_blocks, so a passive reference keeps both list nodes linked. Keep
the filesystem module reference until then, since unlinking s_instances
may touch type->fs_supers.
Make sget_fc() skip SB_DEAD superblocks before invoking test(), and set
SB_DEAD under sb_lock to serialize with those callbacks. This allows
kernfs to free its private information after kill_anon_super() returns.
Keep matching SB_DYING superblocks until SB_DEAD is set so concurrent
mounts still wait for teardown before retrying.
Fixes: 43e15cdbefea ("new helper: iterate_supers_type()")
Reported-by: Karl Mehltretter <kmehltretter@gmail.com>
Closes: https://lore.kernel.org/r/20260903013336.92081-1-kmehltretter@gmail.com
Suggested-by: Jan Kara <jack@suse.cz>
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Tested-by: Karl Mehltretter <kmehltretter@gmail.com>
Assisted-by: LLM
[kmehltretter: supplied the commit message]
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
Changes in v2:
- Use Christian's implementation of Jan's suggestion: keep s_instances
linked until the last passive reference is dropped and make sget_fc()
skip SB_DEAD superblocks under sb_lock.
- Retain the filesystem module reference until put_super().
- Attribute authorship to Christian; retain my commit message.
Testing: All tests run on v1 also passed with Christian's draft. These
included an x86_64 QEMU deterministic KUnit test using the actual CIFS
lookup callback and a Samba DFS reconnect test with concurrent CIFS
mounts and unmounts in the same guest. The deterministic test exercised
the missed-match case; the network stress did not reproduce the narrow
race on baseline.
Link to v1: https://lore.kernel.org/r/20260903013336.92081-1-kmehltretter@gmail.com
Christian's draft:
https://lore.kernel.org/r/20260904-rockkonzert-bergtour-dahin-23c9c5c87d0f@brauner
fs/kernfs/mount.c | 4 ++--
fs/super.c | 39 +++++++++++++++++++--------------------
2 files changed, 21 insertions(+), 22 deletions(-)
diff --git a/fs/kernfs/mount.c b/fs/kernfs/mount.c
index f183a96778b9..a57399021c8b 100644
--- a/fs/kernfs/mount.c
+++ b/fs/kernfs/mount.c
@@ -434,8 +434,8 @@ void kernfs_kill_sb(struct super_block *sb)
up_write(&root->kernfs_supers_rwsem);
/*
- * Remove the superblock from fs_supers/s_instances
- * so we can't find it, before freeing kernfs_super_info.
+ * Mark the superblock dead so sget_fc() can't find it,
+ * before freeing kernfs_super_info.
*/
kill_anon_super(sb);
kfree(info);
diff --git a/fs/super.c b/fs/super.c
index 05e443173038..0f9e13eedb4f 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -433,15 +433,19 @@ static struct super_block *alloc_super(struct file_system_type *type, int flags,
void put_super(struct super_block *s)
{
if (refcount_dec_and_test(&s->s_passive)) {
+ struct file_system_type *type = s->s_type;
spin_lock(&sb_lock);
list_del_init(&s->s_list);
+ hlist_del_init(&s->s_instances);
spin_unlock(&sb_lock);
WARN_ON(s->s_dentry_lru.node);
WARN_ON(s->s_inode_lru.node);
WARN_ON(s->s_mounts);
call_rcu(&s->rcu, destroy_super_rcu);
+ /* The unlink above may touch type->fs_supers, so drop it last. */
+ put_filesystem(type);
}
}
@@ -558,17 +562,6 @@ static void kill_super_notify(struct super_block *sb)
if (sb->s_flags & SB_DEAD)
return;
- /*
- * Remove it from @fs_supers so it isn't found by new
- * sget_fc() walkers anymore. Any concurrent mounter still
- * managing to grab a temporary reference is guaranteed to
- * already see SB_DYING and will wait until we notify them about
- * SB_DEAD.
- */
- spin_lock(&sb_lock);
- hlist_del_init(&sb->s_instances);
- spin_unlock(&sb_lock);
-
/* Drop sget_fc()'s claim; a never-registered entry stays with the sb. */
if (sb->s_super_dev->sd_dev) {
super_dev_put(sb->s_super_dev);
@@ -577,11 +570,15 @@ static void kill_super_notify(struct super_block *sb)
/*
* Let concurrent mounts know that this thing is really dead.
- * We don't need @sb->s_umount here as every concurrent caller
- * will see SB_DYING and either discard the superblock or wait
- * for SB_DEAD.
+ * sget_fc() skips SB_DEAD superblocks and calls test() under
+ * sb_lock, so set it under sb_lock: once we return no test()
+ * runs on this superblock anymore and none will start. Everyone
+ * else already saw SB_DYING and either discarded the superblock
+ * or waits for SB_DEAD.
*/
+ spin_lock(&sb_lock);
super_wake(sb, SB_DEAD);
+ spin_unlock(&sb_lock);
}
/**
@@ -608,7 +605,6 @@ void deactivate_locked_super(struct super_block *s)
list_lru_destroy(&s->s_dentry_lru);
list_lru_destroy(&s->s_inode_lru);
- put_filesystem(fs);
put_super(s);
} else {
super_unlock_excl(s);
@@ -795,12 +791,12 @@ void generic_shutdown_super(struct super_block *sb)
}
/*
* Broadcast to everyone that grabbed a temporary reference to this
- * superblock before we removed it from @fs_supers that the superblock
- * is dying. Every walker of @fs_supers outside of sget_fc() will now
- * discard this superblock and treat it as dead.
+ * superblock that it is dying. Every walker of @fs_supers outside
+ * of sget_fc() will now discard this superblock and treat it as
+ * dead.
*
- * We leave the superblock on @fs_supers so it can be found by
- * sget_fc() until we passed sb->kill_sb().
+ * sget_fc() keeps finding the superblock until SB_DEAD is set, so
+ * a concurrent mounter waits until we passed sb->kill_sb().
*/
super_wake(sb, SB_DYING);
super_unlock_excl(sb);
@@ -879,6 +875,9 @@ struct super_block *sget_fc(struct fs_context *fc,
spin_lock(&sb_lock);
if (test) {
hlist_for_each_entry(old, &fc->fs_type->fs_supers, s_instances) {
+ /* Only unlinked at the last passive reference. */
+ if (super_flags(old, SB_DEAD))
+ continue;
if (test(old, fc))
goto share_extant_sb;
}
--
2.53.0
On Wed, 09 Sep 2026 21:30:34 +0200, Karl Mehltretter wrote:
> iterate_supers_type() drops sb_lock while invoking the callback and keeps
> only a passive reference to the current superblock. That reference keeps
> the object allocated, but does not keep its s_instances node linked.
>
> After the iterator releases s_umount, final teardown can unlink the current
> s_instances node. The iterator then advances through a reinitialized node.
> With the current hlist it stops without visiting the remaining superblocks.
> The unlink moved from generic_shutdown_super() to kill_super_notify(), but
> the cursor lifetime has been unsafe since the helper was introduced.
>
> [...]
I'm moving this into vfs-7.4.misc for now but will bump it to vfs.fixes once
I'm confident this is safe.
---
Applied to the vfs-7.4.misc branch of the vfs/vfs.git tree.
Patches in the vfs-7.4.misc branch should appear in linux-next soon.
Please report any outstanding bugs that were missed during review in a
new review to the original patch series allowing us to drop it.
It's encouraged to provide Acked-bys and Reviewed-bys even though the
patch has now been applied. If possible patch trailers will be updated.
Note that commit hashes shown below are subject to change due to rebase,
trailer updates or similar. If in doubt, please check the listed branch.
tree: https://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs.git
branch: vfs-7.4.misc
[1/1] super: make iterate_supers_type() deletion-safe
https://git.kernel.org/vfs/vfs/c/d1ac530407a3
On Wed 09-09-26 21:30:34, Karl Mehltretter wrote:
> From: Christian Brauner <brauner@kernel.org>
>
> iterate_supers_type() drops sb_lock while invoking the callback and keeps
> only a passive reference to the current superblock. That reference keeps
> the object allocated, but does not keep its s_instances node linked.
>
> After the iterator releases s_umount, final teardown can unlink the current
> s_instances node. The iterator then advances through a reinitialized node.
> With the current hlist it stops without visiting the remaining superblocks.
> The unlink moved from generic_shutdown_super() to kill_super_notify(), but
> the cursor lifetime has been unsafe since the helper was introduced.
>
> The CIFS DFS lookup can consequently miss a matching superblock and return
> -EINVAL.
>
> Move removal from fs_supers to put_super(), alongside removal from
> super_blocks, so a passive reference keeps both list nodes linked. Keep
> the filesystem module reference until then, since unlinking s_instances
> may touch type->fs_supers.
>
> Make sget_fc() skip SB_DEAD superblocks before invoking test(), and set
> SB_DEAD under sb_lock to serialize with those callbacks. This allows
> kernfs to free its private information after kill_anon_super() returns.
> Keep matching SB_DYING superblocks until SB_DEAD is set so concurrent
> mounts still wait for teardown before retrying.
>
> Fixes: 43e15cdbefea ("new helper: iterate_supers_type()")
> Reported-by: Karl Mehltretter <kmehltretter@gmail.com>
> Closes: https://lore.kernel.org/r/20260903013336.92081-1-kmehltretter@gmail.com
> Suggested-by: Jan Kara <jack@suse.cz>
> Cc: stable@vger.kernel.org
> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
> Tested-by: Karl Mehltretter <kmehltretter@gmail.com>
> Assisted-by: LLM
> [kmehltretter: supplied the commit message]
> Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
Looks good to me! Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> Changes in v2:
> - Use Christian's implementation of Jan's suggestion: keep s_instances
> linked until the last passive reference is dropped and make sget_fc()
> skip SB_DEAD superblocks under sb_lock.
> - Retain the filesystem module reference until put_super().
> - Attribute authorship to Christian; retain my commit message.
>
> Testing: All tests run on v1 also passed with Christian's draft. These
> included an x86_64 QEMU deterministic KUnit test using the actual CIFS
> lookup callback and a Samba DFS reconnect test with concurrent CIFS
> mounts and unmounts in the same guest. The deterministic test exercised
> the missed-match case; the network stress did not reproduce the narrow
> race on baseline.
>
> Link to v1: https://lore.kernel.org/r/20260903013336.92081-1-kmehltretter@gmail.com
>
> Christian's draft:
> https://lore.kernel.org/r/20260904-rockkonzert-bergtour-dahin-23c9c5c87d0f@brauner
>
> fs/kernfs/mount.c | 4 ++--
> fs/super.c | 39 +++++++++++++++++++--------------------
> 2 files changed, 21 insertions(+), 22 deletions(-)
>
> diff --git a/fs/kernfs/mount.c b/fs/kernfs/mount.c
> index f183a96778b9..a57399021c8b 100644
> --- a/fs/kernfs/mount.c
> +++ b/fs/kernfs/mount.c
> @@ -434,8 +434,8 @@ void kernfs_kill_sb(struct super_block *sb)
> up_write(&root->kernfs_supers_rwsem);
>
> /*
> - * Remove the superblock from fs_supers/s_instances
> - * so we can't find it, before freeing kernfs_super_info.
> + * Mark the superblock dead so sget_fc() can't find it,
> + * before freeing kernfs_super_info.
> */
> kill_anon_super(sb);
> kfree(info);
> diff --git a/fs/super.c b/fs/super.c
> index 05e443173038..0f9e13eedb4f 100644
> --- a/fs/super.c
> +++ b/fs/super.c
> @@ -433,15 +433,19 @@ static struct super_block *alloc_super(struct file_system_type *type, int flags,
> void put_super(struct super_block *s)
> {
> if (refcount_dec_and_test(&s->s_passive)) {
> + struct file_system_type *type = s->s_type;
>
> spin_lock(&sb_lock);
> list_del_init(&s->s_list);
> + hlist_del_init(&s->s_instances);
> spin_unlock(&sb_lock);
>
> WARN_ON(s->s_dentry_lru.node);
> WARN_ON(s->s_inode_lru.node);
> WARN_ON(s->s_mounts);
> call_rcu(&s->rcu, destroy_super_rcu);
> + /* The unlink above may touch type->fs_supers, so drop it last. */
> + put_filesystem(type);
> }
> }
>
> @@ -558,17 +562,6 @@ static void kill_super_notify(struct super_block *sb)
> if (sb->s_flags & SB_DEAD)
> return;
>
> - /*
> - * Remove it from @fs_supers so it isn't found by new
> - * sget_fc() walkers anymore. Any concurrent mounter still
> - * managing to grab a temporary reference is guaranteed to
> - * already see SB_DYING and will wait until we notify them about
> - * SB_DEAD.
> - */
> - spin_lock(&sb_lock);
> - hlist_del_init(&sb->s_instances);
> - spin_unlock(&sb_lock);
> -
> /* Drop sget_fc()'s claim; a never-registered entry stays with the sb. */
> if (sb->s_super_dev->sd_dev) {
> super_dev_put(sb->s_super_dev);
> @@ -577,11 +570,15 @@ static void kill_super_notify(struct super_block *sb)
>
> /*
> * Let concurrent mounts know that this thing is really dead.
> - * We don't need @sb->s_umount here as every concurrent caller
> - * will see SB_DYING and either discard the superblock or wait
> - * for SB_DEAD.
> + * sget_fc() skips SB_DEAD superblocks and calls test() under
> + * sb_lock, so set it under sb_lock: once we return no test()
> + * runs on this superblock anymore and none will start. Everyone
> + * else already saw SB_DYING and either discarded the superblock
> + * or waits for SB_DEAD.
> */
> + spin_lock(&sb_lock);
> super_wake(sb, SB_DEAD);
> + spin_unlock(&sb_lock);
> }
>
> /**
> @@ -608,7 +605,6 @@ void deactivate_locked_super(struct super_block *s)
> list_lru_destroy(&s->s_dentry_lru);
> list_lru_destroy(&s->s_inode_lru);
>
> - put_filesystem(fs);
> put_super(s);
> } else {
> super_unlock_excl(s);
> @@ -795,12 +791,12 @@ void generic_shutdown_super(struct super_block *sb)
> }
> /*
> * Broadcast to everyone that grabbed a temporary reference to this
> - * superblock before we removed it from @fs_supers that the superblock
> - * is dying. Every walker of @fs_supers outside of sget_fc() will now
> - * discard this superblock and treat it as dead.
> + * superblock that it is dying. Every walker of @fs_supers outside
> + * of sget_fc() will now discard this superblock and treat it as
> + * dead.
> *
> - * We leave the superblock on @fs_supers so it can be found by
> - * sget_fc() until we passed sb->kill_sb().
> + * sget_fc() keeps finding the superblock until SB_DEAD is set, so
> + * a concurrent mounter waits until we passed sb->kill_sb().
> */
> super_wake(sb, SB_DYING);
> super_unlock_excl(sb);
> @@ -879,6 +875,9 @@ struct super_block *sget_fc(struct fs_context *fc,
> spin_lock(&sb_lock);
> if (test) {
> hlist_for_each_entry(old, &fc->fs_type->fs_supers, s_instances) {
> + /* Only unlinked at the last passive reference. */
> + if (super_flags(old, SB_DEAD))
> + continue;
> if (test(old, fc))
> goto share_extant_sb;
> }
> --
> 2.53.0
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
© 2016 - 2026 Red Hat, Inc.