]> www.infradead.org Git - users/hch/misc.git/commitdiff
do_lock_mount(): don't modify path.
authorAl Viro <viro@zeniv.linux.org.uk>
Fri, 22 Aug 2025 21:41:55 +0000 (17:41 -0400)
committerAl Viro <viro@zeniv.linux.org.uk>
Tue, 16 Sep 2025 01:26:42 +0000 (21:26 -0400)
Currently do_lock_mount() has the target path switched to whatever
might be overmounting it.  We _do_ want to have the parent
mount/mountpoint chosen on top of the overmounting pile; however,
the way it's done has unpleasant races - if umount propagation
removes the overmount while we'd been trying to set the environment
up, we might end up failing if our target path strays into that overmount
just before the overmount gets kicked out.

Users of do_lock_mount() do not need the target path changed - they
have all information in res->{parent,mp}; only one place (in
do_move_mount()) currently uses the resulting path->mnt, and that value
is trivial to reconstruct by the original value of path->mnt + chosen
parent mount.

Let's keep the target path unchanged; it avoids a bunch of subtle races
and it's not hard to do:
do
as mount_locked_reader
find the prospective parent mount/mountpoint dentry
grab references if it's not the original target
lock the prospective mountpoint dentry
take namespace_sem exclusive
if prospective parent/mountpoint would be different now
err = -EAGAIN
else if location has been unmounted
err = -ENOENT
else if mountpoint dentry is not allowed to be mounted on
err = -ENOENT
else if beneath and the top of the pile was the absolute root
err = -EINVAL
else
try to get struct mountpoint (by dentry), set
err to 0 on success and -ENO{MEM,ENT} on failure
if err != 0
res->parent = ERR_PTR(err)
drop locks
else
res->parent = prospective parent
drop temporary references
while err == -EAGAIN

A somewhat subtle part is that dropping temporary references is allowed.
Neither mounts nor dentries should be evicted by a thread that holds
namespace_sem.  On success we are dropping those references under
namespace_sem, so we need to be sure that these are not the last
references remaining.  However, on success we'd already verified (under
namespace_sem) that original target is still mounted and that mount
and dentry we are about to drop are still reachable from it via the
mount tree.  That guarantees that we are not about to drop the last
remaining references.

Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
fs/namespace.c

index b6983adaa73b7aa4c544331f0a736351b894cfca..47c7a0517663cc65210524446018181abbc9bcb1 100644 (file)
@@ -2727,6 +2727,27 @@ static int attach_recursive_mnt(struct mount *source_mnt,
        return err;
 }
 
+static inline struct mount *where_to_mount(const struct path *path,
+                                          struct dentry **dentry,
+                                          bool beneath)
+{
+       struct mount *m;
+
+       if (unlikely(beneath)) {
+               m = topmost_overmount(real_mount(path->mnt));
+               *dentry = m->mnt_mountpoint;
+               return m->mnt_parent;
+       }
+       m = __lookup_mnt(path->mnt, path->dentry);
+       if (unlikely(m)) {
+               m = topmost_overmount(m);
+               *dentry = m->mnt.mnt_root;
+               return m;
+       }
+       *dentry = path->dentry;
+       return real_mount(path->mnt);
+}
+
 /**
  * do_lock_mount - acquire environment for mounting
  * @path:      target path
@@ -2758,81 +2779,65 @@ static int attach_recursive_mnt(struct mount *source_mnt,
  * case we also require the location to be at the root of a mount
  * that has a parent (i.e. is not a root of some namespace).
  */
