From 6e07fc6bcc34b05ace0011b7046df933ab9c9008 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Nov 2025 13:08:03 +0100 Subject: [PATCH] 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 6f42239b91e..30dbfde37b6 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 753eb30f65e..12c11a316a7 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 6bb174b9cac..e237f6e6ca2 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 64a0de3fe94..1f0e07d7d4d 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); -- 2.47.3