diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 7247f3d2fc..d8319318a9 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -1310,11 +1310,16 @@ node /org/freedesktop/systemd1 { TryRestartUnit() or ReloadOrTryRestartUnit() for the marked units. - BindMountUnit() can be used to bind mount new files or directories into - a running service mount namespace. + BindMountUnit() can be used to bind mount new files or directories into a + running service mount namespace. If supported by the kernel, any prior mount on the selected target + will be replaced by the new mount. If not supported, any prior mount will be over-mounted, but remain + pinned and inaccessible. + MountImageUnit() can be used to mount new images into a running service - mount namespace. + mount namespace. If supported by the kernel, any prior mount on the selected target will be replaced + by the new mount. If not supported, any prior mount will be over-mounted, but remain pinned and + inaccessible. KillUnit() may be used to kill (i.e. send a signal to) all processes of a unit. It takes the unit name, an enum who and a UNIX diff --git a/man/systemctl.xml b/man/systemctl.xml index d07c8eae28..a673c18c0e 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -663,6 +663,10 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err , , etc.) run in distinct namespaces. + If supported by the kernel, any prior mount on the selected target will be replaced by the + new mount. If not supported, any prior mount will be over-mounted, but remain pinned and + inaccessible. + @@ -693,6 +697,10 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err , , etc.) run in distinct namespaces. + If supported by the kernel, any prior mount on the selected target will be replaced by the + new mount. If not supported, any prior mount will be over-mounted, but remain pinned and + inaccessible. + Example: systemctl mount-image foo.service /tmp/img.raw /var/lib/image root:ro,nosuid systemctl mount-image --mkdir bar.service /tmp/img.raw /var/lib/baz/img diff --git a/src/basic/missing_syscall.h b/src/basic/missing_syscall.h index ddee457f44..83ec137fa5 100644 --- a/src/basic/missing_syscall.h +++ b/src/basic/missing_syscall.h @@ -539,6 +539,10 @@ static inline int missing_open_tree( /* ======================================================================= */ +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif + #if !HAVE_MOVE_MOUNT #ifndef MOVE_MOUNT_F_EMPTY_PATH diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 91d2ca3025..aa81a4b9ba 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2007,8 +2007,12 @@ static int mount_partition( if (m->fsmount_fd >= 0) { /* Case #1: Attach existing fsmount fd to the file system */ - if (move_mount(m->fsmount_fd, "", -EBADF, p, MOVE_MOUNT_F_EMPTY_PATH) < 0) - return -errno; + r = mount_exchange_graceful( + m->fsmount_fd, + p, + FLAGS_SET(flags, DISSECT_IMAGE_TRY_ATOMIC_MOUNT_EXCHANGE)); + if (r < 0) + return log_debug_errno(r, "Failed to mount image on '%s': %m", p); } else { assert(node); diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index f25686a7fd..979fd384fe 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -56,36 +56,37 @@ struct DissectedPartition { }) typedef enum DissectImageFlags { - DISSECT_IMAGE_DEVICE_READ_ONLY = 1 << 0, /* Make device read-only */ - DISSECT_IMAGE_DISCARD_ON_LOOP = 1 << 1, /* Turn on "discard" if on a loop device and file system supports it */ - DISSECT_IMAGE_DISCARD = 1 << 2, /* Turn on "discard" if file system supports it, on all block devices */ - DISSECT_IMAGE_DISCARD_ON_CRYPTO = 1 << 3, /* Turn on "discard" also on crypto devices */ - DISSECT_IMAGE_DISCARD_ANY = DISSECT_IMAGE_DISCARD_ON_LOOP | - DISSECT_IMAGE_DISCARD | - DISSECT_IMAGE_DISCARD_ON_CRYPTO, - DISSECT_IMAGE_GPT_ONLY = 1 << 4, /* Only recognize images with GPT partition tables */ - DISSECT_IMAGE_GENERIC_ROOT = 1 << 5, /* If no partition table or only single generic partition, assume it's the root fs */ - DISSECT_IMAGE_MOUNT_ROOT_ONLY = 1 << 6, /* Mount only the root and /usr partitions */ - DISSECT_IMAGE_MOUNT_NON_ROOT_ONLY = 1 << 7, /* Mount only the non-root and non-/usr partitions */ - DISSECT_IMAGE_VALIDATE_OS = 1 << 8, /* Refuse mounting images that aren't identifiable as OS images */ - DISSECT_IMAGE_VALIDATE_OS_EXT = 1 << 9, /* Refuse mounting images that aren't identifiable as OS extension images */ - DISSECT_IMAGE_RELAX_VAR_CHECK = 1 << 10, /* Don't insist that the UUID of /var is hashed from /etc/machine-id */ - DISSECT_IMAGE_FSCK = 1 << 11, /* File system check the partition before mounting (no effect when combined with DISSECT_IMAGE_READ_ONLY) */ - DISSECT_IMAGE_NO_PARTITION_TABLE = 1 << 12, /* Only recognize single file system images */ - DISSECT_IMAGE_VERITY_SHARE = 1 << 13, /* When activating a verity device, reuse existing one if already open */ - DISSECT_IMAGE_MKDIR = 1 << 14, /* Make top-level directory to mount right before mounting, if missing */ - DISSECT_IMAGE_USR_NO_ROOT = 1 << 15, /* If no root fs is in the image, but /usr is, then allow this (so that we can mount the rootfs as tmpfs or so */ - DISSECT_IMAGE_REQUIRE_ROOT = 1 << 16, /* Don't accept disks without root partition (or at least /usr partition if DISSECT_IMAGE_USR_NO_ROOT is set) */ - DISSECT_IMAGE_MOUNT_READ_ONLY = 1 << 17, /* Make mounts read-only */ - DISSECT_IMAGE_READ_ONLY = DISSECT_IMAGE_DEVICE_READ_ONLY | - DISSECT_IMAGE_MOUNT_READ_ONLY, - DISSECT_IMAGE_GROWFS = 1 << 18, /* Grow file systems in partitions marked for that to the size of the partitions after mount */ - DISSECT_IMAGE_MOUNT_IDMAPPED = 1 << 19, /* Mount mounts with kernel 5.12-style userns ID mapping, if file system type doesn't support uid=/gid= */ - DISSECT_IMAGE_ADD_PARTITION_DEVICES = 1 << 20, /* Create partition devices via BLKPG_ADD_PARTITION */ - DISSECT_IMAGE_PIN_PARTITION_DEVICES = 1 << 21, /* Open dissected partitions and decrypted partitions and pin them by fd */ - DISSECT_IMAGE_RELAX_EXTENSION_CHECK = 1 << 22, /* Don't insist that the extension-release file name matches the image name */ - DISSECT_IMAGE_DISKSEQ_DEVNODE = 1 << 23, /* Prefer /dev/disk/by-diskseq/… device nodes */ - DISSECT_IMAGE_ALLOW_EMPTY = 1 << 24, /* Allow that no usable partitions is present */ + DISSECT_IMAGE_DEVICE_READ_ONLY = 1 << 0, /* Make device read-only */ + DISSECT_IMAGE_DISCARD_ON_LOOP = 1 << 1, /* Turn on "discard" if on a loop device and file system supports it */ + DISSECT_IMAGE_DISCARD = 1 << 2, /* Turn on "discard" if file system supports it, on all block devices */ + DISSECT_IMAGE_DISCARD_ON_CRYPTO = 1 << 3, /* Turn on "discard" also on crypto devices */ + DISSECT_IMAGE_DISCARD_ANY = DISSECT_IMAGE_DISCARD_ON_LOOP | + DISSECT_IMAGE_DISCARD | + DISSECT_IMAGE_DISCARD_ON_CRYPTO, + DISSECT_IMAGE_GPT_ONLY = 1 << 4, /* Only recognize images with GPT partition tables */ + DISSECT_IMAGE_GENERIC_ROOT = 1 << 5, /* If no partition table or only single generic partition, assume it's the root fs */ + DISSECT_IMAGE_MOUNT_ROOT_ONLY = 1 << 6, /* Mount only the root and /usr partitions */ + DISSECT_IMAGE_MOUNT_NON_ROOT_ONLY = 1 << 7, /* Mount only the non-root and non-/usr partitions */ + DISSECT_IMAGE_VALIDATE_OS = 1 << 8, /* Refuse mounting images that aren't identifiable as OS images */ + DISSECT_IMAGE_VALIDATE_OS_EXT = 1 << 9, /* Refuse mounting images that aren't identifiable as OS extension images */ + DISSECT_IMAGE_RELAX_VAR_CHECK = 1 << 10, /* Don't insist that the UUID of /var is hashed from /etc/machine-id */ + DISSECT_IMAGE_FSCK = 1 << 11, /* File system check the partition before mounting (no effect when combined with DISSECT_IMAGE_READ_ONLY) */ + DISSECT_IMAGE_NO_PARTITION_TABLE = 1 << 12, /* Only recognize single file system images */ + DISSECT_IMAGE_VERITY_SHARE = 1 << 13, /* When activating a verity device, reuse existing one if already open */ + DISSECT_IMAGE_MKDIR = 1 << 14, /* Make top-level directory to mount right before mounting, if missing */ + DISSECT_IMAGE_USR_NO_ROOT = 1 << 15, /* If no root fs is in the image, but /usr is, then allow this (so that we can mount the rootfs as tmpfs or so */ + DISSECT_IMAGE_REQUIRE_ROOT = 1 << 16, /* Don't accept disks without root partition (or at least /usr partition if DISSECT_IMAGE_USR_NO_ROOT is set) */ + DISSECT_IMAGE_MOUNT_READ_ONLY = 1 << 17, /* Make mounts read-only */ + DISSECT_IMAGE_READ_ONLY = DISSECT_IMAGE_DEVICE_READ_ONLY | + DISSECT_IMAGE_MOUNT_READ_ONLY, + DISSECT_IMAGE_GROWFS = 1 << 18, /* Grow file systems in partitions marked for that to the size of the partitions after mount */ + DISSECT_IMAGE_MOUNT_IDMAPPED = 1 << 19, /* Mount mounts with kernel 5.12-style userns ID mapping, if file system type doesn't support uid=/gid= */ + DISSECT_IMAGE_ADD_PARTITION_DEVICES = 1 << 20, /* Create partition devices via BLKPG_ADD_PARTITION */ + DISSECT_IMAGE_PIN_PARTITION_DEVICES = 1 << 21, /* Open dissected partitions and decrypted partitions and pin them by fd */ + DISSECT_IMAGE_RELAX_EXTENSION_CHECK = 1 << 22, /* Don't insist that the extension-release file name matches the image name */ + DISSECT_IMAGE_DISKSEQ_DEVNODE = 1 << 23, /* Prefer /dev/disk/by-diskseq/… device nodes */ + DISSECT_IMAGE_ALLOW_EMPTY = 1 << 24, /* Allow that no usable partitions is present */ + DISSECT_IMAGE_TRY_ATOMIC_MOUNT_EXCHANGE = 1 << 25, /* Try to mount the image beneath the specified mountpoint, rather than on top of it, and then umount the top */ } DissectImageFlags; struct DissectedImage { diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index 011508e632..b6d2b6b615 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -730,6 +730,45 @@ int umount_verbose( return 0; } +int mount_exchange_graceful(int fsmount_fd, const char *dest, bool mount_beneath) { + int r; + + assert(fsmount_fd >= 0); + assert(dest); + + /* First, try to mount beneath an existing mount point, and if that works, umount the old mount, + * which is now at the top. This will ensure we can atomically replace a mount. Note that this works + * also in the case where there are submounts down the tree. Mount propagation is allowed but + * restricted to layouts that don't end up propagation the new mount on top of the mount stack. If + * this is not supported (minimum kernel v6.5), or if there is no mount on the mountpoint, we get + * -EINVAL and then we fallback to normal mounting. */ + + r = RET_NERRNO(move_mount( + fsmount_fd, + /* from_path= */ "", + /* to_fd= */ -EBADF, + dest, + MOVE_MOUNT_F_EMPTY_PATH | (mount_beneath ? MOVE_MOUNT_BENEATH : 0))); + if (mount_beneath) { + if (r == -EINVAL) { /* Fallback if mount_beneath is not supported */ + log_debug_errno(r, + "Failed to mount beneath '%s', falling back to overmount", + dest); + return RET_NERRNO(move_mount( + fsmount_fd, + /* from_path= */ "", + /* to_fd= */ -EBADF, + dest, + MOVE_MOUNT_F_EMPTY_PATH)); + } + + if (r >= 0) /* If it is, now remove the old mount */ + return umount_verbose(LOG_DEBUG, dest, UMOUNT_NOFOLLOW|MNT_DETACH); + } + + return r; +} + int mount_option_mangle( const char *options, unsigned long mount_flags, @@ -1155,7 +1194,7 @@ static int mount_in_namespace( (void) mkdir_parents(dest, 0755); if (img) { - DissectImageFlags f = 0; + DissectImageFlags f = DISSECT_IMAGE_TRY_ATOMIC_MOUNT_EXCHANGE; if (make_file_or_directory) f |= DISSECT_IMAGE_MKDIR; @@ -1174,11 +1213,7 @@ static int mount_in_namespace( if (make_file_or_directory) (void) make_mount_point_inode_from_stat(&st, dest, 0700); - r = RET_NERRNO(move_mount(new_mount_fd, - "", - -EBADF, - dest, - MOVE_MOUNT_F_EMPTY_PATH)); + r = mount_exchange_graceful(new_mount_fd, dest, /* mount_beneath= */ true); } if (r < 0) { (void) write(errno_pipe_fd[1], &r, sizeof(r)); diff --git a/src/shared/mount-util.h b/src/shared/mount-util.h index a56530c42e..7c0189480e 100644 --- a/src/shared/mount-util.h +++ b/src/shared/mount-util.h @@ -68,6 +68,8 @@ int umount_verbose( const char *where, int flags); +int mount_exchange_graceful(int fsmount_fd, const char *dest, bool mount_beneath); + int mount_option_mangle( const char *options, unsigned long mount_flags, diff --git a/test/units/testsuite-23.runtime-bind-paths.sh b/test/units/testsuite-23.runtime-bind-paths.sh index 8dc4d9123c..65c2dbf41d 100755 --- a/test/units/testsuite-23.runtime-bind-paths.sh +++ b/test/units/testsuite-23.runtime-bind-paths.sh @@ -23,8 +23,12 @@ systemctl start testsuite-23-namespaced.service # Ensure that inaccessible paths aren't bypassed by the runtime setup, (! systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-fixed /run/inaccessible/testfile-marker-fixed) +echo "MARKER_WRONG" >/run/testsuite-23-marker-wrong echo "MARKER_RUNTIME" >/run/testsuite-23-marker-runtime +# Mount twice to exercise mount-beneath (on kernel 6.5+, on older kernels it will just overmount) +systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-wrong /tmp/testfile-marker-runtime +test "$(systemctl show -P SubState testsuite-23-namespaced.service)" = "running" systemctl bind --mkdir testsuite-23-namespaced.service /run/testsuite-23-marker-runtime /tmp/testfile-marker-runtime timeout 10 bash -xec 'while [[ "$(systemctl show -P SubState testsuite-23-namespaced.service)" == running ]]; do sleep .5; done' diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 3a719d2059..4142e1ebbc 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -363,6 +363,11 @@ ExecStart=/bin/sh -c ' \\ EOF systemctl start testservice-50d.service +# Mount twice to exercise mount-beneath (on kernel 6.5+, on older kernels it will just overmount) +mkdir -p /tmp/wrong/foo +mksquashfs /tmp/wrong/foo /tmp/wrong.raw +systemctl mount-image --mkdir testservice-50d.service /tmp/wrong.raw /tmp/img +test "$(systemctl show -P SubState testservice-50d.service)" = "running" systemctl mount-image --mkdir testservice-50d.service "${image}.raw" /tmp/img root:nosuid while systemctl show -P SubState testservice-50d.service | grep -q running