diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml
index 120027aab0..6234b00dbd 100644
--- a/man/systemd-nsresourced.service.xml
+++ b/man/systemd-nsresourced.service.xml
@@ -40,9 +40,11 @@
Allocations of UIDs/GIDs this way are transient: when a user namespace goes away, its UID/GID range
is returned to the pool of available ranges. In order to ensure that clients cannot gain persistency in
- their transient UID/GID range a BPF-LSM based policy is enforced that ensures that user namespaces set up
- this way can only write to file systems they allocate themselves or that are explicitly allowlisted via
- systemd-nsresourced.
+ their transient UID/GID range a BPF-LSM based policy is enforced that refuses to create inodes — or
+ change ownership of them — that would end up owned by a transient UID/GID on a file system outliving the
+ user namespace the range was assigned to. Writes to file systems the user namespace allocated itself are
+ hence permitted, as those are torn down together with it, and so are writes through id-mapped mounts that
+ translate the transient range into something else before it reaches the disk.
systemd-nsresourced automatically ensures that any registered UID ranges show up
in the system's NSS database via the User/Group Record
@@ -73,7 +75,7 @@
the user namespace. When this option is enabled, the foreign UID range is mapped 1:1 into the user
namespace, allowing processes inside to access and manipulate files owned by the foreign UID range.
- The service provides API calls to allowlist mounts (referenced via their mount file descriptors as
+ The service provides API calls to delegate mounts (referenced via their mount file descriptors as
per Linux fsmount() API), to pass ownership of a cgroup subtree to the user
namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination
this is sufficient to implement fully unprivileged container environments, as implemented by
diff --git a/src/bpf/userns-restrict.bpf.c b/src/bpf/userns-restrict.bpf.c
index 54473d7210..05f245020e 100644
--- a/src/bpf/userns-restrict.bpf.c
+++ b/src/bpf/userns-restrict.bpf.c
@@ -30,13 +30,22 @@
void* bpf_rdonly_cast(const void *, __u32) __ksym;
#endif
-/* BPF module that implements an allowlist of mounts (identified by mount ID) for user namespaces (identified
- * by their inode number in nsfs) that restricts creation of inodes (which would inherit the callers UID/GID)
- * or changing of ownership (similar).
+/* BPF module that keeps user namespaces provisioned by nsresourced (identified by their inode number in
+ * nsfs) from leaving objects owned by their transient UID/GID range behind on file systems that outlive the
+ * namespace. Transient ranges are recycled, so any such object would later be owned by an unrelated client.
*
* This hooks into the various path-based LSM entrypoints that control inode creation as well as chown(), and
- * then looks up the calling process' user namespace in a global map of namespaces, which points us to
- * another map that is simply a list of allowed mnt_ids. */
+ * for each asks the only two questions that matter:
+ *
+ * a) Does the file system die together with the user namespace? Superblocks record the user namespace they
+ * were mounted in, so this is simply whether sb->s_user_ns sits at or below the provisioned namespace.
+ *
+ * b) Will the transient ID actually reach the disk? An idmapped mount may translate it into something
+ * else entirely, in which case nothing recyclable is recorded anywhere.
+ *
+ * Both are answered from fields the kernel stamps itself and that a client cannot influence: unlike the
+ * mount namespace a mount happens to sit in, neither a superblock's user namespace nor a mount's idmapping
+ * can be forged with unshare(), and both are preserved when a mount is cloned. */
// FIXME: ACL adjustments are currently not blocked. There's no path-based LSM hook available in the kernel
// for setting xattrs or ACLs, hence we cannot easily block them, even though we want that. We can get away
@@ -45,22 +54,23 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym;
// not be reachable through it. It still sucks though that a user can persistently add an ACL entry to a file
// with their transient UIDs/GIDs.
-/* kernel currently enforces a maximum usernamespace nesting depth of 32, see create_user_ns() in the kernel sources */
-#define USER_NAMESPACE_DEPTH_MAX 32U
+/* create_user_ns() in the kernel sources refuses to nest below a user namespace whose level is above 32,
+ * hence the deepest reachable level is 33, i.e. a chain of 34 user namespaces including the initial one. */
+#define USER_NAMESPACE_DEPTH_MAX 34U
-struct mnt_id_map {
- __uint(type, BPF_MAP_TYPE_HASH);
- __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */
- __type(key, int);
- __type(value, int);
-};
+/* Mirrors UID_GID_MAP_MAX_BASE_EXTENTS/UID_GID_MAP_MAX_EXTENTS, see include/linux/user_namespace.h */
+#define UID_GID_MAP_MAX_BASE_EXTENTS 5U
+#define UID_GID_MAP_MAX_EXTENTS 340U
+
+/* The kernel's INVALID_UID/INVALID_GID, i.e. what an unmapped translation results in */
+#define UID_GID_INVALID ((uid_t) -1)
struct {
- __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
+ __uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */
__type(key, unsigned); /* userns inode */
- __array(values, struct mnt_id_map);
-} userns_mnt_id_hash SEC(".maps");
+ __type(value, int); /* dummy value */
+} userns_managed SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
@@ -74,10 +84,6 @@ struct {
__uint(max_entries, 4096);
} userns_ringbuf SEC(".maps");
-static inline struct mount *real_mount(struct vfsmount *mnt) {
- return container_of(mnt, struct mount, mnt);
-}
-
static inline bool uid_is_dynamic(uid_t uid) {
return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX;
}
@@ -90,17 +96,43 @@ 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;
+/* Finds the closest user namespace at or above the passed one that nsresourced provisioned. Walking upwards
+ * is what keeps a nested user namespace from shaking off the policy. Returns 1 and stores it in the return
+ * argument if found, 0 if the whole chain was walked without a match, and a negative error if the chain is
+ * longer than the kernel permits (in which case we refuse rather than fail open, as we cannot rule out a
+ * managed ancestor). */
+static int find_managed_userns(struct user_namespace *userns, struct user_namespace **ret) {
+ struct user_namespace *p = userns;
- /* 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;
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
- p = mount_userns;
for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) {
- if (p == userns)
+ unsigned inode;
+
+ inode = p->ns.inum;
+ if (bpf_map_lookup_elem(&userns_managed, &inode)) {
+ *ret = p;
+ return true;
+ }
+
+ p = p->parent;
+ if (!p) /* Walked the whole chain up to the initial namespace without a match. */
+ return false;
+ }
+
+ /* Chain longer than the kernel permits: refuse rather than fail open, as we cannot rule out a
+ * managed ancestor. */
+ return -EPERM;
+}
+
+/* Is the second user namespace the first one itself, or nested below it? */
+static int userns_is_below(struct user_namespace *ancestor, struct user_namespace *ns) {
+ struct user_namespace *p = ns;
+
+ for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) {
+ if (p == ancestor)
return true;
p = p->parent;
@@ -108,7 +140,7 @@ static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v)
break;
}
- /* Hmm, something is fishy if there's more than 32 levels of namespaces involved. Let's better be
+ /* Hmm, something is fishy if there's more than 34 levels of namespaces involved. Let's better be
* safe than sorry, and refuse. */
if (p)
return -EPERM;
@@ -116,68 +148,132 @@ static int userns_owns_mount(struct user_namespace *userns, struct vfsmount *v)
return false;
}
-static int validate_mount(struct vfsmount *v, int ret) {
- struct user_namespace *task_userns;
- unsigned task_userns_inode;
- const struct cred *cred;
- struct task_struct *task;
- void *mnt_id_map;
- struct mount *m;
- int mnt_id, r;
+/* Resolves a uid_gid_map's extent array for the requested direction. Small maps carry their extents inline,
+ * larger ones keep two arrays instead, each sorted by one end (the kernel bisects them, a linear scan
+ * arrives at the same answer). Those two pointers share storage with the inline extent array; the verifier
+ * resolves the overlap to the latter and then refuses an 8 byte load in the middle of it, so we read the
+ * wanted pointer out explicitly. The bpf_core_field_offset() operands have to stay compile time constants,
+ * hence the branch per direction. always_inline so each caller keeps the hand-inlined shape the verifier
+ * already accepts (a register that is a BTF pointer on one branch and a scalar on the other). Returns NULL
+ * for an empty map or on a read failure. */
+static inline __attribute__((always_inline))
+struct uid_gid_extent *uid_gid_map_extents(struct uid_gid_map *map, bool up) {
+ struct uid_gid_extent *array;
- if (ret != 0) /* propagate earlier error */
- return ret;
+ if (map->nr_extents == 0)
+ return NULL;
- /* Get user namespace from task */
- task = (struct task_struct*) bpf_get_current_task_btf();
- cred = task->cred;
- if (!cred)
- return -EPERM;
- task_userns = cred->user_ns;
+ if (map->nr_extents <= UID_GID_MAP_MAX_BASE_EXTENTS)
+ return map->extent;
- /* 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(cred->fsuid.val) && !uid_is_transient((uid_t) cred->fsgid.val))
- return 0;
+ if (up) {
+ if (bpf_probe_read_kernel(&array, sizeof(array),
+ (const void*) map + bpf_core_field_offset(struct uid_gid_map, reverse)) != 0)
+ return NULL;
+ } else {
+ if (bpf_probe_read_kernel(&array, sizeof(array),
+ (const void*) map + bpf_core_field_offset(struct uid_gid_map, forward)) != 0)
+ return NULL;
+ }
- 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;
-
- 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;
-
- 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 */
- if (bpf_map_lookup_elem(mnt_id_map, &mnt_id))
- return 0;
-
- return -EPERM;
+ return array;
}
-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;
+/* Mirrors map_id_range_up()/map_id_range_down() from kernel/user_namespace.c: translates the passed ID
+ * through the map, where the up flag selects which end of the extents is searched. Stores UID_GID_INVALID if
+ * the ID is not mapped, and returns negative if the map could not be read, which callers must keep apart: an
+ * unmapped ID records nothing on disk, whereas a map we failed to read tells us nothing at all. */
+static int uid_gid_map_translate(struct uid_gid_map *map, uid_t id, bool up, uid_t *ret) {
+ struct uid_gid_extent *array;
+ unsigned n;
+
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
+
+ n = map->nr_extents;
+ if (n == 0) { /* Empty map: nothing is mapped, and that is an answer, not a failure. */
+ *ret = UID_GID_INVALID;
+ return 0;
+ }
+
+ array = uid_gid_map_extents(map, up);
+ if (!array) /* Non-empty map whose extents we could not resolve. */
+ return -EPERM;
+
+ /* Copy each extent out instead of indexing the array in place: a variable index into a kernel
+ * pointer is something the verifier refuses. */
+ for (unsigned i = 0; i < UID_GID_MAP_MAX_EXTENTS; i++) {
+ struct uid_gid_extent e;
+ uid_t first;
+
+ if (i >= n)
+ break;
+
+ if (bpf_probe_read_kernel(&e, sizeof(e), array + i) != 0)
+ return -EPERM;
+
+ first = up ? e.lower_first : e.first;
+ if (id >= first && id - first < e.count) {
+ *ret = (up ? e.first : e.lower_first) + (id - first);
+ return 0;
+ }
+ }
+
+ *ret = UID_GID_INVALID;
+ return 0;
+}
+
+/* Mirrors from_vfsuid()/from_vfsgid() from fs/mnt_idmapping.c: the ID that is actually recorded on disk when
+ * the passed ID creates an object on the given mount. The gid flag selects the GID maps over the UID maps.
+ * Stores UID_GID_INVALID if the ID does not map, in which case the kernel refuses the operation with
+ * EOVERFLOW anyway, see fsuidgid_has_mapping(). Returns negative if a map could not be read. */
+static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns, uid_t id, bool gid, uid_t *ret) {
+ struct mnt_idmap *idmap;
+ struct uid_gid_map *m;
+ int r;
+
+ /* All callers pass a valid pointer; the guard just satisfies the pointer-deref checker. */
+ if (!ret)
+ return -EINVAL;
+
+ idmap = mnt->mnt_idmap;
+ if (!idmap) {
+ *ret = id;
+ return 0;
+ }
+
+ m = gid ? &idmap->gid_map : &idmap->uid_map;
+
+ /* Both nop_mnt_idmap and invalid_mnt_idmap carry empty maps. For the former that is an identity
+ * mapping; for the latter the operation fails later on regardless, so treating it as identity only
+ * ever makes us stricter. */
+ if (m->nr_extents > 0) {
+ r = uid_gid_map_translate(m, id, /* up= */ true, &id);
+ if (r < 0)
+ return r;
+ if (id == UID_GID_INVALID) {
+ *ret = UID_GID_INVALID;
+ return 0;
+ }
+ }
+
+ /* initial_idmapping(): the initial user namespace is the only one without a parent. */
+ if (!fs_userns->parent) {
+ *ret = id;
+ return 0;
+ }
+
+ return uid_gid_map_translate(gid ? &fs_userns->gid_map : &fs_userns->uid_map, id, /* up= */ false, ret);
+}
+
+/* Common tail of the inode creation hooks: would the object the calling task is about to create end up
+ * owned by a transient ID on a file system that outlives the user namespace that ID was handed to? */
+static int validate_mount(struct vfsmount *v, int ret) {
+ struct user_namespace *managed, *fs_userns;
const struct cred *cred;
struct task_struct *task;
- struct vfsmount *v;
- void *mnt_id_map;
+ uid_t uid, gid;
int r;
if (ret != 0) /* propagate earlier error */
@@ -188,34 +284,79 @@ int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long u
cred = task->cred;
if (!cred)
return -EPERM;
- task_userns = cred->user_ns;
- v = path->mnt;
- r = userns_owns_mount(task_userns, v);
+ /* fsuid/fsgid are the UID/GID in the initial user namespace. Anything outside the transient ranges
+ * is never recycled by us, hence there's nothing to protect. */
+ if (!uid_is_transient(cred->fsuid.val) && !uid_is_transient((uid_t) cred->fsgid.val))
+ return 0;
+
+ r = find_managed_userns(cred->user_ns, &managed);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Not provisioned by nsresourced? Then this is none of our business. */
+ return 0;
+
+ fs_userns = v->mnt_sb->s_user_ns;
+
+ /* Does the file system die together with the user namespace? Then nothing it carries can outlive
+ * the transient range, and we can allow this. */
+ r = userns_is_below(managed, fs_userns);
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, 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. */
+ /* Refuse if we cannot resolve where the IDs land: an ID we failed to translate is not an ID we know
+ * to be harmless. */
+ r = mount_map_id(v, fs_userns, cred->fsuid.val, /* gid= */ false, &uid);
+ if (r < 0)
+ return r;
+
+ r = mount_map_id(v, fs_userns, (uid_t) cred->fsgid.val, /* gid= */ true, &gid);
+ if (r < 0)
+ return r;
+
+ /* If the mount's idmapping translates our transient IDs into something else, nothing recyclable is
+ * recorded on disk, and we are fine. */
+ if (!uid_is_transient(uid) && !uid_is_transient(gid))
+ return 0;
+
+ return -EPERM;
+}
+
+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 *managed;
+ const struct cred *cred;
+ struct task_struct *task;
+ int r;
+
+ if (ret != 0) /* propagate earlier error */
+ return ret;
+
+ /* Get user namespace from task */
+ task = (struct task_struct*) bpf_get_current_task_btf();
+ cred = task->cred;
+ if (!cred)
+ return -EPERM;
/* 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! */
+ r = find_managed_userns(cred->user_ns, &managed);
+ if (r < 0)
+ return r;
+ if (r == 0)
return 0;
+ r = userns_is_below(managed, path->mnt->mnt_sb->s_user_ns);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+
+ /* Unlike the creation hooks this one is handed the IDs with the mount's idmapping already applied,
+ * i.e. the ones that end up on disk, hence we can look at them directly. See chown_common() in the
+ * kernel sources. */
if (uid_is_transient((uid_t) uid) || uid_is_transient((uid_t) gid))
return -EPERM;
@@ -253,27 +394,26 @@ int BPF_PROG(userns_restrict_task_fix_setgroups, struct cred *new_cred, const st
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
+ * (i.e. present in either the setgroups deny map or the managed userns 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))
+ if (bpf_map_lookup_elem(&userns_managed, &inode))
return 0;
p = p->parent;
+ if (!p) /* Walked the whole chain up to the initial namespace without a match, allow. */
+ return 0;
}
- /* No nsresourced-managed ancestor found, allow. */
- return 0;
+ /* Chain longer than the kernel permits: refuse rather than fail open, like find_managed_userns(). */
+ return -EPERM;
}
SEC("kprobe/retire_userns_sysctls")
@@ -287,7 +427,7 @@ int BPF_KPROBE(userns_restrict_retire_userns_sysctls, struct user_namespace *use
/* 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))
+ if (bpf_map_lookup_elem(&userns_managed, &inode))
goto notify;
if (bpf_map_lookup_elem(&userns_setgroups_deny, &inode))
diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c
index b8d68cbb42..ffe676d7ac 100644
--- a/src/nsresourced/nsresourcework.c
+++ b/src/nsresourced/nsresourcework.c
@@ -1515,13 +1515,8 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para
if (r < 0)
return r;
- /* Register the userns in the BPF map with an empty allowlist */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0);
+ /* Subject the userns to the BPF-LSM policy that keeps its transient range off persistent file systems */
+ r = userns_restrict_register_by_fd(c->bpf, userns_fd);
if (r < 0)
goto fail;
@@ -1742,13 +1737,8 @@ static int vl_method_register_user_namespace(sd_varlink *link, sd_json_variant *
if (r < 0)
return log_debug_errno(r, "Failed to update userns registry: %m");
- /* Register the userns in the BPF map with an empty allowlist */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0);
+ /* Subject the userns to the BPF-LSM policy that keeps its transient range off persistent file systems */
+ r = userns_restrict_register_by_fd(c->bpf, userns_fd);
if (r < 0)
goto fail;
@@ -1887,22 +1877,17 @@ static int vl_method_add_mount_to_user_namespace(sd_varlink *link, sd_json_varia
if (r < 0)
return r;
- /* Add this mount to the user namespace's BPF map allowlist entry. */
- r = userns_restrict_put_by_fd(
- c->bpf,
- userns_fd,
- /* replace= */ false,
- &mount_fd,
- 1);
- if (r < 0)
- return r;
+ /* There's no mount allowlist anymore: the BPF-LSM policy decides per operation whether the ID that
+ * would be recorded on disk is a transient one, which needs no per-mount knowledge. We keep
+ * accepting and validating the call for the sake of older clients, and keep the mount pinned as
+ * before, but there is nothing left to allowlist. */
if (DEBUG_LOGGING) {
if (userns_info->size > 0)
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
+ log_debug("Delegated mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")",
mnt_id, userns_st.st_ino, userns_info->name, userns_info->start_uid);
else
- log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')",
+ log_debug("Delegated mount %i to user namespace " INO_FMT " ('%s')",
mnt_id, userns_st.st_ino, userns_info->name);
}
diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c
index 29125a84a8..3607a26899 100644
--- a/src/nsresourced/test-userns-restrict.c
+++ b/src/nsresourced/test-userns-restrict.c
@@ -11,12 +11,15 @@
#include "fd-util.h"
#include "fileio.h"
#include "namespace-util.h"
+#include "path-util.h"
#include "pidref.h"
#include "process-util.h"
#include "rm-rf.h"
+#include "string-util.h"
#include "tests.h"
#include "tmpfile-util.h"
#include "uid-classification.h"
+#include "user-util.h"
#include "userns-restrict.h"
static int make_tmpfs_fsmount(void) {
@@ -30,6 +33,22 @@ static int make_tmpfs_fsmount(void) {
return TAKE_FD(mntfd);
}
+/* A tmpfs on the host, but idmapped through the passed user namespace, i.e. the equivalent of what mountfsd
+ * hands to clients: our transient UID lands on disk as UID 0, so nothing recyclable is recorded. */
+static int make_idmapped_tmpfs_fsmount(int userns_fd) {
+ _cleanup_close_ int mntfd = -EBADF;
+
+ mntfd = ASSERT_OK(make_tmpfs_fsmount());
+
+ ASSERT_OK_ERRNO(mount_setattr(mntfd, "", AT_EMPTY_PATH,
+ &(struct mount_attr) {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = userns_fd,
+ }, sizeof(struct mount_attr)));
+
+ return TAKE_FD(mntfd);
+}
+
static struct userns_restrict_bpf *bpf_obj = NULL;
STATIC_DESTRUCTOR_REGISTER(bpf_obj, userns_restrict_bpf_freep);
@@ -47,7 +66,8 @@ static int intro(void) {
}
TEST(userns_restrict) {
- _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF;
+ _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF,
+ idmapped_tmpfs = -EBADF;
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
int r;
@@ -64,15 +84,9 @@ TEST(userns_restrict) {
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,
- userns_fd,
- /* replace= */ true,
- /* mount_fds= */ NULL,
- /* n_mount_fds= */ 0));
+ idmapped_tmpfs = ASSERT_OK(make_idmapped_tmpfs_fsmount(userns_fd));
- afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
- bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
r = ASSERT_OK(pidref_safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pidref));
if (r == 0) {
@@ -81,10 +95,10 @@ TEST(userns_restrict) {
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
- /* Allocate tmpfs locally */
+ /* Allocate tmpfs locally, i.e. a file system that dies together with our user namespace */
private_tmpfs = make_tmpfs_fsmount();
- /* These two host mounts should be inaccessible */
+ /* These two host mounts would record our transient UID on disk, hence are inaccessible */
ASSERT_ERROR_ERRNO(openat(host_fd1, "test", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
ASSERT_ERROR_ERRNO(openat(host_tmpfs, "xxx", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "test2", 0666), EPERM);
@@ -94,58 +108,62 @@ TEST(userns_restrict) {
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "yyy", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "yyy2", 0666));
- /* Let's sync with the parent, so that it allowlists more stuff for us */
- ASSERT_OK_ERRNO(eventfd_write(afd, 1));
- uint64_t x;
- ASSERT_OK_ERRNO(eventfd_read(bfd, &x));
+ /* And so should the idmapped host mount: it translates our transient UID to UID 0 before
+ * anything reaches the disk */
+ safe_close(ASSERT_OK_ERRNO(openat(idmapped_tmpfs, "zzz", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
+ ASSERT_OK_ERRNO(mkdirat(idmapped_tmpfs, "zzz2", 0666));
- /* And now we should also have access to the host tmpfs */
- safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "zzz", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
+ /* chown() is subject to the same policy. Unlike the creation hooks it is handed the ID that
+ * ends up on disk, so it needs no idmap arithmetic of its own. Setting a group the inode
+ * already has still reaches the hook, before the change is applied. Giving the host
+ * directory (which outlives us) our transient GID would leave something recyclable behind,
+ * hence is refused... */
+ ASSERT_ERROR_ERRNO(fchownat(host_fd1, "", GID_INVALID, 0, AT_EMPTY_PATH), EPERM);
+
+ /* ...while the same chown() is fine on the file system that dies with us, and on the
+ * idmapped mount where our transient GID is translated to 0 before it reaches the disk. */
+ ASSERT_OK_ERRNO(fchownat(private_tmpfs, "yyy", GID_INVALID, 0, 0));
+ ASSERT_OK_ERRNO(fchownat(idmapped_tmpfs, "zzz", GID_INVALID, 0, 0));
+
+ /* Unsharing a mount namespace hands us clones of every mount we can see, owned by our own
+ * user namespace. That must not buy us anything.
+ *
+ * Note that this has to go through a path, not through the directory fd opened above: an
+ * already open fd pins the original mount, while a path lookup lands on the clone, and it
+ * is the clone that used to be mistaken for one of ours. */
+ ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
+
+ _cleanup_free_ char *p1 = NULL, *p2 = NULL;
+ ASSERT_NOT_NULL(p1 = path_join(t, "newns"));
+ ASSERT_NOT_NULL(p2 = path_join(t, "newns2"));
+
+ ASSERT_ERROR_ERRNO(open(p1, O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(mkdir(p2, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_fd1, "newns3", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_tmpfs, "newns4", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+
+ /* And chown() through the clone must stay refused too, not just inode creation. */
+ ASSERT_ERROR_ERRNO(fchownat(AT_FDCWD, t, GID_INVALID, 0, 0), EPERM);
+
+ /* Neither must nesting a user namespace, which is not in the managed map itself */
+ ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER));
+
+ _cleanup_free_ char *p3 = NULL, *p4 = NULL;
+ ASSERT_NOT_NULL(p3 = path_join(t, "newuser"));
+ ASSERT_NOT_NULL(p4 = path_join(t, "newuser2"));
+
+ ASSERT_ERROR_ERRNO(open(p3, O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(mkdir(p4, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_fd1, "newuser3", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+ ASSERT_ERROR_ERRNO(openat(host_tmpfs, "newuser4", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
+
+ /* …while what was legitimately writable before stays writable */
safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "aaa", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "zzz2", 0666));
- ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "aaa2", 0666));
-
- /* But this one should still fail */
- ASSERT_ERROR_ERRNO(openat(host_fd1, "bbb", O_RDWR|O_CREAT|O_CLOEXEC, 0666), EPERM);
- ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "bbb2", 0666), EPERM);
-
- /* Sync again, to get more stuff allowlisted */
- ASSERT_OK_ERRNO(eventfd_write(afd, 1));
- ASSERT_OK_ERRNO(eventfd_read(bfd, &x));
-
- /* Everything should now be allowed */
- safe_close(ASSERT_OK_ERRNO(openat(host_tmpfs, "ccc", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- safe_close(ASSERT_OK_ERRNO(openat(private_tmpfs, "eee", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(host_tmpfs, "ccc2", 0666));
- safe_close(ASSERT_OK_ERRNO(openat(host_fd1, "ddd2", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
- ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "eee2", 0666));
+ safe_close(ASSERT_OK_ERRNO(openat(idmapped_tmpfs, "bbb", O_RDWR|O_CREAT|O_CLOEXEC, 0666)));
_exit(EXIT_SUCCESS);
}
- uint64_t x;
- ASSERT_OK_ERRNO(eventfd_read(afd, &x));
-
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- userns_fd,
- /* replace= */ false,
- &host_tmpfs,
- 1));
-
- ASSERT_OK_ERRNO(eventfd_write(bfd, 1));
- ASSERT_OK_ERRNO(eventfd_read(afd, &x));
-
- ASSERT_OK(userns_restrict_put_by_fd(
- bpf_obj,
- userns_fd,
- /* replace= */ false,
- &host_fd1,
- 1));
-
- ASSERT_OK_ERRNO(eventfd_write(bfd, 1));
-
ASSERT_OK(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG));
}
@@ -184,23 +202,13 @@ TEST(setgroups_deny) {
* 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_register_by_fd(bpf_obj, deny_userns_fd));
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 */
+ /* Create a userns that is managed 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));
+ ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, allow_userns_fd));
afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
@@ -247,7 +255,7 @@ TEST(setgroups_deny) {
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
+ /* Test 2: setgroups() should be allowed in the managed-only userns (managed map but no setgroups
* deny entry), including in a child user namespace. */
{
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
@@ -258,11 +266,11 @@ TEST(setgroups_deny) {
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 */
+ /* setgroups() should succeed since this userns is only in the managed map */
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) */
+ * managed map entry (not the setgroups deny entry) */
ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER));
ASSERT_OK_ERRNO(eventfd_write(afd, 1));
uint64_t x;
diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c
index 99f5223671..b48abde371 100644
--- a/src/nsresourced/userns-restrict.c
+++ b/src/nsresourced/userns-restrict.c
@@ -8,21 +8,27 @@
#include "bpf-util.h"
#include "bpf-link.h"
-#include "fd-util.h"
#include "log.h"
#include "lsm-util.h"
#include "mkdir.h"
-#include "mountpoint-util.h"
#include "namespace-util.h"
#include "path-util.h"
+#include "rm-rf.h"
#include "string-util.h"
+#include "strv.h"
#include "userns-restrict.h"
#define USERNS_MAX (16U*1024U)
-#define MOUNTS_MAX 4096U
-#define PROGRAM_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/programs"
-#define MAP_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/maps"
+/* No dots in any of the path components: bpffs reserves those for its own use and fails every lookup that
+ * contains one with EPERM, see bpf_lookup() in the kernel sources. */
+#define LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict-v2"
+#define PROGRAM_LINK_PREFIX LINK_PREFIX "/programs"
+#define MAP_LINK_PREFIX LINK_PREFIX "/maps"
+
+/* Programs pinned here enforce the mount allowlist policy we used until v261, which is bypassable with a
+ * single unshare(). They'd stay attached next to ours, hence get rid of them. */
+#define OBSOLETE_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict"
struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) {
#if HAVE_VMLINUX_H
@@ -32,31 +38,12 @@ struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf
}
-#if HAVE_VMLINUX_H
-static int make_inner_hash_map(void) {
- int fd;
-
- fd = compat_bpf_map_create(
- BPF_MAP_TYPE_HASH,
- NULL,
- sizeof(int),
- sizeof(uint32_t),
- MOUNTS_MAX,
- NULL);
- if (fd < 0)
- return log_debug_errno(errno, "Failed to allocate inner BPF map: %m");
-
- return fd;
-}
-#endif
-
int userns_restrict_install(
bool pin,
struct userns_restrict_bpf **ret) {
#if HAVE_VMLINUX_H
_cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL;
- _cleanup_close_ int dummy_mnt_id_hash_fd = -EBADF;
int r;
r = lsm_supported("bpf");
@@ -84,9 +71,15 @@ int userns_restrict_install(
if (pin) {
struct bpf_map *map;
- /* libbpf will only create one level of dirs. Let's create the rest */
- (void) mkdir_p(MAP_LINK_PREFIX, 0755);
- (void) mkdir_p(PROGRAM_LINK_PREFIX, 0755);
+ (void) rm_rf(OBSOLETE_LINK_PREFIX, REMOVE_PHYSICAL);
+
+ /* libbpf will only create one level of dirs. Let's create the rest. Failing here means the
+ * pinning below fails too, so complain rather than run into that without a clue. */
+ FOREACH_STRING(d, MAP_LINK_PREFIX, PROGRAM_LINK_PREFIX) {
+ r = mkdir_p(d, 0755);
+ if (r < 0)
+ log_warning_errno(r, "Failed to create '%s', ignoring: %m", d);
+ }
map = sym_bpf_object__next_map(obj->obj, NULL);
while (map) {
@@ -104,9 +97,9 @@ int userns_restrict_install(
}
}
- r = sym_bpf_map__set_max_entries(obj->maps.userns_mnt_id_hash, USERNS_MAX);
+ r = sym_bpf_map__set_max_entries(obj->maps.userns_managed, USERNS_MAX);
if (r < 0)
- return log_error_errno(r, "Failed to size userns/mnt_id hash table: %m");
+ return log_error_errno(r, "Failed to size managed userns hash table: %m");
r = sym_bpf_map__set_max_entries(obj->maps.userns_ringbuf, USERNS_MAX * sizeof(unsigned));
if (r < 0)
@@ -116,15 +109,6 @@ int userns_restrict_install(
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)
- return dummy_mnt_id_hash_fd;
-
- r = sym_bpf_map__set_inner_map_fd(obj->maps.userns_mnt_id_hash, dummy_mnt_id_hash_fd);
- if (r < 0)
- return log_error_errno(r, "Failed to set inner BPF map: %m");
-
r = userns_restrict_bpf__load(obj);
if (r < 0)
return log_error_errno(r, "Failed to load BPF object: %m");
@@ -191,95 +175,35 @@ int userns_restrict_install(
#endif
}
-int userns_restrict_put_by_inode(
+int userns_restrict_register_by_inode(
struct userns_restrict_bpf *obj,
- uint64_t userns_inode,
- bool replace,
- const int mount_fds[],
- size_t n_mount_fds) {
+ uint64_t userns_inode) {
#if HAVE_VMLINUX_H
- _cleanup_close_ int inner_map_fd = -EBADF;
- _cleanup_free_ int *mnt_ids = NULL;
- uint64_t ino = userns_inode;
- int r, outer_map_fd;
+ uint32_t dummy_value = 1;
+ int map_fd, r;
+ unsigned ino;
assert(obj);
assert(userns_inode != 0);
- assert(n_mount_fds == 0 || mount_fds);
- /* The BPF map type BPF_MAP_TYPE_HASH_OF_MAPS 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, we can't support this with the current BPF code for now. */
+ /* 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, we can't
+ * support this with the current BPF code for now. */
if (userns_inode > UINT32_MAX)
return -EINVAL;
- mnt_ids = new(int, n_mount_fds);
- if (!mnt_ids)
- return -ENOMEM;
+ ino = (unsigned) userns_inode;
- for (size_t i = 0; i < n_mount_fds; i++) {
- r = path_get_mnt_id_at(mount_fds[i], "", mnt_ids + i);
- if (r < 0)
- return log_debug_errno(r, "Failed to get mount ID: %m");
- }
+ map_fd = sym_bpf_map__fd(obj->maps.userns_managed);
+ if (map_fd < 0)
+ return log_debug_errno(map_fd, "Failed to get managed userns BPF map fd: %m");
- outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash);
- if (outer_map_fd < 0)
- return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m");
+ r = sym_bpf_map_update_elem(map_fd, &ino, &dummy_value, BPF_ANY);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add userns inode to managed map: %m");
- if (replace) {
- /* Add if missing, replace if already exists */
- inner_map_fd = make_inner_hash_map();
- if (inner_map_fd < 0)
- return inner_map_fd;
-
- r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_ANY);
- if (r < 0)
- return log_debug_errno(r, "Failed to replace map in inode hash: %m");
- } else {
- /* Let's add an entry for this userns inode if missing. If it exists just extend the existing map. We
- * might race against each other, hence we try a couple of times */
- for (size_t n_try = 10;; n_try--) {
- uint32_t innermap_id;
-
- if (n_try == 0)
- return log_debug_errno(SYNTHETIC_ERRNO(EEXIST),
- "Still cannot create inode entry in BPF map after 10 tries.");
-
- r = sym_bpf_map_lookup_elem(outer_map_fd, &ino, &innermap_id);
- if (r >= 0) {
- inner_map_fd = sym_bpf_map_get_fd_by_id(innermap_id);
- if (inner_map_fd < 0)
- return log_debug_errno(inner_map_fd, "Failed to get file descriptor for inner map: %m");
-
- break;
- }
- if (errno != ENOENT)
- return log_debug_errno(errno, "Failed to look up inode hash entry: %m");
-
- /* No entry for this user namespace yet. Let's create one */
- inner_map_fd = make_inner_hash_map();
- if (inner_map_fd < 0)
- return inner_map_fd;
-
- r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_NOEXIST);
- if (r >= 0)
- break;
- if (errno != EEXIST)
- return log_debug_errno(errno, "Failed to add mount ID list to inode hash: %m");
- }
- }
-
- FOREACH_ARRAY(mntid, mnt_ids, n_mount_fds) {
- uint32_t dummy_value = 1;
-
- r = sym_bpf_map_update_elem(inner_map_fd, mntid, &dummy_value, BPF_ANY);
- if (r < 0)
- return log_debug_errno(r, "Failed to add mount ID to map: %m");
-
- log_debug("Allowing mount %i on userns inode %" PRIu64, *mntid, ino);
- }
+ log_debug("Restricting persistent writes for userns inode %" PRIu64, userns_inode);
return 0;
#else
@@ -287,12 +211,9 @@ int userns_restrict_put_by_inode(
#endif
}
-int userns_restrict_put_by_fd(
+int userns_restrict_register_by_fd(
struct userns_restrict_bpf *obj,
- int userns_fd,
- bool replace,
- const int mount_fds[],
- size_t n_mount_fds) {
+ int userns_fd) {
#if HAVE_VMLINUX_H
struct stat st;
@@ -300,7 +221,6 @@ int userns_restrict_put_by_fd(
assert(obj);
assert(userns_fd >= 0);
- assert(n_mount_fds == 0 || mount_fds);
r = fd_is_namespace(userns_fd, NAMESPACE_USER);
if (r < 0)
@@ -311,12 +231,7 @@ int userns_restrict_put_by_fd(
if (fstat(userns_fd, &st) < 0)
return log_debug_errno(errno, "Failed to fstat() user namespace: %m");
- return userns_restrict_put_by_inode(
- obj,
- st.st_ino,
- replace,
- mount_fds,
- n_mount_fds);
+ return userns_restrict_register_by_inode(obj, st.st_ino);
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled.");
#endif
@@ -325,7 +240,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, setgroups_deny_fd;
+ int r, managed_fd, setgroups_deny_fd;
unsigned u;
assert(obj);
@@ -334,15 +249,17 @@ int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t use
if (userns_inode > UINT32_MAX) /* inodes larger than 32bit are definitely not included in our map, exit early */
return 0;
- outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash);
- if (outer_map_fd < 0)
- return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m");
+ managed_fd = sym_bpf_map__fd(obj->maps.userns_managed);
+ if (managed_fd < 0)
+ return log_debug_errno(managed_fd, "Failed to get managed userns BPF map fd: %m");
u = (uint32_t) userns_inode;
- r = sym_bpf_map_delete_elem(outer_map_fd, &u);
- if (r < 0)
- return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from outer map: %m", userns_inode);
+ /* A missing entry is an ordinary state here (the maps come up empty after a schema change, and
+ * registration is idempotent), so don't let it cut the setgroups deny cleanup below short. */
+ r = sym_bpf_map_delete_elem(managed_fd, &u);
+ if (r < 0 && r != -ENOENT)
+ return log_debug_errno(r, "Failed to remove entry for inode %" PRIu64 " from managed map: %m", userns_inode);
setgroups_deny_fd = sym_bpf_map__fd(obj->maps.userns_setgroups_deny);
if (setgroups_deny_fd < 0)
diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h
index 87457bf368..94330ac6cd 100644
--- a/src/nsresourced/userns-restrict.h
+++ b/src/nsresourced/userns-restrict.h
@@ -8,8 +8,8 @@ struct userns_restrict_bpf;
int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret);
struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj);
-int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds);
-int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds);
+int userns_restrict_register_by_fd(struct userns_restrict_bpf *obj, int userns_fd);
+int userns_restrict_register_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode);
int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode);