From b6aa78f707a7957237d9a479fca615cad0545cbd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Nov 2025 16:12:22 +0100 Subject: [PATCH 01/11] recursedir: introduce new RECURSE_DIR_UNLINK callback return value This introduces RECURSE_DIR_UNLINK + RECURSE_DIR_UNLINK_GRACEFUL as new return values for recurse_dir() callbacks. If either is returned the inode currently processed will be removed and iteration continues with the next item. This is useful to have in the recurse_dir() mechanism itself (rather than implement in the callback itself), due to ordering: we want to pin the inodes via an fd while calling the callbacks, but we have to close it before removal of the inodes. By moving this into the recurse_dir() infra, we can implement this easily. --- src/basic/recurse-dir.c | 58 ++++++++++++++++++++++++----------------- src/basic/recurse-dir.h | 2 ++ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/basic/recurse-dir.c b/src/basic/recurse-dir.c index 9eef4e1d8c..f7f8e4a7ab 100644 --- a/src/basic/recurse-dir.c +++ b/src/basic/recurse-dir.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include +#include #include "alloc-util.h" #include "dirent-util.h" @@ -433,30 +434,25 @@ int recurse_dir( i, statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */ userdata); - if (r == RECURSE_DIR_LEAVE_DIRECTORY) - break; - if (r == RECURSE_DIR_SKIP_ENTRY) - continue; - if (r != RECURSE_DIR_CONTINUE) - return r; + if (r == RECURSE_DIR_CONTINUE) { + r = recurse_dir(subdir_fd, + p, + statx_mask, + n_depth_max - 1, + flags & ~RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */ + func, + userdata); + if (r != 0) + return r; - r = recurse_dir(subdir_fd, - p, - statx_mask, - n_depth_max - 1, - flags & ~RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */ - func, - userdata); - if (r != 0) - return r; - - r = func(RECURSE_DIR_LEAVE, - p, - dir_fd, - subdir_fd, - i, - statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */ - userdata); + r = func(RECURSE_DIR_LEAVE, + p, + dir_fd, + subdir_fd, + i, + statx_mask != 0 ? &sx : NULL, /* only pass sx if user asked for it */ + userdata); + } } else /* Non-directory inode */ r = func(RECURSE_DIR_ENTRY, @@ -469,7 +465,21 @@ int recurse_dir( if (r == RECURSE_DIR_LEAVE_DIRECTORY) break; - if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE)) + + if (IN_SET(r, RECURSE_DIR_UNLINK, RECURSE_DIR_UNLINK_GRACEFUL)) { + int f = subdir_fd >= 0 ? AT_REMOVEDIR : 0; + + /* Close inodes before we try to delete them */ + subdir_fd = safe_close(subdir_fd); + inode_fd = safe_close(inode_fd); + + if (unlinkat(dir_fd, i->d_name, f) < 0) { + if (r != RECURSE_DIR_UNLINK_GRACEFUL) + return -errno; + + log_debug_errno(errno, "Unable to remove '%s', ignoring: %m", p); + } + } else if (!IN_SET(r, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE)) return r; } diff --git a/src/basic/recurse-dir.h b/src/basic/recurse-dir.h index 68abb279f4..af33c239f7 100644 --- a/src/basic/recurse-dir.h +++ b/src/basic/recurse-dir.h @@ -53,6 +53,8 @@ typedef enum RecurseDirEvent { #define RECURSE_DIR_CONTINUE 0 #define RECURSE_DIR_LEAVE_DIRECTORY INT_MIN #define RECURSE_DIR_SKIP_ENTRY (INT_MIN+1) +#define RECURSE_DIR_UNLINK (INT_MIN+2) +#define RECURSE_DIR_UNLINK_GRACEFUL (INT_MIN+3) /* Make sure that the negative errno range and these two special returns don't overlap */ assert_cc(RECURSE_DIR_LEAVE_DIRECTORY < -ERRNO_MAX); From 7e3bdf90d9322a81faa7a0d4c168dee4bf3d6ef2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Nov 2025 11:36:43 +0100 Subject: [PATCH 02/11] iovec-util: add iovec_done_many_and_free() --- src/fundamental/iovec-util-fundamental.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/fundamental/iovec-util-fundamental.h b/src/fundamental/iovec-util-fundamental.h index fbdd76398d..707274f86e 100644 --- a/src/fundamental/iovec-util-fundamental.h +++ b/src/fundamental/iovec-util-fundamental.h @@ -31,6 +31,15 @@ static inline void iovec_done(struct iovec *iovec) { iovec->iov_len = 0; } +static inline void iovec_done_many_and_free(struct iovec *iovec, size_t n) { + if (n > 0) { + assert(iovec); + FOREACH_ARRAY(j, iovec, n) + iovec_done(j); + } + free(iovec); +} + static inline bool iovec_is_set(const struct iovec *iovec) { /* Checks if the iovec points to a non-empty chunk of memory */ return iovec && iovec->iov_len > 0 && iovec->iov_base; From e742d749d3356192ac14e4d9710c6c19ca68cb78 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Nov 2025 22:33:06 +0100 Subject: [PATCH 03/11] dissect-image: make mountfsd_make_directory_fd() return param optional --- src/shared/dissect-image.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index bb83386c5a..25041687f9 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5291,7 +5291,6 @@ int mountfsd_make_directory_fd( assert(parent_fd >= 0); assert(name); - assert(ret_directory_fd); _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); @@ -5337,7 +5336,8 @@ int mountfsd_make_directory_fd( if (directory_fd < 0) return log_error_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); - *ret_directory_fd = TAKE_FD(directory_fd); + if (ret_directory_fd) + *ret_directory_fd = TAKE_FD(directory_fd); return 0; } From 468cfd25a02fccd3139907e39096f1a22bc59995 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Nov 2025 17:25:44 +0100 Subject: [PATCH 04/11] dissect-image: add fd-based flavour of mountfsd_mount_image() --- src/shared/dissect-image.c | 88 +++++++++++++++++++++++++++++--------- src/shared/dissect-image.h | 1 + 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 25041687f9..e1fa383a36 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -630,7 +630,35 @@ static void check_partition_flags( log_debug("Unexpected partition flag %llu set on %s!", bit, node); } } +#endif +static int make_image_name(const char *path, char **ret) { + int r; + + assert(path); + assert(ret); + + _cleanup_free_ char *filename = NULL; + r = path_extract_filename(path, &filename); + if (r < 0) + return r; + + _cleanup_free_ char *name = NULL; + r = raw_strip_suffixes(filename, &name); + if (r < 0) + return r; + + if (!image_name_is_valid(name)) { + log_debug("Image name %s is not valid, ignoring.", strna(name)); + *ret = NULL; + return 0; + } + + *ret = TAKE_PTR(name); + return 1; +} + +#if HAVE_BLKID static int dissected_image_new(const char *path, DissectedImage **ret) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_free_ char *name = NULL; @@ -639,20 +667,9 @@ static int dissected_image_new(const char *path, DissectedImage **ret) { assert(ret); if (path) { - _cleanup_free_ char *filename = NULL; - - r = path_extract_filename(path, &filename); + r = make_image_name(path, &name); if (r < 0) return r; - - r = raw_strip_suffixes(filename, &name); - if (r < 0) - return r; - - if (!image_name_is_valid(name)) { - log_debug("Image name %s is not valid, ignoring.", strna(name)); - name = mfree(name); - } } m = new(DissectedImage, 1); @@ -5022,8 +5039,8 @@ static void mount_image_reply_parameters_done(MountImageReplyParameters *p) { #endif -int mountfsd_mount_image( - const char *path, +int mountfsd_mount_image_fd( + int image_fd, int userns_fd, const ImagePolicy *image_policy, const VeritySettings *verity, @@ -5043,13 +5060,13 @@ int mountfsd_mount_image( }; _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; - _cleanup_close_ int image_fd = -EBADF, verity_data_fd = -EBADF; + _cleanup_close_ int verity_data_fd = -EBADF; _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_free_ char *ps = NULL; const char *error_id; int r; - assert(path); + assert(image_fd >= 0); assert(ret); r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); @@ -5064,10 +5081,6 @@ int mountfsd_mount_image( if (r < 0) return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); - image_fd = open(path, O_RDONLY|O_CLOEXEC); - if (image_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); - r = sd_varlink_push_dup_fd(vl, image_fd); if (r < 0) return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); @@ -5159,7 +5172,7 @@ int mountfsd_mount_image( assert(pp.designator >= 0); if (!di) { - r = dissected_image_new(path, &di); + r = dissected_image_new(/* path= */ NULL, &di); if (r < 0) return log_error_errno(r, "Failed to allocated new dissected image structure: %m"); } @@ -5194,6 +5207,39 @@ int mountfsd_mount_image( #endif } +int mountfsd_mount_image( + const char *path, + int userns_fd, + const ImagePolicy *image_policy, + const VeritySettings *verity, + DissectImageFlags flags, + DissectedImage **ret) { + + int r; + + assert(path); + assert(verity); + assert(ret); + + _cleanup_close_ int image_fd = open(path, O_RDONLY|O_CLOEXEC); + if (image_fd < 0) + 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); + if (r < 0) + return r; + + if (!di->image_name) { + r = make_image_name(path, &di->image_name); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(di); + return 0; +} + int mountfsd_mount_directory_fd( int directory_fd, int userns_fd, diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 9650dce2a4..a07bd907ae 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -268,6 +268,7 @@ 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_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); From 00421e3166fb4660b59e999277255b34e2fa7e85 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Nov 2025 17:26:15 +0100 Subject: [PATCH 05/11] dissect-image: make verity params for mountfsd_mount_image() optional --- src/shared/dissect-image.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index e1fa383a36..6fc5a0d746 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5218,7 +5218,6 @@ int mountfsd_mount_image( int r; assert(path); - assert(verity); assert(ret); _cleanup_close_ int image_fd = open(path, O_RDONLY|O_CLOEXEC); From 6e07fc6bcc34b05ace0011b7046df933ab9c9008 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 13:08:03 +0100 Subject: [PATCH 06/11] json-util: add a generic parser for parsing access mode/umask Let's move this from user-record.c into generic code and let's beef it up a bit: allow parsing octal strings. This is particular relevant given json_variant_new_stat() generates the mode in that format, and we probably should be able to parse our own fields (even though we currently don't do that for the data from json_variant_new_stat()). --- src/libsystemd/sd-json/json-util.c | 42 +++++++++++++++++++++++ src/libsystemd/sd-json/json-util.h | 1 + src/shared/user-record.c | 54 +++--------------------------- src/test/test-json.c | 46 +++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 49 deletions(-) diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index 6f42239b91..30dbfde37b 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -701,3 +701,45 @@ int json_variant_new_fd_info(sd_json_variant **ret, int fd) { JSON_BUILD_PAIR_INTEGER_NON_NEGATIVE("mountId", mntid), SD_JSON_BUILD_PAIR_VARIANT("fileHandle", w)); } + +int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + mode_t *m = ASSERT_PTR(userdata); + int r; + + if (sd_json_variant_is_null(variant)) { + *m = MODE_INVALID; + return 0; + } + + /* Let the SD_JSON_STRICT determine if we allow suid/sgid/sticky or not */ + mode_t limit = FLAGS_SET(flags, SD_JSON_STRICT) ? 0777 : 07777; + + if (sd_json_variant_is_string(variant)) { + /* NB: we parse the mode in the usual octal if a string is specified. */ + + mode_t mode; + r = parse_mode(sd_json_variant_string(variant), &mode); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not a valid access mode string.", strna(name)); + + if (mode > limit) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), + "JSON field '%s' outside of valid range 0%s0%o.", + strna(name), glyph(GLYPH_ELLIPSIS), limit); + + *m = mode; + + } else if (sd_json_variant_is_unsigned(variant)) { + + uint64_t k = sd_json_variant_unsigned(variant); + if (k > (uint64_t) limit) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), + "JSON field '%s' outside of valid range 0%s0%o.", + strna(name), glyph(GLYPH_ELLIPSIS), limit); + + *m = (mode_t) k; + } else + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is neither a number nor a string.", strna(name)); + + return 0; +} diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h index 753eb30f65..12c11a316a 100644 --- a/src/libsystemd/sd-json/json-util.h +++ b/src/libsystemd/sd-json/json-util.h @@ -127,6 +127,7 @@ int json_dispatch_devnum(const char *name, sd_json_variant *variant, sd_json_dis int json_dispatch_ifindex(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); int json_dispatch_log_level(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); int json_dispatch_strv_environment(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); +int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); static inline int json_variant_unbase64_iovec(sd_json_variant *v, struct iovec *ret) { return sd_json_variant_unbase64(v, ret ? &ret->iov_base : NULL, ret ? &ret->iov_len : NULL); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 6bb174b9ca..e237f6e6ca 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -462,50 +462,6 @@ static int json_dispatch_image_path(const char *name, sd_json_variant *variant, return 0; } -static int json_dispatch_umask(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { - mode_t *m = userdata; - uint64_t k; - - if (sd_json_variant_is_null(variant)) { - *m = MODE_INVALID; - return 0; - } - - if (!sd_json_variant_is_unsigned(variant)) - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); - - k = sd_json_variant_unsigned(variant); - if (k > 0777) - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), - "JSON field '%s' outside of valid range 0%s0777.", - strna(name), glyph(GLYPH_ELLIPSIS)); - - *m = (mode_t) k; - return 0; -} - -static int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { - mode_t *m = userdata; - uint64_t k; - - if (sd_json_variant_is_null(variant)) { - *m = MODE_INVALID; - return 0; - } - - if (!sd_json_variant_is_unsigned(variant)) - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); - - k = sd_json_variant_unsigned(variant); - if (k > 07777) - return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), - "JSON field '%s' outside of valid range 0%s07777.", - strna(name), glyph(GLYPH_ELLIPSIS)); - - *m = (mode_t) k; - return 0; -} - static int json_dispatch_locale(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { char **s = userdata; const char *n; @@ -1273,7 +1229,7 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j { "iconName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, icon_name), SD_JSON_STRICT }, { "location", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, location), 0 }, { "shell", SD_JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, - { "umask", SD_JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "umask", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(UserRecord, umask), SD_JSON_STRICT }, { "environment", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_environment, offsetof(UserRecord, environment), 0 }, { "timeZone", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, time_zone), SD_JSON_STRICT }, { "preferredLanguage", SD_JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 }, @@ -1287,7 +1243,7 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j { "diskSize", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 }, { "diskSizeRelative", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, { "skeletonDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), SD_JSON_STRICT }, - { "accessMode", SD_JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "accessMode", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, { "tasksMax", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, { "memoryHigh", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, { "memoryMax", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, @@ -1397,7 +1353,7 @@ static int dispatch_status(const char *name, sd_json_variant *variant, sd_json_d { "rateLimitBeginUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 }, { "rateLimitCount", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 }, { "removable", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(UserRecord, removable), 0 }, - { "accessMode", SD_JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "accessMode", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, { "fileSystemType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, file_system_type), SD_JSON_STRICT }, { "fallbackShell", SD_JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, fallback_shell), 0 }, { "fallbackHomeDirectory", SD_JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, fallback_home_directory), 0 }, @@ -1633,7 +1589,7 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load { "lastChangeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, { "lastPasswordChangeUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, { "shell", SD_JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, - { "umask", SD_JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "umask", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(UserRecord, umask), SD_JSON_STRICT }, { "environment", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_environment, offsetof(UserRecord, environment), 0 }, { "timeZone", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, time_zone), SD_JSON_STRICT }, { "preferredLanguage", SD_JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 }, @@ -1647,7 +1603,7 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load { "diskSize", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 }, { "diskSizeRelative", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, { "skeletonDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), SD_JSON_STRICT }, - { "accessMode", SD_JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "accessMode", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, { "tasksMax", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, { "memoryHigh", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, { "memoryMax", SD_JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, diff --git a/src/test/test-json.c b/src/test/test-json.c index 64a0de3fe9..1f0e07d7d4 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -1477,4 +1477,50 @@ TEST(unit_name) { &data), EINVAL); } +TEST(access_mode) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + ASSERT_OK(sd_json_parse("{" + " \"a\" : \"0755\", " + " \"b\" : 448, " + " \"c\" : null, " + " \"d\" : \"01755\" " + "}", + /* flags= */ 0, + &v, + /* reterr_line= */ NULL, + /* reterr_column= */ NULL)); + + struct { + mode_t a, b, c, d; + } mm = { 1, 2, 3, 4 }; + + ASSERT_OK(sd_json_dispatch( + v, + (const sd_json_dispatch_field[]) { + { "a", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, voffsetof(mm, a), 0 }, + { "b", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, voffsetof(mm, b), 0 }, + { "c", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, voffsetof(mm, c), 0 }, + { "d", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, voffsetof(mm, d), 0 }, + {}, + }, + /* flags= */ 0, + &mm)); + + ASSERT_EQ(mm.a, (mode_t) 0755); + ASSERT_EQ(mm.b, (mode_t) 0700); + ASSERT_EQ(mm.c, MODE_INVALID); + ASSERT_EQ(mm.d, (mode_t) 01755); + + /* retry with SD_JSON_STRICT, where 'd' should not parse anymore */ + ASSERT_ERROR(sd_json_dispatch( + v, + (const sd_json_dispatch_field[]) { + { "d", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, voffsetof(mm, d), SD_JSON_STRICT }, + {}, + }, + /* flags= */ SD_JSON_ALLOW_EXTENSIONS, + &mm), ERANGE); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From 46341769b7146d6d7b80507b0b37f253dd7cfda1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 13:12:40 +0100 Subject: [PATCH 07/11] mountfsd: optionally accept an access mode for the dir created by MakeDirectory() So far we expected the client to access the access mode themselves, and set a restrictive 0700 server-side. However, in some scenarios it's handy if the inode is created server-side already with the right mode, in particular when the client doesn't do anything with the dir just yet, and just needs it to be there with the right mode. If it has to adjust the mode client-side it might otherwise need to fork something off, join the selected userns, adjust the mode, and kill the child off again. --- src/mountfsd/mountwork.c | 16 ++++++++++++---- src/shared/varlink-io.systemd.MountFileSystem.c | 2 ++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 24f6e23e23..0e0bd964ee 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -1003,6 +1003,7 @@ static int vl_method_mount_directory( typedef struct MakeDirectoryParameters { unsigned parent_fd_idx; const char *name; + mode_t mode; } MakeDirectoryParameters; static int vl_method_make_directory( @@ -1012,14 +1013,16 @@ static int vl_method_make_directory( void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { - { "parentFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MakeDirectoryParameters, parent_fd_idx), SD_JSON_MANDATORY }, - { "name", SD_JSON_VARIANT_STRING, json_dispatch_const_filename, offsetof(MakeDirectoryParameters, name), SD_JSON_MANDATORY }, + { "parentFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(MakeDirectoryParameters, parent_fd_idx), SD_JSON_MANDATORY }, + { "name", SD_JSON_VARIANT_STRING, json_dispatch_const_filename, offsetof(MakeDirectoryParameters, name), SD_JSON_MANDATORY }, + { "mode", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_access_mode, offsetof(MakeDirectoryParameters, mode), SD_JSON_STRICT }, VARLINK_DISPATCH_POLKIT_FIELD, {} }; MakeDirectoryParameters p = { .parent_fd_idx = UINT_MAX, + .mode = MODE_INVALID, }; Hashmap **polkit_registry = ASSERT_PTR(userdata); int r; @@ -1028,6 +1031,11 @@ static int vl_method_make_directory( if (r != 0) return r; + if (p.mode == MODE_INVALID) + p.mode = 0700; + else + p.mode &= 0775; /* refuse generating world writable dirs */ + if (p.parent_fd_idx == UINT_MAX) return sd_varlink_error_invalid_parameter_name(link, "parentFileDescriptor"); @@ -1089,11 +1097,11 @@ static int vl_method_make_directory( if (r < 0) return r; - _cleanup_close_ int fd = open_mkdir_at(parent_fd, t, O_CLOEXEC, 0700); + _cleanup_close_ int fd = open_mkdir_at(parent_fd, t, O_CLOEXEC, p.mode); if (fd < 0) return fd; - r = RET_NERRNO(fchmod(fd, 0700)); /* Set mode explicitly, as paranoia regarding umask games */ + r = RET_NERRNO(fchmod(fd, p.mode)); /* Set mode explicitly, as paranoia regarding umask games */ if (r < 0) goto fail; diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c index e9c69f07d8..356f9938aa 100644 --- a/src/shared/varlink-io.systemd.MountFileSystem.c +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -113,6 +113,8 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT(parentFileDescriptor, SD_VARLINK_INT, 0), SD_VARLINK_FIELD_COMMENT("Name of the directory to create."), SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Access mode of the directory to create. Note that the suid, sgid, sticky, world-writable bit is unconditionally masked off."), + SD_VARLINK_DEFINE_INPUT(mode, SD_VARLINK_INT, SD_VARLINK_NULLABLE), VARLINK_DEFINE_POLKIT_INPUT, SD_VARLINK_FIELD_COMMENT("File descriptor referencing the newly created directory."), SD_VARLINK_DEFINE_OUTPUT(directoryFileDescriptor, SD_VARLINK_INT, 0)); From d4f8be786ca788d1323bb46e1ac506f95ba325ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Dec 2025 09:37:07 +0100 Subject: [PATCH 08/11] mountfsd: allow O_NONBLOCK on image fds Let's accept O_NONBLOCK on image fds. It's usually wise to open block device with O_NONBLOCK as it means that drivers with removable media won't block until a medium is inserted. Otherwise it has no effect. It also has no effect when opening regular files. Since block devices and raw devices are the only two things we accept in mountfsd, it's hence safe to allow it. --- src/mountfsd/mountwork.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 0e0bd964ee..4c321ce1c0 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -124,7 +124,7 @@ static int validate_image_fd(int fd, MountImageParameters *p) { return r; } - fl = fd_verify_safe_flags(fd); + fl = fd_verify_safe_flags_full(fd, O_NONBLOCK); if (fl < 0) return log_debug_errno(fl, "Image file descriptor has unsafe flags set: %m"); From a9b60de478de510a5e38c80b195456806cdc2b98 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Dec 2025 09:38:50 +0100 Subject: [PATCH 09/11] mountfsd: return a better error when invalid file flags are specified on image fd We already defined an error for this that we return for invalid dir fds passed in, let's add the same niceness to the image fds. --- src/mountfsd/mountwork.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 4c321ce1c0..7de8784e32 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -353,6 +353,8 @@ static int vl_method_mount_image( } r = validate_image_fd(image_fd, &p); + if (r == -EREMOTEIO) + return sd_varlink_errorbo(link, "io.systemd.MountFileSystem.BadFileDescriptorFlags", SD_JSON_BUILD_PAIR_STRING("parameter", "imageFileDescriptor")); if (r < 0) return r; From b7d1d17b9cb9ce78f8a79ded110201e478284b16 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 13:15:27 +0100 Subject: [PATCH 10/11] dissect-image: teach mountfsd_make_directory() the new access mode parameter --- src/import/import-tar.c | 2 +- src/import/pull-tar.c | 4 ++-- src/shared/discover-image.c | 1 + src/shared/dissect-image.c | 5 ++++- src/shared/dissect-image.h | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/import/import-tar.c b/src/import/import-tar.c index 5aa3a98640..ed96e27222 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -257,7 +257,7 @@ static int tar_import_fork_tar(TarImport *i) { return r; _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(d, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory(d, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) return r; diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index fe28d99d30..25b07b1192 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -297,7 +297,7 @@ static int tar_pull_make_local_copy(TarPull *p) { } _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(t, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory(t, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) return r; @@ -609,7 +609,7 @@ static int tar_pull_job_on_open_disk_tar(PullJob *j) { return r; _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(where, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory(where, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) return r; diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index b09181a73a..fa92b47172 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1491,6 +1491,7 @@ static int unpriviled_clone(Image *i, const char *new_path) { _cleanup_close_ int new_fd = -EBADF; r = mountfsd_make_directory( new_path, + MODE_INVALID, /* flags= */ 0, &new_fd); if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 6fc5a0d746..0957257080 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5329,6 +5329,7 @@ int mountfsd_mount_directory( int mountfsd_make_directory_fd( int parent_fd, const char *name, + mode_t mode, DissectImageFlags flags, int *ret_directory_fd) { @@ -5363,6 +5364,7 @@ int mountfsd_make_directory_fd( &error_id, SD_JSON_BUILD_PAIR_UNSIGNED("parentFileDescriptor", 0), SD_JSON_BUILD_PAIR_STRING("name", name), + SD_JSON_BUILD_PAIR_CONDITION(!IN_SET(mode, MODE_INVALID, 0700), "mode", SD_JSON_BUILD_UNSIGNED(mode)), /* suppress this field if default/unset */ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))); if (r < 0) return r; @@ -5388,6 +5390,7 @@ int mountfsd_make_directory_fd( int mountfsd_make_directory( const char *path, + mode_t mode, DissectImageFlags flags, int *ret_directory_fd) { @@ -5407,5 +5410,5 @@ int mountfsd_make_directory( if (fd < 0) return log_error_errno(r, "Failed to open '%s': %m", parent); - return mountfsd_make_directory_fd(fd, dirname, flags, ret_directory_fd); + return mountfsd_make_directory_fd(fd, dirname, mode, flags, ret_directory_fd); } diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index a07bd907ae..e22f14c61b 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -273,5 +273,5 @@ int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *ima 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); -int mountfsd_make_directory_fd(int parent_fd, const char *name, DissectImageFlags flags, int *ret_directory_fd); -int mountfsd_make_directory(const char *path, DissectImageFlags flags, int *ret_directory_fd); +int mountfsd_make_directory_fd(int parent_fd, const char *name, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); +int mountfsd_make_directory(const char *path, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); From 9cfefbf9835eb1f6d6a82d74d1be2d2a23952a1a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Dec 2025 09:40:22 +0100 Subject: [PATCH 11/11] dissect-image: automatically convert O_PATH fd to proper fd in mountfsd client Let's make the mountfsd client code a bit easier to use, and convert an O_PATH fd to a real one if necessary, automatically. --- src/shared/dissect-image.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 0957257080..28e40606ae 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5081,6 +5081,12 @@ int mountfsd_mount_image_fd( if (r < 0) return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + _cleanup_close_ int reopened_fd = -EBADF; + + image_fd = fd_reopen_condition(image_fd, O_CLOEXEC|O_NOCTTY|O_NONBLOCK|(FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY) ? O_RDONLY : O_RDWR), O_PATH, &reopened_fd); + if (image_fd < 0) + return log_error_errno(image_fd, "Failed to reopen fd: %m"); + r = sd_varlink_push_dup_fd(vl, image_fd); if (r < 0) return log_error_errno(r, "Failed to push image fd into varlink connection: %m");