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