diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml
index 3c662d04ed..04ec02d228 100644
--- a/man/systemd-nsresourced.service.xml
+++ b/man/systemd-nsresourced.service.xml
@@ -48,7 +48,11 @@
such as overlayfs writes to a separate upper layer that would not be covered by the
checks on the overlay mount itself, mounting a writable overlayfs 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.
+ is an id-mapped mount that translates the transient range away before it reaches the disk. The policy
+ does not cover cgroupfs, 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.
systemd-nsresourced automatically ensures that any registered UID ranges show up
in the system's NSS database via the User/Group Record
diff --git a/src/bpf/userns-restrict.bpf.c b/src/bpf/userns-restrict.bpf.c
index 35856a5330..1f266b38bb 100644
--- a/src/bpf/userns-restrict.bpf.c
+++ b/src/bpf/userns-restrict.bpf.c
@@ -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;
diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c
index 28b9251c01..cbb928de20 100644
--- a/src/nsresourced/test-userns-restrict.c
+++ b/src/nsresourced/test-userns-restrict.c
@@ -7,6 +7,8 @@
#include
#include
+#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