]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared: use move_pivot_root() for services
authorChristian Brauner <brauner@kernel.org>
Wed, 23 Nov 2022 15:15:20 +0000 (16:15 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Thu, 24 Nov 2022 09:58:26 +0000 (10:58 +0100)
Currently, services use mount_move_root() in order to setup the root
directory of services using a mount namespace. This relies on MS_MOVE
and chroot(). However, this has serious drawbacks even for relatively
simple mount propagation scenarios.

What systemd currently does is roughly equivalent to the following shell
code:

  unshare --mount --propagation=shared
  cd /
  mount --make-rslave /
  mkdir /new-root
  mount --rbind / /new-root
  cd /new-root
  mount --move /new-root /
  chroot .

This looks simple enough but has the consequence that two separate mount
trees exist for the lifetime of the service. The first one was created
when the mount namespace was created, and the second one when a new
mount for the rootfs was created. The first mount tree sticks around as
a shadow mount tree. Both mount trees are dependent mounts with the host
rootfs as their dominating mount.

Now, when mount propagation is triggered by the host by e.g.,

   mount --bind /opt /mnt

it means that two propagation events are generated. I'm skipping over
the exact kernel details as they aren't that important. The gist is that
for every propagation event that is generated a second one is generated
for the shadow mount tree. In other words, the kernel creates two copies
for each mount that is propagated instead of one.

This isn't necessary. We can simply change the sequence above to:

  unshare --mount --propagation=shared
  cd /
  mount --make-rslave /
  mkdir /new-root
  # stash fd to old rootfs
  # stash fd to new rootfs
  mount --rbind / /new-root
  mkdir /new-root
  cd /new-root
  pivot_root . .
  # new root is tucked under old root
  # chdir into old rootfs via stashed fd
  umount -l /old-root

The pivot_root allows us to get rid of the old mount tree that was
created when the mount namespace was created. So after this sequence
only one mount tree is alive. Plus, it's safer and nicer. Moving mounts
isn't pleasnt.

This patch doesn't convert nspawn yet as the requirements are more
tricky given that it wants to preserve the rootfs as a shared mount
which goes against pivot_root() requirements.

Signed-off-by: Christian Brauner (Microsoft) <brauner@kernel.org>
src/core/namespace.c
src/shared/mount-util.c
src/shared/mount-util.h

index 7752e48fb0bfbb8f0e8cc0a4033f60722e556f58..c0d0cc9715f3aec892dc07429c0a88f703bd65e1 100644 (file)
@@ -2486,7 +2486,7 @@ int setup_namespace(
                 goto finish;
 
         /* MS_MOVE does not work on MS_SHARED so the remount MS_SHARED will be done later */
-        r = mount_move_root(root);
+        r = mount_pivot_root(root);
         if (r == -EINVAL && root_directory) {
                 /* If we are using root_directory and we don't have privileges (ie: user manager in a user
                  * namespace) and the root_directory is already a mount point in the parent namespace,
@@ -2496,7 +2496,7 @@ int setup_namespace(
                 r = mount_nofollow_verbose(LOG_DEBUG, root, root, NULL, MS_BIND|MS_REC, NULL);
                 if (r < 0)
                         goto finish;
-                r = mount_move_root(root);
+                r = mount_pivot_root(root);
         }
         if (r < 0) {
                 log_debug_errno(r, "Failed to mount root with MS_MOVE: %m");
index 1f827e206111bda8357db1f02a30db348181f306..681d698800b1ab8477f9093b3c8a7eddd81c2787 100644 (file)
@@ -21,6 +21,7 @@
 #include "fs-util.h"
 #include "glyph-util.h"
 #include "hashmap.h"
+#include "initrd-util.h"
 #include "label.h"
 #include "libmount-util.h"
 #include "missing_mount.h"
@@ -489,6 +490,52 @@ int mount_move_root(const char *path) {
         return RET_NERRNO(chdir("/"));
 }
 
+int mount_pivot_root(const char *path) {
+        _cleanup_close_ int fd_oldroot = -EBADF, fd_newroot = -EBADF;
+
+        assert(path);
+
+        /* pivot_root() isn't currently supported in the initramfs. */
+        if (in_initrd())
+                return mount_move_root(path);
+
+        fd_oldroot = open("/", O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+        if (fd_oldroot < 0)
+                return log_debug_errno(errno, "Failed to open old rootfs");
+
+        fd_newroot = open(path, O_PATH|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+        if (fd_newroot < 0)
+                return log_debug_errno(errno, "Failed to open new rootfs '%s': %m", path);
+
+        /* Change into the new rootfs. */
+        if (fchdir(fd_newroot) < 0)
+                return log_debug_errno(errno, "Failed to change into new rootfs '%s': %m", path);
+
+        /* Let the kernel tuck the new root under the old one. */
+        if (pivot_root(".", ".") < 0)
+                return log_debug_errno(errno, "Failed to pivot root to new rootfs '%s': %m", path);
+
+
+        /* At this point the new root is tucked under the old root. If we want
+         * to unmount it we cannot be fchdir()ed into it. So escape back to the
+         * old root. */
+        if (fchdir(fd_oldroot) < 0)
+                return log_debug_errno(errno, "Failed to change back to old rootfs: %m");
+
+        /* Note, usually we should set mount propagation up here but we'll
+         * assume that the caller has already done that. */
+
+        /* Get rid of the old root and reveal our brand new root. */
+        if (umount2(".", MNT_DETACH) < 0)
+                return log_debug_errno(errno, "Failed to unmount old rootfs: %m");
+
+        if (fchdir(fd_newroot) < 0)
+                return log_debug_errno(errno, "Failed to switch to new rootfs '%s': %m", path);
+
+        return 0;
+}
+
+
 int repeat_unmount(const char *path, int flags) {
         bool done = false;
 
index 8b07611ec858c815f0b7980df18c0b30b8e4f2fe..29b9ed02f7ca5efda2db43b58caa09469805a581 100644 (file)
@@ -55,6 +55,7 @@ static inline int bind_remount_recursive(const char *prefix, unsigned long new_f
 int bind_remount_one_with_mountinfo(const char *path, unsigned long new_flags, unsigned long flags_mask, FILE *proc_self_mountinfo);
 
 int mount_move_root(const char *path);
+int mount_pivot_root(const char *path);
 
 DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(FILE*, endmntent, NULL);
 #define _cleanup_endmntent_ _cleanup_(endmntentp)