mirror of
https://github.com/systemd/systemd.git
synced 2026-07-24 16:25:52 +00:00
Additions to nsresourced and vmspawn required for making use of the foreign UID range in mkosi (#40415)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@
|
||||
#include "mountpoint-util.h"
|
||||
#include "namespace-util.h"
|
||||
#include "netlink-util.h"
|
||||
#include "nsresource.h"
|
||||
#include "pidref.h"
|
||||
#include "process-util.h"
|
||||
#include "random-util.h"
|
||||
#include "siphash24.h"
|
||||
#include "socket-util.h"
|
||||
#include "stat-util.h"
|
||||
#include "string-table.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "time-util.h"
|
||||
@@ -75,6 +77,21 @@ typedef struct LookupParameters {
|
||||
const char *service;
|
||||
} LookupParameters;
|
||||
|
||||
typedef enum AllocateUserRangeType {
|
||||
ALLOCATE_USER_RANGE_MANAGED,
|
||||
ALLOCATE_USER_RANGE_SELF,
|
||||
_ALLOCATE_USER_RANGE_TYPE_MAX,
|
||||
_ALLOCATE_USER_RANGE_TYPE_INVALID = -EINVAL,
|
||||
} AllocateUserRangeType;
|
||||
|
||||
static const char *const allocate_user_range_type_table[_ALLOCATE_USER_RANGE_TYPE_MAX] = {
|
||||
[ALLOCATE_USER_RANGE_MANAGED] = "managed",
|
||||
[ALLOCATE_USER_RANGE_SELF] = "self",
|
||||
};
|
||||
|
||||
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(allocate_user_range_type, AllocateUserRangeType);
|
||||
static JSON_DISPATCH_ENUM_DEFINE(dispatch_allocate_user_range_type, AllocateUserRangeType, allocate_user_range_type_from_string);
|
||||
|
||||
static int build_user_json(UserNamespaceInfo *userns_info, uid_t offset, sd_json_variant **ret) {
|
||||
_cleanup_free_ char *name = NULL, *realname = NULL;
|
||||
UserDisposition disposition;
|
||||
@@ -357,16 +374,19 @@ static int vl_method_get_memberships(sd_varlink *link, sd_json_variant *paramete
|
||||
return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
|
||||
}
|
||||
|
||||
static int uid_is_available(
|
||||
int registry_dir_fd,
|
||||
uid_t candidate) {
|
||||
|
||||
static int uid_is_available(int registry_dir_fd, uid_t candidate, int parent_userns_fd) {
|
||||
int r;
|
||||
|
||||
assert(registry_dir_fd >= 0);
|
||||
|
||||
log_debug("Checking if UID " UID_FMT " is available.", candidate);
|
||||
|
||||
uint64_t parent_userns_inode = 0;
|
||||
struct stat parent_st;
|
||||
if (fstat(parent_userns_fd, &parent_st) < 0)
|
||||
return log_debug_errno(errno, "Failed to fstat parent user namespace: %m");
|
||||
parent_userns_inode = parent_st.st_ino;
|
||||
|
||||
r = userns_registry_uid_exists(registry_dir_fd, candidate);
|
||||
if (r < 0)
|
||||
return r;
|
||||
@@ -379,17 +399,65 @@ static int uid_is_available(
|
||||
if (r > 0)
|
||||
return false;
|
||||
|
||||
r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL);
|
||||
if (r >= 0)
|
||||
return false;
|
||||
if (r != -ESRCH)
|
||||
/* Also check delegation files. If parent_userns_inode is set and matches the delegation's userns
|
||||
* inode, the UID is available because the parent owns that delegation. */
|
||||
r = userns_registry_delegation_uid_exists(registry_dir_fd, candidate);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo delegation = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
r = userns_registry_load_delegation_by_uid(registry_dir_fd, candidate, &delegation);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = groupdb_by_gid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL);
|
||||
if (r >= 0)
|
||||
return false;
|
||||
if (r != -ESRCH)
|
||||
if (delegation.userns_inode != parent_userns_inode)
|
||||
return false;
|
||||
|
||||
/* The parent userns owns this delegation, so the UID is available for nested allocation */
|
||||
log_debug("UID " UID_FMT " is delegated by parent userns inode %" PRIu64 ", available for nested allocation.",
|
||||
candidate, parent_userns_inode);
|
||||
}
|
||||
|
||||
r = userns_registry_delegation_gid_exists(registry_dir_fd, (gid_t) candidate);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo delegation = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
r = userns_registry_load_delegation_by_gid(registry_dir_fd, candidate, &delegation);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (delegation.userns_inode != parent_userns_inode)
|
||||
return false;
|
||||
|
||||
/* The parent userns owns this delegation, so the UID is available for nested allocation */
|
||||
log_debug("UID " UID_FMT " is delegated by parent userns inode %" PRIu64 ", available for nested allocation.",
|
||||
candidate, parent_userns_inode);
|
||||
}
|
||||
|
||||
r = is_our_namespace(parent_userns_fd, NAMESPACE_USER);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to check if parent user namespace is our user namespace: %m");
|
||||
|
||||
if (r > 0) {
|
||||
/* Only check userdb if we're allocating from our current user namespace. userdb won't be
|
||||
* to tell us anything on whether UIDs/GIDs in another user namespace are in use or not. On
|
||||
* top of that, for nspawn containers registered with machined's userdb implementation, it
|
||||
* would tell us that any ranges delegated to the container are in use (which is true in the
|
||||
* nsresourced user namespace, but not in the nspawn user namespace). */
|
||||
|
||||
r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL);
|
||||
if (r >= 0)
|
||||
return false;
|
||||
if (r != -ESRCH)
|
||||
return r;
|
||||
|
||||
r = groupdb_by_gid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret= */ NULL);
|
||||
if (r >= 0)
|
||||
return false;
|
||||
if (r != -ESRCH)
|
||||
return r;
|
||||
}
|
||||
|
||||
log_debug("UID " UID_FMT " is available.", candidate);
|
||||
|
||||
@@ -433,19 +501,114 @@ static int name_is_available(
|
||||
return true;
|
||||
}
|
||||
|
||||
static int allocate_now(
|
||||
static int allocate_one(
|
||||
int registry_dir_fd,
|
||||
UserNamespaceInfo *info,
|
||||
int *ret_lock_fd) {
|
||||
const char *name,
|
||||
uint32_t size,
|
||||
int parent_userns_fd,
|
||||
UIDRange *candidates,
|
||||
uid_t *ret_candidate) {
|
||||
|
||||
static const uint8_t hash_key[16] = {
|
||||
0xd4, 0xd7, 0x33, 0xa7, 0x4d, 0xd3, 0x42, 0xcd,
|
||||
0xaa, 0xe9, 0x45, 0xd0, 0xfb, 0xec, 0x79, 0xee,
|
||||
};
|
||||
|
||||
_cleanup_(uid_range_freep) UIDRange *valid_range = NULL;
|
||||
uid_t candidate, uidmin, uidmax, uidmask;
|
||||
_cleanup_(uid_range_freep) UIDRange *copy = NULL;
|
||||
uid_t candidate, uidmin, uidmax;
|
||||
unsigned n_tries = 100;
|
||||
size_t idx;
|
||||
int r;
|
||||
|
||||
assert(registry_dir_fd >= 0);
|
||||
assert(candidates);
|
||||
assert(ret_candidate);
|
||||
|
||||
switch (size) {
|
||||
|
||||
case NSRESOURCE_UIDS_64K:
|
||||
uidmin = CONTAINER_UID_BASE_MIN;
|
||||
uidmax = CONTAINER_UID_BASE_MAX;
|
||||
break;
|
||||
|
||||
case NSRESOURCE_UIDS_1:
|
||||
uidmin = DYNAMIC_UID_MIN;
|
||||
uidmax = DYNAMIC_UID_MAX;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
||||
/* Make a copy of candidates that we can modify for the selection algorithm */
|
||||
r = uid_range_copy(candidates, ©);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to copy UID range: %m");
|
||||
|
||||
/* Clip the copy with the valid UID range for this allocation size */
|
||||
r = uid_range_clip(copy, uidmin, uidmax);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to intersect UID range: %m");
|
||||
|
||||
/* Partition entries into entries of exactly the right size */
|
||||
r = uid_range_partition(copy, size);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to partition UID ranges: %m");
|
||||
|
||||
if (uid_range_is_empty(copy))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate.");
|
||||
|
||||
log_debug("Partitioned UID range into %zu entries of size %" PRIu32, copy->n_entries, size);
|
||||
|
||||
/* Start from a hash of the input name if we have one, use random values afterwards. */
|
||||
idx = name ? siphash24_string(name, hash_key) : random_u32();
|
||||
for (;; idx = random_u32()) {
|
||||
if (uid_range_is_empty(copy))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "All candidate UIDs already taken.");
|
||||
|
||||
if (--n_tries <= 0)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available.");
|
||||
|
||||
idx %= copy->n_entries;
|
||||
|
||||
candidate = copy->entries[idx].start;
|
||||
|
||||
/* We only check the base UID for each range. Pass the parent userns inode so that
|
||||
* allocating from a delegated range owned by the parent is allowed. */
|
||||
r = uid_is_available(registry_dir_fd, candidate, parent_userns_fd);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate);
|
||||
if (r > 0)
|
||||
break;
|
||||
|
||||
log_debug("UID range " UID_FMT " already taken.", candidate);
|
||||
|
||||
/* Remove this unavailable range from candidates so we don't try it again */
|
||||
r = uid_range_remove(copy, candidate, size);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to remove unavailable range from candidates: %m");
|
||||
}
|
||||
|
||||
/* Remove the allocated range from the original candidates */
|
||||
r = uid_range_remove(candidates, candidate, size);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to remove allocated range from candidates: %m");
|
||||
|
||||
*ret_candidate = candidate;
|
||||
|
||||
log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + size - 1);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int allocate_now(
|
||||
int registry_dir_fd,
|
||||
int userns_fd,
|
||||
int parent_userns_fd,
|
||||
UserNamespaceInfo *info,
|
||||
int *ret_lock_fd) {
|
||||
|
||||
_cleanup_(uid_range_freep) UIDRange *candidates = NULL;
|
||||
uid_t candidate;
|
||||
int r;
|
||||
|
||||
/* Returns the following error codes:
|
||||
@@ -456,33 +619,12 @@ static int allocate_now(
|
||||
*/
|
||||
|
||||
assert(registry_dir_fd >= 0);
|
||||
assert(userns_fd >= 0);
|
||||
assert(info);
|
||||
|
||||
switch (info->size) {
|
||||
|
||||
case 0x10000U:
|
||||
uidmin = CONTAINER_UID_BASE_MIN;
|
||||
uidmax = CONTAINER_UID_BASE_MAX;
|
||||
uidmask = (uid_t) UINT32_C(0xFFFF0000);
|
||||
break;
|
||||
|
||||
case 1U:
|
||||
uidmin = DYNAMIC_UID_MIN;
|
||||
uidmax = DYNAMIC_UID_MAX;
|
||||
uidmask = (uid_t) UINT32_C(0xFFFFFFFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
||||
r = uid_range_load_userns(/* path= */ NULL, UID_RANGE_USERNS_INSIDE, &valid_range);
|
||||
r = uid_range_load_userns_by_fd(parent_userns_fd, UID_RANGE_USERNS_INSIDE, &candidates);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Check early whether we have any chance at all given our own uid range */
|
||||
if (!uid_range_overlaps(valid_range, uidmin, uidmax))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate.");
|
||||
return log_debug_errno(r, "Failed to read userns UID range: %m");
|
||||
|
||||
_cleanup_close_ int lock_fd = -EBADF;
|
||||
lock_fd = userns_registry_lock(registry_dir_fd);
|
||||
@@ -508,45 +650,83 @@ static int allocate_now(
|
||||
if (r == 0)
|
||||
return -EEXIST;
|
||||
|
||||
for (candidate = siphash24_string(info->name, hash_key) & UINT32_MAX;; /* Start from a hash of the input name */
|
||||
candidate = random_u32()) { /* Use random values afterwards */
|
||||
|
||||
if (--n_tries <= 0)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available.");
|
||||
|
||||
candidate = (candidate % (uidmax - uidmin)) + uidmin;
|
||||
candidate &= uidmask;
|
||||
|
||||
if (!uid_range_covers(valid_range, candidate, info->size))
|
||||
continue;
|
||||
|
||||
/* We only check the base UID for each range (!) */
|
||||
r = uid_is_available(registry_dir_fd, candidate);
|
||||
/* If the source UID/GID are already set we're doing a "self" user namespace and don't need to
|
||||
* allocate a transient range. */
|
||||
if (!uid_is_valid(info->start_uid) && !gid_is_valid(info->start_gid)) {
|
||||
r = allocate_one(
|
||||
registry_dir_fd,
|
||||
info->name, info->size,
|
||||
parent_userns_fd,
|
||||
candidates,
|
||||
&candidate);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate);
|
||||
if (r > 0) {
|
||||
info->start_uid = candidate;
|
||||
info->start_gid = (gid_t) candidate;
|
||||
return r;
|
||||
|
||||
log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + info->size - 1);
|
||||
|
||||
if (ret_lock_fd)
|
||||
*ret_lock_fd = TAKE_FD(lock_fd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_debug("UID range " UID_FMT " already taken.", candidate);
|
||||
info->start_uid = candidate;
|
||||
info->start_gid = (gid_t) candidate;
|
||||
}
|
||||
|
||||
/* Now allocate delegated ranges if requested */
|
||||
if (info->n_delegates > 0) {
|
||||
assert(info->delegates);
|
||||
|
||||
FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) {
|
||||
r = allocate_one(
|
||||
registry_dir_fd,
|
||||
/* name= */ NULL,
|
||||
delegate->size,
|
||||
parent_userns_fd,
|
||||
candidates,
|
||||
&candidate);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
delegate->userns_inode = info->userns_inode;
|
||||
delegate->start_uid = candidate;
|
||||
delegate->start_gid = (gid_t) candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret_lock_fd)
|
||||
*ret_lock_fd = TAKE_FD(lock_fd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
|
||||
static int write_userns_mappings(PidRef *pidref, const char *uidmap, const char *gidmap) {
|
||||
const char *pmap;
|
||||
int r;
|
||||
|
||||
assert(pidref);
|
||||
assert(uidmap);
|
||||
assert(gidmap);
|
||||
|
||||
pmap = procfs_file_alloca(pidref->pid, "uid_map");
|
||||
r = write_string_file(pmap, uidmap, /* flags= */ 0);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m");
|
||||
|
||||
pmap = procfs_file_alloca(pidref->pid, "gid_map");
|
||||
r = write_string_file(pmap, gidmap, /* flags= */ 0);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_userns(
|
||||
int userns_fd,
|
||||
int parent_userns_fd,
|
||||
const UserNamespaceInfo *userns_info,
|
||||
bool map_foreign) {
|
||||
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_close_ int efd = -EBADF;
|
||||
uint64_t u;
|
||||
int r;
|
||||
|
||||
assert(usernsfd >= 0);
|
||||
assert(userns_fd >= 0);
|
||||
assert(parent_userns_fd >= 0);
|
||||
assert(userns_info);
|
||||
assert(uid_is_valid(userns_info->target_uid));
|
||||
assert(uid_is_valid(userns_info->start_uid));
|
||||
@@ -566,7 +746,7 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
|
||||
if (r == 0) {
|
||||
/* child */
|
||||
|
||||
if (setns(usernsfd, CLONE_NEWUSER) < 0) {
|
||||
if (setns(userns_fd, CLONE_NEWUSER) < 0) {
|
||||
log_error_errno(errno, "Failed to join user namespace: %m");
|
||||
goto child_fail;
|
||||
}
|
||||
@@ -588,22 +768,167 @@ static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) {
|
||||
|
||||
/* Now write mapping */
|
||||
|
||||
_cleanup_free_ char *pmap = NULL;
|
||||
_cleanup_(uid_range_freep) UIDRange *outside_range = NULL;
|
||||
r = uid_range_load_userns_by_fd_full(parent_userns_fd, UID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &outside_range);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read userns UID range: %m");
|
||||
|
||||
if (asprintf(&pmap, "/proc/" PID_FMT "/uid_map", pidref.pid) < 0)
|
||||
_cleanup_(uid_range_freep) UIDRange *inside_range = NULL;
|
||||
r = uid_range_load_userns_by_fd_full(parent_userns_fd, UID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &inside_range);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read userns UID range: %m");
|
||||
|
||||
uid_t start_uid;
|
||||
r = uid_range_translate(outside_range, inside_range, userns_info->start_uid, &start_uid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", userns_info->start_uid);
|
||||
|
||||
/* Let's enforce that the transient UID/GID ranges are mapped 1:1 in the parent user namespace, to
|
||||
* avoid any weird mapping shenanigans that might happen otherwise. */
|
||||
|
||||
if (uid_is_transient(userns_info->start_uid) && start_uid != userns_info->start_uid)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Transient UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")",
|
||||
userns_info->start_uid, start_uid);
|
||||
|
||||
/* Build uid_map content: primary mapping + delegated mappings (1:1) */
|
||||
_cleanup_free_ char *uidmap = NULL;
|
||||
if (asprintf(&uidmap, UID_FMT " " UID_FMT " %" PRIu32 "\n",
|
||||
userns_info->target_uid, start_uid, userns_info->size) < 0)
|
||||
return log_oom();
|
||||
|
||||
r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " %" PRIu32 "\n", userns_info->target_uid, userns_info->start_uid, userns_info->size);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m");
|
||||
log_debug("UID mapping: " UID_FMT " " UID_FMT " %" PRIu32,
|
||||
userns_info->target_uid, userns_info->start_uid, userns_info->size);
|
||||
|
||||
pmap = mfree(pmap);
|
||||
if (asprintf(&pmap, "/proc/" PID_FMT "/gid_map", pidref.pid) < 0)
|
||||
FOREACH_ARRAY(delegate, userns_info->delegates, userns_info->n_delegates) {
|
||||
r = uid_range_translate(outside_range, inside_range, delegate->start_uid, &start_uid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", userns_info->start_uid);
|
||||
|
||||
if (start_uid != delegate->start_uid)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Delegated transient UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")",
|
||||
delegate->start_uid, start_uid);
|
||||
|
||||
if (strextendf(&uidmap,
|
||||
UID_FMT " " UID_FMT " %" PRIu32 "\n",
|
||||
delegate->start_uid,
|
||||
start_uid,
|
||||
delegate->size) < 0)
|
||||
return log_oom();
|
||||
|
||||
log_debug("UID mapping: " UID_FMT " " UID_FMT " %" PRIu32,
|
||||
delegate->start_uid, start_uid, delegate->size);
|
||||
}
|
||||
|
||||
if (map_foreign) {
|
||||
r = uid_range_translate(outside_range, inside_range, FOREIGN_UID_MIN, &start_uid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent userns: %m", FOREIGN_UID_MIN);
|
||||
|
||||
if (start_uid != FOREIGN_UID_MIN)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Foreign UID range not mapped 1:1 in parent userns ("UID_FMT" -> "UID_FMT")",
|
||||
FOREIGN_UID_MIN, start_uid);
|
||||
|
||||
if (strextendf(&uidmap, UID_FMT " " UID_FMT " %" PRIu32 "\n",
|
||||
FOREIGN_UID_MIN, start_uid, NSRESOURCE_UIDS_64K) < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
outside_range = uid_range_free(outside_range);
|
||||
inside_range = uid_range_free(inside_range);
|
||||
|
||||
r = uid_range_load_userns_by_fd_full(parent_userns_fd, GID_RANGE_USERNS_OUTSIDE, /* coalesce= */ false, &outside_range);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read userns GID range: %m");
|
||||
|
||||
r = uid_range_load_userns_by_fd_full(parent_userns_fd, GID_RANGE_USERNS_INSIDE, /* coalesce= */ false, &inside_range);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to read userns GID range: %m");
|
||||
|
||||
gid_t start_gid;
|
||||
r = uid_range_translate(outside_range, inside_range, userns_info->start_gid, &start_gid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", userns_info->start_gid);
|
||||
|
||||
if (gid_is_transient(userns_info->start_gid) && start_gid != userns_info->start_gid)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Transient GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")",
|
||||
userns_info->start_gid, start_gid);
|
||||
|
||||
_cleanup_free_ char *gidmap = NULL;
|
||||
if (asprintf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n",
|
||||
userns_info->target_gid, start_gid, userns_info->size) < 0)
|
||||
return log_oom();
|
||||
|
||||
r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " %" PRIu32 "\n", userns_info->target_gid, userns_info->start_gid, userns_info->size);
|
||||
log_debug("GID mapping: " GID_FMT " " GID_FMT " %" PRIu32,
|
||||
userns_info->target_gid, userns_info->start_gid, userns_info->size);
|
||||
|
||||
FOREACH_ARRAY(delegate, userns_info->delegates, userns_info->n_delegates) {
|
||||
r = uid_range_translate(outside_range, inside_range, delegate->start_gid, &start_gid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", userns_info->start_gid);
|
||||
|
||||
if (start_gid != delegate->start_gid)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Delegated transient GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")",
|
||||
delegate->start_gid, start_gid);
|
||||
|
||||
/* Delegated ranges are mapped 1:1 (inside GID == outside GID) */
|
||||
if (strextendf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n",
|
||||
delegate->start_gid,
|
||||
start_gid,
|
||||
delegate->size) < 0)
|
||||
return log_oom();
|
||||
|
||||
log_debug("GID mapping: " GID_FMT " " GID_FMT " %" PRIu32,
|
||||
delegate->start_gid, start_gid, delegate->size);
|
||||
}
|
||||
|
||||
if (map_foreign) {
|
||||
r = uid_range_translate(outside_range, inside_range, FOREIGN_UID_MIN, &start_gid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent userns: %m", FOREIGN_UID_MIN);
|
||||
|
||||
if (start_gid != FOREIGN_UID_MIN)
|
||||
return log_debug_errno(
|
||||
SYNTHETIC_ERRNO(ERANGE),
|
||||
"Foreign GID range not mapped 1:1 in parent userns ("GID_FMT" -> "GID_FMT")",
|
||||
FOREIGN_UID_MIN, start_gid);
|
||||
|
||||
if (strextendf(&gidmap, GID_FMT " " GID_FMT " %" PRIu32 "\n",
|
||||
FOREIGN_UID_MIN, start_gid, NSRESOURCE_UIDS_64K) < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = is_our_namespace(parent_userns_fd, NAMESPACE_USER);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m");
|
||||
return log_debug_errno(r, "Failed to check if parent user namespace refers to our own user namespace: %m");
|
||||
if (r > 0)
|
||||
return write_userns_mappings(&pidref, uidmap, gidmap);
|
||||
|
||||
/* The kernel is paranoid that the uid_map and gid_map files are written either from the user
|
||||
* namespace itself or its parent user namespace, so we have to join the parent user namespace to
|
||||
* write the files. */
|
||||
|
||||
r = pidref_safe_fork("(sd-userns)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT, /* ret= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) {
|
||||
if (setns(parent_userns_fd, CLONE_NEWUSER) < 0) {
|
||||
log_error_errno(errno, "Failed to join parent user namespace: %m");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
r = write_userns_mappings(&pidref, uidmap, gidmap);
|
||||
_exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* We are done! */
|
||||
|
||||
@@ -755,14 +1080,23 @@ static int validate_name(sd_varlink *link, const char *name, bool mangle, char *
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int validate_target_and_size(sd_varlink *link, uid_t target, uint32_t size) {
|
||||
static int validate_target_and_size(sd_varlink *link, uid_t target, uint32_t size, AllocateUserRangeType type) {
|
||||
assert(link);
|
||||
|
||||
if (!IN_SET(size, 1U, 0x10000))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "size");
|
||||
if (type == ALLOCATE_USER_RANGE_SELF) {
|
||||
/* Self userns must have size 1 and target must be 0 or unset */
|
||||
if (size != 1)
|
||||
return sd_varlink_error_invalid_parameter_name(link, "size");
|
||||
|
||||
if (!uid_is_valid(target) || target > UINT32_MAX - size)
|
||||
return sd_varlink_error_invalid_parameter_name(link, "target");
|
||||
if (!IN_SET(target, UID_INVALID, 0))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "target");
|
||||
} else {
|
||||
if (!IN_SET(size, 1U, 0x10000))
|
||||
return sd_varlink_error_invalid_parameter_name(link, "size");
|
||||
|
||||
if (!uid_is_valid(target) || target > UINT32_MAX - size)
|
||||
return sd_varlink_error_invalid_parameter_name(link, "target");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -836,20 +1170,26 @@ static int validate_userns_is_empty(sd_varlink *link, int userns_fd) {
|
||||
|
||||
typedef struct AllocateParameters {
|
||||
const char *name;
|
||||
AllocateUserRangeType type;
|
||||
uint32_t size;
|
||||
uid_t target;
|
||||
unsigned userns_fd_idx;
|
||||
bool mangle_name;
|
||||
uint32_t delegate_container_ranges;
|
||||
bool map_foreign;
|
||||
} AllocateParameters;
|
||||
|
||||
static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AllocateParameters, name), SD_JSON_MANDATORY },
|
||||
{ "size", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, size), SD_JSON_MANDATORY },
|
||||
{ "target", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(AllocateParameters, target), 0 },
|
||||
{ "userNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), SD_JSON_MANDATORY },
|
||||
{ "mangleName", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(AllocateParameters, mangle_name), 0 },
|
||||
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(AllocateParameters, name), SD_JSON_MANDATORY },
|
||||
{ "size", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, size), SD_JSON_MANDATORY },
|
||||
{ "target", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(AllocateParameters, target), 0 },
|
||||
{ "userNamespaceFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), SD_JSON_MANDATORY },
|
||||
{ "mangleName", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(AllocateParameters, mangle_name), 0 },
|
||||
{ "type", SD_JSON_VARIANT_STRING, dispatch_allocate_user_range_type, offsetof(AllocateParameters, type), 0 },
|
||||
{ "delegateContainerRanges", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(AllocateParameters, delegate_container_ranges), 0 },
|
||||
{ "mapForeign", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_stdbool, offsetof(AllocateParameters, map_foreign), 0 },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -857,9 +1197,12 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
|
||||
_cleanup_free_ char *userns_name = NULL;
|
||||
Context *c = ASSERT_PTR(userdata);
|
||||
uid_t peer_uid;
|
||||
gid_t peer_gid;
|
||||
struct stat userns_st;
|
||||
AllocateParameters p = {
|
||||
.type = ALLOCATE_USER_RANGE_MANAGED,
|
||||
.size = UINT32_MAX,
|
||||
.target = UID_INVALID,
|
||||
.userns_fd_idx = UINT_MAX,
|
||||
};
|
||||
int r;
|
||||
@@ -875,14 +1218,20 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (p.type != ALLOCATE_USER_RANGE_SELF && p.target == UID_INVALID)
|
||||
p.target = 0;
|
||||
|
||||
r = validate_name(link, p.name, p.mangle_name, &userns_name);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = validate_target_and_size(link, p.target, p.size);
|
||||
r = validate_target_and_size(link, p.target, p.size, p.type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (p.delegate_container_ranges > USER_NAMESPACE_DELEGATIONS_MAX)
|
||||
return sd_varlink_error(link, "io.systemd.NamespaceResource.TooManyDelegations", NULL);
|
||||
|
||||
userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx);
|
||||
if (userns_fd < 0)
|
||||
return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m");
|
||||
@@ -898,10 +1247,18 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
|
||||
if (fstat(userns_fd, &userns_st) < 0)
|
||||
return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m");
|
||||
|
||||
_cleanup_close_ int parent_userns_fd = ioctl(userns_fd, NS_GET_PARENT);
|
||||
if (parent_userns_fd < 0)
|
||||
return log_debug_errno(errno, "Failed to get parent user namespace: %m");
|
||||
|
||||
r = sd_varlink_get_peer_uid(link, &peer_uid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_varlink_get_peer_gid(link, &peer_gid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
const char *polkit_details[] = {
|
||||
"name", userns_name,
|
||||
NULL,
|
||||
@@ -942,7 +1299,48 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
|
||||
userns_info->target_uid = p.target;
|
||||
userns_info->target_gid = (gid_t) p.target;
|
||||
|
||||
r = allocate_now(registry_dir_fd, userns_info, &lock_fd);
|
||||
if (p.type == ALLOCATE_USER_RANGE_SELF) {
|
||||
/* The start UID/GID will be mapped to the parent userns in write_userns(). If a self
|
||||
* mapping to the peer UID/GID is requested, we have to map the target UID/GID ourselves here
|
||||
* as write_userns() doesn't take care of that. */
|
||||
|
||||
userns_info->start_uid = peer_uid;
|
||||
userns_info->start_gid = peer_gid;
|
||||
|
||||
if (p.target == UID_INVALID) {
|
||||
r = uid_range_translate_userns_fd(
|
||||
parent_userns_fd,
|
||||
UID_RANGE_USERNS_OUTSIDE,
|
||||
peer_uid,
|
||||
&userns_info->target_uid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate UID "UID_FMT" to parent user namespace: %m", peer_uid);
|
||||
|
||||
r = uid_range_translate_userns_fd(
|
||||
parent_userns_fd,
|
||||
GID_RANGE_USERNS_OUTSIDE,
|
||||
peer_gid,
|
||||
&userns_info->target_gid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to translate GID "GID_FMT" to parent user namespace: %m", peer_gid);
|
||||
}
|
||||
}
|
||||
|
||||
/* Set up delegation arrays if requested */
|
||||
if (p.delegate_container_ranges > 0) {
|
||||
userns_info->delegates = new0(DelegatedUserNamespaceInfo, p.delegate_container_ranges);
|
||||
if (!userns_info->delegates)
|
||||
return -ENOMEM;
|
||||
|
||||
FOREACH_ARRAY(delegate, userns_info->delegates, p.delegate_container_ranges) {
|
||||
*delegate = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
delegate->size = NSRESOURCE_UIDS_64K;
|
||||
}
|
||||
|
||||
userns_info->n_delegates = p.delegate_container_ranges;
|
||||
}
|
||||
|
||||
r = allocate_now(registry_dir_fd, userns_fd, parent_userns_fd, userns_info, &lock_fd);
|
||||
if (r == -EHOSTDOWN) /* The needed UID range is not delegated to us */
|
||||
return sd_varlink_error(link, "io.systemd.NamespaceResource.DynamicRangeUnavailable", NULL);
|
||||
if (r == -EBUSY) /* All used up */
|
||||
@@ -968,7 +1366,16 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
r = write_userns(userns_fd, userns_info);
|
||||
if (p.type == ALLOCATE_USER_RANGE_SELF) {
|
||||
/* For "self" allocations we deny setgroups() via the BPF LSM. We can't use
|
||||
* /proc/self/setgroups for this as that is transitive and also applies to child user
|
||||
* namespaces. The BPF LSM hook only applies to the specific user namespace. */
|
||||
r = userns_restrict_setgroups_deny_by_fd(c->bpf, userns_fd);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
r = write_userns(userns_fd, parent_userns_fd, userns_info, p.map_foreign);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "uid-classification.h"
|
||||
#include "user-util.h"
|
||||
#include "userns-registry.h"
|
||||
|
||||
@@ -56,6 +57,23 @@ int userns_registry_lock(int dir_fd) {
|
||||
return TAKE_FD(lock_fd);
|
||||
}
|
||||
|
||||
void delegated_userns_info_done(DelegatedUserNamespaceInfo *info) {
|
||||
if (!info)
|
||||
return;
|
||||
|
||||
info->ancestor_userns = mfree(info->ancestor_userns);
|
||||
info->n_ancestor_userns = 0;
|
||||
}
|
||||
|
||||
void delegated_userns_info_done_many(DelegatedUserNamespaceInfo infos[], size_t n) {
|
||||
assert(infos || n == 0);
|
||||
|
||||
FOREACH_ARRAY(info, infos, n)
|
||||
delegated_userns_info_done(info);
|
||||
|
||||
free(infos);
|
||||
}
|
||||
|
||||
UserNamespaceInfo* userns_info_new(void) {
|
||||
UserNamespaceInfo *info = new(UserNamespaceInfo, 1);
|
||||
if (!info)
|
||||
@@ -79,6 +97,8 @@ UserNamespaceInfo *userns_info_free(UserNamespaceInfo *userns) {
|
||||
free(userns->cgroups);
|
||||
free(userns->name);
|
||||
|
||||
delegated_userns_info_done_many(userns->delegates, userns->n_delegates);
|
||||
|
||||
strv_free(userns->netifs);
|
||||
|
||||
return mfree(userns);
|
||||
@@ -128,6 +148,100 @@ static int dispatch_cgroups_array(const char *name, sd_json_variant *variant, sd
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dispatch_delegates_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
UserNamespaceInfo *info = ASSERT_PTR(userdata);
|
||||
DelegatedUserNamespaceInfo *delegates = NULL;
|
||||
size_t n = 0;
|
||||
int r;
|
||||
|
||||
CLEANUP_ARRAY(delegates, n, delegated_userns_info_done_many);
|
||||
|
||||
if (sd_json_variant_is_null(variant)) {
|
||||
delegated_userns_info_done_many(info->delegates, info->n_delegates);
|
||||
info->delegates = NULL;
|
||||
info->n_delegates = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!sd_json_variant_is_array(variant))
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
|
||||
|
||||
size_t elements = sd_json_variant_elements(variant);
|
||||
if (elements > USER_NAMESPACE_DELEGATIONS_MAX)
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(E2BIG), "Too many delegations.");
|
||||
|
||||
delegates = new(DelegatedUserNamespaceInfo, elements);
|
||||
if (!delegates)
|
||||
return json_log_oom(variant, flags);
|
||||
|
||||
sd_json_variant *e;
|
||||
JSON_VARIANT_ARRAY_FOREACH(e, variant) {
|
||||
static const sd_json_dispatch_field delegate_dispatch_table[] = {
|
||||
{ "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(DelegatedUserNamespaceInfo, userns_inode), 0 },
|
||||
{ "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_uid), SD_JSON_MANDATORY },
|
||||
{ "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_gid), SD_JSON_MANDATORY },
|
||||
{ "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(DelegatedUserNamespaceInfo, size), SD_JSON_MANDATORY },
|
||||
{}
|
||||
};
|
||||
|
||||
delegates[n] = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
|
||||
r = sd_json_dispatch(e, delegate_dispatch_table, flags, &delegates[n]);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!uid_is_valid(delegates[n].start_uid) || !gid_is_valid(delegates[n].start_gid))
|
||||
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid delegate UID/GID.");
|
||||
|
||||
if (delegates[n].size == 0)
|
||||
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid delegate size.");
|
||||
|
||||
n++;
|
||||
}
|
||||
|
||||
delegated_userns_info_done_many(info->delegates, info->n_delegates);
|
||||
info->delegates = TAKE_PTR(delegates);
|
||||
info->n_delegates = n;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dispatch_ancestor_userns_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
DelegatedUserNamespaceInfo *info = ASSERT_PTR(userdata);
|
||||
_cleanup_free_ uint64_t *ancestor_userns = NULL;
|
||||
size_t n = 0;
|
||||
|
||||
if (sd_json_variant_is_null(variant)) {
|
||||
info->ancestor_userns = mfree(info->ancestor_userns);
|
||||
info->n_ancestor_userns = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!sd_json_variant_is_array(variant))
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
|
||||
|
||||
ancestor_userns = new(uint64_t, sd_json_variant_elements(variant));
|
||||
if (!ancestor_userns)
|
||||
return json_log_oom(variant, flags);
|
||||
|
||||
sd_json_variant *e;
|
||||
JSON_VARIANT_ARRAY_FOREACH(e, variant) {
|
||||
if (!sd_json_variant_is_unsigned(e))
|
||||
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an unsigned integer.");
|
||||
|
||||
uint64_t v = sd_json_variant_unsigned(e);
|
||||
if (v == 0)
|
||||
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid ancestor userns inode 0.");
|
||||
|
||||
ancestor_userns[n++] = v;
|
||||
}
|
||||
|
||||
free_and_replace(info->ancestor_userns, ancestor_userns);
|
||||
info->n_ancestor_userns = n;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) {
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
@@ -141,6 +255,7 @@ static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **
|
||||
{ "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 },
|
||||
{ "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 },
|
||||
{ "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 },
|
||||
{ "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -171,8 +286,6 @@ static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **
|
||||
|
||||
if (userns_info->userns_inode == 0)
|
||||
return -EBADMSG;
|
||||
if (userns_info->start_uid == 0 || userns_info->start_gid == 0)
|
||||
return -EBADMSG;
|
||||
|
||||
if (userns_info->size == 0) {
|
||||
if (uid_is_valid(userns_info->start_uid) || uid_is_valid(userns_info->target_uid))
|
||||
@@ -443,6 +556,18 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
|
||||
return r;
|
||||
}
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *delegates_array = NULL;
|
||||
FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) {
|
||||
r = sd_json_variant_append_arraybo(
|
||||
&delegates_array,
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("userns", delegate->userns_inode),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("start", delegate->start_uid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("startGid", delegate->start_gid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("size", delegate->size));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *def = NULL;
|
||||
r = sd_json_buildo(
|
||||
&def,
|
||||
@@ -455,7 +580,8 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
|
||||
SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->start_gid), "startGid", SD_JSON_BUILD_UNSIGNED(info->start_gid)),
|
||||
SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->target_gid), "targetGid", SD_JSON_BUILD_UNSIGNED(info->target_gid)),
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)),
|
||||
JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs));
|
||||
JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs),
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!!delegates_array, "delegates", SD_JSON_BUILD_VARIANT(delegates_array)));
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -484,7 +610,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (uid_is_valid(info->start_uid)) {
|
||||
if (uid_is_transient(info->start_uid)) {
|
||||
if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0) {
|
||||
r = log_oom_debug();
|
||||
goto fail;
|
||||
@@ -497,7 +623,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
|
||||
}
|
||||
}
|
||||
|
||||
if (gid_is_valid(info->start_gid)) {
|
||||
if (gid_is_transient(info->start_gid)) {
|
||||
if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0) {
|
||||
r = log_oom_debug();
|
||||
goto fail;
|
||||
@@ -531,6 +657,82 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* Store delegation files */
|
||||
FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *delegate_def = NULL, *ancestor_array = NULL;
|
||||
_cleanup_free_ char *delegate_buf = NULL, *delegate_uid_fn = NULL, *delegate_gid_fn = NULL;
|
||||
|
||||
if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) < 0) {
|
||||
r = log_oom_debug();
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* Check if this delegation already exists. If so, this is a recursive
|
||||
* subdelegation: we need to preserve the chain of previous owners so that
|
||||
* ownership can be restored when the current owner goes away. */
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo existing = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
|
||||
r = userns_registry_load_delegation_by_uid(dir_fd, delegate->start_uid, &existing);
|
||||
if (r >= 0) {
|
||||
/* Delegation file exists — append old owner to ancestor chain */
|
||||
FOREACH_ARRAY(ancestor_userns, existing.ancestor_userns, existing.n_ancestor_userns) {
|
||||
r = sd_json_variant_append_arrayb(
|
||||
&ancestor_array,
|
||||
SD_JSON_BUILD_UNSIGNED(*ancestor_userns));
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* userns_registry_store() is also called to update existing entries in the registry
|
||||
* in which case we don't need to update the ownership of the delegated UID ranges. */
|
||||
if (delegate->userns_inode != existing.userns_inode) {
|
||||
r = sd_json_variant_append_arrayb(
|
||||
&ancestor_array,
|
||||
SD_JSON_BUILD_UNSIGNED(existing.userns_inode));
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
} else if (r != -ENOENT) {
|
||||
log_debug_errno(r, "Failed to load existing delegation for UID " UID_FMT ": %m", delegate->start_uid);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
r = sd_json_buildo(
|
||||
&delegate_def,
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("userns", delegate->userns_inode),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("start", delegate->start_uid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("startGid", delegate->start_gid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("size", delegate->size),
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!!ancestor_array, "ancestorUserns", SD_JSON_BUILD_VARIANT(ancestor_array)));
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
r = sd_json_variant_format(delegate_def, /* flags= */ 0, &delegate_buf);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to format delegation JSON object: %m");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
r = write_string_file_at(dir_fd, delegate_uid_fn, delegate_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to write delegation data to '%s' in registry: %m", delegate_uid_fn);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* Create GID symlink pointing to the UID file */
|
||||
if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) < 0) {
|
||||
r = log_oom_debug();
|
||||
goto fail;
|
||||
}
|
||||
|
||||
r = linkat_replace(dir_fd, delegate_uid_fn, dir_fd, delegate_gid_fn);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to link delegation data to '%s' in registry: %m", delegate_gid_fn);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
@@ -547,6 +749,17 @@ fail:
|
||||
if (uid_fn)
|
||||
(void) unlinkat(dir_fd, uid_fn, AT_REMOVEDIR);
|
||||
|
||||
/* Clean up any delegation files we created */
|
||||
FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) {
|
||||
_cleanup_free_ char *delegate_uid_fn = NULL, *delegate_gid_fn = NULL;
|
||||
|
||||
if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) >= 0)
|
||||
(void) unlinkat(dir_fd, delegate_uid_fn, /* flags= */ 0);
|
||||
|
||||
if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) >= 0)
|
||||
(void) unlinkat(dir_fd, delegate_gid_fn, /* flags= */ 0);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -568,31 +781,39 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) {
|
||||
if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
ret = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0));
|
||||
r = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", reg_fn));
|
||||
|
||||
_cleanup_free_ char *link1_fn = NULL;
|
||||
link1_fn = strjoin("n", info->name, ".userns");
|
||||
if (!link1_fn)
|
||||
return log_oom_debug();
|
||||
|
||||
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link1_fn, 0)));
|
||||
r = RET_NERRNO(unlinkat(dir_fd, link1_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link1_fn));
|
||||
|
||||
if (uid_is_valid(info->start_uid)) {
|
||||
if (uid_is_transient(info->start_uid)) {
|
||||
_cleanup_free_ char *link2_fn = NULL;
|
||||
|
||||
if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start_uid) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link2_fn, 0)));
|
||||
r = RET_NERRNO(unlinkat(dir_fd, link2_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link2_fn));
|
||||
}
|
||||
|
||||
if (uid_is_valid(info->start_gid)) {
|
||||
if (gid_is_transient(info->start_gid)) {
|
||||
_cleanup_free_ char *link3_fn = NULL;
|
||||
|
||||
if (asprintf(&link3_fn, "g" GID_FMT ".userns", info->start_gid) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link3_fn, 0)));
|
||||
r = RET_NERRNO(unlinkat(dir_fd, link3_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", link3_fn));
|
||||
}
|
||||
|
||||
_cleanup_free_ char *uid_fn = NULL;
|
||||
@@ -603,11 +824,90 @@ int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) {
|
||||
if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, owner_fn, 0)));
|
||||
r = RET_NERRNO(unlinkat(dir_fd, owner_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", owner_fn));
|
||||
|
||||
r = RET_NERRNO(unlinkat(dir_fd, uid_fn, AT_REMOVEDIR));
|
||||
if (r != -ENOTEMPTY)
|
||||
RET_GATHER(ret, r);
|
||||
if (r < 0 && r != -ENOTEMPTY)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", uid_fn));
|
||||
|
||||
/* Remove or restore delegation files */
|
||||
FOREACH_ARRAY(delegate, info->delegates, info->n_delegates) {
|
||||
/* Check if this delegation has ancestor user namespaces. If so, restore ownership to
|
||||
* the last ancestor instead of removing the delegation file entirely. */
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo existing = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
|
||||
r = userns_registry_load_delegation_by_uid(dir_fd, delegate->start_uid, &existing);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r,
|
||||
"Failed to load delegated UID range starting at "UID_FMT":"GID_FMT" for userns %"PRIu64": %m",
|
||||
delegate->start_uid, delegate->start_gid, delegate->userns_inode);
|
||||
RET_GATHER(ret, r);
|
||||
continue;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *delegate_uid_fn = NULL;
|
||||
if (asprintf(&delegate_uid_fn, "u" UID_FMT ".delegate", delegate->start_uid) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
if (existing.n_ancestor_userns > 0) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *delegate_def = NULL, *ancestor_array = NULL;
|
||||
_cleanup_free_ char *delegate_buf = NULL;
|
||||
|
||||
/* Pop the last ancestor userns inode to become the new owner */
|
||||
uint64_t new_owner = existing.ancestor_userns[existing.n_ancestor_userns - 1];
|
||||
|
||||
log_debug("Moving ownership of delegated UID range from %"PRIu64" to %"PRIu64".",
|
||||
delegate->userns_inode, new_owner);
|
||||
|
||||
/* Rebuild ancestor array without the last entry */
|
||||
for (size_t j = 0; j + 1 < existing.n_ancestor_userns; j++) {
|
||||
r = sd_json_variant_append_arrayb(
|
||||
&ancestor_array,
|
||||
SD_JSON_BUILD_UNSIGNED(existing.ancestor_userns[j]));
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to append to JSON array: %m");
|
||||
}
|
||||
|
||||
r = sd_json_buildo(
|
||||
&delegate_def,
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("userns", new_owner),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("start", existing.start_uid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("startGid", existing.start_gid),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("size", existing.size),
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!!ancestor_array, "ancestorUserns", SD_JSON_BUILD_VARIANT(ancestor_array)));
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to build delegate JSON object: %m");
|
||||
|
||||
r = sd_json_variant_format(delegate_def, /* flags= */ 0, &delegate_buf);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to format delegation JSON object: %m");
|
||||
|
||||
r = write_string_file_at(dir_fd, delegate_uid_fn, delegate_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to write restored delegation data to '%s' in registry: %m", delegate_uid_fn));
|
||||
|
||||
/* GID link already points to the UID file, no need to update it */
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug("Removing delegated UID range starting at "UID_FMT":"GID_FMT" for userns %"PRIu64 ".",
|
||||
delegate->start_uid, delegate->start_gid, delegate->userns_inode);
|
||||
|
||||
/* No ancestor chain — just remove the delegation files */
|
||||
r = RET_NERRNO(unlinkat(dir_fd, delegate_uid_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", delegate_uid_fn));
|
||||
|
||||
_cleanup_free_ char *delegate_gid_fn = NULL;
|
||||
if (asprintf(&delegate_gid_fn, "g" GID_FMT ".delegate", delegate->start_gid) < 0)
|
||||
return log_oom_debug();
|
||||
|
||||
r = RET_NERRNO(unlinkat(dir_fd, delegate_gid_fn, 0));
|
||||
if (r < 0)
|
||||
RET_GATHER(ret, log_debug_errno(r, "Failed to remove %s: %m", delegate_gid_fn));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -822,3 +1122,135 @@ int userns_registry_per_uid(int dir_fd, uid_t owner) {
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
int userns_registry_delegation_uid_exists(int dir_fd, uid_t start) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
|
||||
assert(dir_fd >= 0);
|
||||
|
||||
if (!uid_is_valid(start))
|
||||
return -ENOENT;
|
||||
|
||||
if (start == 0)
|
||||
return true;
|
||||
|
||||
if (asprintf(&fn, "u" UID_FMT ".delegate", start) < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0)
|
||||
return errno == ENOENT ? false : -errno;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int userns_registry_delegation_gid_exists(int dir_fd, gid_t start) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
|
||||
assert(dir_fd >= 0);
|
||||
|
||||
if (!gid_is_valid(start))
|
||||
return -ENOENT;
|
||||
|
||||
if (start == 0)
|
||||
return true;
|
||||
|
||||
if (asprintf(&fn, "g" GID_FMT ".delegate", start) < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0)
|
||||
return errno == ENOENT ? false : -errno;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int userns_registry_load_delegation(int dir_fd, const char *filename, DelegatedUserNamespaceInfo *ret) {
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(DelegatedUserNamespaceInfo, userns_inode), SD_JSON_MANDATORY },
|
||||
{ "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_uid), SD_JSON_MANDATORY },
|
||||
{ "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(DelegatedUserNamespaceInfo, start_gid), 0 },
|
||||
{ "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(DelegatedUserNamespaceInfo, size), SD_JSON_MANDATORY },
|
||||
{ "ancestorUserns", SD_JSON_VARIANT_ARRAY, dispatch_ancestor_userns_array, 0, 0 },
|
||||
{}
|
||||
};
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
_cleanup_close_ int registry_fd = -EBADF;
|
||||
int r;
|
||||
|
||||
if (dir_fd < 0) {
|
||||
registry_fd = userns_registry_open_fd();
|
||||
if (registry_fd < 0)
|
||||
return registry_fd;
|
||||
|
||||
dir_fd = registry_fd;
|
||||
}
|
||||
|
||||
r = sd_json_parse_file_at(/* f= */ NULL, dir_fd, filename, /* flags= */ 0, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
|
||||
r = sd_json_dispatch(v, dispatch_table, /* flags= */ 0, &data);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (data.userns_inode == 0)
|
||||
return -EBADMSG;
|
||||
if (data.size == 0)
|
||||
return -EBADMSG;
|
||||
|
||||
if (ret)
|
||||
*ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int userns_registry_load_delegation_by_uid(int dir_fd, uid_t start, DelegatedUserNamespaceInfo *ret) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
int r;
|
||||
|
||||
if (!uid_is_valid(start))
|
||||
return -ENOENT;
|
||||
|
||||
if (asprintf(&fn, "u" UID_FMT ".delegate", start) < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
r = userns_registry_load_delegation(dir_fd, fn, &data);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (data.start_uid != start)
|
||||
return -EBADMSG;
|
||||
|
||||
if (ret)
|
||||
*ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int userns_registry_load_delegation_by_gid(int dir_fd, gid_t start, DelegatedUserNamespaceInfo *ret) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
int r;
|
||||
|
||||
if (!gid_is_valid(start))
|
||||
return -ENOENT;
|
||||
|
||||
if (asprintf(&fn, "g" UID_FMT ".delegate", start) < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
_cleanup_(delegated_userns_info_done) DelegatedUserNamespaceInfo data = DELEGATED_USER_NAMESPACE_INFO_NULL;
|
||||
r = userns_registry_load_delegation(dir_fd, fn, &data);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (data.start_gid != start)
|
||||
return -EBADMSG;
|
||||
|
||||
if (ret)
|
||||
*ret = TAKE_GENERIC(data, DelegatedUserNamespaceInfo, DELEGATED_USER_NAMESPACE_INFO_NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -195,4 +195,306 @@ TEST(uid_range_coalesce) {
|
||||
ASSERT_EQ(p->entries[0].nr, 115U);
|
||||
}
|
||||
|
||||
TEST(uid_range_clip) {
|
||||
_cleanup_(uid_range_freep) UIDRange *p = NULL;
|
||||
|
||||
/* Build a range: 100-199, 300-399, 500-599 */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "300-399"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "500-599"));
|
||||
ASSERT_EQ(uid_range_entries(p), 3U);
|
||||
|
||||
/* Intersect with range that covers all entries */
|
||||
ASSERT_OK(uid_range_clip(p, 0, 1000));
|
||||
ASSERT_EQ(uid_range_entries(p), 3U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
ASSERT_EQ(p->entries[1].start, 300U);
|
||||
ASSERT_EQ(p->entries[1].nr, 100U);
|
||||
ASSERT_EQ(p->entries[2].start, 500U);
|
||||
ASSERT_EQ(p->entries[2].nr, 100U);
|
||||
|
||||
/* Intersect with range that excludes first and last entries */
|
||||
ASSERT_OK(uid_range_clip(p, 200, 499));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 300U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Test partial overlap - trimming from both sides */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_clip(p, 150, 180));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 150U);
|
||||
ASSERT_EQ(p->entries[0].nr, 31U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Test intersection that removes all entries */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_clip(p, 500, 600));
|
||||
ASSERT_TRUE(uid_range_is_empty(p));
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Test invalid min > max */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_ERROR(uid_range_clip(p, 200, 100), EINVAL);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Test with max == UINT32_MAX (should not overflow) */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_clip(p, 0, UINT32_MAX));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Test with both min and max at extremes */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "500-599"));
|
||||
ASSERT_OK(uid_range_clip(p, 150, UINT32_MAX));
|
||||
ASSERT_EQ(uid_range_entries(p), 2U);
|
||||
ASSERT_EQ(p->entries[0].start, 150U);
|
||||
ASSERT_EQ(p->entries[0].nr, 50U);
|
||||
ASSERT_EQ(p->entries[1].start, 500U);
|
||||
ASSERT_EQ(p->entries[1].nr, 100U);
|
||||
}
|
||||
|
||||
TEST(uid_range_partition) {
|
||||
_cleanup_(uid_range_freep) UIDRange *p = NULL;
|
||||
|
||||
/* Single entry that divides evenly */
|
||||
ASSERT_OK(uid_range_add_str(&p, "0-299"));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_OK(uid_range_partition(p, 100));
|
||||
ASSERT_EQ(uid_range_entries(p), 3U);
|
||||
ASSERT_EQ(p->entries[0].start, 0U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
ASSERT_EQ(p->entries[1].start, 100U);
|
||||
ASSERT_EQ(p->entries[1].nr, 100U);
|
||||
ASSERT_EQ(p->entries[2].start, 200U);
|
||||
ASSERT_EQ(p->entries[2].nr, 100U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Entry with remainder (gets truncated) */
|
||||
ASSERT_OK(uid_range_add_str(&p, "0-249"));
|
||||
ASSERT_OK(uid_range_partition(p, 100));
|
||||
ASSERT_EQ(uid_range_entries(p), 2U);
|
||||
ASSERT_EQ(p->entries[0].start, 0U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
ASSERT_EQ(p->entries[1].start, 100U);
|
||||
ASSERT_EQ(p->entries[1].nr, 100U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Entry smaller than partition size - gets dropped */
|
||||
ASSERT_OK(uid_range_add_str(&p, "0-49"));
|
||||
ASSERT_OK(uid_range_partition(p, 100));
|
||||
ASSERT_TRUE(uid_range_is_empty(p));
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Multiple entries */
|
||||
ASSERT_OK(uid_range_add_str(&p, "0-199"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "1000-1299"));
|
||||
ASSERT_EQ(uid_range_entries(p), 2U);
|
||||
ASSERT_OK(uid_range_partition(p, 100));
|
||||
ASSERT_EQ(uid_range_entries(p), 5U);
|
||||
ASSERT_EQ(p->entries[0].start, 0U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
ASSERT_EQ(p->entries[1].start, 100U);
|
||||
ASSERT_EQ(p->entries[1].nr, 100U);
|
||||
ASSERT_EQ(p->entries[2].start, 1000U);
|
||||
ASSERT_EQ(p->entries[2].nr, 100U);
|
||||
ASSERT_EQ(p->entries[3].start, 1100U);
|
||||
ASSERT_EQ(p->entries[3].nr, 100U);
|
||||
ASSERT_EQ(p->entries[4].start, 1200U);
|
||||
ASSERT_EQ(p->entries[4].nr, 100U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Partition size of 1 */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-102"));
|
||||
ASSERT_OK(uid_range_partition(p, 1));
|
||||
ASSERT_EQ(uid_range_entries(p), 3U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 1U);
|
||||
ASSERT_EQ(p->entries[1].start, 101U);
|
||||
ASSERT_EQ(p->entries[1].nr, 1U);
|
||||
ASSERT_EQ(p->entries[2].start, 102U);
|
||||
ASSERT_EQ(p->entries[2].nr, 1U);
|
||||
}
|
||||
|
||||
TEST(uid_range_copy) {
|
||||
_cleanup_(uid_range_freep) UIDRange *p = NULL, *copy = NULL;
|
||||
|
||||
/* Copy NULL range */
|
||||
ASSERT_OK(uid_range_copy(NULL, ©));
|
||||
ASSERT_TRUE(uid_range_is_empty(copy));
|
||||
|
||||
copy = uid_range_free(copy);
|
||||
|
||||
/* Copy empty range */
|
||||
p = new0(UIDRange, 1);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_OK(uid_range_copy(p, ©));
|
||||
ASSERT_NOT_NULL(copy);
|
||||
ASSERT_TRUE(uid_range_is_empty(copy));
|
||||
|
||||
p = uid_range_free(p);
|
||||
copy = uid_range_free(copy);
|
||||
|
||||
/* Copy range with entries */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "300-399"));
|
||||
ASSERT_OK(uid_range_copy(p, ©));
|
||||
ASSERT_TRUE(uid_range_equal(p, copy));
|
||||
|
||||
/* Verify it's a deep copy - modifying original doesn't affect copy */
|
||||
ASSERT_OK(uid_range_add_str(&p, "500-599"));
|
||||
ASSERT_FALSE(uid_range_equal(p, copy));
|
||||
ASSERT_EQ(uid_range_entries(copy), 2U);
|
||||
}
|
||||
|
||||
TEST(uid_range_remove) {
|
||||
_cleanup_(uid_range_freep) UIDRange *p = NULL;
|
||||
|
||||
/* Build a range: 100-199 */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
|
||||
/* Remove with size 0 - no-op */
|
||||
ASSERT_OK(uid_range_remove(p, 150, 0));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
|
||||
/* Remove range that doesn't overlap - no change */
|
||||
ASSERT_OK(uid_range_remove(p, 0, 50));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
|
||||
ASSERT_OK(uid_range_remove(p, 300, 50));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 100U);
|
||||
|
||||
/* Remove from the start of the entry */
|
||||
ASSERT_OK(uid_range_remove(p, 100, 10));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 110U);
|
||||
ASSERT_EQ(p->entries[0].nr, 90U);
|
||||
|
||||
/* Remove from the end of the entry */
|
||||
ASSERT_OK(uid_range_remove(p, 190, 10));
|
||||
ASSERT_EQ(uid_range_entries(p), 1U);
|
||||
ASSERT_EQ(p->entries[0].start, 110U);
|
||||
ASSERT_EQ(p->entries[0].nr, 80U);
|
||||
|
||||
/* Remove from the middle - splits the entry */
|
||||
ASSERT_OK(uid_range_remove(p, 140, 20));
|
||||
ASSERT_EQ(uid_range_entries(p), 2U);
|
||||
ASSERT_EQ(p->entries[0].start, 110U);
|
||||
ASSERT_EQ(p->entries[0].nr, 30U);
|
||||
ASSERT_EQ(p->entries[1].start, 160U);
|
||||
ASSERT_EQ(p->entries[1].nr, 30U);
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Remove entire entry */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_remove(p, 100, 100));
|
||||
ASSERT_TRUE(uid_range_is_empty(p));
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Remove range larger than entry */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_remove(p, 50, 200));
|
||||
ASSERT_TRUE(uid_range_is_empty(p));
|
||||
|
||||
p = uid_range_free(p);
|
||||
|
||||
/* Remove affecting multiple entries */
|
||||
ASSERT_OK(uid_range_add_str(&p, "100-199"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "300-399"));
|
||||
ASSERT_OK(uid_range_add_str(&p, "500-599"));
|
||||
ASSERT_EQ(uid_range_entries(p), 3U);
|
||||
|
||||
/* Remove range spanning the middle entry completely and trimming others */
|
||||
ASSERT_OK(uid_range_remove(p, 150, 400));
|
||||
ASSERT_EQ(uid_range_entries(p), 2U);
|
||||
ASSERT_EQ(p->entries[0].start, 100U);
|
||||
ASSERT_EQ(p->entries[0].nr, 50U);
|
||||
ASSERT_EQ(p->entries[1].start, 550U);
|
||||
ASSERT_EQ(p->entries[1].nr, 50U);
|
||||
}
|
||||
|
||||
TEST(uid_range_translate) {
|
||||
_cleanup_(uid_range_freep) UIDRange *o = NULL, *i = NULL;
|
||||
uid_t uid;
|
||||
|
||||
ASSERT_OK(uid_range_add_str_full(&o, "200-299", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_add_str_full(&i, "100-199", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_translate(o, i, 250, &uid));
|
||||
ASSERT_EQ(uid, 150U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 150, &uid));
|
||||
ASSERT_EQ(uid, 250U);
|
||||
|
||||
ASSERT_OK(uid_range_add_str_full(&o, "300-399", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_add_str_full(&i, "350-449", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_translate(o, i, 350, &uid));
|
||||
ASSERT_EQ(uid, 400U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 400, &uid));
|
||||
ASSERT_EQ(uid, 350U);
|
||||
|
||||
/* Test translating at range boundaries */
|
||||
ASSERT_OK(uid_range_translate(o, i, 200, &uid));
|
||||
ASSERT_EQ(uid, 100U);
|
||||
ASSERT_OK(uid_range_translate(o, i, 299, &uid));
|
||||
ASSERT_EQ(uid, 199U);
|
||||
ASSERT_OK(uid_range_translate(o, i, 300, &uid));
|
||||
ASSERT_EQ(uid, 350U);
|
||||
ASSERT_OK(uid_range_translate(o, i, 399, &uid));
|
||||
ASSERT_EQ(uid, 449U);
|
||||
|
||||
/* Test reverse translation at boundaries */
|
||||
ASSERT_OK(uid_range_translate(i, o, 100, &uid));
|
||||
ASSERT_EQ(uid, 200U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 199, &uid));
|
||||
ASSERT_EQ(uid, 299U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 350, &uid));
|
||||
ASSERT_EQ(uid, 300U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 449, &uid));
|
||||
ASSERT_EQ(uid, 399U);
|
||||
|
||||
/* Test UID not in any range returns ESRCH */
|
||||
ASSERT_ERROR(uid_range_translate(o, i, 0, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(o, i, 199, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(o, i, 400, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(i, o, 0, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(i, o, 99, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(i, o, 200, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(i, o, 349, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(i, o, 450, &uid), ESRCH);
|
||||
|
||||
o = uid_range_free(o);
|
||||
i = uid_range_free(i);
|
||||
|
||||
/* Test with single-element ranges */
|
||||
ASSERT_OK(uid_range_add_str_full(&o, "1000", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_add_str_full(&i, "5000", /* coalesce= */ false));
|
||||
ASSERT_OK(uid_range_translate(o, i, 1000, &uid));
|
||||
ASSERT_EQ(uid, 5000U);
|
||||
ASSERT_OK(uid_range_translate(i, o, 5000, &uid));
|
||||
ASSERT_EQ(uid, 1000U);
|
||||
ASSERT_ERROR(uid_range_translate(o, i, 999, &uid), ESRCH);
|
||||
ASSERT_ERROR(uid_range_translate(o, i, 1001, &uid), ESRCH);
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user