From e227cdac03dffa896e02ed7260f9149c4c382715 Mon Sep 17 00:00:00 2001 From: Josef Bacik Date: Sun, 4 Oct 2026 13:54:57 +0000 Subject: [PATCH] namei: don't assert S_ISDIR on a stale rcu-walk inode lookup_inode_permission_may_exec() asserts that it is only ever handed a directory, on the grounds that link_path_walk() has already checked d_can_lookup(). In rcu-walk that does not follow. step_into() samples ->d_inode and validates it against ->d_seq, but link_path_walk() loads ->d_flags again afterwards, and nothing orders that load against changes of the dentry: CPU0 (rcu-walk of "x/y") CPU1 step_into(x) inode = x->d_inode // regular file read_seqcount_retry(&x->d_seq) == 0 nd->inode = inode unlink("x") d_delete() // x goes negative mkdir("x") d_instantiate() // same dentry d_can_lookup(x) // true now may_lookup() VFS_BUG_ON_INODE(!S_ISDIR(nd->inode->i_mode)) With CONFIG_PREEMPT_RCU the walker can be preempted in that window, so it is not limited to a few instructions. mkdir() only gets the same dentry back if the filesystem keeps negative dentries around. tmpfs doesn't, ext4 does. Apart from the assertion the stale inode is harmless, and has been there since before the assertion was added: nd->seq no longer matches x->d_seq, so lookup_fast(), follow_dotdot_rcu() and try_to_unlazy() all fail with -ECHILD and the walk is redone in ref-walk, whatever the permission check on the old inode said. Keep the assertion for ref-walk, where the dentry is pinned and can't change type, and skip it for rcu-walk. No change for kernels without CONFIG_DEBUG_VFS. Fixes: e631df89cd5d ("fs: speed up path lookup with cheaper handling of MAY_EXEC") Signed-off-by: Josef Bacik Signed-off-by: Christian Brauner (Amutable) --- fs/namei.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fs/namei.c b/fs/namei.c index 22c4f328b8a9..deddab4ebda3 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -683,8 +683,14 @@ EXPORT_SYMBOL(inode_permission); static __always_inline int lookup_inode_permission_may_exec(const struct mnt_idmap *idmap, struct inode *inode, int mask) { - /* Lookup already checked this to return -ENOTDIR */ - VFS_BUG_ON_INODE(!S_ISDIR(inode->i_mode), inode); + /* + * Lookup already checked this to return -ENOTDIR. In rcu-walk that + * check looks at ->d_flags after ->d_seq had been validated, so it can + * see a dentry which went negative and then became a directory while + * nd->inode is still the old non-directory. Any such walk fails the + * next ->d_seq check and gets redone in ref-walk. + */ + VFS_BUG_ON_INODE(!S_ISDIR(inode->i_mode) && !(mask & MAY_NOT_BLOCK), inode); VFS_BUG_ON((mask & ~MAY_NOT_BLOCK) != 0); mask |= MAY_EXEC;