diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md index e475e2fd68c..3bb1a984fb8 100644 --- a/docs/UIDS-GIDS.md +++ b/docs/UIDS-GIDS.md @@ -145,8 +145,13 @@ possible. available locally whose UID/GID ownerships do not make sense in the local context but only within the OS image itself. This 64K UID range can be used to have a clearly defined ownership even on the host, that can be mapped via - idmapped mount to a dynamic runtime UID range as needed. (These numbers in - hexadecimal are 0x7FFE0000…0x7FFEFFFF.) + idmapped mount to a dynamic runtime UID range as needed. These numbers in + hexadecimal are 0x7FFE0000…0x7FFEFFFF. Note that all users have full access + to the foreign UID range, hence it is recommended to never make foreign UID + range owned inodes accessible in directories accessible to other users. In + other words, always make sure each foreign UID range owned inode is inside + of a directory with mode `0700` (or stricter) owned by the only user that + should have access to the foreign UID range owned inode(s). Note for the `DynamicUser=` and the `systemd-nspawn` allocation ranges: when a UID allocation takes place NSS is checked for collisions first, and a different diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index da598d090c6..99e6147b2b1 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -938,6 +938,33 @@ + + + + Takes a non-negative integer. Requests that the specified number of additional 64K + UID/GID ranges are delegated into the container's user namespace. These delegated ranges are mapped + 1:1 (i.e. the same UID/GID values are used inside and outside the user namespace) and can be used by + nested containers to allocate their own transient UID/GID ranges via + systemd-nsresourced.service8. + + This option requires , as the delegation is performed + by + systemd-nsresourced.service8 + as part of the user namespace allocation. The maximum number of delegated ranges is 16. Defaults to + 0, i.e. no delegation. + + When this option is used with a non-zero value, the + systemd-nsresourced.service8 + Varlink socket (/run/systemd/io.systemd.NamespaceResource) is automatically + bind-mounted into the container along with the necessary discovery symlinks in + /run/systemd/userdb/ and /run/varlink/registry/. This + allows processes inside the container to contact + systemd-nsresourced on the host in order to allocate nested user namespaces from + the delegated ranges. + + + + diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml index 787312d858f..120027aab0e 100644 --- a/man/systemd-nsresourced.service.xml +++ b/man/systemd-nsresourced.service.xml @@ -52,6 +52,27 @@ registered with this service. Moreover, UIDs and GIDs are always allocated together, and symmetrically. + The allocation API supports delegated ranges: additional UID/GID ranges that + are mapped 1:1 into the user namespace rather than being translated to a target UID/GID. These delegated + ranges enable nested user namespace scenarios where a container needs to create child user namespaces + with their own transient UID ranges. Normally, the kernel restricts which UIDs can be mapped into a user + namespace to those that are also mapped in the parent. Delegated ranges solve this by pre-allocating + additional ranges that are visible inside the user namespace and can be used by nested + AllocateUserRange() calls. Up to 16 delegated ranges can be requested per user + namespace, each of size 65536. The ranges are allocated from the container UID ranges as per + Users, Groups, UIDs and GIDs on systemd Systems. + + The allocation API also supports identity mappings: instead of allocating a + transient UID/GID range, the user namespace can be configured to map the caller's UID/GID to root (UID + 0) inside the namespace, or to itself. Identity mappings can be combined with delegated ranges to enter + a privileged user namespace from which the container can be set up after which the container can run in + one of the delegated ranges. Identity mapped users are not subject to BPF-LSM write restrictions unlike + the transient ranges. + + Additionally, the allocation API supports mapping the foreign UID range into + the user namespace. When this option is enabled, the foreign UID range is mapped 1:1 into the user + namespace, allowing processes inside to access and manipulate files owned by the foreign UID range. + The service provides API calls to allowlist mounts (referenced via their mount file descriptors as per Linux fsmount() API), to pass ownership of a cgroup subtree to the user namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml index 6492a8911aa..bf9526df806 100644 --- a/man/systemd.nspawn.xml +++ b/man/systemd.nspawn.xml @@ -301,6 +301,23 @@ + + PrivateUsersDelegate= + + Takes a non-negative integer. Configures delegation of additional 64K UID/GID ranges + into the container's user namespace for use by nested containers. When set to a value greater than + zero, the + systemd-nsresourced.service8 + varlink socket will be bind-mounted into the container so that processes inside the container can + allocate further user namespaces from the delegated ranges. This is equivalent to the + command line switch. Requires + PrivateUsers=managed. Defaults to 0. See + systemd-nspawn1 + for details. + + + + NotifyReady= diff --git a/meson.build b/meson.build index b50466dcfd0..e021b568ca5 100644 --- a/meson.build +++ b/meson.build @@ -1798,7 +1798,7 @@ if conf.get('BPF_FRAMEWORK') == 1 ] endif - bpf_o_unstripped_cmd += ['-I.'] + bpf_o_unstripped_cmd += ['-I.', '-include', 'config.h'] if cc.get_id() == 'gcc' or meson.is_cross_build() if cc.get_id() != 'gcc' diff --git a/src/basic/uid-classification.h b/src/basic/uid-classification.h index 5b75c0ab786..58692f1ed2c 100644 --- a/src/basic/uid-classification.h +++ b/src/basic/uid-classification.h @@ -8,11 +8,11 @@ assert_cc((CONTAINER_UID_BASE_MIN & 0xFFFFU) == 0); assert_cc((CONTAINER_UID_BASE_MAX & 0xFFFFU) == 0); /* Given we assign 64K UIDs to containers, the last container UID is 0xFFFF larger than the base */ -#define CONTAINER_UID_MIN (CONTAINER_UID_BASE_MIN) -#define CONTAINER_UID_MAX (CONTAINER_UID_BASE_MAX + 0xFFFFU) +#define CONTAINER_UID_MIN ((uid_t) CONTAINER_UID_BASE_MIN) +#define CONTAINER_UID_MAX ((uid_t) CONTAINER_UID_BASE_MAX + 0xFFFFU) assert_cc((FOREIGN_UID_BASE & 0xFFFFU) == 0); -#define FOREIGN_UID_MIN (FOREIGN_UID_BASE) +#define FOREIGN_UID_MIN (FOREIGN_UID_BASE + 0U) #define FOREIGN_UID_MAX (FOREIGN_UID_BASE + 0xFFFFU) bool uid_is_system(uid_t uid); @@ -46,6 +46,14 @@ static inline bool gid_is_foreign(gid_t gid) { return uid_is_foreign((uid_t) gid); } +static inline bool uid_is_transient(uid_t uid) { + return uid_is_container(uid) || uid_is_dynamic(uid); +} + +static inline bool gid_is_transient(gid_t gid) { + return uid_is_container((uid_t) gid) || uid_is_dynamic((uid_t) gid); +} + typedef struct UGIDAllocationRange { uid_t system_alloc_uid_min; uid_t system_uid_max; diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 1aaf760468b..31305952ba4 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include "alloc-util.h" @@ -115,7 +116,7 @@ int uid_range_add_internal(UIDRange **range, uid_t start, uid_t nr, bool coalesc return 0; } -int uid_range_add_str(UIDRange **range, const char *s) { +int uid_range_add_str_full(UIDRange **range, const char *s, bool coalesce) { uid_t start, end; int r; @@ -126,7 +127,7 @@ int uid_range_add_str(UIDRange **range, const char *s) { if (r < 0) return r; - return uid_range_add_internal(range, start, end - start + 1, /* coalesce= */ true); + return uid_range_add_internal(range, start, end - start + 1, coalesce); } int uid_range_next_lower(const UIDRange *range, uid_t *uid) { @@ -230,7 +231,7 @@ bool uid_range_is_empty(const UIDRange *range) { return true; } -int uid_range_load_userns(const char *path, UIDRangeUsernsMode mode, UIDRange **ret) { +int uid_range_load_userns_full(const char *path, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret) { _cleanup_(uid_range_freep) UIDRange *range = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -280,13 +281,14 @@ int uid_range_load_userns(const char *path, UIDRangeUsernsMode mode, UIDRange ** return r; } - uid_range_coalesce(range); + if (coalesce) + uid_range_coalesce(range); *ret = TAKE_PTR(range); return 0; } -int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret) { +int uid_range_load_userns_by_fd_full(int userns_fd, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret) { _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; int r; @@ -299,7 +301,7 @@ int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange if (r < 0) return r; if (r > 0) - return uid_range_load_userns(/* path= */ NULL, mode, ret); + return uid_range_load_userns_full(/* path= */ NULL, mode, coalesce, ret); r = userns_enter_and_pin(userns_fd, &pidref); if (r < 0) @@ -309,7 +311,7 @@ int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange pidref.pid, IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "uid_map" : "gid_map"); - return uid_range_load_userns(p, mode, ret); + return uid_range_load_userns_full(p, mode, coalesce, ret); } bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { @@ -332,6 +334,225 @@ bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { return false; } +int uid_range_clip(UIDRange *range, uid_t min, uid_t max) { + assert(range); + + if (min > max) + return -EINVAL; + + size_t t = 0; + FOREACH_ARRAY(e, range->entries, range->n_entries) { + uid_t entry_end = e->start + e->nr; /* one past the last UID in entry */ + + /* Skip entries completely outside [min, max] */ + if (entry_end <= min || e->start > max) + continue; + + /* Trim the entry to fit within [min, max] */ + uid_t new_start = MAX(e->start, min); + /* entry_end is exclusive, avoid overflow when max == UINT32_MAX */ + uid_t new_end = entry_end <= max ? entry_end : max + 1; + assert(new_end > new_start); + + range->entries[t++] = (UIDRangeEntry) { + .start = new_start, + .nr = new_end - new_start, + }; + } + + range->n_entries = t; + + return 0; +} + +int uid_range_partition(UIDRange *range, uid_t size) { + assert(range); + assert(size > 0); + + /* Partitions the UID range entries into buckets of the given size. Any entry larger than the given + * size will be partitioned into multiple entries, each of the given size. Any leftover UIDs in the + * entry are dropped. Any entries smaller than the given size are also dropped. */ + + /* Count how many entries we'll need after partitioning */ + size_t n_new_entries = 0; + FOREACH_ARRAY(e, range->entries, range->n_entries) + n_new_entries += e->nr / size; + + if (n_new_entries == 0) { + range->n_entries = 0; + return 0; + } + + if (n_new_entries > range->n_entries && !GREEDY_REALLOC(range->entries, n_new_entries)) + return -ENOMEM; + + /* Work backwards to avoid overwriting entries we still need to read */ + size_t t = n_new_entries; + for (size_t i = range->n_entries; i > 0; i--) { + UIDRangeEntry *e = range->entries + i - 1; + unsigned n_parts = e->nr / size; + + for (unsigned j = n_parts; j > 0; j--) + range->entries[--t] = (UIDRangeEntry) { + .start = e->start + (j - 1) * size, + .nr = size, + }; + } + + range->n_entries = n_new_entries; + + return 0; +} + +int uid_range_copy(const UIDRange *range, UIDRange **ret) { + assert(ret); + + if (!range) { + *ret = NULL; + return 0; + } + + _cleanup_(uid_range_freep) UIDRange *copy = new0(UIDRange, 1); + if (!copy) + return -ENOMEM; + + if (range->n_entries > 0) { + copy->entries = newdup(UIDRangeEntry, range->entries, range->n_entries); + if (!copy->entries) + return -ENOMEM; + + copy->n_entries = range->n_entries; + } + + *ret = TAKE_PTR(copy); + return 0; +} + +int uid_range_remove(UIDRange *range, uid_t start, uid_t size) { + assert(range); + + if (size == 0) + return 0; + + uid_t end = start + size; /* one past the last UID to remove */ + + for (size_t i = 0; i < range->n_entries; i++) { + UIDRangeEntry *e = range->entries + i; + uid_t entry_end = e->start + e->nr; + + /* No overlap */ + if (entry_end <= start || e->start >= end) + continue; + + /* Check if this removal splits the entry into two parts */ + if (e->start < start && entry_end > end) { + /* Need to split: grow the array first */ + if (!GREEDY_REALLOC(range->entries, range->n_entries + 1)) + return -ENOMEM; + + /* Re-fetch pointer after potential realloc */ + e = range->entries + i; + entry_end = e->start + e->nr; + + /* Shift everything after this entry to make room */ + memmove(range->entries + i + 2, range->entries + i + 1, + (range->n_entries - i - 1) * sizeof(UIDRangeEntry)); + range->n_entries++; + + /* First part: before the removed range */ + range->entries[i] = (UIDRangeEntry) { + .start = e->start, + .nr = start - e->start, + }; + + /* Second part: after the removed range */ + range->entries[i + 1] = (UIDRangeEntry) { + .start = end, + .nr = entry_end - end, + }; + + /* Skip the newly inserted entry */ + i++; + continue; + } + + /* Removal covers the entire entry */ + if (start <= e->start && end >= entry_end) { + memmove(e, e + 1, (range->n_entries - i - 1) * sizeof(UIDRangeEntry)); + range->n_entries--; + i--; + continue; + } + + /* Removal trims the start of the entry */ + if (start <= e->start && end > e->start) { + e->nr = entry_end - end; + e->start = end; + continue; + } + + /* Removal trims the end of the entry */ + if (start < entry_end && end >= entry_end) { + e->nr = start - e->start; + continue; + } + } + + return 0; +} + +int uid_range_translate(const UIDRange *outside, const UIDRange *inside, uid_t uid, uid_t *ret) { + assert(uid_range_entries(outside) == uid_range_entries(inside)); + assert(ret); + + /* Given two UID ranges that represent the outside UID range of a user namespace (the 2nd and 3rd + * columns in /proc/xxx/uid_map) and the inside UID range of a user namespace (the 1st and 3rd + * columns in /proc/xxx/uid_map), translates the given UID from the outside range to the inside + * range. For example, given the following UID range: + * + * 0 1000 1 + * + * calling uid_range_translate(outside, inside, 1000) will return 0 as the output UID. Alternatively, + * calling uid_range_translate(inside, outside, 0) will return 1000 as the output UID. + */ + + for (size_t i = 0; i < uid_range_entries(outside); i++) + assert(outside->entries[i].nr == inside->entries[i].nr); + + for (size_t i = 0; i < uid_range_entries(outside); i++) { + const UIDRangeEntry *e = outside->entries + i; + + if (uid < e->start || uid >= e->start + e->nr) + continue; + + *ret = inside->entries[i].start + uid - e->start; + return 0; + } + + return -ESRCH; +} + +int uid_range_translate_userns_fd(int userns_fd, UIDRangeUsernsMode mode, uid_t uid, uid_t *ret) { + int r; + + assert(userns_fd >= 0); + assert(IN_SET(mode, UID_RANGE_USERNS_OUTSIDE, GID_RANGE_USERNS_OUTSIDE)); + + _cleanup_(uid_range_freep) UIDRange *outside_range = NULL; + r = uid_range_load_userns_by_fd_full(userns_fd, mode, /* coalesce= */ false, &outside_range); + if (r < 0) + return r; + + mode = mode == UID_RANGE_USERNS_OUTSIDE ? UID_RANGE_USERNS_INSIDE : GID_RANGE_USERNS_INSIDE; + + _cleanup_(uid_range_freep) UIDRange *inside_range = NULL; + r = uid_range_load_userns_by_fd_full(userns_fd, mode, /* coalesce= */ false, &inside_range); + if (r < 0) + return r; + + return uid_range_translate(outside_range, inside_range, uid, ret); +} + bool uid_range_equal(const UIDRange *a, const UIDRange *b) { if (a == b) return true; diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index c28b02fa7d1..08d707ae259 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -19,7 +19,10 @@ int uid_range_add_internal(UIDRange **range, uid_t start, uid_t nr, bool coalesc static inline int uid_range_add(UIDRange **range, uid_t start, uid_t nr) { return uid_range_add_internal(range, start, nr, true); } -int uid_range_add_str(UIDRange **range, const char *s); +int uid_range_add_str_full(UIDRange **range, const char *s, bool coalesce); +static inline int uid_range_add_str(UIDRange **range, const char *s) { + return uid_range_add_str_full(range, s, true); +} int uid_range_next_lower(const UIDRange *range, uid_t *uid); @@ -48,11 +51,24 @@ typedef enum UIDRangeUsernsMode { _UID_RANGE_USERNS_MODE_INVALID = -EINVAL, } UIDRangeUsernsMode; -int uid_range_load_userns(const char *path, UIDRangeUsernsMode mode, UIDRange **ret); -int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret); +int uid_range_load_userns_full(const char *path, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret); +static inline int uid_range_load_userns(const char *path, UIDRangeUsernsMode mode, UIDRange **ret) { + return uid_range_load_userns_full(path, mode, true, ret); +} +int uid_range_load_userns_by_fd_full(int userns_fd, UIDRangeUsernsMode mode, bool coalesce, UIDRange **ret); +static inline int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret) { + return uid_range_load_userns_by_fd_full(userns_fd, mode, true, ret); +} bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); +int uid_range_clip(UIDRange *range, uid_t min, uid_t max); +int uid_range_partition(UIDRange *range, uid_t size); +int uid_range_copy(const UIDRange *range, UIDRange **ret); +int uid_range_remove(UIDRange *range, uid_t start, uid_t size); +int uid_range_translate(const UIDRange *outside, const UIDRange *inside, uid_t uid, uid_t *ret); +int uid_range_translate_userns_fd(int userns_fd, UIDRangeUsernsMode mode, uid_t uid, uid_t *ret); + int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret); uid_t uid_range_base(const UIDRange *range); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 250be1af3d1..aafbd872ae7 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2178,10 +2178,10 @@ static int run(int argc, char *argv[]) { else r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); if (r < 0) { - if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_MOUNT, ACTION_UMOUNT, ACTION_WITH, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_SHIFT)) + if ((r != -ENOENT && !ERRNO_IS_PRIVILEGE(r)) || arg_action == ACTION_ATTACH) return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); - log_debug_errno(r, "Lacking permissions to set up loopback block device for %s, using service: %m", arg_image); + log_debug_errno(r, "Lacking permissions or missing /dev/loop-control to set up loopback block device for %s, using service: %m", arg_image); arg_via_service = true; } else { if (arg_loop_ref) { diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index bd2ffbf8b79..905324ffdda 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -1208,30 +1208,60 @@ static int vl_method_mount_directory( uid_t start; if (userns_fd >= 0) { + /* Load ranges without coalescing to preserve the 1:1 correspondence + * between inside and outside entries */ _cleanup_(uid_range_freep) UIDRange *uid_range_outside = NULL, *uid_range_inside = NULL, *gid_range_outside = NULL, *gid_range_inside = NULL; - r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &uid_range_outside); + r = uid_range_load_userns_by_fd_full(userns_fd, UID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &uid_range_outside); if (r < 0) return log_debug_errno(r, "Failed to load outside UID range of provided userns: %m"); - r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_INSIDE, &uid_range_inside); + + r = uid_range_load_userns_by_fd_full(userns_fd, UID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &uid_range_inside); if (r < 0) return log_debug_errno(r, "Failed to load inside UID range of provided userns: %m"); - r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &gid_range_outside); + + r = uid_range_load_userns_by_fd_full(userns_fd, GID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &gid_range_outside); if (r < 0) return log_debug_errno(r, "Failed to load outside GID range of provided userns: %m"); - r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_INSIDE, &gid_range_inside); + + r = uid_range_load_userns_by_fd_full(userns_fd, GID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &gid_range_inside); if (r < 0) return log_debug_errno(r, "Failed to load inside GID range of provided userns: %m"); - /* Be very strict for now */ + /* UID and GID mappings must match */ if (!uid_range_equal(uid_range_outside, gid_range_outside) || - !uid_range_equal(uid_range_inside, gid_range_inside) || - uid_range_outside->n_entries != 1 || - uid_range_outside->entries[0].nr != 0x10000 || - uid_range_inside->n_entries != 1 || - uid_range_inside->entries[0].start != 0 || - uid_range_inside->entries[0].nr != 0x10000) + !uid_range_equal(uid_range_inside, gid_range_inside)) return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + /* Must have at least one entry, and inside/outside must have matching entry counts */ + if (uid_range_is_empty(uid_range_outside) || + uid_range_outside->n_entries != uid_range_inside->n_entries) + return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* The first range must be a root UID in the transient range (i.e. aligned + * to a 64K boundary) and mapped to 0 inside the user namespace (size 65536) */ + if (!uid_is_transient(uid_range_outside->entries[0].start) || + (uid_range_outside->entries[0].start & 0xFFFFU) != 0 || + uid_range_outside->entries[0].nr != NSRESOURCE_UIDS_64K || + uid_range_inside->entries[0].start != 0 || + uid_range_inside->entries[0].nr != NSRESOURCE_UIDS_64K) + return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* All remaining entries must also be root UIDs in the transient range and + * mapped 1:1, which identifies them as delegated ranges. The last entry + * may also be the root UID in the foreign UID range. */ + for (size_t i = 1; i < uid_range_outside->n_entries; i++) { + bool is_last = i + 1 == uid_range_outside->n_entries; + uid_t entry_start = uid_range_outside->entries[i].start; + + if (!(uid_is_transient(entry_start) || + (is_last && uid_is_foreign(entry_start))) || + (entry_start & 0xFFFFU) != 0 || + uid_range_outside->entries[i].nr != NSRESOURCE_UIDS_64K || + uid_range_outside->entries[i].start != uid_range_inside->entries[i].start || + uid_range_outside->entries[i].nr != uid_range_inside->entries[i].nr) + return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + } + start = uid_range_outside->entries[0].start; } else start = 0; diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf index a44a13ef29e..cdad70706e6 100644 --- a/src/nspawn/nspawn-gperf.gperf +++ b/src/nspawn/nspawn-gperf.gperf @@ -34,6 +34,7 @@ Exec.MachineID, config_parse_id128, 0, Exec.WorkingDirectory, config_parse_path, 0, offsetof(Settings, working_directory) Exec.PivotRoot, config_parse_pivot_root, 0, 0 Exec.PrivateUsers, config_parse_private_users, 0, 0 +Exec.PrivateUsersDelegate, config_parse_unsigned, 0, offsetof(Settings, delegate_container_ranges) Exec.NotifyReady, config_parse_tristate, 0, offsetof(Settings, notify_ready) Exec.SystemCallFilter, config_parse_syscall_filter, 0, 0 Exec.LimitCPU, config_parse_rlimit, RLIMIT_CPU, offsetof(Settings, rlimit) diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 197c6e2f79a..84c342b83c1 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -175,6 +175,7 @@ typedef struct Settings { char *pivot_root_old; UserNamespaceMode userns_mode; uid_t uid_shift, uid_range; + unsigned delegate_container_ranges; int notify_ready; char **syscall_allow_list; char **syscall_deny_list; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 08afa171ae8..722be8bbf7c 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -214,6 +214,7 @@ static char **arg_property = NULL; static sd_bus_message *arg_property_message = NULL; static UserNamespaceMode arg_userns_mode; /* initialized depending on arg_privileged in run() */ static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U; +static unsigned arg_delegate_container_ranges = 0; static UserNamespaceOwnership arg_userns_ownership = _USER_NAMESPACE_OWNERSHIP_INVALID; static int arg_kill_signal = 0; static SettingsMask arg_settings_mask = 0; @@ -430,6 +431,10 @@ static int help(void) { " --private-users-ownership=MODE\n" " Adjust ('chown') or map ('map') OS tree ownership\n" " to private UID/GID range\n" + " --private-users-delegate=N\n" + " Delegate N additional 64K UID/GID ranges for use\n" + " by nested containers (requires managed user\n" + " namespaces)\n" " -U Equivalent to --private-users=pick and\n" " --private-users-ownership=auto\n" "\n%3$sNetworking:%4$s\n" @@ -710,6 +715,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_TEMPLATE, ARG_PROPERTY, ARG_PRIVATE_USERS, + ARG_PRIVATE_USERS_DELEGATE, ARG_KILL_SIGNAL, ARG_SETTINGS, ARG_CHDIR, @@ -794,6 +800,7 @@ static int parse_argv(int argc, char *argv[]) { { "private-users", optional_argument, NULL, ARG_PRIVATE_USERS }, { "private-users-chown", optional_argument, NULL, ARG_PRIVATE_USERS_CHOWN }, /* obsolete */ { "private-users-ownership",required_argument, NULL, ARG_PRIVATE_USERS_OWNERSHIP}, + { "private-users-delegate", required_argument, NULL, ARG_PRIVATE_USERS_DELEGATE }, { "kill-signal", required_argument, NULL, ARG_KILL_SIGNAL }, { "settings", required_argument, NULL, ARG_SETTINGS }, { "chdir", required_argument, NULL, ARG_CHDIR }, @@ -1249,6 +1256,14 @@ static int parse_argv(int argc, char *argv[]) { arg_settings_mask |= SETTING_USERNS; break; + case ARG_PRIVATE_USERS_DELEGATE: + r = safe_atou(optarg, &arg_delegate_container_ranges); + if (r < 0) + return log_error_errno(r, "Failed to parse --private-users-delegate= parameter: %s", optarg); + + arg_settings_mask |= SETTING_USERNS; + break; + case ARG_KILL_SIGNAL: if (streq(optarg, "help")) return DUMP_STRING_TABLE(signal, int, _NSIG); @@ -1648,6 +1663,9 @@ static int verify_arguments(void) { if (arg_userns_mode == USER_NAMESPACE_MANAGED && !arg_private_network) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Managed user namespace operation requires private networking, as otherwise /sys/ may not be mounted."); + if (arg_delegate_container_ranges > 0 && arg_userns_mode != USER_NAMESPACE_MANAGED) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--private-users-delegate= requires --private-users=managed."); + if (!(arg_clone_ns_flags & CLONE_NEWPID) || !(arg_clone_ns_flags & CLONE_NEWUTS)) { arg_register = false; @@ -2881,6 +2899,43 @@ static int setup_machine_id(const char *directory) { return 0; } +static int setup_varlink_socket(const char *directory, const char *name) { + int r; + + assert(directory); + + if (arg_delegate_container_ranges == 0) + return 0; + + r = make_run_host(directory); + if (r < 0) + return r; + + _cleanup_free_ char *src = path_join("/run/systemd", name); + if (!src) + return log_oom(); + + _cleanup_free_ char *dest = path_join(directory, "/run/host", name); + if (!dest) + return log_oom(); + + r = touch(dest); + if (r < 0) + return log_error_errno(r, "Failed to create %s: %m", dest); + + r = userns_lchown(dest, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to chown %s: %m", dest); + + return mount_nofollow_verbose( + LOG_ERR, + src, + dest, + /* fstype= */ NULL, + MS_BIND|MS_RDONLY, + /* options= */ NULL); +} + static int recursive_chown(const char *directory, uid_t shift, uid_t range) { int r; @@ -4371,6 +4426,14 @@ static int outer_child( if (r < 0) return r; + r = setup_varlink_socket(directory, "io.systemd.NamespaceResource"); + if (r < 0) + return r; + + r = setup_varlink_socket(directory, "io.systemd.MountFileSystem"); + if (r < 0) + return r; + /* The same stuff as the $container env var, but nicely readable for the entire payload */ free(p); p = path_join(directory, "/run/host/container-manager"); @@ -4885,6 +4948,7 @@ static int merge_settings(Settings *settings, const char *path) { arg_uid_shift = settings->uid_shift; arg_uid_range = settings->uid_range; arg_userns_ownership = settings->userns_ownership; + arg_delegate_container_ranges = settings->delegate_container_ranges; } } @@ -6158,10 +6222,11 @@ static int run(int argc, char *argv[]) { goto finish; } - userns_fd = nsresource_allocate_userns( + userns_fd = nsresource_allocate_userns_full( nsresource_link, userns_name, - NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ + NSRESOURCE_UIDS_64K, + arg_delegate_container_ranges); if (userns_fd < 0) { r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); goto finish; diff --git a/src/nsresourced/bpf/userns-restrict/userns-restrict.bpf.c b/src/nsresourced/bpf/userns-restrict/userns-restrict.bpf.c index dbb0858682d..25d609bf38f 100644 --- a/src/nsresourced/bpf/userns-restrict/userns-restrict.bpf.c +++ b/src/nsresourced/bpf/userns-restrict/userns-restrict.bpf.c @@ -20,6 +20,9 @@ #include #include +#define CONTAINER_UID_MIN ((uid_t) CONTAINER_UID_BASE_MIN) +#define CONTAINER_UID_MAX ((uid_t) CONTAINER_UID_BASE_MAX + 0xFFFFU) + #ifndef bpf_core_cast /* bpf_rdonly_cast() was introduced in libbpf commit 688879f together with * the definition of a bpf_core_cast macro. So use that one to avoid @@ -59,6 +62,13 @@ struct { __array(values, struct mnt_id_map); } userns_mnt_id_hash SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ + __type(key, unsigned); /* userns inode */ + __type(value, int); /* dummy value */ +} userns_setgroups_deny SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 4096); @@ -68,28 +78,30 @@ static inline struct mount *real_mount(struct vfsmount *mnt) { return container_of(mnt, struct mount, mnt); } -static int validate_mount(struct vfsmount *v) { - struct user_namespace *mount_userns, *task_userns, *p; - unsigned task_userns_inode; - struct task_struct *task; - void *mnt_id_map; +static inline bool uid_is_dynamic(uid_t uid) { + return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX; +} + +static inline bool uid_is_container(uid_t uid) { + return CONTAINER_UID_MIN <= uid && uid <= CONTAINER_UID_MAX; +} + +static inline bool uid_is_transient(uid_t uid) { + return uid_is_dynamic(uid) || uid_is_container(uid); +} + +static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v) { + struct user_namespace *mount_userns, *p; struct mount *m; - int mnt_id; /* Get user namespace from vfsmount */ m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount)); mount_userns = m->mnt_ns->user_ns; - /* Get user namespace from task */ - task = (struct task_struct*) bpf_get_current_task_btf(); - task_userns = task->cred->user_ns; - - /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say - * yes immediately. */ p = mount_userns; for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) { - if (p == task_userns) - return 0; /* our task's user namespace (or a child thereof) owns this superblock: allow! */ + if (p == userns) + return true; p = p->parent; if (!p) @@ -101,6 +113,42 @@ static int validate_mount(struct vfsmount *v) { if (p) return -EPERM; + return false; +} + +static int validate_mount(struct vfsmount *v, int ret) { + struct user_namespace *task_userns; + unsigned task_userns_inode; + struct task_struct *task; + void *mnt_id_map; + struct mount *m; + int mnt_id, r; + + if (ret != 0) /* propagate earlier error */ + return ret; + + /* Get user namespace from task */ + task = (struct task_struct*) bpf_get_current_task_btf(); + task_userns = task->cred->user_ns; + + /* fsuid/fsgid are the UID/GID in the initial user namespace, before any idmapped mounts have been + * applied. There is no way (yet) to figure out what the UID/GID that will be written to disk will be + * after idmapped mounts are taken into account, hence we have to rely on an allowlist of mounts + * populated by userspace which tells us if a mount has an appropriate uid mapping in place to + * translate the transient UID range to something else. For other UIDs/GIDs, there's no need to do + * these checks as we don't insist on idmapped mounts or such for UIDs/GIDs outside the transient + * ranges. */ + if (!uid_is_transient(task->cred->fsuid.val) && !uid_is_transient((uid_t) task->cred->fsgid.val)) + return 0; + + r = userns_owns_mount(task_userns, v); + if (r < 0) + return r; + /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say + * yes immediately. */ + if (r > 0) + return 0; + /* This is a mount foreign to our task's user namespace, let's consult our allow list */ task_userns_inode = task_userns->ns.inum; @@ -108,6 +156,7 @@ static int validate_mount(struct vfsmount *v) { if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */ return 0; + m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount)); mnt_id = m->mnt_id; /* Otherwise, say yes if the mount ID is allowlisted */ @@ -117,59 +166,129 @@ static int validate_mount(struct vfsmount *v) { return -EPERM; } -static int validate_path(const struct path *path, int ret) { - struct inode *inode; +SEC("lsm/path_chown") +int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long uid, unsigned long long gid, int ret) { + struct user_namespace *task_userns; + unsigned task_userns_inode; + struct task_struct *task; struct vfsmount *v; + void *mnt_id_map; + int r; if (ret != 0) /* propagate earlier error */ return ret; + /* Get user namespace from task */ + task = (struct task_struct*) bpf_get_current_task_btf(); + task_userns = task->cred->user_ns; v = path->mnt; - return validate_mount(v); -} + r = userns_owns_mount(task_userns, v); + if (r < 0) + return r; + /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say + * yes immediately. */ + if (r > 0) + return 0; -SEC("lsm/path_chown") -int BPF_PROG(userns_restrict_path_chown, struct path *path, void* uid, void *gid, int ret) { - return validate_path(path, ret); + /* This is a mount foreign to our task's user namespace, if the user namespace was provisioned by + * nsresourced, refuse any UIDs/GIDs in the transient ranges. Note that we can only do this check in + * the chown() hook because it receives the UID/GID with idmapped mounts already taken into account, + * unlike the other hooks where we cannot (yet) figure out the UID/GID after idmapped mounts are + * applied. Hence in the other hooks we have to rely on the mount allowlist to ensure the transient + * fsuid/fsgid will be translated to something else when written to disk but in the chown() hook we + * can check the provided UID/GID directly to see if it is transient or not. */ + + /* User namespaces that were not provisioned by nsresourced can still write to the transient ranges + * so that we don't break use cases like systemd-nspawn's --private-users=pick switch. */ + + task_userns_inode = task_userns->ns.inum; + + mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &task_userns_inode); + if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */ + return 0; + + if (uid_is_transient((uid_t) uid) || uid_is_transient((uid_t) gid)) + return -EPERM; + + return 0; } SEC("lsm/path_mkdir") int BPF_PROG(userns_restrict_path_mkdir, struct path *dir, struct dentry *dentry, umode_t mode, int ret) { - return validate_path(dir, ret); + return validate_mount(dir->mnt, ret); } /* The mknod hook covers all file creations, including regular files, in case the reader is looking for a * missing hook for open(). */ SEC("lsm/path_mknod") int BPF_PROG(userns_restrict_path_mknod, const struct path *dir, struct dentry *dentry, umode_t mode, unsigned dev, int ret) { - return validate_path(dir, ret); + return validate_mount(dir->mnt, ret); } SEC("lsm/path_symlink") int BPF_PROG(userns_restrict_path_symlink, const struct path *dir, struct dentry *dentry, const char *old_name, int ret) { - return validate_path(dir, ret); + return validate_mount(dir->mnt, ret); } SEC("lsm/path_link") int BPF_PROG(userns_restrict_path_link, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry, int ret) { - return validate_path(new_dir, ret); + return validate_mount(new_dir->mnt, ret); +} + +SEC("lsm/task_fix_setgroups") +int BPF_PROG(userns_restrict_task_fix_setgroups, struct cred *new_cred, const struct cred *old, int ret) { + struct user_namespace *p; + unsigned inode; + + if (ret != 0) /* propagate earlier error */ + return ret; + + /* Walk the task's user namespace and its ancestors to find the first one managed by nsresourced + * (i.e. present in either the setgroups deny map or the mount ID hash map). This is necessary + * because a task could otherwise trivially bypass the setgroups() restriction by unsharing the user + * namespace and mapping the same users and groups. */ + p = new_cred->user_ns; + for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) { + if (!p) + break; + + inode = p->ns.inum; + + if (bpf_map_lookup_elem(&userns_setgroups_deny, &inode)) + return -EPERM; + + if (bpf_map_lookup_elem(&userns_mnt_id_hash, &inode)) + return 0; + + p = p->parent; + } + + /* No nsresourced-managed ancestor found, allow. */ + return 0; } SEC("kprobe/retire_userns_sysctls") int BPF_KPROBE(userns_restrict_retire_userns_sysctls, struct user_namespace *userns) { unsigned inode; - void *mnt_id_map; /* Inform userspace that a user namespace just went away. I wish there was a nicer way to hook into * user namespaces being deleted than using kprobes, but couldn't find any. */ userns = bpf_rdonly_cast(userns, bpf_core_type_id_kernel(struct user_namespace)); inode = userns->ns.inum; - mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &inode); - if (!mnt_id_map) /* No rules installed for this userns? Then send no notification. */ - return 0; + /* Check each map separately to avoid the compiler merging the two lookups into a pointer OR + * operation, which the BPF verifier rejects. */ + if (bpf_map_lookup_elem(&userns_mnt_id_hash, &inode)) + goto notify; + if (bpf_map_lookup_elem(&userns_setgroups_deny, &inode)) + goto notify; + + /* No rules installed for this userns? Then send no notification. */ + return 0; + +notify: bpf_ringbuf_output(&userns_ringbuf, &inode, sizeof(inode), 0); return 0; } diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c index 60d3a01ce06..3b2450529c3 100644 --- a/src/nsresourced/nsresourcework.c +++ b/src/nsresourced/nsresourcework.c @@ -34,12 +34,14 @@ #include "mountpoint-util.h" #include "namespace-util.h" #include "netlink-util.h" +#include "nsresource.h" #include "pidref.h" #include "process-util.h" #include "random-util.h" #include "siphash24.h" #include "socket-util.h" #include "stat-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -75,6 +77,21 @@ typedef struct LookupParameters { const char *service; } LookupParameters; +typedef enum AllocateUserRangeType { + ALLOCATE_USER_RANGE_MANAGED, + ALLOCATE_USER_RANGE_SELF, + _ALLOCATE_USER_RANGE_TYPE_MAX, + _ALLOCATE_USER_RANGE_TYPE_INVALID = -EINVAL, +} AllocateUserRangeType; + +static const char *const allocate_user_range_type_table[_ALLOCATE_USER_RANGE_TYPE_MAX] = { + [ALLOCATE_USER_RANGE_MANAGED] = "managed", + [ALLOCATE_USER_RANGE_SELF] = "self", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(allocate_user_range_type, AllocateUserRangeType); +static JSON_DISPATCH_ENUM_DEFINE(dispatch_allocate_user_range_type, AllocateUserRangeType, allocate_user_range_type_from_string); + static int build_user_json(UserNamespaceInfo *userns_info, uid_t offset, sd_json_variant **ret) { _cleanup_free_ char *name = NULL, *realname = NULL; UserDisposition disposition; @@ -357,16 +374,19 @@ static int vl_method_get_memberships(sd_varlink *link, sd_json_variant *paramete return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); } -static int uid_is_available( - int registry_dir_fd, - uid_t candidate) { - +static int uid_is_available(int registry_dir_fd, uid_t candidate, int parent_userns_fd) { int r; assert(registry_dir_fd >= 0); log_debug("Checking if UID " UID_FMT " is available.", candidate); + uint64_t parent_userns_inode = 0; + struct stat parent_st; + if (fstat(parent_userns_fd, &parent_st) < 0) + return log_debug_errno(errno, "Failed to fstat parent user namespace: %m"); + parent_userns_inode = parent_st.st_ino; + r = userns_registry_uid_exists(registry_dir_fd, candidate); if (r < 0) return r; @@ -379,17 +399,65 @@ static int uid_is_available( if (r > 0) return false; - r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL); - if (r >= 0) - return false; - if (r != -ESRCH) + /* Also check delegation files. If parent_userns_inode is set and matches the delegation's userns + * inode, the UID is available because the parent owns that delegation. */ + r = userns_registry_delegation_uid_exists(registry_dir_fd, candidate); + if (r < 0) return r; + if (r > 0) { + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo delegation = DELEGATED_USER_NAMESPACE_INFO_NULL; + r = userns_registry_load_delegation_by_uid(registry_dir_fd, candidate, &delegation); + if (r < 0) + return r; - r = groupdb_by_gid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL); - if (r >= 0) - return false; - if (r != -ESRCH) + if (delegation.userns_inode != parent_userns_inode) + return false; + + /* The parent userns owns this delegation, so the UID is available for nested allocation */ + log_debug("UID " UID_FMT " is delegated by parent userns inode %" PRIu64 ", available for nested allocation.", + candidate, parent_userns_inode); + } + + r = userns_registry_delegation_gid_exists(registry_dir_fd, (gid_t) candidate); + if (r < 0) return r; + if (r > 0) { + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo delegation = DELEGATED_USER_NAMESPACE_INFO_NULL; + r = userns_registry_load_delegation_by_gid(registry_dir_fd, candidate, &delegation); + if (r < 0) + return r; + + if (delegation.userns_inode != parent_userns_inode) + return false; + + /* The parent userns owns this delegation, so the UID is available for nested allocation */ + log_debug("UID " UID_FMT " is delegated by parent userns inode %" PRIu64 ", available for nested allocation.", + candidate, parent_userns_inode); + } + + r = is_our_namespace(parent_userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to check if parent user namespace is our user namespace: %m"); + + if (r > 0) { + /* Only check userdb if we're allocating from our current user namespace. userdb won't be + * to tell us anything on whether UIDs/GIDs in another user namespace are in use or not. On + * top of that, for nspawn containers registered with machined's userdb implementation, it + * would tell us that any ranges delegated to the container are in use (which is true in the + * nsresourced user namespace, but not in the nspawn user namespace). */ + + r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + r = groupdb_by_gid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + } log_debug("UID " UID_FMT " is available.", candidate); @@ -433,19 +501,114 @@ static int name_is_available( return true; } -static int allocate_now( +static int allocate_one( int registry_dir_fd, - UserNamespaceInfo *info, - int *ret_lock_fd) { + const char *name, + uint32_t size, + int parent_userns_fd, + UIDRange *candidates, + uid_t *ret_candidate) { static const uint8_t hash_key[16] = { 0xd4, 0xd7, 0x33, 0xa7, 0x4d, 0xd3, 0x42, 0xcd, 0xaa, 0xe9, 0x45, 0xd0, 0xfb, 0xec, 0x79, 0xee, }; - - _cleanup_(uid_range_freep) UIDRange *valid_range = NULL; - uid_t candidate, uidmin, uidmax, uidmask; + _cleanup_(uid_range_freep) UIDRange *copy = NULL; + uid_t candidate, uidmin, uidmax; unsigned n_tries = 100; + size_t idx; + int r; + + assert(registry_dir_fd >= 0); + assert(candidates); + assert(ret_candidate); + + switch (size) { + + case NSRESOURCE_UIDS_64K: + uidmin = CONTAINER_UID_BASE_MIN; + uidmax = CONTAINER_UID_BASE_MAX; + break; + + case NSRESOURCE_UIDS_1: + uidmin = DYNAMIC_UID_MIN; + uidmax = DYNAMIC_UID_MAX; + break; + + default: + assert_not_reached(); + } + + /* Make a copy of candidates that we can modify for the selection algorithm */ + r = uid_range_copy(candidates, ©); + if (r < 0) + return log_debug_errno(r, "Failed to copy UID range: %m"); + + /* Clip the copy with the valid UID range for this allocation size */ + r = uid_range_clip(copy, uidmin, uidmax); + if (r < 0) + return log_debug_errno(r, "Failed to intersect UID range: %m"); + + /* Partition entries into entries of exactly the right size */ + r = uid_range_partition(copy, size); + if (r < 0) + return log_debug_errno(r, "Failed to partition UID ranges: %m"); + + if (uid_range_is_empty(copy)) + return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate."); + + log_debug("Partitioned UID range into %zu entries of size %" PRIu32, copy->n_entries, size); + + /* Start from a hash of the input name if we have one, use random values afterwards. */ + idx = name ? siphash24_string(name, hash_key) : random_u32(); + for (;; idx = random_u32()) { + if (uid_range_is_empty(copy)) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "All candidate UIDs already taken."); + + if (--n_tries <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available."); + + idx %= copy->n_entries; + + candidate = copy->entries[idx].start; + + /* We only check the base UID for each range. Pass the parent userns inode so that + * allocating from a delegated range owned by the parent is allowed. */ + r = uid_is_available(registry_dir_fd, candidate, parent_userns_fd); + if (r < 0) + return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate); + if (r > 0) + break; + + log_debug("UID range " UID_FMT " already taken.", candidate); + + /* Remove this unavailable range from candidates so we don't try it again */ + r = uid_range_remove(copy, candidate, size); + if (r < 0) + return log_debug_errno(r, "Failed to remove unavailable range from candidates: %m"); + } + + /* Remove the allocated range from the original candidates */ + r = uid_range_remove(candidates, candidate, size); + if (r < 0) + return log_debug_errno(r, "Failed to remove allocated range from candidates: %m"); + + *ret_candidate = candidate; + + log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + size - 1); + + return 0; +} + +static int allocate_now( + int registry_dir_fd, + int userns_fd, + int parent_userns_fd, + UserNamespaceInfo *info, + int *ret_lock_fd) { + + _cleanup_(uid_range_freep) UIDRange *candidates = NULL; + uid_t candidate; int r; /* Returns the following error codes: @@ -456,33 +619,12 @@ static int allocate_now( */ assert(registry_dir_fd >= 0); + assert(userns_fd >= 0); assert(info); - switch (info->size) { - - case 0x10000U: - uidmin = CONTAINER_UID_BASE_MIN; - uidmax = CONTAINER_UID_BASE_MAX; - uidmask = (uid_t) UINT32_C(0xFFFF0000); - break; - - case 1U: - uidmin = DYNAMIC_UID_MIN; - uidmax = DYNAMIC_UID_MAX; - uidmask = (uid_t) UINT32_C(0xFFFFFFFF); - break; - - default: - assert_not_reached(); - } - - r = uid_range_load_userns(/* path= */ NULL, UID_RANGE_USERNS_INSIDE, &valid_range); + r = uid_range_load_userns_by_fd(parent_userns_fd, UID_RANGE_USERNS_INSIDE, &candidates); if (r < 0) - return r; - - /* Check early whether we have any chance at all given our own uid range */ - if (!uid_range_overlaps(valid_range, uidmin, uidmax)) - return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate."); + return log_debug_errno(r, "Failed to read userns UID range: %m"); _cleanup_close_ int lock_fd = -EBADF; lock_fd = userns_registry_lock(registry_dir_fd); @@ -508,45 +650,83 @@ static int allocate_now( if (r == 0) return -EEXIST; - for (candidate = siphash24_string(info->name, hash_key) & UINT32_MAX;; /* Start from a hash of the input name */ - candidate = random_u32()) { /* Use random values afterwards */ - - if (--n_tries <= 0) - return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available."); - - candidate = (candidate % (uidmax - uidmin)) + uidmin; - candidate &= uidmask; - - if (!uid_range_covers(valid_range, candidate, info->size)) - continue; - - /* We only check the base UID for each range (!) */ - r = uid_is_available(registry_dir_fd, candidate); + /* If the source UID/GID are already set we're doing a "self" user namespace and don't need to + * allocate a transient range. */ + if (!uid_is_valid(info->start_uid) && !gid_is_valid(info->start_gid)) { + r = allocate_one( + registry_dir_fd, + info->name, info->size, + parent_userns_fd, + candidates, + &candidate); if (r < 0) - return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate); - if (r > 0) { - info->start_uid = candidate; - info->start_gid = (gid_t) candidate; + return r; - log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + info->size - 1); - - if (ret_lock_fd) - *ret_lock_fd = TAKE_FD(lock_fd); - - return 0; - } - - log_debug("UID range " UID_FMT " already taken.", candidate); + info->start_uid = candidate; + info->start_gid = (gid_t) candidate; } + + /* Now allocate delegated ranges if requested */ + if (info->n_delegates > 0) { + assert(info->delegates); + + FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) { + r = allocate_one( + registry_dir_fd, + /* name= */ NULL, + delegate->size, + parent_userns_fd, + candidates, + &candidate); + if (r < 0) + return r; + + delegate->userns_inode = info->userns_inode; + delegate->start_uid = candidate; + delegate->start_gid = (gid_t) candidate; + } + } + + if (ret_lock_fd) + *ret_lock_fd = TAKE_FD(lock_fd); + + return 0; } -static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) { +static int write_userns_mappings(PidRef *pidref, const char *uidmap, const char *gidmap) { + const char *pmap; + int r; + + assert(pidref); + assert(uidmap); + assert(gidmap); + + pmap = procfs_file_alloca(pidref->pid, "uid_map"); + r = write_string_file(pmap, uidmap, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m"); + + pmap = procfs_file_alloca(pidref->pid, "gid_map"); + r = write_string_file(pmap, gidmap, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m"); + + return 0; +} + +static int write_userns( + int userns_fd, + int parent_userns_fd, + const UserNamespaceInfo *userns_info, + bool map_foreign) { + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; _cleanup_close_ int efd = -EBADF; uint64_t u; int r; - assert(usernsfd >= 0); + assert(userns_fd >= 0); + assert(parent_userns_fd >= 0); assert(userns_info); assert(uid_is_valid(userns_info->target_uid)); assert(uid_is_valid(userns_info->start_uid)); @@ -566,7 +746,7 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) { if (r == 0) { /* child */ - if (setns(usernsfd, CLONE_NEWUSER) < 0) { + if (setns(userns_fd, CLONE_NEWUSER) < 0) { log_error_errno(errno, "Failed to join user namespace: %m"); goto child_fail; } @@ -588,22 +768,167 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) { /* Now write mapping */ - _cleanup_free_ char *pmap = NULL; + _cleanup_(uid_range_freep) UIDRange *outside_range = NULL; + r = uid_range_load_userns_by_fd_full(parent_userns_fd, UID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &outside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); - if (asprintf(&pmap, "/proc/" PID_FMT "/uid_map", pidref.pid) < 0) + _cleanup_(uid_range_freep) UIDRange *inside_range = NULL; + r = uid_range_load_userns_by_fd_full(parent_userns_fd, UID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &inside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); + + uid_t start_uid; + r = uid_range_translate(outside_range, inside_range, userns_info->start_uid, &start_uid); + if (r < 0) + return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", userns_info->start_uid); + + /* Let's enforce that the transient UID/GID ranges are mapped 1:1 in the parent user namespace, to + * avoid any weird mapping shenanigans that might happen otherwise. */ + + if (uid_is_transient(userns_info->start_uid) && start_uid != userns_info->start_uid) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Transient UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")", + userns_info->start_uid, start_uid); + + /* Build uid_map content: primary mapping + delegated mappings (1:1) */ + _cleanup_free_ char *uidmap = NULL; + if (asprintf(&uidmap, UID_FMT " " UID_FMT " %" PRIu32 "\n", + userns_info->target_uid, start_uid, userns_info->size) < 0) return log_oom(); - r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " %" PRIu32 "\n", userns_info->target_uid, userns_info->start_uid, userns_info->size); - if (r < 0) - return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m"); + log_debug("UID mapping: " UID_FMT " " UID_FMT " %" PRIu32, + userns_info->target_uid, userns_info->start_uid, userns_info->size); - pmap = mfree(pmap); - if (asprintf(&pmap, "/proc/" PID_FMT "/gid_map", pidref.pid) < 0) + FOREACH_ARRAY(delegate, userns_info->delegates, userns_info->n_delegates) { + r = uid_range_translate(outside_range, inside_range, delegate->start_uid, &start_uid); + if (r < 0) + return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", userns_info->start_uid); + + if (start_uid != delegate->start_uid) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Delegated transient UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")", + delegate->start_uid, start_uid); + + if (strextendf(&uidmap, + UID_FMT " " UID_FMT " %" PRIu32 "\n", + delegate->start_uid, + start_uid, + delegate->size) < 0) + return log_oom(); + + log_debug("UID mapping: " UID_FMT " " UID_FMT " %" PRIu32, + delegate->start_uid, start_uid, delegate->size); + } + + if (map_foreign) { + r = uid_range_translate(outside_range, inside_range, FOREIGN_UID_MIN, &start_uid); + if (r < 0) + return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", FOREIGN_UID_MIN); + + if (start_uid != FOREIGN_UID_MIN) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Foreign UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")", + FOREIGN_UID_MIN, start_uid); + + if (strextendf(&uidmap, UID_FMT " " UID_FMT " %" PRIu32 "\n", + FOREIGN_UID_MIN, start_uid, NSRESOURCE_UIDS_64K) < 0) + return log_oom(); + } + + outside_range = uid_range_free(outside_range); + inside_range = uid_range_free(inside_range); + + r = uid_range_load_userns_by_fd_full(parent_userns_fd, GID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &outside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + + r = uid_range_load_userns_by_fd_full(parent_userns_fd, GID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &inside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + + gid_t start_gid; + r = uid_range_translate(outside_range, inside_range, userns_info->start_gid, &start_gid); + if (r < 0) + return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", userns_info->start_gid); + + if (gid_is_transient(userns_info->start_gid) && start_gid != userns_info->start_gid) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Transient GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")", + userns_info->start_gid, start_gid); + + _cleanup_free_ char *gidmap = NULL; + if (asprintf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n", + userns_info->target_gid, start_gid, userns_info->size) < 0) return log_oom(); - r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " %" PRIu32 "\n", userns_info->target_gid, userns_info->start_gid, userns_info->size); + log_debug("GID mapping: " GID_FMT " " GID_FMT " %" PRIu32, + userns_info->target_gid, userns_info->start_gid, userns_info->size); + + FOREACH_ARRAY(delegate, userns_info->delegates, userns_info->n_delegates) { + r = uid_range_translate(outside_range, inside_range, delegate->start_gid, &start_gid); + if (r < 0) + return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", userns_info->start_gid); + + if (start_gid != delegate->start_gid) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Delegated transient GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")", + delegate->start_gid, start_gid); + + /* Delegated ranges are mapped 1:1 (inside GID == outside GID) */ + if (strextendf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n", + delegate->start_gid, + start_gid, + delegate->size) < 0) + return log_oom(); + + log_debug("GID mapping: " GID_FMT " " GID_FMT " %" PRIu32, + delegate->start_gid, start_gid, delegate->size); + } + + if (map_foreign) { + r = uid_range_translate(outside_range, inside_range, FOREIGN_UID_MIN, &start_gid); + if (r < 0) + return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", FOREIGN_UID_MIN); + + if (start_gid != FOREIGN_UID_MIN) + return log_debug_errno( + SYNTHETIC_ERRNO(ERANGE), + "Foreign GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")", + FOREIGN_UID_MIN, start_gid); + + if (strextendf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n", + FOREIGN_UID_MIN, start_gid, NSRESOURCE_UIDS_64K) < 0) + return log_oom(); + } + + r = is_our_namespace(parent_userns_fd, NAMESPACE_USER); if (r < 0) - return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m"); + return log_debug_errno(r, "Failed to check if parent user namespace refers to our own user namespace: %m"); + if (r > 0) + return write_userns_mappings(&pidref, uidmap, gidmap); + + /* The kernel is paranoid that the uid_map and gid_map files are written either from the user + * namespace itself or its parent user namespace, so we have to join the parent user namespace to + * write the files. */ + + r = pidref_safe_fork("(sd-userns)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT, /* ret= */ NULL); + if (r < 0) + return r; + if (r == 0) { + if (setns(parent_userns_fd, CLONE_NEWUSER) < 0) { + log_error_errno(errno, "Failed to join parent user namespace: %m"); + _exit(EXIT_FAILURE); + } + + r = write_userns_mappings(&pidref, uidmap, gidmap); + _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); + } /* We are done! */ @@ -755,14 +1080,23 @@ static int validate_name(sd_varlink *link, const char *name, bool mangle, char * return 0; } -static int validate_target_and_size(sd_varlink *link, uid_t target, uint32_t size) { +static int validate_target_and_size(sd_varlink *link, uid_t target, uint32_t size, AllocateUserRangeType type) { assert(link); - if (!IN_SET(size, 1U, 0x10000)) - return sd_varlink_error_invalid_parameter_name(link, "size"); + if (type == ALLOCATE_USER_RANGE_SELF) { + /* Self userns must have size 1 and target must be 0 or unset */ + if (size != 1) + return sd_varlink_error_invalid_parameter_name(link, "size"); - if (!uid_is_valid(target) || target > UINT32_MAX - size) - return sd_varlink_error_invalid_parameter_name(link, "target"); + if (!IN_SET(target, UID_INVALID, 0)) + return sd_varlink_error_invalid_parameter_name(link, "target"); + } else { + if (!IN_SET(size, 1U, 0x10000)) + return sd_varlink_error_invalid_parameter_name(link, "size"); + + if (!uid_is_valid(target) || target > UINT32_MAX - size) + return sd_varlink_error_invalid_parameter_name(link, "target"); + } return 0; } @@ -836,20 +1170,26 @@ static int validate_userns_is_empty(sd_varlink *link, int userns_fd) { typedef struct AllocateParameters { const char *name; + AllocateUserRangeType type; uint32_t size; uid_t target; unsigned userns_fd_idx; bool mangle_name; + uint32_t delegate_container_ranges; + bool map_foreign; } AllocateParameters; static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { - { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AllocateParameters, name), SD_JSON_MANDATORY }, - { "size", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, size), SD_JSON_MANDATORY }, - { "target", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(AllocateParameters, target), 0 }, - { "userNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), SD_JSON_MANDATORY }, - { "mangleName", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(AllocateParameters, mangle_name), 0 }, + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AllocateParameters, name), SD_JSON_MANDATORY }, + { "size", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, size), SD_JSON_MANDATORY }, + { "target", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(AllocateParameters, target), 0 }, + { "userNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), SD_JSON_MANDATORY }, + { "mangleName", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(AllocateParameters, mangle_name), 0 }, + { "type", SD_JSON_VARIANT_STRING, dispatch_allocate_user_range_type, offsetof(AllocateParameters, type), 0 }, + { "delegateContainerRanges", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, delegate_container_ranges), 0 }, + { "mapForeign", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_stdbool, offsetof(AllocateParameters, map_foreign), 0 }, {} }; @@ -857,9 +1197,12 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para _cleanup_free_ char *userns_name = NULL; Context *c = ASSERT_PTR(userdata); uid_t peer_uid; + gid_t peer_gid; struct stat userns_st; AllocateParameters p = { + .type = ALLOCATE_USER_RANGE_MANAGED, .size = UINT32_MAX, + .target = UID_INVALID, .userns_fd_idx = UINT_MAX, }; int r; @@ -875,14 +1218,20 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para if (r != 0) return r; + if (p.type != ALLOCATE_USER_RANGE_SELF && p.target == UID_INVALID) + p.target = 0; + r = validate_name(link, p.name, p.mangle_name, &userns_name); if (r != 0) return r; - r = validate_target_and_size(link, p.target, p.size); + r = validate_target_and_size(link, p.target, p.size, p.type); if (r != 0) return r; + if (p.delegate_container_ranges > USER_NAMESPACE_DELEGATIONS_MAX) + return sd_varlink_error(link, "io.systemd.NamespaceResource.TooManyDelegations", NULL); + userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m"); @@ -898,10 +1247,18 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para if (fstat(userns_fd, &userns_st) < 0) return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + _cleanup_close_ int parent_userns_fd = ioctl(userns_fd, NS_GET_PARENT); + if (parent_userns_fd < 0) + return log_debug_errno(errno, "Failed to get parent user namespace: %m"); + r = sd_varlink_get_peer_uid(link, &peer_uid); if (r < 0) return r; + r = sd_varlink_get_peer_gid(link, &peer_gid); + if (r < 0) + return r; + const char *polkit_details[] = { "name", userns_name, NULL, @@ -942,7 +1299,48 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para userns_info->target_uid = p.target; userns_info->target_gid = (gid_t) p.target; - r = allocate_now(registry_dir_fd, userns_info, &lock_fd); + if (p.type == ALLOCATE_USER_RANGE_SELF) { + /* The start UID/GID will be mapped to the parent userns in write_userns(). If a self + * mapping to the peer UID/GID is requested, we have to map the target UID/GID ourselves here + * as write_userns() doesn't take care of that. */ + + userns_info->start_uid = peer_uid; + userns_info->start_gid = peer_gid; + + if (p.target == UID_INVALID) { + r = uid_range_translate_userns_fd( + parent_userns_fd, + UID_RANGE_USERNS_OUTSIDE, + peer_uid, + &userns_info->target_uid); + if (r < 0) + return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent user namespace: %m", peer_uid); + + r = uid_range_translate_userns_fd( + parent_userns_fd, + GID_RANGE_USERNS_OUTSIDE, + peer_gid, + &userns_info->target_gid); + if (r < 0) + return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent user namespace: %m", peer_gid); + } + } + + /* Set up delegation arrays if requested */ + if (p.delegate_container_ranges > 0) { + userns_info->delegates = new0(DelegatedUserNamespaceInfo, p.delegate_container_ranges); + if (!userns_info->delegates) + return -ENOMEM; + + FOREACH_ARRAY(delegate, userns_info->delegates, p.delegate_container_ranges) { + *delegate = DELEGATED_USER_NAMESPACE_INFO_NULL; + delegate->size = NSRESOURCE_UIDS_64K; + } + + userns_info->n_delegates = p.delegate_container_ranges; + } + + r = allocate_now(registry_dir_fd, userns_fd, parent_userns_fd, userns_info, &lock_fd); if (r == -EHOSTDOWN) /* The needed UID range is not delegated to us */ return sd_varlink_error(link, "io.systemd.NamespaceResource.DynamicRangeUnavailable", NULL); if (r == -EBUSY) /* All used up */ @@ -968,7 +1366,16 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para if (r < 0) goto fail; - r = write_userns(userns_fd, userns_info); + if (p.type == ALLOCATE_USER_RANGE_SELF) { + /* For "self" allocations we deny setgroups() via the BPF LSM. We can't use + * /proc/self/setgroups for this as that is transitive and also applies to child user + * namespaces. The BPF LSM hook only applies to the specific user namespace. */ + r = userns_restrict_setgroups_deny_by_fd(c->bpf, userns_fd); + if (r < 0) + goto fail; + } + + r = write_userns(userns_fd, parent_userns_fd, userns_info, p.map_foreign); if (r < 0) goto fail; diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c index dc06b7b08f7..29125a84a8d 100644 --- a/src/nsresourced/test-userns-restrict.c +++ b/src/nsresourced/test-userns-restrict.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include #include @@ -8,12 +9,14 @@ #include "errno-util.h" #include "fd-util.h" +#include "fileio.h" #include "namespace-util.h" #include "pidref.h" #include "process-util.h" #include "rm-rf.h" #include "tests.h" #include "tmpfile-util.h" +#include "uid-classification.h" #include "userns-restrict.h" static int make_tmpfs_fsmount(void) { @@ -50,10 +53,16 @@ TEST(userns_restrict) { int r; ASSERT_OK(mkdtemp_malloc(NULL, &t)); + /* Make sure the dir is owned by the transient UID we'll be using so we don't get rejected with a + * permission error before we even get to the BPF-LSM. */ + ASSERT_OK_ERRNO(chown(t, CONTAINER_UID_MIN, CONTAINER_UID_MIN)); host_fd1 = ASSERT_OK_ERRNO(open(t, O_DIRECTORY|O_CLOEXEC)); host_tmpfs = ASSERT_OK(make_tmpfs_fsmount()); - userns_fd = ASSERT_OK(userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true)); + + _cleanup_free_ char *idmap = NULL; + ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN)); + userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true)); ASSERT_OK(userns_restrict_put_by_fd( bpf_obj, @@ -69,7 +78,7 @@ TEST(userns_restrict) { if (r == 0) { _cleanup_close_ int private_tmpfs = -EBADF; - ASSERT_OK_ERRNO(setns(userns_fd, CLONE_NEWUSER)); + ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF)); ASSERT_OK_ERRNO(unshare(CLONE_NEWNS)); /* Allocate tmpfs locally */ @@ -140,4 +149,137 @@ TEST(userns_restrict) { ASSERT_OK(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG)); } +static void write_child_mappings(PidRef *child, int parent_userns_fd) { + /* The kernel requires uid_map/gid_map to be written from the parent user namespace of the + * target namespace. Fork a helper that joins the parent userns and writes the mappings from + * there, mirroring what write_userns() does in nsresourcework.c. */ + int r; + + r = ASSERT_OK(pidref_safe_fork("(sd-write-map)", FORK_DEATHSIG_SIGKILL|FORK_WAIT|FORK_LOG, NULL)); + if (r == 0) { + char path[STRLEN("/proc//uid_map") + DECIMAL_STR_MAX(pid_t) + 1]; + + ASSERT_OK_ERRNO(setns(parent_userns_fd, CLONE_NEWUSER)); + + xsprintf(path, "/proc/" PID_FMT "/uid_map", child->pid); + ASSERT_OK(write_string_file(path, "0 0 1\n", WRITE_STRING_FILE_DISABLE_BUFFER)); + + xsprintf(path, "/proc/" PID_FMT "/gid_map", child->pid); + ASSERT_OK(write_string_file(path, "0 0 1\n", WRITE_STRING_FILE_DISABLE_BUFFER)); + + _exit(EXIT_SUCCESS); + } +} + +TEST(setgroups_deny) { + _cleanup_close_ int deny_userns_fd = -EBADF, allow_userns_fd = -EBADF, + afd = -EBADF, bfd = -EBADF; + int r; + + _cleanup_free_ char *idmap = NULL; + ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN)); + + /* Create a userns that will have setgroups() denied via BPF. We don't set setgroups_deny here + * because that uses /proc/self/setgroups which is transitive and we want to test the BPF-LSM + * denial specifically. */ + deny_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false)); + + ASSERT_OK(userns_restrict_put_by_fd( + bpf_obj, + deny_userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0)); + ASSERT_OK(userns_restrict_setgroups_deny_by_fd(bpf_obj, deny_userns_fd)); + + /* Create a userns that is managed (in mount ID hash) but does NOT have setgroups() denied */ + allow_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false)); + + ASSERT_OK(userns_restrict_put_by_fd( + bpf_obj, + allow_userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0)); + + afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC)); + bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC)); + + /* Test 1: setgroups() should be denied in the deny userns, including after unsharing into a child + * user namespace (the ancestor walk should find the deny entry). */ + { + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; + + r = ASSERT_OK(pidref_safe_fork("(test-deny)", FORK_LOG|FORK_DEATHSIG_SIGKILL, &pidref)); + if (r == 0) { + /* Enter the userns manually without going through namespace_enter(), because + * that calls reset_uid_gid() which calls setgroups() internally. Since the + * BPF LSM denies setgroups(), reset_uid_gid() would fail before calling + * setresuid()/setresgid(), leaving us as the overflow UID without + * capabilities. */ + ASSERT_OK_ERRNO(setns(deny_userns_fd, CLONE_NEWUSER)); + ASSERT_OK_ERRNO(setresgid(0, 0, 0)); + ASSERT_OK_ERRNO(setresuid(0, 0, 0)); + + /* setgroups() should be denied by BPF LSM */ + ASSERT_ERROR_ERRNO(setgroups(0, NULL), EPERM); + + /* Unshare into a child user namespace. The parent will write the mappings + * for us since writing /proc/self/uid_map from inside the userns fails + * because the proc mount belongs to the init user namespace. */ + ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER)); + ASSERT_OK_ERRNO(eventfd_write(afd, 1)); + uint64_t x; + ASSERT_OK_ERRNO(eventfd_read(bfd, &x)); + + /* setgroups() should still be denied because the ancestor walk finds the + * deny entry on the parent user namespace */ + ASSERT_ERROR_ERRNO(setgroups(0, NULL), EPERM); + + _exit(EXIT_SUCCESS); + } + + uint64_t x; + ASSERT_OK_ERRNO(eventfd_read(afd, &x)); + write_child_mappings(&pidref, deny_userns_fd); + ASSERT_OK_ERRNO(eventfd_write(bfd, 1)); + + ASSERT_OK(pidref_wait_for_terminate_and_check("(test-deny)", &pidref, WAIT_LOG)); + } + + /* Test 2: setgroups() should be allowed in the managed-only userns (mount ID hash but no setgroups + * deny entry), including in a child user namespace. */ + { + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; + + r = ASSERT_OK(pidref_safe_fork("(test-allow)", FORK_LOG|FORK_DEATHSIG_SIGKILL, &pidref)); + if (r == 0) { + ASSERT_OK_ERRNO(setns(allow_userns_fd, CLONE_NEWUSER)); + ASSERT_OK_ERRNO(setresgid(0, 0, 0)); + ASSERT_OK_ERRNO(setresuid(0, 0, 0)); + + /* setgroups() should succeed since this userns is only in the mount ID hash */ + ASSERT_OK_ERRNO(setgroups(0, NULL)); + + /* Also should work in a child userns since the ancestor walk finds the + * mount ID hash entry (not the setgroups deny entry) */ + ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER)); + ASSERT_OK_ERRNO(eventfd_write(afd, 1)); + uint64_t x; + ASSERT_OK_ERRNO(eventfd_read(bfd, &x)); + + ASSERT_OK_ERRNO(setgroups(0, NULL)); + + _exit(EXIT_SUCCESS); + } + + uint64_t x; + ASSERT_OK_ERRNO(eventfd_read(afd, &x)); + write_child_mappings(&pidref, allow_userns_fd); + ASSERT_OK_ERRNO(eventfd_write(bfd, 1)); + + ASSERT_OK(pidref_wait_for_terminate_and_check("(test-allow)", &pidref, WAIT_LOG)); + } +} + DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c index 97a222cfc40..371a35086f4 100644 --- a/src/nsresourced/userns-registry.c +++ b/src/nsresourced/userns-registry.c @@ -20,6 +20,7 @@ #include "stat-util.h" #include "string-util.h" #include "strv.h" +#include "uid-classification.h" #include "user-util.h" #include "userns-registry.h" @@ -56,6 +57,23 @@ int userns_registry_lock(int dir_fd) { return TAKE_FD(lock_fd); } +void delegated_userns_info_done(DelegatedUserNamespaceInfo *info) { + if (!info) + return; + + info->ancestor_userns = mfree(info->ancestor_userns); + info->n_ancestor_userns = 0; +} + +void delegated_userns_info_done_many(DelegatedUserNamespaceInfo infos[], size_t n) { + assert(infos || n == 0); + + FOREACH_ARRAY(info, infos, n) + delegated_userns_info_done(info); + + free(infos); +} + UserNamespaceInfo* userns_info_new(void) { UserNamespaceInfo *info = new(UserNamespaceInfo, 1); if (!info) @@ -79,6 +97,8 @@ UserNamespaceInfo *userns_info_free(UserNamespaceInfo *userns) { free(userns->cgroups); free(userns->name); + delegated_userns_info_done_many(userns->delegates, userns->n_delegates); + strv_free(userns->netifs); return mfree(userns); @@ -128,6 +148,100 @@ static int dispatch_cgroups_array(const char *name, sd_json_variant *variant, sd return 0; } +static int dispatch_delegates_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + UserNamespaceInfo *info = ASSERT_PTR(userdata); + DelegatedUserNamespaceInfo *delegates = NULL; + size_t n = 0; + int r; + + CLEANUP_ARRAY(delegates, n, delegated_userns_info_done_many); + + if (sd_json_variant_is_null(variant)) { + delegated_userns_info_done_many(info->delegates, info->n_delegates); + info->delegates = NULL; + info->n_delegates = 0; + return 0; + } + + if (!sd_json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + size_t elements = sd_json_variant_elements(variant); + if (elements > USER_NAMESPACE_DELEGATIONS_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(E2BIG), "Too many delegations."); + + delegates = new(DelegatedUserNamespaceInfo, elements); + if (!delegates) + return json_log_oom(variant, flags); + + sd_json_variant *e; + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + static const sd_json_dispatch_field delegate_dispatch_table[] = { + { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(DelegatedUserNamespaceInfo, userns_inode), 0 }, + { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_uid), SD_JSON_MANDATORY }, + { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_gid), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(DelegatedUserNamespaceInfo, size), SD_JSON_MANDATORY }, + {} + }; + + delegates[n] = DELEGATED_USER_NAMESPACE_INFO_NULL; + + r = sd_json_dispatch(e, delegate_dispatch_table, flags, &delegates[n]); + if (r < 0) + return r; + + if (!uid_is_valid(delegates[n].start_uid) || !gid_is_valid(delegates[n].start_gid)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid delegate UID/GID."); + + if (delegates[n].size == 0) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid delegate size."); + + n++; + } + + delegated_userns_info_done_many(info->delegates, info->n_delegates); + info->delegates = TAKE_PTR(delegates); + info->n_delegates = n; + + return 0; +} + +static int dispatch_ancestor_userns_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + DelegatedUserNamespaceInfo *info = ASSERT_PTR(userdata); + _cleanup_free_ uint64_t *ancestor_userns = NULL; + size_t n = 0; + + if (sd_json_variant_is_null(variant)) { + info->ancestor_userns = mfree(info->ancestor_userns); + info->n_ancestor_userns = 0; + return 0; + } + + if (!sd_json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + ancestor_userns = new(uint64_t, sd_json_variant_elements(variant)); + if (!ancestor_userns) + return json_log_oom(variant, flags); + + sd_json_variant *e; + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + if (!sd_json_variant_is_unsigned(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an unsigned integer."); + + uint64_t v = sd_json_variant_unsigned(e); + if (v == 0) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid ancestor userns inode 0."); + + ancestor_userns[n++] = v; + } + + free_and_replace(info->ancestor_userns, ancestor_userns); + info->n_ancestor_userns = n; + + return 0; +} + static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) { static const sd_json_dispatch_field dispatch_table[] = { @@ -141,6 +255,7 @@ static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo ** { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 }, { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 }, { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 }, + { "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 }, {} }; @@ -171,8 +286,6 @@ static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo ** if (userns_info->userns_inode == 0) return -EBADMSG; - if (userns_info->start_uid == 0 || userns_info->start_gid == 0) - return -EBADMSG; if (userns_info->size == 0) { if (uid_is_valid(userns_info->start_uid) || uid_is_valid(userns_info->target_uid)) @@ -443,6 +556,18 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { return r; } + _cleanup_(sd_json_variant_unrefp) sd_json_variant *delegates_array = NULL; + FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) { + r = sd_json_variant_append_arraybo( + &delegates_array, + SD_JSON_BUILD_PAIR_UNSIGNED("userns", delegate->userns_inode), + SD_JSON_BUILD_PAIR_UNSIGNED("start", delegate->start_uid), + SD_JSON_BUILD_PAIR_UNSIGNED("startGid", delegate->start_gid), + SD_JSON_BUILD_PAIR_UNSIGNED("size", delegate->size)); + if (r < 0) + return r; + } + _cleanup_(sd_json_variant_unrefp) sd_json_variant *def = NULL; r = sd_json_buildo( &def, @@ -455,7 +580,8 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->start_gid), "startGid", SD_JSON_BUILD_UNSIGNED(info->start_gid)), SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->target_gid), "targetGid", SD_JSON_BUILD_UNSIGNED(info->target_gid)), SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)), - JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs)); + JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs), + SD_JSON_BUILD_PAIR_CONDITION(!!delegates_array, "delegates", SD_JSON_BUILD_VARIANT(delegates_array))); if (r < 0) return r; @@ -484,7 +610,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { goto fail; } - if (uid_is_valid(info->start_uid)) { + if (uid_is_transient(info->start_uid)) { if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0) { r = log_oom_debug(); goto fail; @@ -497,7 +623,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { } } - if (gid_is_valid(info->start_gid)) { + if (gid_is_transient(info->start_gid)) { if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0) { r = log_oom_debug(); goto fail; @@ -531,6 +657,82 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { goto fail; } + /* Store delegation files */ + FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *delegate_def = NULL, *ancestor_array = NULL; + _cleanup_free_ char *delegate_buf = NULL, *delegate_uid_fn = NULL, *delegate_gid_fn = NULL; + + if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) < 0) { + r = log_oom_debug(); + goto fail; + } + + /* Check if this delegation already exists. If so, this is a recursive + * subdelegation: we need to preserve the chain of previous owners so that + * ownership can be restored when the current owner goes away. */ + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo existing = DELEGATED_USER_NAMESPACE_INFO_NULL; + + r = userns_registry_load_delegation_by_uid(dir_fd, delegate->start_uid, &existing); + if (r >= 0) { + /* Delegation file exists — append old owner to ancestor chain */ + FOREACH_ARRAY(ancestor_userns, existing.ancestor_userns, existing.n_ancestor_userns) { + r = sd_json_variant_append_arrayb( + &ancestor_array, + SD_JSON_BUILD_UNSIGNED(*ancestor_userns)); + if (r < 0) + goto fail; + } + + /* userns_registry_store() is also called to update existing entries in the registry + * in which case we don't need to update the ownership of the delegated UID ranges. */ + if (delegate->userns_inode != existing.userns_inode) { + r = sd_json_variant_append_arrayb( + &ancestor_array, + SD_JSON_BUILD_UNSIGNED(existing.userns_inode)); + if (r < 0) + goto fail; + } + + } else if (r != -ENOENT) { + log_debug_errno(r, "Failed to load existing delegation for UID " UID_FMT ": %m", delegate->start_uid); + goto fail; + } + + r = sd_json_buildo( + &delegate_def, + SD_JSON_BUILD_PAIR_UNSIGNED("userns", delegate->userns_inode), + SD_JSON_BUILD_PAIR_UNSIGNED("start", delegate->start_uid), + SD_JSON_BUILD_PAIR_UNSIGNED("startGid", delegate->start_gid), + SD_JSON_BUILD_PAIR_UNSIGNED("size", delegate->size), + SD_JSON_BUILD_PAIR_CONDITION(!!ancestor_array, "ancestorUserns", SD_JSON_BUILD_VARIANT(ancestor_array))); + if (r < 0) + goto fail; + + r = sd_json_variant_format(delegate_def, /* flags= */ 0, &delegate_buf); + if (r < 0) { + log_debug_errno(r, "Failed to format delegation JSON object: %m"); + goto fail; + } + + r = write_string_file_at(dir_fd, delegate_uid_fn, delegate_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) { + log_debug_errno(r, "Failed to write delegation data to '%s' in registry: %m", delegate_uid_fn); + goto fail; + } + + /* Create GID symlink pointing to the UID file */ + if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) < 0) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, delegate_uid_fn, dir_fd, delegate_gid_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link delegation data to '%s' in registry: %m", delegate_gid_fn); + goto fail; + } + } + return 0; fail: @@ -547,6 +749,17 @@ fail: if (uid_fn) (void) unlinkat(dir_fd, uid_fn, AT_REMOVEDIR); + /* Clean up any delegation files we created */ + FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) { + _cleanup_free_ char *delegate_uid_fn = NULL, *delegate_gid_fn = NULL; + + if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) >= 0) + (void) unlinkat(dir_fd, delegate_uid_fn, /* flags= */ 0); + + if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) >= 0) + (void) unlinkat(dir_fd, delegate_gid_fn, /* flags= */ 0); + } + return r; } @@ -568,31 +781,39 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) { if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0) return log_oom_debug(); - ret = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0)); + r = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", reg_fn)); _cleanup_free_ char *link1_fn = NULL; link1_fn = strjoin("n", info->name, ".userns"); if (!link1_fn) return log_oom_debug(); - RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link1_fn, 0))); + r = RET_NERRNO(unlinkat(dir_fd, link1_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link1_fn)); - if (uid_is_valid(info->start_uid)) { + if (uid_is_transient(info->start_uid)) { _cleanup_free_ char *link2_fn = NULL; if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0) return log_oom_debug(); - RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link2_fn, 0))); + r = RET_NERRNO(unlinkat(dir_fd, link2_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link2_fn)); } - if (uid_is_valid(info->start_gid)) { + if (gid_is_transient(info->start_gid)) { _cleanup_free_ char *link3_fn = NULL; if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0) return log_oom_debug(); - RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link3_fn, 0))); + r = RET_NERRNO(unlinkat(dir_fd, link3_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link3_fn)); } _cleanup_free_ char *uid_fn = NULL; @@ -603,11 +824,90 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) { if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0) return log_oom_debug(); - RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, owner_fn, 0))); + r = RET_NERRNO(unlinkat(dir_fd, owner_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", owner_fn)); r = RET_NERRNO(unlinkat(dir_fd, uid_fn, AT_REMOVEDIR)); - if (r != -ENOTEMPTY) - RET_GATHER(ret, r); + if (r < 0 && r != -ENOTEMPTY) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", uid_fn)); + + /* Remove or restore delegation files */ + FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) { + /* Check if this delegation has ancestor user namespaces. If so, restore ownership to + * the last ancestor instead of removing the delegation file entirely. */ + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo existing = DELEGATED_USER_NAMESPACE_INFO_NULL; + + r = userns_registry_load_delegation_by_uid(dir_fd, delegate->start_uid, &existing); + if (r < 0) { + log_debug_errno(r, + "Failed to load delegated UID range starting at "UID_FMT":"GID_FMT" for userns %"PRIu64": %m", + delegate->start_uid, delegate->start_gid, delegate->userns_inode); + RET_GATHER(ret, r); + continue; + } + + _cleanup_free_ char *delegate_uid_fn = NULL; + if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) < 0) + return log_oom_debug(); + + if (existing.n_ancestor_userns > 0) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *delegate_def = NULL, *ancestor_array = NULL; + _cleanup_free_ char *delegate_buf = NULL; + + /* Pop the last ancestor userns inode to become the new owner */ + uint64_t new_owner = existing.ancestor_userns[existing.n_ancestor_userns - 1]; + + log_debug("Moving ownership of delegated UID range from %"PRIu64" to %"PRIu64".", + delegate->userns_inode, new_owner); + + /* Rebuild ancestor array without the last entry */ + for (size_t j = 0; j + 1 < existing.n_ancestor_userns; j++) { + r = sd_json_variant_append_arrayb( + &ancestor_array, + SD_JSON_BUILD_UNSIGNED(existing.ancestor_userns[j])); + if (r < 0) + return log_debug_errno(r, "Failed to append to JSON array: %m"); + } + + r = sd_json_buildo( + &delegate_def, + SD_JSON_BUILD_PAIR_UNSIGNED("userns", new_owner), + SD_JSON_BUILD_PAIR_UNSIGNED("start", existing.start_uid), + SD_JSON_BUILD_PAIR_UNSIGNED("startGid", existing.start_gid), + SD_JSON_BUILD_PAIR_UNSIGNED("size", existing.size), + SD_JSON_BUILD_PAIR_CONDITION(!!ancestor_array, "ancestorUserns", SD_JSON_BUILD_VARIANT(ancestor_array))); + if (r < 0) + return log_debug_errno(r, "Failed to build delegate JSON object: %m"); + + r = sd_json_variant_format(delegate_def, /* flags= */ 0, &delegate_buf); + if (r < 0) + return log_debug_errno(r, "Failed to format delegation JSON object: %m"); + + r = write_string_file_at(dir_fd, delegate_uid_fn, delegate_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to write restored delegation data to '%s' in registry: %m", delegate_uid_fn)); + + /* GID link already points to the UID file, no need to update it */ + continue; + } + + log_debug("Removing delegated UID range starting at "UID_FMT":"GID_FMT" for userns %"PRIu64 ".", + delegate->start_uid, delegate->start_gid, delegate->userns_inode); + + /* No ancestor chain — just remove the delegation files */ + r = RET_NERRNO(unlinkat(dir_fd, delegate_uid_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", delegate_uid_fn)); + + _cleanup_free_ char *delegate_gid_fn = NULL; + if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) < 0) + return log_oom_debug(); + + r = RET_NERRNO(unlinkat(dir_fd, delegate_gid_fn, 0)); + if (r < 0) + RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", delegate_gid_fn)); + } return ret; } @@ -822,3 +1122,135 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) { return n; } + +int userns_registry_delegation_uid_exists(int dir_fd, uid_t start) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!uid_is_valid(start)) + return -ENOENT; + + if (start == 0) + return true; + + if (asprintf(&fn, "u" UID_FMT ".delegate", start) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_delegation_gid_exists(int dir_fd, gid_t start) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!gid_is_valid(start)) + return -ENOENT; + + if (start == 0) + return true; + + if (asprintf(&fn, "g" GID_FMT ".delegate", start) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +static int userns_registry_load_delegation(int dir_fd, const char *filename, DelegatedUserNamespaceInfo *ret) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(DelegatedUserNamespaceInfo, userns_inode), SD_JSON_MANDATORY }, + { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_uid), SD_JSON_MANDATORY }, + { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_gid), 0 }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(DelegatedUserNamespaceInfo, size), SD_JSON_MANDATORY }, + { "ancestorUserns", SD_JSON_VARIANT_ARRAY, dispatch_ancestor_userns_array, 0, 0 }, + {} + }; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_close_ int registry_fd = -EBADF; + int r; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + r = sd_json_parse_file_at(/* f= */ NULL, dir_fd, filename, /* flags= */ 0, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL); + if (r < 0) + return r; + + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL; + + r = sd_json_dispatch(v, dispatch_table, /* flags= */ 0, &data); + if (r < 0) + return r; + + if (data.userns_inode == 0) + return -EBADMSG; + if (data.size == 0) + return -EBADMSG; + + if (ret) + *ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL); + + return 0; +} + +int userns_registry_load_delegation_by_uid(int dir_fd, uid_t start, DelegatedUserNamespaceInfo *ret) { + _cleanup_free_ char *fn = NULL; + int r; + + if (!uid_is_valid(start)) + return -ENOENT; + + if (asprintf(&fn, "u" UID_FMT ".delegate", start) < 0) + return -ENOMEM; + + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL; + r = userns_registry_load_delegation(dir_fd, fn, &data); + if (r < 0) + return r; + + if (data.start_uid != start) + return -EBADMSG; + + if (ret) + *ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL); + + return 0; +} + +int userns_registry_load_delegation_by_gid(int dir_fd, gid_t start, DelegatedUserNamespaceInfo *ret) { + _cleanup_free_ char *fn = NULL; + int r; + + if (!gid_is_valid(start)) + return -ENOENT; + + if (asprintf(&fn, "g" UID_FMT ".delegate", start) < 0) + return -ENOMEM; + + _cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL; + r = userns_registry_load_delegation(dir_fd, fn, &data); + if (r < 0) + return r; + + if (data.start_gid != start) + return -EBADMSG; + + if (ret) + *ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL); + + return 0; +} diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h index fee2623a3b5..f08b238861a 100644 --- a/src/nsresourced/userns-registry.h +++ b/src/nsresourced/userns-registry.h @@ -5,6 +5,26 @@ #define USER_NAMESPACE_CGROUPS_DELEGATE_MAX 16U #define USER_NAMESPACE_NETIFS_DELEGATE_MAX 16U +#define USER_NAMESPACE_DELEGATIONS_MAX 16U + +typedef struct DelegatedUserNamespaceInfo { + uint64_t userns_inode; + uid_t start_uid; + gid_t start_gid; + uint32_t size; + /* We track all the previous owners of the delegation so we can restore the previous owner of each + * delegated range when a user namespace with delegated ranges is freed. */ + uint64_t *ancestor_userns; + size_t n_ancestor_userns; +} DelegatedUserNamespaceInfo; + +#define DELEGATED_USER_NAMESPACE_INFO_NULL (DelegatedUserNamespaceInfo) { \ + .start_uid = UID_INVALID, \ + .start_gid = GID_INVALID, \ +} + +void delegated_userns_info_done(DelegatedUserNamespaceInfo *info); +void delegated_userns_info_done_many(DelegatedUserNamespaceInfo infos[], size_t n); typedef struct UserNamespaceInfo { uid_t owner; @@ -18,6 +38,8 @@ typedef struct UserNamespaceInfo { uint64_t *cgroups; size_t n_cgroups; char **netifs; + DelegatedUserNamespaceInfo *delegates; + size_t n_delegates; } UserNamespaceInfo; UserNamespaceInfo* userns_info_new(void); @@ -51,3 +73,8 @@ int userns_registry_uid_exists(int dir_fd, uid_t start); int userns_registry_gid_exists(int dir_fd, gid_t start); int userns_registry_per_uid(int dir_fd, uid_t owner); + +int userns_registry_delegation_uid_exists(int dir_fd, uid_t start); +int userns_registry_delegation_gid_exists(int dir_fd, gid_t start); +int userns_registry_load_delegation_by_uid(int dir_fd, uid_t start, DelegatedUserNamespaceInfo *ret); +int userns_registry_load_delegation_by_gid(int dir_fd, gid_t start, DelegatedUserNamespaceInfo *ret); diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c index 6a8306de66a..c0d7f8a82da 100644 --- a/src/nsresourced/userns-restrict.c +++ b/src/nsresourced/userns-restrict.c @@ -111,6 +111,10 @@ int userns_restrict_install( if (r < 0) return log_error_errno(r, "Failed to size userns ring buffer: %m"); + r = sym_bpf_map__set_max_entries(obj->maps.userns_setgroups_deny, USERNS_MAX); + if (r < 0) + return log_error_errno(r, "Failed to size userns setgroups deny hash table: %m"); + /* Dummy map to satisfy the verifier */ dummy_mnt_id_hash_fd = make_inner_hash_map(); if (dummy_mnt_id_hash_fd < 0) @@ -320,7 +324,7 @@ int userns_restrict_put_by_fd( int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode) { #if HAVE_VMLINUX_H - int r, outer_map_fd; + int r, outer_map_fd, setgroups_deny_fd; unsigned u; assert(obj); @@ -339,8 +343,77 @@ int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t use if (r < 0) return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from outer map: %m", userns_inode); + setgroups_deny_fd = sym_bpf_map__fd(obj->maps.userns_setgroups_deny); + if (setgroups_deny_fd < 0) + return log_debug_errno(setgroups_deny_fd, "Failed to get setgroups deny BPF map fd: %m"); + + r = sym_bpf_map_delete_elem(setgroups_deny_fd, &u); + if (r < 0 && r != -ENOENT) + return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from setgroups deny map: %m", userns_inode); + return 0; #else return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); #endif } + +int userns_restrict_setgroups_deny_by_inode( + struct userns_restrict_bpf *obj, + uint64_t userns_inode) { + +#if HAVE_VMLINUX_H + int map_fd, r; + uint32_t dummy = 1; + unsigned ino; + + assert(obj); + assert(userns_inode != 0); + + /* The BPF map only supports 32bit keys, and user namespace inode numbers are 32bit too, even though + * ino_t is 64bit these days. Should we ever run into a 64bit inode let's refuse early. */ + if (userns_inode > UINT32_MAX) + return -EINVAL; + + ino = (unsigned) userns_inode; + + map_fd = sym_bpf_map__fd(obj->maps.userns_setgroups_deny); + if (map_fd < 0) + return log_debug_errno(map_fd, "Failed to get setgroups deny BPF map fd: %m"); + + r = sym_bpf_map_update_elem(map_fd, &ino, &dummy, BPF_ANY); + if (r < 0) + return log_debug_errno(r, "Failed to add userns inode to setgroups deny map: %m"); + + log_debug("Denying setgroups() on userns inode %" PRIu64, userns_inode); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +#endif +} + +int userns_restrict_setgroups_deny_by_fd( + struct userns_restrict_bpf *obj, + int userns_fd) { + +#if HAVE_VMLINUX_H + struct stat st; + int r; + + assert(obj); + assert(userns_fd >= 0); + + r = fd_is_namespace(userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to determine if file descriptor is user namespace: %m"); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADF), "User namespace fd is not actually a user namespace fd."); + + if (fstat(userns_fd, &st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace: %m"); + + return userns_restrict_setgroups_deny_by_inode(obj, st.st_ino); +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +#endif +} diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h index 21a81feaff4..f0673d15944 100644 --- a/src/nsresourced/userns-restrict.h +++ b/src/nsresourced/userns-restrict.h @@ -13,4 +13,7 @@ int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t usern int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode); +int userns_restrict_setgroups_deny_by_fd(struct userns_restrict_bpf *obj, int userns_fd); +int userns_restrict_setgroups_deny_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode); + DEFINE_TRIVIAL_CLEANUP_FUNC(struct userns_restrict_bpf*, userns_restrict_bpf_free); diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c index 615f99eff10..6f70bcaf1f3 100644 --- a/src/shared/nsresource.c +++ b/src/shared/nsresource.c @@ -75,7 +75,7 @@ int nsresource_connect(sd_varlink **ret) { return 0; } -int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size) { +int nsresource_allocate_userns_full(sd_varlink *vl, const char *name, uint64_t size, uint64_t delegate_container_ranges) { _cleanup_close_ int userns_fd = -EBADF; _cleanup_free_ char *_name = NULL; const char *error_id; @@ -120,7 +120,8 @@ int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size) SD_JSON_BUILD_PAIR_STRING("name", name), SD_JSON_BUILD_PAIR_BOOLEAN("mangleName", true), SD_JSON_BUILD_PAIR_UNSIGNED("size", size), - SD_JSON_BUILD_PAIR_UNSIGNED("userNamespaceFileDescriptor", userns_fd_idx)); + SD_JSON_BUILD_PAIR_UNSIGNED("userNamespaceFileDescriptor", userns_fd_idx), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("delegateContainerRanges", delegate_container_ranges)); if (r < 0) return log_debug_errno(r, "Failed to call AllocateUserRange() varlink call: %m"); if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceInterfaceNotSupported")) diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h index 93957a10c82..5633fd9bf35 100644 --- a/src/shared/nsresource.h +++ b/src/shared/nsresource.h @@ -17,7 +17,10 @@ int nsresource_connect(sd_varlink **ret); * operations under the original identity, until the connection is closed. The 'link' parameter may be passed * as NULL in which case a short-lived connection is created, just to execute the requested operation. */ -int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size); +int nsresource_allocate_userns_full(sd_varlink *vl, const char *name, uint64_t size, uint64_t delegate_container_ranges); +static inline int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size) { + return nsresource_allocate_userns_full(vl, name, size, /* delegate_container_ranges= */ 0); +} int nsresource_register_userns(sd_varlink *vl, const char *name, int userns_fd); int nsresource_add_mount(sd_varlink *vl, int userns_fd, int mount_fd); int nsresource_add_cgroup(sd_varlink *vl, int userns_fd, int cgroup_fd); diff --git a/src/shared/varlink-io.systemd.NamespaceResource.c b/src/shared/varlink-io.systemd.NamespaceResource.c index 03bfc411347..4e592e496c9 100644 --- a/src/shared/varlink-io.systemd.NamespaceResource.c +++ b/src/shared/varlink-io.systemd.NamespaceResource.c @@ -2,18 +2,31 @@ #include "varlink-io.systemd.NamespaceResource.h" +static SD_VARLINK_DEFINE_ENUM_TYPE( + AllocateUserRangeType, + SD_VARLINK_FIELD_COMMENT("Allocate a transient UID/GID range from the dynamic range pool. This is the default."), + SD_VARLINK_DEFINE_ENUM_VALUE(managed), + SD_VARLINK_FIELD_COMMENT("Create a user namespace that maps the peer UID/GID to itself instead of allocating a transient UID range."), + SD_VARLINK_DEFINE_ENUM_VALUE(self)); + static SD_VARLINK_DEFINE_METHOD( AllocateUserRange, SD_VARLINK_FIELD_COMMENT("The name for the user namespace, a short string that must be fit to be included in a file name and in a user name. This name is included in the user records announced via NSS and is otherwise useful for debugging."), SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("Controls whether to mangle the provided name if needed so that it is suitable for naming a user namespace. If true this will shorten the name as necessary or randomize it if that's not sufficient. If null defaults to false."), SD_VARLINK_DEFINE_INPUT(mangleName, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), - SD_VARLINK_FIELD_COMMENT("The number of UIDs to assign. Must be 1 or 65536."), + SD_VARLINK_FIELD_COMMENT("The number of UIDs to assign. Must be 1 or 65536. If type is 'self', must be 1."), SD_VARLINK_DEFINE_INPUT(size, SD_VARLINK_INT, 0), - SD_VARLINK_FIELD_COMMENT("The target UID inside the user namespace. If not specified defaults to 0."), + SD_VARLINK_FIELD_COMMENT("The target UID inside the user namespace. If not specified defaults to 0. If type is 'self', must be 0 or unset in which case the peer UID is mapped to itself."), SD_VARLINK_DEFINE_INPUT(target, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("A file descriptor to an allocated userns with no current UID range assignments"), SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("The type of allocation to perform. If 'managed' (the default), a transient UID/GID range is allocated from the dynamic range pool. If 'self', the peer UID/GID is mapped to itself. Defaults to 'managed'."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(type, AllocateUserRangeType, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Number of transient 64K container UID/GID ranges to delegate. These are mapped 1:1 into the user namespace and can be used by nested user namespaces for container workloads. Must be between 0 and 16. Defaults to 0."), + SD_VARLINK_DEFINE_INPUT(delegateContainerRanges, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, map the foreign UID range 1:1 into the user namespace."), + SD_VARLINK_DEFINE_INPUT(mapForeign, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The name assigned to the user namespace. (This is particularly interesting in case mangleName was enabled)."), SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); @@ -69,11 +82,14 @@ static SD_VARLINK_DEFINE_ERROR(UserNamespaceWithoutUserRange); static SD_VARLINK_DEFINE_ERROR(TooManyControlGroups); static SD_VARLINK_DEFINE_ERROR(ControlGroupAlreadyAdded); static SD_VARLINK_DEFINE_ERROR(TooManyNetworkInterfaces); +static SD_VARLINK_DEFINE_ERROR(TooManyDelegations); SD_VARLINK_DEFINE_INTERFACE( io_systemd_NamespaceResource, "io.systemd.NamespaceResource", SD_VARLINK_INTERFACE_COMMENT("Allocate transient UID ranges for user namespace, and assign mounts, cgroups and networking devices to them"), + SD_VARLINK_SYMBOL_COMMENT("The type of user range allocation to perform."), + &vl_type_AllocateUserRangeType, SD_VARLINK_SYMBOL_COMMENT("Assigns a UID range to a client-allocated user namespace that has no UID range assigned so far, and registers it for assignment of other resources."), &vl_method_AllocateUserRange, SD_VARLINK_SYMBOL_COMMENT("Registers an already initialized user namespace for assignment of resources."), @@ -103,4 +119,6 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_SYMBOL_COMMENT("The specified cgroup has already been added to the user namespace."), &vl_error_ControlGroupAlreadyAdded, SD_VARLINK_SYMBOL_COMMENT("The per-user namespace limit of network interfaces has been reached."), - &vl_error_TooManyNetworkInterfaces); + &vl_error_TooManyNetworkInterfaces, + SD_VARLINK_SYMBOL_COMMENT("The specified number of delegations exceeds the maximum allowed."), + &vl_error_TooManyDelegations); diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c index 69c39b05750..6eef98153ef 100644 --- a/src/test/test-uid-range.c +++ b/src/test/test-uid-range.c @@ -195,4 +195,306 @@ TEST(uid_range_coalesce) { ASSERT_EQ(p->entries[0].nr, 115U); } +TEST(uid_range_clip) { + _cleanup_(uid_range_freep) UIDRange *p = NULL; + + /* Build a range: 100-199, 300-399, 500-599 */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_add_str(&p, "300-399")); + ASSERT_OK(uid_range_add_str(&p, "500-599")); + ASSERT_EQ(uid_range_entries(p), 3U); + + /* Intersect with range that covers all entries */ + ASSERT_OK(uid_range_clip(p, 0, 1000)); + ASSERT_EQ(uid_range_entries(p), 3U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 100U); + ASSERT_EQ(p->entries[1].start, 300U); + ASSERT_EQ(p->entries[1].nr, 100U); + ASSERT_EQ(p->entries[2].start, 500U); + ASSERT_EQ(p->entries[2].nr, 100U); + + /* Intersect with range that excludes first and last entries */ + ASSERT_OK(uid_range_clip(p, 200, 499)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 300U); + ASSERT_EQ(p->entries[0].nr, 100U); + + p = uid_range_free(p); + + /* Test partial overlap - trimming from both sides */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_clip(p, 150, 180)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 150U); + ASSERT_EQ(p->entries[0].nr, 31U); + + p = uid_range_free(p); + + /* Test intersection that removes all entries */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_clip(p, 500, 600)); + ASSERT_TRUE(uid_range_is_empty(p)); + + p = uid_range_free(p); + + /* Test invalid min > max */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_ERROR(uid_range_clip(p, 200, 100), EINVAL); + + p = uid_range_free(p); + + /* Test with max == UINT32_MAX (should not overflow) */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_clip(p, 0, UINT32_MAX)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 100U); + + p = uid_range_free(p); + + /* Test with both min and max at extremes */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_add_str(&p, "500-599")); + ASSERT_OK(uid_range_clip(p, 150, UINT32_MAX)); + ASSERT_EQ(uid_range_entries(p), 2U); + ASSERT_EQ(p->entries[0].start, 150U); + ASSERT_EQ(p->entries[0].nr, 50U); + ASSERT_EQ(p->entries[1].start, 500U); + ASSERT_EQ(p->entries[1].nr, 100U); +} + +TEST(uid_range_partition) { + _cleanup_(uid_range_freep) UIDRange *p = NULL; + + /* Single entry that divides evenly */ + ASSERT_OK(uid_range_add_str(&p, "0-299")); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_OK(uid_range_partition(p, 100)); + ASSERT_EQ(uid_range_entries(p), 3U); + ASSERT_EQ(p->entries[0].start, 0U); + ASSERT_EQ(p->entries[0].nr, 100U); + ASSERT_EQ(p->entries[1].start, 100U); + ASSERT_EQ(p->entries[1].nr, 100U); + ASSERT_EQ(p->entries[2].start, 200U); + ASSERT_EQ(p->entries[2].nr, 100U); + + p = uid_range_free(p); + + /* Entry with remainder (gets truncated) */ + ASSERT_OK(uid_range_add_str(&p, "0-249")); + ASSERT_OK(uid_range_partition(p, 100)); + ASSERT_EQ(uid_range_entries(p), 2U); + ASSERT_EQ(p->entries[0].start, 0U); + ASSERT_EQ(p->entries[0].nr, 100U); + ASSERT_EQ(p->entries[1].start, 100U); + ASSERT_EQ(p->entries[1].nr, 100U); + + p = uid_range_free(p); + + /* Entry smaller than partition size - gets dropped */ + ASSERT_OK(uid_range_add_str(&p, "0-49")); + ASSERT_OK(uid_range_partition(p, 100)); + ASSERT_TRUE(uid_range_is_empty(p)); + + p = uid_range_free(p); + + /* Multiple entries */ + ASSERT_OK(uid_range_add_str(&p, "0-199")); + ASSERT_OK(uid_range_add_str(&p, "1000-1299")); + ASSERT_EQ(uid_range_entries(p), 2U); + ASSERT_OK(uid_range_partition(p, 100)); + ASSERT_EQ(uid_range_entries(p), 5U); + ASSERT_EQ(p->entries[0].start, 0U); + ASSERT_EQ(p->entries[0].nr, 100U); + ASSERT_EQ(p->entries[1].start, 100U); + ASSERT_EQ(p->entries[1].nr, 100U); + ASSERT_EQ(p->entries[2].start, 1000U); + ASSERT_EQ(p->entries[2].nr, 100U); + ASSERT_EQ(p->entries[3].start, 1100U); + ASSERT_EQ(p->entries[3].nr, 100U); + ASSERT_EQ(p->entries[4].start, 1200U); + ASSERT_EQ(p->entries[4].nr, 100U); + + p = uid_range_free(p); + + /* Partition size of 1 */ + ASSERT_OK(uid_range_add_str(&p, "100-102")); + ASSERT_OK(uid_range_partition(p, 1)); + ASSERT_EQ(uid_range_entries(p), 3U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 1U); + ASSERT_EQ(p->entries[1].start, 101U); + ASSERT_EQ(p->entries[1].nr, 1U); + ASSERT_EQ(p->entries[2].start, 102U); + ASSERT_EQ(p->entries[2].nr, 1U); +} + +TEST(uid_range_copy) { + _cleanup_(uid_range_freep) UIDRange *p = NULL, *copy = NULL; + + /* Copy NULL range */ + ASSERT_OK(uid_range_copy(NULL, ©)); + ASSERT_TRUE(uid_range_is_empty(copy)); + + copy = uid_range_free(copy); + + /* Copy empty range */ + p = new0(UIDRange, 1); + ASSERT_NOT_NULL(p); + ASSERT_OK(uid_range_copy(p, ©)); + ASSERT_NOT_NULL(copy); + ASSERT_TRUE(uid_range_is_empty(copy)); + + p = uid_range_free(p); + copy = uid_range_free(copy); + + /* Copy range with entries */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_add_str(&p, "300-399")); + ASSERT_OK(uid_range_copy(p, ©)); + ASSERT_TRUE(uid_range_equal(p, copy)); + + /* Verify it's a deep copy - modifying original doesn't affect copy */ + ASSERT_OK(uid_range_add_str(&p, "500-599")); + ASSERT_FALSE(uid_range_equal(p, copy)); + ASSERT_EQ(uid_range_entries(copy), 2U); +} + +TEST(uid_range_remove) { + _cleanup_(uid_range_freep) UIDRange *p = NULL; + + /* Build a range: 100-199 */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + + /* Remove with size 0 - no-op */ + ASSERT_OK(uid_range_remove(p, 150, 0)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 100U); + + /* Remove range that doesn't overlap - no change */ + ASSERT_OK(uid_range_remove(p, 0, 50)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 100U); + + ASSERT_OK(uid_range_remove(p, 300, 50)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 100U); + + /* Remove from the start of the entry */ + ASSERT_OK(uid_range_remove(p, 100, 10)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 110U); + ASSERT_EQ(p->entries[0].nr, 90U); + + /* Remove from the end of the entry */ + ASSERT_OK(uid_range_remove(p, 190, 10)); + ASSERT_EQ(uid_range_entries(p), 1U); + ASSERT_EQ(p->entries[0].start, 110U); + ASSERT_EQ(p->entries[0].nr, 80U); + + /* Remove from the middle - splits the entry */ + ASSERT_OK(uid_range_remove(p, 140, 20)); + ASSERT_EQ(uid_range_entries(p), 2U); + ASSERT_EQ(p->entries[0].start, 110U); + ASSERT_EQ(p->entries[0].nr, 30U); + ASSERT_EQ(p->entries[1].start, 160U); + ASSERT_EQ(p->entries[1].nr, 30U); + + p = uid_range_free(p); + + /* Remove entire entry */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_remove(p, 100, 100)); + ASSERT_TRUE(uid_range_is_empty(p)); + + p = uid_range_free(p); + + /* Remove range larger than entry */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_remove(p, 50, 200)); + ASSERT_TRUE(uid_range_is_empty(p)); + + p = uid_range_free(p); + + /* Remove affecting multiple entries */ + ASSERT_OK(uid_range_add_str(&p, "100-199")); + ASSERT_OK(uid_range_add_str(&p, "300-399")); + ASSERT_OK(uid_range_add_str(&p, "500-599")); + ASSERT_EQ(uid_range_entries(p), 3U); + + /* Remove range spanning the middle entry completely and trimming others */ + ASSERT_OK(uid_range_remove(p, 150, 400)); + ASSERT_EQ(uid_range_entries(p), 2U); + ASSERT_EQ(p->entries[0].start, 100U); + ASSERT_EQ(p->entries[0].nr, 50U); + ASSERT_EQ(p->entries[1].start, 550U); + ASSERT_EQ(p->entries[1].nr, 50U); +} + +TEST(uid_range_translate) { + _cleanup_(uid_range_freep) UIDRange *o = NULL, *i = NULL; + uid_t uid; + + ASSERT_OK(uid_range_add_str_full(&o, "200-299", /* coalesce= */ false)); + ASSERT_OK(uid_range_add_str_full(&i, "100-199", /* coalesce= */ false)); + ASSERT_OK(uid_range_translate(o, i, 250, &uid)); + ASSERT_EQ(uid, 150U); + ASSERT_OK(uid_range_translate(i, o, 150, &uid)); + ASSERT_EQ(uid, 250U); + + ASSERT_OK(uid_range_add_str_full(&o, "300-399", /* coalesce= */ false)); + ASSERT_OK(uid_range_add_str_full(&i, "350-449", /* coalesce= */ false)); + ASSERT_OK(uid_range_translate(o, i, 350, &uid)); + ASSERT_EQ(uid, 400U); + ASSERT_OK(uid_range_translate(i, o, 400, &uid)); + ASSERT_EQ(uid, 350U); + + /* Test translating at range boundaries */ + ASSERT_OK(uid_range_translate(o, i, 200, &uid)); + ASSERT_EQ(uid, 100U); + ASSERT_OK(uid_range_translate(o, i, 299, &uid)); + ASSERT_EQ(uid, 199U); + ASSERT_OK(uid_range_translate(o, i, 300, &uid)); + ASSERT_EQ(uid, 350U); + ASSERT_OK(uid_range_translate(o, i, 399, &uid)); + ASSERT_EQ(uid, 449U); + + /* Test reverse translation at boundaries */ + ASSERT_OK(uid_range_translate(i, o, 100, &uid)); + ASSERT_EQ(uid, 200U); + ASSERT_OK(uid_range_translate(i, o, 199, &uid)); + ASSERT_EQ(uid, 299U); + ASSERT_OK(uid_range_translate(i, o, 350, &uid)); + ASSERT_EQ(uid, 300U); + ASSERT_OK(uid_range_translate(i, o, 449, &uid)); + ASSERT_EQ(uid, 399U); + + /* Test UID not in any range returns ESRCH */ + ASSERT_ERROR(uid_range_translate(o, i, 0, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(o, i, 199, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(o, i, 400, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(i, o, 0, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(i, o, 99, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(i, o, 200, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(i, o, 349, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(i, o, 450, &uid), ESRCH); + + o = uid_range_free(o); + i = uid_range_free(i); + + /* Test with single-element ranges */ + ASSERT_OK(uid_range_add_str_full(&o, "1000", /* coalesce= */ false)); + ASSERT_OK(uid_range_add_str_full(&i, "5000", /* coalesce= */ false)); + ASSERT_OK(uid_range_translate(o, i, 1000, &uid)); + ASSERT_EQ(uid, 5000U); + ASSERT_OK(uid_range_translate(i, o, 5000, &uid)); + ASSERT_EQ(uid, 1000U); + ASSERT_ERROR(uid_range_translate(o, i, 999, &uid), ESRCH); + ASSERT_ERROR(uid_range_translate(o, i, 1001, &uid), ESRCH); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index d328bd2cb43..d68a621e060 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2,9 +2,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -17,6 +19,7 @@ #include "alloc-util.h" #include "architecture.h" #include "bootspec.h" +#include "build-path.h" #include "build.h" #include "bus-error.h" #include "bus-internal.h" @@ -72,6 +75,7 @@ #include "sync-util.h" #include "terminal-util.h" #include "tmpfile-util.h" +#include "uid-classification.h" #include "unit-name.h" #include "user-record.h" #include "user-util.h" @@ -806,6 +810,22 @@ static int parse_argv(int argc, char *argv[]) { if (arg_ephemeral && arg_extra_drives.n_drives > 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --ephemeral with --extra-drive="); + if (arg_uid_shift != UID_INVALID && !arg_directory) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--private-users= is only supported in combination with --directory=."); + + if (arg_directory && arg_uid_shift == UID_INVALID) { + struct stat st; + if (stat(arg_directory, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", arg_directory); + + r = stat_verify_directory(&st); + if (r < 0) + return log_error_errno(r, "'%s' is not a directory: %m", arg_directory); + + arg_uid_shift = st.st_uid; + arg_uid_range = 0x10000; + } + if (argc > optind) { arg_kernel_cmdline_extra = strv_copy(argv + optind); if (!arg_kernel_cmdline_extra) @@ -1487,7 +1507,6 @@ static int start_virtiofsd( uid_t target_uid, uid_t uid_range, const char *runtime_dir, - const char *sd_socket_activate, char **ret_listen_address, PidRef *ret_pidref) { @@ -1511,19 +1530,55 @@ static int start_virtiofsd( if (asprintf(&listen_address, "%s/sock-%"PRIx64, runtime_dir, random_u64()) < 0) return log_oom(); + union sockaddr_union su; + r = sockaddr_un_set_path(&su.un, listen_address); + if (r < 0) + return r; + + _cleanup_close_ int sock = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (sock < 0) + return log_error_errno(errno, "Failed to create unix socket: %m"); + + if (bind(sock, &su.sa, r) < 0) + return log_error_errno(errno, "Failed to bind unix socket to '%s': %m", listen_address); + + if (listen(sock, SOMAXCONN_DELUXE) < 0) + return log_error_errno(errno, "Failed to listen on unix socket '%s': %m", listen_address); + + _cleanup_free_ char *sockstr = NULL; + if (asprintf(&sockstr, "%i", sock) < 0) + return log_oom(); + /* QEMU doesn't support submounts so don't announce them */ _cleanup_strv_free_ char **argv = strv_new( - sd_socket_activate, - "--listen", listen_address, virtiofsd, - "--shared-dir", directory, + "--shared-dir", source_uid == FOREIGN_UID_MIN ? "/run/systemd/mount-rootfs" : directory, "--xattr", - "--fd", "3", + "--fd", sockstr, + "--sandbox=chroot", "--no-announce-submounts"); if (!argv) return log_oom(); - if (source_uid != UID_INVALID && target_uid != UID_INVALID && uid_range != UID_INVALID) { + _cleanup_close_ int userns_fd = -EBADF, mapped_fd = -EBADF; + + if (source_uid == FOREIGN_UID_MIN) { + assert(target_uid == 0); + assert(uid_range == 0x10000); + + userns_fd = nsresource_allocate_userns(/* vl= */ NULL, /* name= */ NULL, NSRESOURCE_UIDS_64K); + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to allocate user namespace for virtiofsd: %m"); + + _cleanup_close_ int directory_fd = open(directory, O_DIRECTORY|O_CLOEXEC|O_PATH); + if (directory_fd < 0) + return log_error_errno(directory_fd, "Failed to open '%s': %m", directory); + + r = mountfsd_mount_directory_fd(/* vl= */ NULL, directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mapped_fd); + if (r < 0) + return r; + + } else if (!IN_SET(source_uid, FOREIGN_UID_MIN, UID_INVALID) && target_uid != UID_INVALID && uid_range != UID_INVALID) { r = strv_extend(&argv, "--translate-uid"); if (r < 0) return log_oom(); @@ -1541,9 +1596,49 @@ static int start_virtiofsd( return log_oom(); } - r = fork_notify(argv, ret_pidref); + r = pidref_safe_fork_full( + "(virtiofsd)", + (const int[3]) { -EBADF, STDOUT_FILENO, STDERR_FILENO }, + (int[]) { sock, userns_fd, mapped_fd }, + source_uid == FOREIGN_UID_MIN ? 3 : 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG|FORK_REARRANGE_STDIO, + ret_pidref); if (r < 0) return r; + if (r == 0) { + /* Child */ + + r = namespace_enter( + /* pidns_fd= */ -EBADF, + /* mntns_fd= */ -EBADF, + /* netns_fd= */ -EBADF, + userns_fd, + /* root_fd= */ -EBADF); + if (r < 0) { + log_error_errno(r, "Failed to enter user namespace for virtiofsd: %m"); + _exit(EXIT_FAILURE); + } + + if (userns_fd >= 0 && unshare(CLONE_NEWNS) < 0) { + log_error_errno(errno, "Failed to unshare mount namespace %m"); + _exit(EXIT_FAILURE); + } + + if (mapped_fd >= 0 && move_mount(mapped_fd, "", AT_FDCWD, "/run/systemd/mount-rootfs", MOVE_MOUNT_F_EMPTY_PATH) < 0) { + log_error_errno(errno, "Failed to move mount file descriptor to '/run/systemd/mount-rootfs': %m"); + _exit(EXIT_FAILURE); + } + + r = fd_cloexec(sock, false); + if (r < 0) { + log_error_errno(r, "Failed to disable cloexec on socket: %m"); + _exit(EXIT_FAILURE); + } + + invoke_callout_binary(argv[0], argv); + log_error_errno(errno, "Failed to execute '%s': %m", argv[0]); + _exit(EXIT_FAILURE); + } if (ret_listen_address) *ret_listen_address = TAKE_PTR(listen_address); @@ -2413,7 +2508,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { /* target_uid= */ 0, /* uid_range= */ arg_uid_range, runtime_dir, - sd_socket_activate, &listen_address, &child); if (r < 0) @@ -2512,7 +2606,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { /* target_uid= */ m->target_uid, /* uid_range= */ 1U, runtime_dir, - sd_socket_activate, &listen_address, &child); if (r < 0) diff --git a/test/units/TEST-50-DISSECT.mountfsd.sh b/test/units/TEST-50-DISSECT.mountfsd.sh index 94f802e780d..12a72f8257d 100755 --- a/test/units/TEST-50-DISSECT.mountfsd.sh +++ b/test/units/TEST-50-DISSECT.mountfsd.sh @@ -60,6 +60,63 @@ if (SYSTEMD_LOG_TARGET=console varlinkctl call \ exit 0 fi +# Test delegated UID ranges +# Verify that delegated ranges show up in uid_map (6 lines: 1 primary + 2 container ranges + 3 dynamic users) +test "$(run0 -u testuser --pipe unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-delegate","size":65536,"userNamespaceFileDescriptor":0,"delegateContainerRanges":2}' \ + -- cat /proc/self/uid_map | wc -l)" -eq 3 + +# Test that delegateContainerRanges > 16 fails with TooManyDelegations error +(! run0 -u testuser --pipe unshare --user varlinkctl call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-fail","size":65536,"userNamespaceFileDescriptor":0,"delegateContainerRanges":17}') |& + grep "io.systemd.NamespaceResource.TooManyDelegations" >/dev/null + +# Test self mapping +# Verify that self mapping maps the peer UID to root (uid_map should show "0 1") +test "$(run0 -u testuser --pipe unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-id","target":0,"size":1,"userNamespaceFileDescriptor":0,"type":"self"}' \ + -- cat /proc/self/uid_map | awk '{print $1, $3}')" = "0 1" + +# Test nested delegation with self mapping +test "$(run0 -u testuser --pipe unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-delegate2","type":"self","size":1,"userNamespaceFileDescriptor":0,"delegateContainerRanges":3}' \ + -- unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-delegate3","size":65536,"userNamespaceFileDescriptor":0,"delegateContainerRanges":2}' \ + -- cat /proc/self/uid_map | wc -l)" -eq 3 + +# Test mapForeign parameter +# Verify that the foreign UID range is mapped into the user namespace +# When mapForeign is true, uid_map should have 2 lines: primary range + foreign range +test "$(run0 -u testuser --pipe unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-foreign","size":65536,"userNamespaceFileDescriptor":0,"mapForeign":true}' \ + -- cat /proc/self/uid_map | wc -l)" -eq 2 + +# Verify the foreign range is mapped 1:1. +test "$(run0 -u testuser --pipe unshare --user varlinkctl --exec call \ + --push-fd=/proc/self/ns/user \ + /run/systemd/userdb/io.systemd.NamespaceResource \ + io.systemd.NamespaceResource.AllocateUserRange \ + '{"name":"test-foreign2","size":65536,"userNamespaceFileDescriptor":0,"mapForeign":true}' \ + -- cat /proc/self/uid_map | grep -c 2147352576)" -eq 1 + # This should work without the key systemd-dissect --image-policy='root=verity:=absent+unused' --mtree /var/tmp/unpriv.raw >/dev/null systemd-dissect --image-policy='root=verity+signed:=absent+unused' --mtree /var/tmp/unpriv.raw >/dev/null diff --git a/tmpfiles.d/systemd-nspawn.conf b/tmpfiles.d/systemd-nspawn.conf index 40e6787233e..8254650e930 100644 --- a/tmpfiles.d/systemd-nspawn.conf +++ b/tmpfiles.d/systemd-nspawn.conf @@ -21,3 +21,8 @@ Q /var/lib/machines 0700 - - - R! /var/lib/machines/.#* R! /.#machine.* + +# If the nsresourced/mountfsd sockets are mounted into /run/host, symlink them to their canonical +# location in /run/systemd. +L? /run/systemd/io.systemd.NamespaceResource - - - - /run/host/io.systemd.NamespaceResource +L? /run/systemd/io.systemd.MountFileSystem - - - - /run/host/io.systemd.MountFileSystem diff --git a/tmpfiles.d/systemd.conf.in b/tmpfiles.d/systemd.conf.in index 6436400cde6..f601cb87f9d 100644 --- a/tmpfiles.d/systemd.conf.in +++ b/tmpfiles.d/systemd.conf.in @@ -18,6 +18,7 @@ d$ /run/systemd/sessions 0755 root root - d$ /run/systemd/users 0755 root root - d /run/systemd/machines 0755 root root - d$ /run/systemd/shutdown 0755 root root - +d /run/systemd/dissect-root 0000 root root - d /run/log 0755 root root - diff --git a/units/systemd-mountfsd.socket b/units/systemd-mountfsd.socket index 431369a1a18..a3e19cc418c 100644 --- a/units/systemd-mountfsd.socket +++ b/units/systemd-mountfsd.socket @@ -13,6 +13,7 @@ Documentation=man:systemd-mountfsd.service(8) DefaultDependencies=no Conflicts=shutdown.target Before=sockets.target shutdown.target +ConditionPathExists=!/run/host/io.systemd.MountFileSystem [Socket] ListenStream=/run/systemd/io.systemd.MountFileSystem diff --git a/units/systemd-nsresourced.service.in b/units/systemd-nsresourced.service.in index 0e2d6b3628c..143f9a9a3d2 100644 --- a/units/systemd-nsresourced.service.in +++ b/units/systemd-nsresourced.service.in @@ -18,7 +18,7 @@ After=modprobe@tun.service DefaultDependencies=no [Service] -CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_CHOWN CAP_FOWNER CAP_NET_ADMIN +CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_CHOWN CAP_FOWNER CAP_NET_ADMIN CAP_SETFCAP ExecStart={{LIBEXECDIR}}/systemd-nsresourced IPAddressDeny=any LimitNOFILE={{HIGH_RLIMIT_NOFILE}} diff --git a/units/systemd-nsresourced.socket b/units/systemd-nsresourced.socket index c159a5676ae..6b4a883df30 100644 --- a/units/systemd-nsresourced.socket +++ b/units/systemd-nsresourced.socket @@ -13,6 +13,7 @@ Documentation=man:systemd-nsresourced.service(8) DefaultDependencies=no Conflicts=shutdown.target Before=sockets.target shutdown.target +ConditionPathExists=!/run/host/io.systemd.NamespaceResource [Socket] ListenStream=/run/systemd/io.systemd.NamespaceResource