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

From: Jeff Layton

Date: Fri Oct 23 2026 - 07:30:19 EST


On Fri, 2026-10-23 at 19:12 +0800, Zhao Gongyi wrote:
> 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;
> }

LGTM!

Reviewed-by: Jeff Layton <jlayton@xxxxxxxxxx>