From d381a0d9d5df99196b4ada524f02f85ec18033ac Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Sun, 12 Jul 2026 22:04:34 +0200 Subject: [PATCH] nsresourced: judge transient range writes by their real destination nsresourced hands out transient, recyclable UID/GID ranges to unprivileged user namespaces. So that a client cannot gain persistence by leaving inodes owned by its transient range on a file system that outlives the namespace -- where a later, unrelated client that recycles the range would inherit them -- a BPF-LSM policy vetoes inode creation and chown() that would end up owned by a transient ID. The policy this replaces keyed its decision on properties of the mount a write targeted. Those can be forged: a single unshare(CLONE_NEWNS) hands the namespace clones of every mount it can see, each backed by the very same superblock but no longer recognisable by the original check, and the mount namespace's owning user namespace is attacker-chosen as well. Decide instead from two questions about the object the write would actually create, both answered from fields the kernel stamps itself and that survive mount cloning and cannot be forged with unshare(): a) Does the file system die with the user namespace? A superblock records the user namespace it was mounted in (sb->s_user_ns); if that sits at or below the managed namespace, nothing it carries can outlive the transient range. b) Will the transient ID actually reach the disk? An idmapped mount (vfsmount->mnt_idmap) may translate it into something else first, in which case nothing recyclable is recorded. This mirrors the kernel's own from_vfsuid()/from_vfsgid(). Walking the user-namespace chain upwards, rather than looking only at the exact namespace, is what keeps a nested namespace from shaking off the policy. The map schema changed incompatibly (the old mount-id allowlist became the flat userns_managed inode set), so the BPF objects are pinned under a fresh directory, /sys/fs/bpf/systemd/userns-restrict-v2, and the obsolete unsuffixed tree from previous versions is removed on install. Removing the old link pins detaches the previous, bypassable programs, so they do not linger attached next to the new ones. --- man/systemd-nsresourced.service.xml | 10 +- src/bpf/userns-restrict.bpf.c | 356 +++++++++++++++++-------- src/nsresourced/nsresourcework.c | 35 +-- src/nsresourced/test-userns-restrict.c | 154 ++++++----- src/nsresourced/userns-restrict.c | 183 ++++--------- src/nsresourced/userns-restrict.h | 4 +- 6 files changed, 397 insertions(+), 345 deletions(-) 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);