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