]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
do_make_slave(): choose new master sanely
authorAl Viro <viro@zeniv.linux.org.uk>
Tue, 24 Jun 2025 22:12:56 +0000 (18:12 -0400)
committerAl Viro <viro@zeniv.linux.org.uk>
Sun, 29 Jun 2025 23:03:30 +0000 (19:03 -0400)
When mount changes propagation type so that it doesn't propagate
events any more (MS_PRIVATE, MS_SLAVE, MS_UNBINDABLE), we need
to make sure that event propagation between other mounts is
unaffected.

We need to make sure that events from peers and master of that mount
(if any) still reach everything that used to be on its ->mnt_slave_list.

If mount has neither peers nor master, we simply need to dissolve
its ->mnt_slave_list and clear ->mnt_master of everything in there.

If mount has peers, we transfer everything in ->mnt_slave_list of
this mount into that of some of those peers (and adjust ->mnt_master
accordingly).

If mount has a master but no peers, we transfer everything in
->mnt_slave_list of this mount into that of its master (adjusting
->mnt_master, etc.).

There are two problems with the current implementation:
* there's a long-obsolete logics in choosing the peer -
once upon a time it made sense to prefer the peer that had the
same ->mnt_root as our mount, but that had been pointless since
2014 ("smarter propagate_mnt()")
* the most common caller of that thing is umount_tree()
taking the mounts out of propagation graph.  In that case it's
possible to have ->mnt_slave_list contents moved many times,
since the replacement master is likely to be taken out by the
same umount_tree(), etc.

Take the choice of replacement master into a separate function
(propagation_source()) and teach it to skip the candidates that
are going to be taken out.

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

index 9723f05cda5f150bf9966dabe23b4bd936bb552b..91d10af867bdef5ae36e26d82057f32e0424ed19 100644 (file)
@@ -65,40 +65,45 @@ int get_dominating_id(struct mount *mnt, const struct path *root)
        return 0;
 }
 
+static inline bool will_be_unmounted(struct mount *m)
+{
+       return m->mnt.mnt_flags & MNT_UMOUNT;
+}
+
+static struct mount *propagation_source(struct mount *mnt)
+{
+       do {
+               struct mount *m;
+               for (m = next_peer(mnt); m != mnt; m = next_peer(m)) {
+                       if (!will_be_unmounted(m))
+                               return m;
+               }
+               mnt = mnt->mnt_master;
+       } while (mnt && will_be_unmounted(mnt));
+       return mnt;
+}
+
 static int do_make_slave(struct mount *mnt)
 {
-       struct mount *master, *slave_mnt;
+       struct mount *master = propagation_source(mnt);
+       struct mount *slave_mnt;
 
        if (list_empty(&mnt->mnt_share)) {
                mnt_release_group_id(mnt);
-               CLEAR_MNT_SHARED(mnt);
-               master = mnt->mnt_master;
-               if (!master) {
-                       struct list_head *p = &mnt->mnt_slave_list;
-                       while (!list_empty(p)) {
-                               slave_mnt = list_first_entry(p,
-                                               struct mount, mnt_slave);
-                               list_del_init(&slave_mnt->mnt_slave);
-                               slave_mnt->mnt_master = NULL;
-                       }
-                       return 0;
-               }
        } else {
-               struct mount *m;
-               /*
-                * slave 'mnt' to a peer mount that has the
-                * same root dentry. If none is available then
-                * slave it to anything that is available.
-                */
-               for (m = master = next_peer(mnt); m != mnt; m = next_peer(m)) {
-                       if (m->mnt.mnt_root == mnt->mnt.mnt_root) {
-                               master = m;
-                               break;
-                       }
-               }
                list_del_init(&mnt->mnt_share);
                mnt->mnt_group_id = 0;
-               CLEAR_MNT_SHARED(mnt);
+       }
+       CLEAR_MNT_SHARED(mnt);
+       if (!master) {
+               struct list_head *p = &mnt->mnt_slave_list;
+               while (!list_empty(p)) {
+                       slave_mnt = list_first_entry(p,
+                                       struct mount, mnt_slave);
+                       list_del_init(&slave_mnt->mnt_slave);
+                       slave_mnt->mnt_master = NULL;
+               }
+               return 0;
        }
        list_for_each_entry(slave_mnt, &mnt->mnt_slave_list, mnt_slave)
                slave_mnt->mnt_master = master;
@@ -443,11 +448,6 @@ static inline bool is_candidate(struct mount *m)
        return m->mnt_t_flags & T_UMOUNT_CANDIDATE;
 }
 
-static inline bool will_be_unmounted(struct mount *m)
-{
-       return m->mnt.mnt_flags & MNT_UMOUNT;
-}
-
 static void umount_one(struct mount *m, struct list_head *to_umount)
 {
        m->mnt.mnt_flags |= MNT_UMOUNT;