-static void do_lock_mount(struct path *path, struct pinned_mountpoint *res, bool beneath)
+static void do_lock_mount(const struct path *path,
+                         struct pinned_mountpoint *res,
+                         bool beneath)
 {
-       struct vfsmount *mnt = path->mnt;
-       struct dentry *dentry;
-       struct path under = {};
-       int err = -ENOENT;
+       int err;
 
        if (unlikely(beneath) && !path_mounted(path)) {
                res->parent = ERR_PTR(-EINVAL);
                return;
        }
 
-       for (;;) {
-               struct mount *m = real_mount(mnt);
-
-               if (beneath) {
-                       path_put(&under);
-                       read_seqlock_excl(&mount_lock);
-                       if (unlikely(!mnt_has_parent(m))) {
-                               read_sequnlock_excl(&mount_lock);
-                               res->parent = ERR_PTR(-EINVAL);
-                               return;
+       do {
+               struct dentry *dentry, *d;
+               struct mount *m, *n;
+
+               scoped_guard(mount_locked_reader) {
+                       m = where_to_mount(path, &dentry, beneath);
+                       if (&m->mnt != path->mnt) {
+                               mntget(&m->mnt);
+                               dget(dentry);
                        }
-                       under.mnt = mntget(&m->mnt_parent->mnt);
-                       under.dentry = dget(m->mnt_mountpoint);
-                       read_sequnlock_excl(&mount_lock);
-                       dentry = under.dentry;
-               } else {
-                       dentry = path->dentry;
                }
 
                inode_lock(dentry->d_inode);
                namespace_lock();
 
-               if (unlikely(cant_mount(dentry) || !is_mounted(mnt)))
-                       break;          // not to be mounted on
+               // check if the chain of mounts (if any) has changed.
+               scoped_guard(mount_locked_reader)
+                       n = where_to_mount(path, &d, beneath);
 
-               if (beneath && unlikely(m->mnt_mountpoint != dentry ||
-                                       &m->mnt_parent->mnt != under.mnt)) {
-                       namespace_unlock();
-                       inode_unlock(dentry->d_inode);
-                       continue;       // got moved
-               }
+               if (unlikely(n != m || dentry != d))
+                       err = -EAGAIN;          // something moved, retry
+               else if (unlikely(cant_mount(dentry) || !is_mounted(path->mnt)))
+                       err = -ENOENT;          // not to be mounted on
+               else if (beneath && &m->mnt == path->mnt && !m->overmount)
+                       err = -EINVAL;
+               else
+                       err = get_mountpoint(dentry, res);
 
-               mnt = lookup_mnt(path);
-               if (unlikely(mnt)) {
+               if (unlikely(err)) {
+                       res->parent = ERR_PTR(err);
                        namespace_unlock();
                        inode_unlock(dentry->d_inode);
-                       path_put(path);
-                       path->mnt = mnt;
-                       path->dentry = dget(mnt->mnt_root);
-                       continue;       // got overmounted
+               } else {
+                       res->parent = m;
                }
-               err = get_mountpoint(dentry, res);
-               if (err)
-                       break;
-               if (beneath) {
-                       /*
-                        * @under duplicates the references that will stay
-                        * at least until namespace_unlock(), so the path_put()
-                        * below is safe (and OK to do under namespace_lock -
-                        * we are not dropping the final references here).
-                        */
-                       path_put(&under);
-                       res->parent = real_mount(path->mnt)->mnt_parent;
-                       return;
+               /*
+                * Drop the temporary references.  This is subtle - on success
+                * we are doing that under namespace_sem, which would normally
+                * be forbidden.  However, in that case we are guaranteed that
+                * refcounts won't reach zero, since we know that path->mnt
+                * is mounted and thus all mounts reachable from it are pinned
+                * and stable, along with their mountpoints and roots.
+                */
+               if (&m->mnt != path->mnt) {
+                       dput(dentry);
+                       mntput(&m->mnt);
                }
-               res->parent = real_mount(path->mnt);
-               return;
-       }
-       namespace_unlock();
-       inode_unlock(dentry->d_inode);
-       if (beneath)
-               path_put(&under);
-       res->parent = ERR_PTR(err);
+       } while (err == -EAGAIN);
 }
 
 static void __unlock_mount(struct pinned_mountpoint *m)
@@ -3613,6 +3618,8 @@ static int do_move_mount(struct path *old_path,
        if (beneath) {
                struct mount *over = real_mount(new_path->mnt);
 
+               if (mp.parent != over->mnt_parent)
+                       over = mp.parent->overmount;
                err = can_move_mount_beneath(old, over, mp.mp);
                if (err)
                        return err;