Additions to nsresourced and vmspawn required for making use of the foreign UID range in mkosi (#40415)

This commit is contained in:
Daan De Meyer
2026-02-25 13:49:19 +01:00
committed by GitHub
31 changed files with 2284 additions and 187 deletions

View File

@@ -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

View File

@@ -938,6 +938,33 @@
<xi:include href="version-info.xml" xpointer="v230"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--private-users-delegate=</option></term>
<listitem><para>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
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
<para>This option requires <option>--private-users=managed</option>, as the delegation is performed
by
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
as part of the user namespace allocation. The maximum number of delegated ranges is 16. Defaults to
0, i.e. no delegation.</para>
<para>When this option is used with a non-zero value, the
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
Varlink socket (<filename>/run/systemd/io.systemd.NamespaceResource</filename>) is automatically
bind-mounted into the container along with the necessary discovery symlinks in
<filename>/run/systemd/userdb/</filename> and <filename>/run/varlink/registry/</filename>. This
allows processes inside the container to contact
<command>systemd-nsresourced</command> on the host in order to allocate nested user namespaces from
the delegated ranges.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><option>-U</option></term>

View File

@@ -52,6 +52,27 @@
registered with this service. Moreover, UIDs and GIDs are always allocated together, and
symmetrically.</para>
<para>The allocation API supports <emphasis>delegated ranges</emphasis>: 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
<function>AllocateUserRange()</function> 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
<ulink url="https://systemd.io/UIDS-GIDS">Users, Groups, UIDs and GIDs on systemd Systems</ulink>.</para>
<para>The allocation API also supports <emphasis>identity mappings</emphasis>: 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.</para>
<para>Additionally, the allocation API supports mapping the <emphasis>foreign UID range</emphasis> 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.</para>
<para>The service provides API calls to allowlist mounts (referenced via their mount file descriptors as
per Linux <function>fsmount()</function> 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

View File

@@ -301,6 +301,23 @@
<xi:include href="version-info.xml" xpointer="v230"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>PrivateUsersDelegate=</varname></term>
<listitem><para>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
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
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
<option>--private-users-delegate=</option> command line switch. Requires
<varname>PrivateUsers=managed</varname>. Defaults to 0. See
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
for details.</para>
<xi:include href="version-info.xml" xpointer="v260"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>NotifyReady=</varname></term>

View File

@@ -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'

View File

@@ -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;

View File

@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sched.h>
#include <string.h>
#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;

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -20,6 +20,9 @@
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#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;
}

View File

@@ -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, &copy);
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;

View File

@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <grp.h>
#include <sched.h>
#include <sys/eventfd.h>
#include <sys/mount.h>
@@ -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);

View File

@@ -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(&reg_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;
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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"))

View File

@@ -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);

View File

@@ -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);

View File

@@ -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, &copy));
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, &copy));
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, &copy));
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);

View File

@@ -2,9 +2,11 @@
#include <getopt.h>
#include <poll.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
@@ -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)

View File

@@ -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 <peer_uid> 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

View File

@@ -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

View File

@@ -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 -

View File

@@ -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

View File

@@ -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}}

View File

@@ -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