From 78b40aea611256228e898fc6a6dab414a1798889 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Tue, 21 Oct 2025 00:39:16 +0100 Subject: [PATCH 1/2] mountfsd: add support for mount options RootImageOptions=/ExtensionImages=/MountImages= all support custom mount options, but mountfsd does not support it. Add varlink parameters to allow callers to specify mount options so that those directives can work as expected. Given the security implications of some mount options (e.g.: ACLs, SELinux), require additional privileges to use custom mount options. Pass the list of requested options to polkit details, so that the policy can check the exact details if needed, for example to only allow nosuid and reject the rest. --- src/mountfsd/mountwork.c | 101 ++++++++++++++++-- .../varlink-io.systemd.MountFileSystem.c | 2 + 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 78b546fcfe..d1ed690939 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -17,6 +17,7 @@ #include "dissect-image.h" #include "env-util.h" #include "errno-util.h" +#include "escape.h" #include "fd-util.h" #include "fs-util.h" #include "format-util.h" @@ -61,6 +62,44 @@ static const ImagePolicy image_policy_untrusted = { .default_flags = PARTITION_POLICY_IGNORE, }; +static int json_dispatch_image_options(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + _cleanup_(mount_options_free_allp) MountOptions *options = NULL; + MountOptions **p = ASSERT_PTR(userdata); + + if (sd_json_variant_is_null(variant)) { + *p = mount_options_free_all(*p); + return 0; + } + + if (!sd_json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + const char *k; + sd_json_variant *e; + JSON_VARIANT_OBJECT_FOREACH(k, e, variant) { + PartitionDesignator pd = partition_designator_from_string(k); + if (pd < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid partition designator '%s'.", strna(k)); + + if (!sd_json_variant_is_string(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Mount option for partition '%s' is not a string.", strna(k)); + + if (!options) { + options = new0(MountOptions, 1); + if (!options) + return -ENOMEM; + } + + options->options[pd] = strdup(sd_json_variant_string(e)); + if (!options->options[pd]) + return -ENOMEM; + } + + mount_options_free_all(*p); + *p = TAKE_PTR(options); + return 0; +} + static int json_dispatch_image_policy(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { _cleanup_(image_policy_freep) ImagePolicy *q = NULL; ImagePolicy **p = ASSERT_PTR(userdata); @@ -92,6 +131,7 @@ typedef struct MountImageParameters { int growfs; char *password; ImagePolicy *image_policy; + MountOptions *options; bool verity_sharing; struct iovec verity_root_hash; struct iovec verity_root_hash_sig; @@ -105,6 +145,7 @@ static void mount_image_parameters_done(MountImageParameters *p) { p->image_policy = image_policy_free(p->image_policy); iovec_done(&p->verity_root_hash); iovec_done(&p->verity_root_hash_sig); + p->options = mount_options_free_all(p->options); } static int validate_image_fd(int fd, MountImageParameters *p) { @@ -285,6 +326,41 @@ static int validate_userns(sd_varlink *link, int *userns_fd) { return 0; } +static int mount_options_to_polkit_details(const MountOptions *options, char **ret_mount_options_concat) { + _cleanup_free_ char *mount_options_concat = NULL; + int r; + + assert(ret_mount_options_concat); + + if (!options) { + *ret_mount_options_concat = NULL; + return 0; + } + + for (PartitionDesignator i = 0; i < _PARTITION_DESIGNATOR_MAX; i++) { + _cleanup_free_ char *escaped = NULL; + + if (isempty(options->options[i])) + continue; + + escaped = shell_escape(options->options[i], ":"); + if (!escaped) + return log_oom_debug(); + + r = strextendf_with_separator( + &mount_options_concat, + ",", + "%s:%s", + partition_designator_to_string(i), + escaped); + if (r < 0) + return r; + } + + *ret_mount_options_concat = TAKE_PTR(mount_options_concat); + return 0; +} + static int vl_method_mount_image( sd_varlink *link, sd_json_variant *parameters, @@ -298,6 +374,7 @@ static int vl_method_mount_image( { "growFileSystems", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountImageParameters, growfs), 0 }, { "password", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(MountImageParameters, password), 0 }, { "imagePolicy", SD_JSON_VARIANT_STRING, json_dispatch_image_policy, offsetof(MountImageParameters, image_policy), 0 }, + { "mountOptions", SD_JSON_VARIANT_OBJECT, json_dispatch_image_options, offsetof(MountImageParameters, options), 0 }, { "veritySharing", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(MountImageParameters, verity_sharing), 0 }, { "verityDataFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountImageParameters, verity_data_fd_idx), 0 }, { "verityRootHash", SD_JSON_VARIANT_STRING, json_dispatch_unhex_iovec, offsetof(MountImageParameters, verity_root_hash), 0 }, @@ -362,10 +439,14 @@ static int vl_method_mount_image( if (r != 0) return r; - r = verify_trusted_image_fd_by_path(image_fd); - if (r < 0) - return r; - image_is_trusted = r; + /* Mount options could be used to thwart security measures such as ACLs or SELinux so if they are + * specified don't mark the image as trusted so that it requires additional privileges to use. */ + if (!p.options) { + r = verify_trusted_image_fd_by_path(image_fd); + if (r < 0) + return r; + image_is_trusted = r; + } if (p.verity_data_fd_idx != UINT_MAX) { verity_data_fd = sd_varlink_peek_dup_fd(link, p.verity_data_fd_idx); @@ -385,8 +466,16 @@ static int vl_method_mount_image( verity.root_hash_sig = TAKE_STRUCT(p.verity_root_hash_sig); } + /* Let the polkit rule know what mount options the caller tries to use, so that rules can decide + * whether to allow or deny the operation based on what the options are. */ + _cleanup_free_ char *mount_options_concat = NULL; + r = mount_options_to_polkit_details(p.options, &mount_options_concat); + if (r < 0) + return r; + const char *polkit_details[] = { "read_only", one_zero(p.read_only > 0), + !isempty(mount_options_concat) ? "mount_options" : NULL, mount_options_concat, NULL, }; @@ -410,7 +499,7 @@ static int vl_method_mount_image( r = varlink_verify_polkit_async_full( link, /* bus= */ NULL, - polkit_action, + p.options ? polkit_untrusted_action : polkit_action, /* Using mount options requires higher privs */ polkit_details, /* good_user= */ UID_INVALID, polkit_flags, @@ -482,7 +571,7 @@ static int vl_method_mount_image( r = dissect_loop_device( loop, &verity, - /* mount_options= */ NULL, + p.options, use_policy, /* image_filter= */ NULL, dissect_flags, diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c index 356f9938aa..121102164a 100644 --- a/src/shared/varlink-io.systemd.MountFileSystem.c +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -60,6 +60,8 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(password, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Takes an image policy string (see systemd.image-policy(7) for details) to apply while mounting the image"), SD_VARLINK_DEFINE_INPUT(imagePolicy, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The mount options to be used for the partitions of the image, keyed by the partition designator. Requires elevated privileges via polkit if specified, the polkit request details will list them in the 'mount_options' field."), + SD_VARLINK_DEFINE_INPUT(mountOptions, SD_VARLINK_STRING, SD_VARLINK_MAP|SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Whether to automatically reuse already set up dm-verity devices that share the same roothash."), SD_VARLINK_DEFINE_INPUT(veritySharing, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("File descriptor of the file containing the dm-verity data, if the image is a bare filesystem rather than a DDI."), From 3a8759e5d4b2923af43ea1f0eafb8ae9c2159417 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Tue, 21 Oct 2025 00:39:44 +0100 Subject: [PATCH 2/2] dissect: support mount options when going through mountfsd RootImageOptions=/ExtensionImages=/MountImages= all support custom mount options, use the new mountfsd parameters to configure them if they are specified. This requires additioanl privileges via polkit due to security implications of mount options, so document an example policy that allows to use the nosuid mount option. --- ...stem-or-user-ns-mountfsd-mount-options.xml | 32 ++++++++++++++++ man/systemd.exec.xml | 9 +++++ src/core/namespace.c | 1 + src/dissect/dissect.c | 1 + src/nspawn/nspawn.c | 1 + src/shared/dissect-image.c | 19 +++++++++- src/shared/dissect-image.h | 4 +- test/units/TEST-50-DISSECT.mountfsd.sh | 37 +++++++++++++++++++ test/units/TEST-50-DISSECT.sh | 1 + 9 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 man/system-or-user-ns-mountfsd-mount-options.xml diff --git a/man/system-or-user-ns-mountfsd-mount-options.xml b/man/system-or-user-ns-mountfsd-mount-options.xml new file mode 100644 index 0000000000..f81adc64b3 --- /dev/null +++ b/man/system-or-user-ns-mountfsd-mount-options.xml @@ -0,0 +1,32 @@ + + + + + + + + + <para id="plural">When enabled for services running in per-user instances of the service manager + using mount options is disabled by default, due to the security implications. It is possible to use a + <ulink url="https://www.freedesktop.org/software/polkit/docs/latest/">polkit</ulink> policy to allow + specific mount options, for example: + + <example id="example"> + <title>A polkit policy that allows mounting the root partition with nosuid + + /etc/polkit-1/rules.d/mountoptions.rules: + + +polkit.addRule(function(action, subject) { + if (action.id == "io.systemd.mount-file-system.mount-untrusted-image-privately" && + action.lookup("mount_options") == "root:nosuid") { + return polkit.Result.YES; + } +}); + + + + + diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index be74cb52ef..27f9f6122d 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -226,6 +226,9 @@ + + + @@ -524,6 +527,9 @@ + + + @@ -591,6 +597,9 @@ + + + diff --git a/src/core/namespace.c b/src/core/namespace.c index 422c18d238..172e37d3b0 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2619,6 +2619,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { r = mountfsd_mount_image( p->root_image, userns_fd, + p->root_image_options, p->root_image_policy, p->verity, dissect_image_flags, diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 8883dc6e26..65fb826663 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2188,6 +2188,7 @@ static int run(int argc, char *argv[]) { r = mountfsd_mount_image( arg_image, userns_fd, + /* options= */ NULL, arg_image_policy, &arg_verity_settings, arg_flags, diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index bb90cc1c94..80bbd27fb0 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6381,6 +6381,7 @@ static int run(int argc, char *argv[]) { r = mountfsd_mount_image( arg_image, userns_fd, + /* options= */ NULL, arg_image_policy, &arg_verity_settings, dissect_image_flags, diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 03edb6a72d..14120f2d57 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4934,6 +4934,7 @@ int verity_dissect_and_mount( r = mountfsd_mount_image( src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, userns_fd, + options, image_policy, verity, dissect_image_flags, @@ -5100,6 +5101,7 @@ static void mount_image_reply_parameters_done(MountImageReplyParameters *p) { int mountfsd_mount_image_fd( int image_fd, int userns_fd, + const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, @@ -5171,6 +5173,19 @@ int mountfsd_mount_image_fd( return log_error_errno(r, "Failed to push verity data fd into varlink connection: %m"); } + _cleanup_(sd_json_variant_unrefp) sd_json_variant *mount_options = NULL; + for (PartitionDesignator i = 0; i < _PARTITION_DESIGNATOR_MAX; i++) { + const char *o = mount_options_from_designator(options, i); + if (!o) + continue; + + r = sd_json_variant_merge_objectbo( + &mount_options, + SD_JSON_BUILD_PAIR_STRING(partition_designator_to_string(i), o)); + if (r < 0) + return log_error_errno(r, "Failed to build mount options array: %m"); + } + sd_json_variant *reply = NULL; r = varlink_callbo_and_log( vl, @@ -5182,6 +5197,7 @@ int mountfsd_mount_image_fd( SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY)), SD_JSON_BUILD_PAIR_BOOLEAN("growFileSystems", FLAGS_SET(flags, DISSECT_IMAGE_GROWFS)), SD_JSON_BUILD_PAIR_CONDITION(!!ps, "imagePolicy", SD_JSON_BUILD_STRING(ps)), + JSON_BUILD_PAIR_VARIANT_NON_NULL("mountOptions", mount_options), SD_JSON_BUILD_PAIR_BOOLEAN("veritySharing", FLAGS_SET(flags, DISSECT_IMAGE_VERITY_SHARE)), SD_JSON_BUILD_PAIR_CONDITION(verity_data_fd >= 0, "verityDataFileDescriptor", SD_JSON_BUILD_UNSIGNED(userns_fd >= 0 ? 2 : 1)), SD_JSON_BUILD_PAIR_CONDITION(verity && iovec_is_set(&verity->root_hash), "verityRootHash", JSON_BUILD_IOVEC_HEX(&verity->root_hash)), @@ -5274,6 +5290,7 @@ int mountfsd_mount_image_fd( int mountfsd_mount_image( const char *path, int userns_fd, + const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, @@ -5289,7 +5306,7 @@ int mountfsd_mount_image( return log_error_errno(errno, "Failed to open '%s': %m", path); _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; - r = mountfsd_mount_image_fd(image_fd, userns_fd, image_policy, verity, flags, &di); + r = mountfsd_mount_image_fd(image_fd, userns_fd, options, image_policy, verity, flags, &di); if (r < 0) return r; diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 16b42c2ed3..55c291c4ab 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -268,8 +268,8 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); -int mountfsd_mount_image_fd(int image_fd, int userns_fd, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); -int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_image_fd(int image_fd, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_image(const char *path, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); int mountfsd_mount_directory_fd(int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index cf2a2284e9..94f802e780 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -91,6 +91,43 @@ if [ "$VERITY_SIG_SUPPORTED" -eq 1 ]; then --property ExtensionImagePolicy=root=verity+signed+absent:usr=verity+signed+absent \ bash -c "test -e \"/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity\" && test -e \"/dev/mapper/$(/etc/polkit-1/rules.d/mountoptions.rules <<'EOF' +polkit.addRule(function(action, subject) { + if (action.id == "io.systemd.mount-file-system.mount-untrusted-image-privately" && + action.lookup("mount_options") == "root:nosuid") { + return polkit.Result.YES; + } +}); +EOF + systemctl try-reload-or-restart polkit.service + + systemd-run -M testuser@ --user --pipe --wait \ + --property RootImage="$MINIMAL_IMAGE.gpt" \ + --property RootImageOptions="root:nosuid" \ + sh -c "test -e \"/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity\" && mount | grep -F squashfs | grep -q -F nosuid" + + systemd-run -M testuser@ --user --pipe --wait \ + --property RootImage="$MINIMAL_IMAGE.raw" \ + --property ExtensionImages=/tmp/app0.raw \ + --property MountImages=/tmp/app0.raw:/var/tmp:nosuid \ + sh -c "test -e \"/dev/mapper/${MINIMAL_IMAGE_ROOTHASH}-verity\" && test -e \"/dev/mapper/$(