[PATCH v2] filelock: keep superblock active through delegation wait

From: Zhao Gongyi

Date: Fri Oct 23 2026 - 07:17:53 EST


A caller may drop its path after try_break_deleg() returns a delegated
inode but before break_deleg_wait() finishes. The extra inode reference
held in struct delegated_inode does not keep the superblock active.

After a lazy unmount, closing the delegation holder can release the last
mount reference and shut down the superblock while another task waits for
the delegation break. The waiter's final iput() can then enter the
filesystem's eviction code after its superblock-private state was freed.
This has been observed as a KASAN use-after-free in shmem_evict_inode().

When try_break_deleg() saves a delegated inode, take an active reference
to its superblock while the caller still pins the mount. Keep that
reference through break_deleg_wait() and the final iput(), then release
it with deactivate_super(). This protects every caller of the helpers,
including the mknod, mkdir, symlink and rmdir paths that drop their path
before waiting.

Fixes: e12d203b8c88 ("vfs: allow mkdir to wait for delegation break on parent")
Fixes: 4fa76319cd0c ("vfs: allow rmdir to wait for delegation break on parent")
Fixes: c826229c6a82 ("vfs: make vfs_create break delegations on parent directory")
Fixes: e8960c1b2ee9 ("vfs: make vfs_mknod break delegations on parent directory")
Fixes: 92bf53577f01 ("vfs: make vfs_symlink break delegations on parent dir")
Link: https://lore.kernel.org/r/20261016093421.90725-1-zhaogongyi@xxxxxxxxxxxxx
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Zhao Gongyi <zhaogongyi@xxxxxxxxxxxxx>
---
Changes since v1:
- Hold an active superblock reference in the delegation helpers instead
of adding mount references to the four fs/namei.c callers, as suggested
by Al Viro and Christian Brauner.
- Use &inode->i_sb->s_active in atomic_inc().

Validation:
Built an arm64 KASAN Image with LLVM=1 on af32da41b032, using the same
config, guest image, and local arm64 cpufeature compatibility change as
the v1 tests. The tested filelock.h diff is byte-identical to this patch,
which is based on v7.3-rc7 (24a19b953e2a). Image SHA-256:
b7ad342b0020c31ab2be97ceb441ebc38c5840f023350462bb816552fd175870

Ran the existing syzkaller reproducers under arm64 KVM with procs=6 and
repeat=0. Each test had one run:

syscall/variant duration programs wait stack samples
mkdirat 300 s 2028 7
symlinkat 300 s 2064 8
mknodat S_IFREG 600 s 4168 18
mknodat S_IFIFO 600 s 4131 17
rmdir 600 s 4174 16

The stack samples identified the corresponding filename_*() waiter in
break_lease(). Every VM remained responsive through the requested run;
no Busy inodes, KASAN, WARNING, BUG, or panic appeared in serial output.
The unpatched baseline crashes reported in v1 were not rerun.

include/linux/filelock.h | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/include/linux/filelock.h b/include/linux/filelock.h
index ec11cc6b4..e909db319 100644
--- a/include/linux/filelock.h
+++ b/include/linux/filelock.h
@@ -530,8 +530,8 @@ static inline bool is_delegated(struct delegated_inode *di)
*
* Break delegations in a non-blocking fashion. If there are
* outstanding delegations and @di is set, then an extra reference
- * will be taken on @inode and @di->di_inode will be populated so
- * that it may be waited upon.
+ * will be taken on @inode and its superblock, and @di->di_inode will
+ * be populated so that it may be waited upon.
*
* Returns 0 if there is no need to wait or an error. If -EWOULDBLOCK
* is returned, then @di will be populated (if non-NULL).
@@ -545,16 +545,21 @@ static inline int try_break_deleg(struct inode *inode, unsigned int flags,
if (ret == -EWOULDBLOCK && di) {
di->di_inode = inode;
ihold(inode);
+ /* The caller may drop its mount before break_deleg_wait(). */
+ atomic_inc(&inode->i_sb->s_active);
}
return ret;
}

static inline int break_deleg_wait(struct delegated_inode *di)
{
+ struct inode *inode = di->di_inode;
+ struct super_block *sb = inode->i_sb;
int ret;

- ret = break_deleg(di->di_inode, 0);
- iput(di->di_inode);
+ ret = break_deleg(inode, 0);
+ iput(inode);
+ deactivate_super(sb);
di->di_inode = NULL;
return ret;
}
--
2.39.5 (Apple Git-154)