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 @@
+
+
+
+
+
+
+
+
+ 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
+ polkit policy to allow
+ specific mount options, for example:
+
+
+ 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 fa6f63c24f..106e1dcdb5 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 fc36d79863..f4686cc4ed 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -2621,6 +2621,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 346472ef0d..94f796f675 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -2190,6 +2190,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/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/nspawn/nspawn.c b/src/nspawn/nspawn.c
index b388cbd7ed..a7b43cba91 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -6384,6 +6384,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/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."),
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/$(