diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c index df12e352b0..41436e5552 100644 --- a/src/nsresourced/nsresourced-manager.c +++ b/src/nsresourced/nsresourced-manager.c @@ -372,6 +372,52 @@ static void manager_release_userns_by_inode(Manager *m, uint64_t inode) { userns_registry_release_by_userns_inode(manager_bpf(m), m->registry_fd, inode); } +/* Re-adds a registered, still-alive user namespace to the BPF maps. This is idempotent when the pinned + * maps already carry the entry (the common restart case), and load-bearing after an nsresourced upgrade + * that had to replace the pinned maps because their schema changed: the new maps come up empty, so + * without this the namespaces still alive from before the upgrade would silently lose their write + * restriction (and, for "self" allocations, their setgroups() denial). */ +/* Borrows the registry entry if the caller already has it at hand, and loads its own copy otherwise. */ +static void manager_restore_userns_policy(Manager *m, uint64_t inode, UserNamespaceInfo *info) { + _cleanup_(userns_info_freep) UserNamespaceInfo *loaded = NULL; + struct userns_restrict_bpf *bpf; + int r; + + assert(m); + assert(inode != 0); + + bpf = manager_bpf(m); + if (!bpf) /* BPF disabled at build time or its setup failed — nothing to restore. */ + return; + + if (!info) { + r = userns_registry_load_by_userns_inode(m->registry_fd, inode, &loaded); + if (r < 0) + return (void) log_debug_errno(r, "Failed to load registry entry for user namespace %" PRIu64 " while restoring BPF policy, ignoring: %m", inode); + + info = loaded; + } + + r = userns_restrict_register_by_inode(bpf, inode); + if (r < 0) + return (void) log_debug_errno(r, "Failed to restore write restriction for user namespace %" PRIu64 ", ignoring: %m", inode); + + /* Entries written before we recorded this carry no answer, and those are precisely the entries an + * upgrade finds. Fall back to what the flag mirrors: a "self" allocation maps the caller's own UID + * instead of a transient range, so it is the one whose start UID is its owner and outside the + * ranges we hand out. Without that last condition a managed size-1 allocation that happened to be + * given its own caller's UID would be mistaken for one, and we would deny setgroups() on a + * namespace that never had it denied. */ + bool deny = info->setgroups_deny >= 0 ? info->setgroups_deny : + (info->size == 1 && info->start_uid == info->owner && !uid_is_transient(info->start_uid)); + + if (deny) { + r = userns_restrict_setgroups_deny_by_inode(bpf, inode); + if (r < 0) + log_debug_errno(r, "Failed to restore setgroups() denial for user namespace %" PRIu64 ", ignoring: %m", inode); + } +} + static int manager_scan_registry(Manager *m, Set **registry_inodes) { _cleanup_free_ DirectoryEntries *de = NULL; int r; @@ -602,11 +648,32 @@ static int manager_setup_bpf(Manager *m) { return 0; } + +/* Puts the programs loaded by manager_setup_bpf() in effect. Deliberately separate, so that the maps are + * already seeded from the registry by the time the first hook runs. */ +static int manager_attach_bpf(Manager *m) { + int r; + + assert(m); + + if (!m->userns_restrict_bpf) /* Setup failed earlier, and we already told the user. */ + return 0; + + r = userns_restrict_attach(m->userns_restrict_bpf, /* pin= */ true); + if (r < 0) + return log_error_errno(r, "Failed to attach BPF programs: %m"); + + return 0; +} #else static int manager_setup_bpf(Manager *m) { log_notice("Not setting up BPF subsystem, as functionality has been disabled at compile time."); return 0; } + +static int manager_attach_bpf(Manager *m) { + return 0; +} #endif int manager_startup(Manager *m) { @@ -647,33 +714,55 @@ int manager_startup(Manager *m) { manager_release_userns_by_inode(m, inode); } - /* Look for registry entries whose user namespace has died without us getting a BPF - * notification — e.g. because the BPF ring buffer overflowed, the kprobe is missing, or - * something else dropped the fd store entry without going through our cleanup path. Each - * registry entry stores the kernel's unique namespace identifier; ask the kernel to open - * the namespace by that identifier and release the entry if the lookup fails. Entries - * written by older versions don't carry the identifier, and old kernels (or running - * outside the initial user namespace) don't support lookup by it — in those cases we leave - * the entry alone. */ + /* Walk the registry entries for two purposes: reap the dead ones, and re-establish the + * BPF-LSM policy for the live ones. + * + * A namespace may have died without us getting a BPF notification — e.g. because the BPF ring + * buffer overflowed, the kprobe is missing, or something else dropped the fd store entry without + * going through our cleanup path. Each registry entry stores the kernel's unique namespace + * identifier; ask the kernel to open the namespace by that identifier and release the entry if + * the lookup fails. Entries written by older versions don't carry the identifier, and old kernels + * (or running outside the initial user namespace) don't support lookup by it — in those cases we + * leave the entry alone. + * + * Everything that isn't reaped is (re-)added to the BPF maps. Normally the pinned maps already + * carry these entries and the update is a no-op, but after an upgrade that replaced the pinned + * maps with an incompatible schema the new maps start empty, and this is what keeps namespaces + * that predate the upgrade protected. */ + bool can_probe = true; SET_FOREACH(p, registry_inodes) { + _cleanup_(userns_info_freep) UserNamespaceInfo *info = NULL; uint64_t inode = PTR_TO_UINT32(p); - r = userns_registry_reap_if_dead(manager_bpf(m), m->registry_fd, inode); - if (r < 0) { - log_debug_errno(r, "Failed to probe liveness of user namespace %" PRIu64 ", ignoring: %m", inode); - continue; + if (can_probe) { + r = userns_registry_reap_if_dead(manager_bpf(m), m->registry_fd, inode, &info); + if (r < 0) + log_debug_errno(r, "Failed to probe liveness of user namespace %" PRIu64 ", assuming alive: %m", inode); + else if (r == USERNS_REAP_RELEASED) + continue; /* Dead and released — nothing to restore. */ + else if (r == USERNS_REAP_UNSUPPORTED) { + /* Can't look namespaces up by id at all here (old kernel, or not in the + * initial user namespace). Stop probing (and logging) once per entry, but + * still restore the policy for this and the remaining entries below. */ + log_debug("Cannot detect stale registry entries, restoring the rest unconditionally."); + can_probe = false; + } + /* USERNS_REAP_ALIVE or _INDETERMINATE fall through to the restore below. */ } - if (r == USERNS_REAP_UNSUPPORTED) { - /* Can't look namespaces up by id at all here (old kernel, or not in the - * initial user namespace) — no entry is probeable, so stop rather than - * continuing to probe (and log) once per entry. */ - log_debug("Cannot detect stale registry entries, skipping the rest."); - break; - } - /* USERNS_REAP_RELEASED, _ALIVE, or _INDETERMINATE — nothing more to do for this entry. */ + + /* Re-establish the BPF-LSM policy for the still-live namespace. Idempotent when the pinned + * maps already carry it, essential after an upgrade replaced them with empty ones. */ + manager_restore_userns_policy(m, inode, info); } + /* The maps now describe every namespace we are responsible for, so the policy can go into + * effect. Until this point the previous version's programs, if any, were still attached and + * enforcing, so no namespace was ever left unguarded. */ + r = manager_attach_bpf(m); + if (r < 0) + return r; + r = manager_make_listen_socket(m); if (r < 0) return r; diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c index ffe676d7ac..6138f9cd0f 100644 --- a/src/nsresourced/nsresourcework.c +++ b/src/nsresourced/nsresourcework.c @@ -405,7 +405,7 @@ static int registry_range_is_available( if (r < 0) return r; - r = userns_registry_reap_if_dead(bpf, registry_dir_fd, owner->userns_inode); + r = userns_registry_reap_if_dead(bpf, registry_dir_fd, owner->userns_inode, /* ret_info= */ NULL); if (r < 0) return r; if (r == USERNS_REAP_RELEASED) @@ -465,7 +465,7 @@ static int delegation_range_is_available( /* Owned by some other namespace. If that namespace is dead, reclaim it (restoring the * range to its ancestor) and loop to re-evaluate; otherwise the range is genuinely * taken. */ - r = userns_registry_reap_if_dead(bpf, registry_dir_fd, delegation.userns_inode); + r = userns_registry_reap_if_dead(bpf, registry_dir_fd, delegation.userns_inode, /* ret_info= */ NULL); if (r < 0) return r; if (r == USERNS_REAP_RELEASED) @@ -1457,6 +1457,9 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para userns_info->size = p.size; userns_info->target_uid = p.target; userns_info->target_gid = (gid_t) p.target; + /* For "self" allocations we deny setgroups() via the BPF-LSM (see below). Record it so the manager + * can re-establish the denial after a restart that reset the BPF maps. */ + userns_info->setgroups_deny = p.type == ALLOCATE_USER_RANGE_SELF; if (p.type == ALLOCATE_USER_RANGE_SELF) { /* The start UID/GID will be mapped to the parent userns in write_userns(). If a self @@ -1520,7 +1523,7 @@ static int vl_method_allocate_user_range(sd_varlink *link, sd_json_variant *para if (r < 0) goto fail; - if (p.type == ALLOCATE_USER_RANGE_SELF) { + if (userns_info->setgroups_deny > 0) { /* For "self" allocations we deny setgroups() via the BPF LSM. We can't use * /proc/self/setgroups for this as that is transitive and also applies to child user * namespaces. The BPF LSM hook only applies to the specific user namespace. */ diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c index 1bddeb70c4..69e70eabaf 100644 --- a/src/nsresourced/test-userns-restrict.c +++ b/src/nsresourced/test-userns-restrict.c @@ -62,6 +62,8 @@ static int intro(void) { return log_tests_skipped("Lacking privileges"); ASSERT_OK(r); + ASSERT_OK(userns_restrict_attach(bpf_obj, /* pin= */ false)); + return 0; } diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c index 0da6a6a7c6..26d8c28e57 100644 --- a/src/nsresourced/userns-registry.c +++ b/src/nsresourced/userns-registry.c @@ -83,6 +83,7 @@ UserNamespaceInfo* userns_info_new(void) { .target_uid = UID_INVALID, .start_gid = GID_INVALID, .target_gid = GID_INVALID, + .setgroups_deny = -1, }; return info; @@ -241,18 +242,19 @@ static int dispatch_ancestor_userns_array(const char *name, sd_json_variant *var static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) { static const sd_json_dispatch_field dispatch_table[] = { - { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY }, - { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY }, - { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY }, - { "usernsId", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_id), 0 }, - { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 }, - { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_uid), 0 }, - { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_uid), 0 }, - { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_gid), 0 }, - { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 }, - { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 }, - { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 }, - { "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 }, + { "owner", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), SD_JSON_MANDATORY }, + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserNamespaceInfo, name), SD_JSON_MANDATORY }, + { "userns", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), SD_JSON_MANDATORY }, + { "usernsId", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_id), 0 }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 }, + { "start", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_uid), 0 }, + { "target", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_uid), 0 }, + { "startGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start_gid), 0 }, + { "targetGid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target_gid), 0 }, + { "setgroupsDeny", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(UserNamespaceInfo, setgroups_deny), 0 }, + { "cgroups", SD_JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 }, + { "netifs", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(UserNamespaceInfo, netifs), 0 }, + { "delegates", SD_JSON_VARIANT_ARRAY, dispatch_delegates_array, 0, 0 }, {} }; @@ -539,13 +541,16 @@ void userns_registry_release_by_userns_inode(struct userns_restrict_bpf *bpf, in release_userns_inode_resources(bpf, inode); } -int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode) { +int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode, UserNamespaceInfo **ret_info) { _cleanup_(userns_info_freep) UserNamespaceInfo *info = NULL; int r; assert(dir_fd >= 0); assert(inode != 0); + if (ret_info) + *ret_info = NULL; + /* This is the shared engine behind both the runtime allocation hot path and the startup * registry sweep: it reclaims ranges blocked by a dead namespace that no BPF death event * cleaned up, without waiting for the manager to restart. */ @@ -558,19 +563,30 @@ int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, ui if (r < 0) return r; - if (info->userns_id == 0) - return USERNS_REAP_INDETERMINATE; /* Entry predates ns id tracking, can't probe authoritatively. */ + if (info->userns_id == 0) { + /* Entry predates ns id tracking, can't probe authoritatively. */ + if (ret_info) + *ret_info = TAKE_PTR(info); + return USERNS_REAP_INDETERMINATE; + } _cleanup_close_ int probe_fd = namespace_open_by_id(info->userns_id); - if (probe_fd >= 0) - return USERNS_REAP_ALIVE; /* Still alive (or the inode was recycled for a live namespace). */ - if (ERRNO_IS_NEG_PRIVILEGE(probe_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(probe_fd) || probe_fd == -EINVAL) + if (probe_fd >= 0) { + /* Still alive (or the inode was recycled for a live namespace). */ + if (ret_info) + *ret_info = TAKE_PTR(info); + return USERNS_REAP_ALIVE; + } + if (ERRNO_IS_NEG_PRIVILEGE(probe_fd) || ERRNO_IS_NEG_NOT_SUPPORTED(probe_fd) || probe_fd == -EINVAL) { /* EPERM/EACCES (not in the initial user namespace, missing CAP_SYS_ADMIN), or * ENOTSUP/ENOSYS/EINVAL (kernel new enough for NS_GET_ID but too old for * open_by_handle_at() lookup by id on nsfs) — we can't confirm death for this or any * other entry. The userns_id == 0 case is already handled above, so -EINVAL here means * the lookup is unsupported rather than a bad id. */ + if (ret_info) + *ret_info = TAKE_PTR(info); return USERNS_REAP_UNSUPPORTED; + } if (probe_fd != -ESTALE) return probe_fd; /* Unexpected probe error. */ @@ -693,6 +709,7 @@ int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { SD_JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->target_uid), "target", SD_JSON_BUILD_UNSIGNED(info->target_uid)), SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->start_gid), "startGid", SD_JSON_BUILD_UNSIGNED(info->start_gid)), SD_JSON_BUILD_PAIR_CONDITION(gid_is_valid(info->target_gid), "targetGid", SD_JSON_BUILD_UNSIGNED(info->target_gid)), + SD_JSON_BUILD_PAIR_CONDITION(info->setgroups_deny >= 0, "setgroupsDeny", SD_JSON_BUILD_BOOLEAN(info->setgroups_deny > 0)), SD_JSON_BUILD_PAIR_CONDITION(!!cgroup_array, "cgroups", SD_JSON_BUILD_VARIANT(cgroup_array)), JSON_BUILD_PAIR_STRV_NON_EMPTY("netifs", info->netifs), SD_JSON_BUILD_PAIR_CONDITION(!!delegates_array, "delegates", SD_JSON_BUILD_VARIANT(delegates_array))); diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h index 4c88b45cfd..802307d7eb 100644 --- a/src/nsresourced/userns-registry.h +++ b/src/nsresourced/userns-registry.h @@ -37,6 +37,10 @@ typedef struct UserNamespaceInfo { uid_t target_uid; gid_t start_gid; gid_t target_gid; + int setgroups_deny; /* Whether setgroups() is denied for this namespace via the BPF-LSM, so the + * denial can be re-established after a restart that reset the BPF maps. + * Negative in entries written before we recorded this, and stays that way + * when such an entry is updated, so the answer is never faked. */ uint64_t *cgroups; size_t n_cgroups; char **netifs; @@ -93,8 +97,10 @@ typedef enum UserNamespaceReapStatus { * namespace id and, if it is authoritatively dead, releases its registry entry (restoring any ranges * it received via delegation to their ancestors). Returns a non-negative UserNamespaceReapStatus * describing what happened, or a negative errno on genuine failure. The caller must hold the registry - * lock (or otherwise be free of concurrent writers). */ -int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode); + * lock (or otherwise be free of concurrent writers). If ret_info is passed it receives the entry this + * had to load anyway, so that callers that go on to work on the entry need not parse it a second time. + * It is set to NULL whenever no entry survives the call. */ +int userns_registry_reap_if_dead(struct userns_restrict_bpf *bpf, int dir_fd, uint64_t inode, UserNamespaceInfo **ret_info); int userns_registry_store(int dir_fd, UserNamespaceInfo *info); int userns_registry_remove(int dir_fd, UserNamespaceInfo *info); diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c index b48abde371..0f6f021e1d 100644 --- a/src/nsresourced/userns-restrict.c +++ b/src/nsresourced/userns-restrict.c @@ -71,8 +71,6 @@ int userns_restrict_install( if (pin) { struct bpf_map *map; - (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) { @@ -113,6 +111,30 @@ int userns_restrict_install( if (r < 0) return log_error_errno(r, "Failed to load BPF object: %m"); + /* Pin the maps already here: the programs are not attached yet, so nothing enforces anything, but + * the caller may now seed the maps before userns_restrict_attach() puts the policy in effect. */ + if (pin) { + r = sym_bpf_object__pin_maps(obj->obj, NULL); + if (r < 0) + return log_error_errno(r, "Failed to pin BPF maps: %m"); + } + + if (ret) + *ret = TAKE_PTR(obj); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +#endif +} + +int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin) { + +#if HAVE_VMLINUX_H + int r; + + assert(obj); + for (int i = 0; i < obj->skeleton->prog_cnt; i++) { _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; struct bpf_prog_skeleton *ps = obj->skeleton->progs + i; @@ -160,14 +182,11 @@ int userns_restrict_install( *ps->link = TAKE_PTR(link); } - if (pin) { - r = sym_bpf_object__pin_maps(obj->obj, NULL); - if (r < 0) - return log_error_errno(r, "Failed to pin BPF maps: %m"); - } - - if (ret) - *ret = TAKE_PTR(obj); + /* Only now that our own programs enforce the policy, drop the ones the previous version left + * pinned. Removing their links detaches them, so doing this any earlier would hand the namespaces + * that predate an upgrade a stretch with no policy attached at all. */ + if (pin) + (void) rm_rf(OBSOLETE_LINK_PREFIX, REMOVE_PHYSICAL); return 0; #else diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h index 94330ac6cd..a229bd64bf 100644 --- a/src/nsresourced/userns-restrict.h +++ b/src/nsresourced/userns-restrict.h @@ -5,7 +5,14 @@ struct userns_restrict_bpf; +/* Bringing the policy up is split in two so that callers can seed the maps in between: the programs only + * start enforcing once userns_restrict_attach() ran, and whatever they find in the maps at that point is + * what they enforce from their very first invocation. Attaching first and filling the maps afterwards would + * leave a window in which every namespace looks unmanaged and the policy fails open. Callers that only + * manipulate the maps of an already running policy (i.e. the workers) stop after the install step. */ int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret); +int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin); + struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj); int userns_restrict_register_by_fd(struct userns_restrict_bpf *obj, int userns_fd);