From 7eb016d60b7c6ee19c3420f27428ea4758a34f87 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 28 Jul 2026 11:36:07 +0200 Subject: [PATCH] nsresourced: restore the BPF-LSM policy from the registry on startup The managed-namespace state lives only in the pinned BPF maps, which survive a plain restart. But an upgrade from a version whose map schema differs replaces those maps (they are pinned under a new directory), so they come up empty, and nothing repopulated them: every user namespace that was already running would silently lose its write restriction (and, for "self" allocations, its setgroups() denial) until it exited. Re-add every live registry entry to the BPF maps during manager_startup(), in the same pass that reaps the dead ones. userns_managed is reconstructed for all entries; the setgroups() denial is reconstructed from a flag now persisted in the registry, since it applies only to "self" allocations, which the map alone cannot tell apart. The update is idempotent when the pinned maps already carry the entries (the common restart case), and load-bearing after an upgrade that replaced them with empty ones. Ordering matters here: the programs are loaded but left unattached until the maps have been repopulated, and only then does the previous version's pin directory get removed, which is what detaches its programs. So the old policy keeps enforcing right up to the moment the new one takes over, and the new programs never run against a managed set that is still empty. Doing it the other way around would leave every pre-upgrade namespace failing open for the duration of the registry sweep, which is long enough for a client looping over create() to land a transient owned inode on a persistent file system. Entries written before that flag existed are exactly the ones the first such upgrade finds, so they get no answer from it. Recognize those by what the flag mirrors: a "self" allocation maps the UID of the caller rather than a transient range, i.e. it is the single UID entry whose start UID is its owner. This does not help a downgrade to a version without this change: such a version cannot clean up the newer pin directory it knows nothing about, so those programs stay attached until the next reboot clears the tmpfs-backed bpffs, with enforcement reverting to the older policy in the meantime. --- src/nsresourced/nsresourced-manager.c | 129 +++++++++++++++++++++---- src/nsresourced/nsresourcework.c | 9 +- src/nsresourced/test-userns-restrict.c | 2 + src/nsresourced/userns-registry.c | 53 ++++++---- src/nsresourced/userns-registry.h | 10 +- src/nsresourced/userns-restrict.c | 39 ++++++-- src/nsresourced/userns-restrict.h | 7 ++ 7 files changed, 196 insertions(+), 53 deletions(-) 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);