diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index 1654e49d84..20eda5b0ef 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -1838,11 +1838,35 @@ int get_sub_mounts(const char *prefix, SubMount **ret_mounts, size_t *ret_n_moun continue; } - mount_fd = RET_NERRNO(open_tree(AT_FDCWD, path, OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_RECURSIVE)); - if (mount_fd == -ENOENT) /* The path may be hidden by another over-mount or already unmounted. */ - continue; - if (mount_fd < 0) - return log_debug_errno(mount_fd, "Failed to open subtree of mounted filesystem '%s': %m", path); + /* If possible on a newer kernel, use MS_PRIVATE to decouple it from the original mount. + * Otherwise MNT_DETACH of the source path could propagate through and unmount the + * just-moved nested children at the destination (relevant for preserving nested mounts + * under sysext hierarchies). */ + static bool mount_attr_unsupported = false; + + if (!mount_attr_unsupported) { + mount_fd = open_tree_attr_with_fallback( + AT_FDCWD, path, + OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_RECURSIVE, + &(struct mount_attr) { .propagation = MS_PRIVATE }); + if (mount_fd == -ENOENT) /* The path may be hidden by another over-mount or already unmounted. */ + continue; + if (mount_fd < 0 && ERRNO_IS_NEG_NOT_SUPPORTED(mount_fd)) { + /* On a kernel older than 5.12 without mount_setattr() we do the regular + * clone. Nested mounts under sysext and similar cases may get lost. */ + log_debug_errno(mount_fd, "mount_setattr() not supported, falling back to plain open_tree() without MS_PRIVATE: %m"); + mount_attr_unsupported = true; + } else if (mount_fd < 0) + return log_debug_errno(mount_fd, "Failed to open subtree of mounted filesystem '%s': %m", path); + } + + if (mount_attr_unsupported) { + mount_fd = RET_NERRNO(open_tree(AT_FDCWD, path, OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_RECURSIVE)); + if (mount_fd == -ENOENT) + continue; + if (mount_fd < 0) + return log_debug_errno(mount_fd, "Failed to open subtree of mounted filesystem '%s': %m", path); + } p = strdup(path); if (!p) diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 4cc9883628..e789af2e2f 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -480,9 +480,13 @@ static int move_submounts(const char *src, const char *dst) { if (child_fd < 0) return log_error_errno(errno, "Failed to pin mountpoint %s: %m", t); - r = mount_follow_verbose(LOG_ERR, m->path, FORMAT_PROC_FD_PATH(child_fd), /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); + /* Instead of a bind mount we attach the detached clone produced by + * open_tree_attr_with_fallback() from get_sub_mounts() because that has no propagation + * relationship with the original anymore and the MNT_DETACH below won't propagate for + * nested mounts. */ + r = RET_NERRNO(move_mount(m->mount_fd, "", child_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH)); if (r < 0) - return r; + return log_error_errno(r, "Failed to move mount '%s' to '%s': %m", m->path, t); (void) umount_verbose(LOG_WARNING, m->path, MNT_DETACH); } diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh index fd62dcfe81..fd7a164478 100755 --- a/test/units/TEST-50-DISSECT.sysext.sh +++ b/test/units/TEST-50-DISSECT.sysext.sh @@ -1863,6 +1863,57 @@ if systemctl --quiet is-active "$ext_unit"; then fi ) +( init_trap +: "Nested tmpfs submounts under the hierarchy survive merge/refresh/unmerge round-trip" +fake_root=${roots_dir:+"$roots_dir/nested-submounts"} +hierarchy=/opt + +# Don't run the test if the inner mount won't be preserved due to an old kernel +if ! systemd-analyze compare-versions "$(uname -r)" ge 5.12; then + echo >&2 "Kernel too old for mount_setattr (need >= 5.12), skipping nested submount test" + exit 0 +fi + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image "$fake_root" "$hierarchy" +prepare_hierarchy "$fake_root" "$hierarchy" + +# Two tmpfs mounts, one nested in the hierarchy under the other. Reproduces the nested mount layout from +# https://github.com/flatcar/Flatcar/issues/2111 and verifies that we preserve nested mounts across merge, +# refresh, and unmerge. +outer_mp="$fake_root$hierarchy/outer" +inner_mp="$outer_mp/inner" +mkdir -p "$outer_mp" +mount -t tmpfs tmpfs "$outer_mp" +prepend_trap "umount -l ${outer_mp@Q} 2>/dev/null || true" +mkdir -p "$inner_mp" +mount -t tmpfs tmpfs "$inner_mp" +prepend_trap "umount -l ${inner_mp@Q} 2>/dev/null || true" +touch "$outer_mp/outer-marker" +touch "$inner_mp/inner-marker" + +run_systemd_sysext "$fake_root" merge +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +mountpoint "$outer_mp" +mountpoint "$inner_mp" +test -f "$outer_mp/outer-marker" +test -f "$inner_mp/inner-marker" + +run_systemd_sysext "$fake_root" refresh --always-refresh=yes +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +mountpoint "$outer_mp" +mountpoint "$inner_mp" +test -f "$outer_mp/outer-marker" +test -f "$inner_mp/inner-marker" + +run_systemd_sysext "$fake_root" unmerge +extension_verify_after_unmerge "$fake_root" "$hierarchy" -h +mountpoint "$outer_mp" +mountpoint "$inner_mp" +test -f "$outer_mp/outer-marker" +test -f "$inner_mp/inner-marker" +) + } # End of run_sysext_tests