From 1ebbb0b0f4a243990bfd98d593fa03ddda1fd523 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 16 Oct 2025 12:58:06 +0100 Subject: [PATCH 1/3] test: add coverage for RootImage= in user units Follow-up for 046a1487db00ca1a98b8cc3f5bcecb8b1f1a214b --- test/units/TEST-50-DISSECT.mountfsd.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index d2ce67714a..52fa94ecc5 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -67,6 +67,14 @@ systemd-dissect --image-policy='root=verity+signed:=absent+unused' --mtree /var/ # This should fail before we install the key (! systemd-dissect --image-policy='root=signed:=absent+unused' --mtree /var/tmp/unpriv.raw >/dev/null) +# If the kernel support is present unprivileged user units should be able to use verity images too +if [ "$VERITY_SIG_SUPPORTED" -eq 1 ]; then + systemd-run -M testuser@ --user --pipe --wait \ + --property PrivateUsers=yes \ + --property RootImage="$MINIMAL_IMAGE.gpt" \ + test -e "/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity" +fi + # Install key in keychain mkdir -p /run/verity.d cp /tmp/test-50-unpriv-cert.crt /run/verity.d/ From 29e97643e7772ded441686c911b048e0dec9129c Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Wed, 15 Oct 2025 18:49:16 +0100 Subject: [PATCH 2/3] Support ExtensionImages=/MountImages= in user services via mountfsd Support for RootImage= was added by 046a1487db00ca1a98b8cc3f5bcecb8b1f1a214b but it was not wired in for ExtensionImages=/MountImages= --- src/core/namespace.c | 8 +-- src/shared/dissect-image.c | 106 ++++++++++++++++++++++--------------- src/shared/dissect-image.h | 2 +- src/shared/mount-util.c | 3 ++ 4 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 16159fdf84..00d760f7ff 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1617,7 +1617,8 @@ static int mount_mqueuefs(const MountEntry *m) { static int mount_image( MountEntry *m, const char *root_directory, - const ImagePolicy *image_policy) { + const ImagePolicy *image_policy, + RuntimeScope runtime_scope) { _cleanup_(extension_release_data_done) ExtensionReleaseData rdata = {}; ImageClass required_class = _IMAGE_CLASS_INVALID; @@ -1652,6 +1653,7 @@ static int mount_image( &rdata, required_class, &m->verity, + runtime_scope, /* ret_image= */ NULL); if (r == -ENOENT && m->ignore) return 0; @@ -2038,10 +2040,10 @@ static int apply_one_mount( return mount_mqueuefs(m); case MOUNT_IMAGE: - return mount_image(m, NULL, p->mount_image_policy); + return mount_image(m, NULL, p->mount_image_policy, p->runtime_scope); case MOUNT_EXTENSION_IMAGE: - return mount_image(m, root_directory, p->extension_image_policy); + return mount_image(m, root_directory, p->extension_image_policy, p->runtime_scope); case MOUNT_OVERLAY: return mount_overlay(m); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 4554848e92..de9475e6d3 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -60,6 +60,7 @@ #include "proc-cmdline.h" #include "process-util.h" #include "resize-fs.h" +#include "runtime-scope.h" #include "signal-util.h" #include "siphash24.h" #include "stat-util.h" @@ -4416,11 +4417,13 @@ int verity_dissect_and_mount( const ExtensionReleaseData *extension_release_data, ImageClass required_class, VeritySettings *verity, + RuntimeScope runtime_scope, DissectedImage **ret_image) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_(verity_settings_done) VeritySettings local_verity = VERITY_SETTINGS_DEFAULT; + _cleanup_close_ int userns_fd = -EBADF; DissectImageFlags dissect_image_flags; bool relax_extension_release_check; int r; @@ -4451,55 +4454,70 @@ int verity_dissect_and_mount( DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | DISSECT_IMAGE_VERITY_SHARE; - /* Note that we don't use loop_device_make here, as the FD is most likely O_PATH which would not be - * accepted by LOOP_CONFIGURE, so just let loop_device_make_by_path reopen it as a regular FD. */ - r = loop_device_make_by_path( - src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, - /* open_flags= */ -1, - /* sector_size= */ UINT32_MAX, - verity->data_path ? 0 : LO_FLAGS_PARTSCAN, - LOCK_SH, - &loop_device); - if (r < 0) - return log_debug_errno(r, "Failed to create loop device for image: %m"); + if (runtime_scope == RUNTIME_SCOPE_SYSTEM) { + /* Note that we don't use loop_device_make here, as the FD is most likely O_PATH which would not be + * accepted by LOOP_CONFIGURE, so just let loop_device_make_by_path reopen it as a regular FD. */ + r = loop_device_make_by_path( + src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, + /* open_flags= */ -1, + /* sector_size= */ UINT32_MAX, + verity->data_path ? 0 : LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop_device); + if (r < 0) + return log_debug_errno(r, "Failed to create loop device for image: %m"); - r = dissect_loop_device( - loop_device, - verity, - options, - image_policy, - image_filter, - dissect_image_flags, - &dissected_image); - /* No partition table? Might be a single-filesystem image, try again */ - if (!verity->data_path && r == -ENOPKG) - r = dissect_loop_device( + r = dissect_loop_device( loop_device, verity, options, image_policy, image_filter, - dissect_image_flags | DISSECT_IMAGE_NO_PARTITION_TABLE, + dissect_image_flags, &dissected_image); - if (r < 0) - return log_debug_errno(r, "Failed to dissect image: %m"); + /* No partition table? Might be a single-filesystem image, try again */ + if (!verity->data_path && r == -ENOPKG) + r = dissect_loop_device( + loop_device, + verity, + options, + image_policy, + image_filter, + dissect_image_flags | DISSECT_IMAGE_NO_PARTITION_TABLE, + &dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_load_verity_sig_partition(dissected_image, loop_device->fd, verity); - if (r < 0) - return r; + r = dissected_image_load_verity_sig_partition(dissected_image, loop_device->fd, verity); + if (r < 0) + return r; - r = dissected_image_guess_verity_roothash(dissected_image, verity); - if (r < 0) - return r; + r = dissected_image_guess_verity_roothash(dissected_image, verity); + if (r < 0) + return r; - r = dissected_image_decrypt( - dissected_image, - NULL, - verity, - image_policy, - dissect_image_flags); - if (r < 0) - return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + r = dissected_image_decrypt( + dissected_image, + NULL, + verity, + image_policy, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + } else { + userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); + + r = mountfsd_mount_image( + src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, + userns_fd, + image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + return r; + } if (dest) { r = mkdir_p_label(dest, 0755); @@ -4515,14 +4533,16 @@ int verity_dissect_and_mount( dest, /* uid_shift= */ UID_INVALID, /* uid_range= */ UID_INVALID, - /* userns_fd= */ -EBADF, + userns_fd, dissect_image_flags); if (r < 0) return log_debug_errno(r, "Failed to mount image: %m"); - r = loop_device_flock(loop_device, LOCK_UN); - if (r < 0) - return log_debug_errno(r, "Failed to unlock loopback device: %m"); + if (loop_device) { + r = loop_device_flock(loop_device, LOCK_UN); + if (r < 0) + return log_debug_errno(r, "Failed to unlock loopback device: %m"); + } /* If we got os-release values from the caller, then we need to match them with the image's * extension-release.d/ content. Return -EINVAL if there's any mismatch. diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index cd87ff769a..a050f10701 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -238,7 +238,7 @@ bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesi int mount_image_privately_interactively(const char *path, const ImagePolicy *image_policy, DissectImageFlags flags, char **ret_directory, int *ret_dir_fd, LoopDevice **ret_loop_device); -int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const ImagePolicy *image_policy, const ImageFilter *image_filter, const ExtensionReleaseData *required_release_data, ImageClass required_class, VeritySettings *verity, DissectedImage **ret_image); +int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const ImagePolicy *image_policy, const ImageFilter *image_filter, const ExtensionReleaseData *required_release_data, ImageClass required_class, VeritySettings *verity, RuntimeScope runtime_scope, DissectedImage **ret_image); int dissect_fstype_ok(const char *fstype); diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index d4b86e3fe9..c6d1373caf 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -27,6 +27,7 @@ #include "path-util.h" #include "pidref.h" #include "process-util.h" +#include "runtime-scope.h" #include "set.h" #include "sort-util.h" #include "stat-util.h" @@ -1005,6 +1006,7 @@ static int mount_in_namespace_legacy( /* extension_release_data= */ NULL, /* required_class= */ _IMAGE_CLASS_INVALID, /* verity= */ NULL, + RUNTIME_SCOPE_SYSTEM, /* ret_image= */ NULL); else r = mount_follow_verbose(LOG_DEBUG, FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, NULL, MS_BIND, NULL); @@ -1227,6 +1229,7 @@ static int mount_in_namespace( /* extension_release_data= */ NULL, /* required_class= */ _IMAGE_CLASS_INVALID, /* verity= */ NULL, + RUNTIME_SCOPE_SYSTEM, &img); if (r < 0) return log_debug_errno(r, From 68b476a29838c73c17af7eac7866704bba3da3f0 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Wed, 15 Oct 2025 20:05:03 +0100 Subject: [PATCH 3/3] core: also enable PrivateUsers= for user services when using images via mountfsd RootDirectory= and other options already implicitly enable PrivateUsers= since 6ef721cbc7dadee4ae878ecf0076d87e57233908 if they are set in user units, so that they can work out of the box. Now with mountfsd support we can do the same for the images settings, so enable them and document them. --- man/system-or-user-ns-mountfsd.xml | 23 +++++++++++++++++++++++ man/systemd.exec.xml | 8 ++++---- src/core/exec-invoke.c | 3 +++ test/units/TEST-50-DISSECT.mountfsd.sh | 1 - 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 man/system-or-user-ns-mountfsd.xml diff --git a/man/system-or-user-ns-mountfsd.xml b/man/system-or-user-ns-mountfsd.xml new file mode 100644 index 0000000000..192090f396 --- /dev/null +++ b/man/system-or-user-ns-mountfsd.xml @@ -0,0 +1,23 @@ + + + + + + + + + <para id="singular">When enabled for services running in per-user instances of the service manager + this option implicitly enables <varname>PrivateUsers=</varname> (requires unprivileged user namespaces + support to be enabled in the kernel via the <literal>kernel.unprivileged_userns_clone=</literal> sysctl) + and also relies on + <citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para> + + <para id="plural">When enabled for services running in per-user instances of the service manager + these options implicitly enable <varname>PrivateUsers=</varname> (requires unprivileged user namespaces + support to be enabled in the kernel via the <literal>kernel.unprivileged_userns_clone=</literal> sysctl) + and also rely on + <citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para> + +</refsect1> diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index c0c147045e..6ffe3b9989 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -201,7 +201,7 @@ <xi:include href="vpick.xml" xpointer="image"/> - <xi:include href="system-only.xml" xpointer="singular"/> + <xi:include href="system-or-user-ns-mountfsd.xml" xpointer="singular"/> <xi:include href="version-info.xml" xpointer="v233"/></listitem> </varlistentry> @@ -225,7 +225,7 @@ <constant>esp</constant>, <constant>xbootldr</constant>, <constant>tmp</constant>, <constant>var</constant>.</para> - <xi:include href="system-only.xml" xpointer="singular"/> + <xi:include href="system-or-user-ns-mountfsd.xml" xpointer="singular"/> <xi:include href="version-info.xml" xpointer="v247"/></listitem> </varlistentry> @@ -523,7 +523,7 @@ <varname>PrivateDevices=</varname> below, as it may change the setting of <varname>DevicePolicy=</varname>.</para> - <xi:include href="system-only.xml" xpointer="singular"/> + <xi:include href="system-or-user-ns-mountfsd.xml" xpointer="singular"/> <xi:include href="version-info.xml" xpointer="v247"/></listitem> </varlistentry> @@ -590,7 +590,7 @@ <xi:include href="vpick.xml" xpointer="image"/> - <xi:include href="system-only.xml" xpointer="singular"/> + <xi:include href="system-or-user-ns-mountfsd.xml" xpointer="singular"/> <xi:include href="version-info.xml" xpointer="v248"/></listitem> </varlistentry> diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index e02d2ddee6..93b5080ff6 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4486,6 +4486,9 @@ static bool exec_needs_cap_sys_admin(const ExecContext *context, const ExecParam context->n_temporary_filesystems > 0 || context->root_directory || !strv_isempty(context->extension_directories) || + context->root_image || + context->n_mount_images > 0 || + context->n_extension_images > 0 || context->protect_system != PROTECT_SYSTEM_NO || context->protect_home != PROTECT_HOME_NO || exec_needs_pid_namespace(context, params) || diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index 52fa94ecc5..b6ff5012bf 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -70,7 +70,6 @@ systemd-dissect --image-policy='root=verity+signed:=absent+unused' --mtree /var/ # If the kernel support is present unprivileged user units should be able to use verity images too if [ "$VERITY_SIG_SUPPORTED" -eq 1 ]; then systemd-run -M testuser@ --user --pipe --wait \ - --property PrivateUsers=yes \ --property RootImage="$MINIMAL_IMAGE.gpt" \ test -e "/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity" fi