nsresourced: Exclude cgroupfs from superblock check

cgroupfs has a single shared global superblock but
programs running in managed user namespaces still
need to be able to write it to make cgroup delegation
work. d381a0d9d5 broke
this by insisting the user namespace owns the
superblock. Let's fix this by excluding cgroupfs from
this check.
This commit is contained in:
Daan De Meyer authored and Daan De Meyer committed 2026-09-29 16:20:41 +02:00
1 parent 5bffb0af1d
commit 889bc48f10
3 files changed
+80 -1

No files matched your search

+5 -1
View File
@@ -48,7 +48,11 @@
such as <literal>overlayfs</literal> writes to a separate upper layer that would not be covered by the
checks on the overlay mount itself, mounting a writable <literal>overlayfs</literal> instance is refused
for these user namespaces unless its upper layer, too, is torn down together with the user namespace, or
is an id-mapped mount that translates the transient range away before it reaches the disk.</para>
is an id-mapped mount that translates the transient range away before it reaches the disk. The policy
does not cover <literal>cgroupfs</literal>, whose superblock is shared system-wide and cannot be
id-mapped: the only part of it a transient range owns is the cgroup subtree delegated to the user
namespace (see below), which is torn down together with the user namespace, hence user namespaces may
freely create cgroups below it.</para>
<para><command>systemd-nsresourced</command> automatically ensures that any registered UID ranges show up
in the system's NSS database via the <ulink url="https://systemd.io/USER_GROUP_API">User/Group Record
+15
View File
@@ -67,6 +67,7 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym;
/* From include/uapi/linux/magic.h, which vmlinux.h does not provide. */
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
#define CGROUP2_SUPER_MAGIC 0x63677270
struct {
__uint(type, BPF_MAP_TYPE_HASH);
@@ -270,6 +271,14 @@ static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns,
return uid_gid_map_translate(gid ? &fs_userns->gid_map : &fs_userns->uid_map, id, /* up= */ false, ret);
}
/* cgroupfs is exempt from this policy: there is a single cgroupfs superblock, owned by the initial user
* namespace (every mount of it shares that one), and it cannot be idmapped. That's fine: DAC makes sure a
* transient range only ever owns the cgroup delegated to it via AddControlGroupToUserNamespace(), which we
* remove along with the client, best-effort (a cgroup that refuses to go away outlives its range). */
static bool mount_is_exempt(struct vfsmount *v) {
return v->mnt_sb->s_magic == CGROUP2_SUPER_MAGIC;
}
/* 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) {
@@ -299,6 +308,9 @@ static int validate_mount(struct vfsmount *v, int ret) {
if (r == 0) /* Not provisioned by nsresourced? Then this is none of our business. */
return 0;
if (mount_is_exempt(v))
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
@@ -351,6 +363,9 @@ int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long u
if (r == 0)
return 0;
if (mount_is_exempt(path->mnt))
return 0;
r = userns_is_below(managed, path->mnt->mnt_sb->s_user_ns);
if (r < 0)
return r;
+60
View File
@@ -7,6 +7,8 @@
#include <sys/stat.h>
#include <unistd.h>
#include "cgroup-setup.h"
#include "cgroup-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
@@ -346,6 +348,64 @@ TEST(userns_restrict_overlayfs) {
(void) umount(tb);
}
TEST(userns_restrict_cgroupfs) {
_cleanup_close_ int userns_fd = -EBADF, undelegated_fd = -EBADF, delegated_fd = -EBADF;
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
_cleanup_free_ char *cg = NULL, *undelegated = NULL, *delegated = NULL, *idmap = NULL;
int r;
if (cg_is_available() <= 0)
return (void) log_tests_skipped("cgroupfs is not mounted");
/* Shifted, as cg_create() and friends prefix what we return here with /sys/fs/cgroup, which in a
* container without its own cgroup namespace is not where the unshifted path is rooted. */
r = cg_pid_get_path_shifted(0, /* cached_root= */ NULL, &cg);
if (r < 0)
return (void) log_tests_skipped_errno(r, "Failed to determine our own cgroup");
ASSERT_NOT_NULL(undelegated = path_join(cg, "test-userns-restrict"));
ASSERT_NOT_NULL(delegated = path_join(undelegated, "delegated"));
/* Always start clean, in case an earlier run failed halfway through */
(void) cg_trim(undelegated, /* delete_root= */ true);
r = cg_create(undelegated);
if (ERRNO_IS_NEG_FS_WRITE_REFUSED(r) || r == -ENOENT)
return (void) log_tests_skipped_errno(r, "Failed to create cgroup %s", undelegated);
ASSERT_OK(r);
ASSERT_OK(cg_create(delegated));
undelegated_fd = ASSERT_OK(cg_path_open(undelegated));
delegated_fd = ASSERT_OK(cg_path_open(delegated));
/* Hand the inner cgroup to the transient range, the way AddControlGroupToUserNamespace() does. */
ASSERT_OK_ERRNO(fchown(delegated_fd, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
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_register_by_fd(bpf_obj, userns_fd));
r = ASSERT_OK(pidref_safe_fork("(test-cgroup)", FORK_DEATHSIG_SIGKILL, &pidref));
if (r == 0) {
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
/* The delegated cgroup is owned by our transient range, so this must work. */
ASSERT_OK_ERRNO(mkdirat(delegated_fd, "payload", 0755));
/* chown() is subject to the same policy, and hence exempt here as well. */
ASSERT_OK_ERRNO(fchownat(delegated_fd, "payload", UID_INVALID, 0, 0));
/* Its parent is not ours, so we should get EACCES from DAC. */
ASSERT_ERROR_ERRNO(mkdirat(undelegated_fd, "payload", 0755), EACCES);
_exit(EXIT_SUCCESS);
}
ASSERT_OK(pidref_wait_for_terminate_and_check("(test-cgroup)", &pidref, WAIT_LOG));
ASSERT_OK(cg_trim(undelegated, /* delete_root= */ true));
}
static void write_child_mappings(PidRef *child, int parent_userns_fd) {
/* The kernel requires uid_map/gid_map to be written from the parent user namespace of the
* target namespace. Fork a helper that joins the parent userns and writes the mappings from