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: e631df89cd ("fs: speed up path lookup with cheaper handling of MAY_EXEC")
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Josef Bacik 2026-10-04 13:54:57 +00:00 committed by Christian Brauner
parent d4a952a954
commit e227cdac03
No known key found for this signature in database
GPG Key ID: 91C61BC06578DCA2

View File

@ -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;