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