From 1ced75f76eb5267fee6adeabddec9a87d9cc1ebe Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Wed, 4 Nov 2026 10:37:59 +0100 Subject: [PATCH] nsresourced: restrict Btrfs subvolume creation ioctls Btrfs subvolume creation bypasses the path and inode creation hooks. A managed user namespace can therefore leave a subvolume root owned by its transient range on a file system that outlives the namespace. Let's apply the existing creation restrictions to the native and compat ioctl calls for subvolume creation. The tests check persistent and idmapped mounts, nested user namespaces, unrelated ioctls and other file systems. The compat ioctl hook is absent on older kernels. Let's preserve the native policy when that hook isn't available. (cherry picked from commit bdd20e5ff6fef7c1bd32b2b1cda39aae2154c338) (cherry picked from commit ebe9b22a272594bb36e7eb014e8b30e7182201cf) --- src/bpf/userns-restrict.bpf.c | 27 +++++++++ src/nsresourced/test-userns-restrict.c | 81 ++++++++++++++++++++++++++ src/nsresourced/userns-restrict.c | 9 +++ src/shared/bpf-util.c | 2 + src/shared/bpf-util.h | 1 + 5 files changed, 120 insertions(+) diff --git a/src/bpf/userns-restrict.bpf.c b/src/bpf/userns-restrict.bpf.c index 81b57ba58bf..763b9b7d089 100644 --- a/src/bpf/userns-restrict.bpf.c +++ b/src/bpf/userns-restrict.bpf.c @@ -15,6 +15,7 @@ #include "vmlinux.h" +#include #include /* IWYU pragma: keep */ #include #include @@ -66,6 +67,7 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym; #define UID_GID_INVALID ((uid_t) -1) /* From include/uapi/linux/magic.h, which vmlinux.h does not provide. */ +#define BTRFS_SUPER_MAGIC 0x9123683E #define OVERLAYFS_SUPER_MAGIC 0x794c7630 #define CGROUP2_SUPER_MAGIC 0x63677270 @@ -75,6 +77,10 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym; /* From include/uapi/asm-generic/fcntl.h. Only archs without BPF trampolines (no LSM programs) deviate. */ #define __O_TMPFILE (1U << 22) /* O_TMPFILE without O_DIRECTORY */ +/* The Btrfs UAPI header conflicts with vmlinux.h, so define the subvolume creation commands here. */ +#define BTRFS_IOC_SUBVOL_CREATE _IOC(_IOC_WRITE, 0x94, 14, 4096) +#define BTRFS_IOC_SUBVOL_CREATE_V2 _IOC(_IOC_WRITE, 0x94, 24, 4096) + struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ @@ -429,6 +435,27 @@ int BPF_PROG(userns_restrict_path_rename, const struct path *old_dir, struct den return validate_mount(old_dir->mnt, ret); } +/* Btrfs creates subvolume root inodes without calling the path or inode creation hooks. */ +static int validate_ioctl(struct file *file, unsigned cmd, int ret) { + if (cmd != BTRFS_IOC_SUBVOL_CREATE && cmd != BTRFS_IOC_SUBVOL_CREATE_V2) + return ret; + + if (file->f_path.mnt->mnt_sb->s_magic != BTRFS_SUPER_MAGIC) + return ret; + + return validate_mount(file->f_path.mnt, ret); +} + +SEC("lsm/file_ioctl") +int BPF_PROG(userns_restrict_file_ioctl, struct file *file, unsigned cmd, unsigned long arg, int ret) { + return validate_ioctl(file, cmd, ret); +} + +SEC("lsm/file_ioctl_compat") +int BPF_PROG(userns_restrict_file_ioctl_compat, struct file *file, unsigned cmd, unsigned long arg, int ret) { + return validate_ioctl(file, cmd, ret); +} + /* overlayfs' struct ovl_fs and struct ovl_layer are private to the module and hence absent from vmlinux.h * whenever overlayfs is built as a module. We declare the two fields we need as CO-RE "type flavors": libbpf * strips the ___local suffix and relocates these against the kernel's real ovl_fs/ovl_layer. This both diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c index ed39ce3e7e3..44c0bc20326 100644 --- a/src/nsresourced/test-userns-restrict.c +++ b/src/nsresourced/test-userns-restrict.c @@ -1,13 +1,17 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include +#include +#include #include #include #include +#include #include #include #include +#include "btrfs-util.h" #include "cgroup-setup.h" #include "cgroup-util.h" #include "errno-util.h" @@ -19,7 +23,9 @@ #include "pidref.h" #include "process-util.h" #include "rm-rf.h" +#include "stat-util.h" #include "string-util.h" +#include "strv.h" #include "tests.h" #include "tmpfile-util.h" #include "uid-classification.h" @@ -213,6 +219,81 @@ TEST(userns_restrict) { ASSERT_OK(pidref_wait_for_terminate_and_check("(test)", &pidref, WAIT_LOG)); } +TEST(userns_restrict_btrfs) { + _cleanup_(rm_rf_subvolume_and_freep) char *t = NULL; + _cleanup_close_ int dir_fd = -EBADF, host_fd = -EBADF, mapped_fd = -EBADF, + mount_fd = -EBADF, idmapped_fd = -EBADF, userns_fd = -EBADF, + tmpfs_mount = -EBADF, tmpfs_fd = -EBADF; + _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL; + _cleanup_free_ char *idmap = NULL; + struct stat st; + int r; + + ASSERT_OK(mkdtemp_malloc("/var/tmp/test-userns-restrict-btrfs.XXXXXX", &t)); + dir_fd = ASSERT_OK(xopenat(AT_FDCWD, t, O_DIRECTORY)); + if (ASSERT_OK(fd_is_fs_type(dir_fd, BTRFS_SUPER_MAGIC)) == 0) + return (void) log_tests_skipped("Temporary directory is not on Btrfs"); + + host_fd = ASSERT_OK(xopenat_full(dir_fd, "host", O_DIRECTORY|O_CREAT, /* xopen_flags= */ 0, 0700)); + mapped_fd = ASSERT_OK(xopenat_full(dir_fd, "mapped", O_DIRECTORY|O_CREAT, /* xopen_flags= */ 0, 0700)); + ASSERT_OK_ERRNO(fchown(host_fd, CONTAINER_UID_MIN, CONTAINER_UID_MIN)); + + ASSERT_OK(btrfs_subvol_make(host_fd, "probe")); + ASSERT_OK(btrfs_subvol_remove_at(host_fd, "probe", /* flags= */ 0)); + + ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN)); + userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true)); + + mount_fd = ASSERT_OK_ERRNO(open_tree(mapped_fd, "", OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_EMPTY_PATH)); + ASSERT_OK_ERRNO(mount_setattr(mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = MOUNT_ATTR_IDMAP, + .userns_fd = userns_fd, + }, sizeof(struct mount_attr))); + idmapped_fd = ASSERT_OK(xopenat(mount_fd, ".", O_DIRECTORY)); + + tmpfs_mount = ASSERT_OK(make_tmpfs_fsmount()); + tmpfs_fd = ASSERT_OK(xopenat(tmpfs_mount, ".", O_DIRECTORY)); + + ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd)); + + r = ASSERT_OK(pidref_safe_fork("(btrfs)", FORK_DEATHSIG_SIGKILL, &pidref)); + if (r == 0) { + struct btrfs_ioctl_vol_args legacy = { .name = "legacy" }; + struct btrfs_ioctl_vol_args_v2 modern = { .name = "modern" }; + struct btrfs_ioctl_fs_info_args info = {}; + + ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF)); + ASSERT_OK_ERRNO(unshare(CLONE_NEWNS)); + + ASSERT_ERROR_ERRNO(ioctl(host_fd, BTRFS_IOC_SUBVOL_CREATE, &legacy), EPERM); + ASSERT_ERROR_ERRNO(ioctl(host_fd, BTRFS_IOC_SUBVOL_CREATE_V2, &modern), EPERM); + ASSERT_OK_ERRNO(ioctl(idmapped_fd, BTRFS_IOC_SUBVOL_CREATE, &legacy)); + ASSERT_OK_ERRNO(ioctl(idmapped_fd, BTRFS_IOC_SUBVOL_CREATE_V2, &modern)); + ASSERT_OK_ERRNO(ioctl(host_fd, BTRFS_IOC_FS_INFO, &info)); + + ASSERT_ERROR_ERRNO(ioctl(tmpfs_fd, BTRFS_IOC_SUBVOL_CREATE, &legacy), ENOTTY); + ASSERT_ERROR_ERRNO(ioctl(tmpfs_fd, BTRFS_IOC_SUBVOL_CREATE_V2, &modern), ENOTTY); + + ASSERT_OK_ERRNO(unshare(CLONE_NEWUSER)); + ASSERT_ERROR_ERRNO(ioctl(host_fd, BTRFS_IOC_SUBVOL_CREATE, &legacy), EPERM); + ASSERT_ERROR_ERRNO(ioctl(host_fd, BTRFS_IOC_SUBVOL_CREATE_V2, &modern), EPERM); + + _exit(EXIT_SUCCESS); + } + + ASSERT_OK(pidref_wait_for_terminate_and_check("(btrfs)", &pidref, WAIT_LOG)); + + FOREACH_STRING(name, "legacy", "modern") { + ASSERT_OK_ERRNO(fstatat(mapped_fd, name, &st, 0)); + ASSERT_EQ(st.st_uid, 0U); + ASSERT_EQ(st.st_gid, 0U); + } + + ASSERT_OK_ERRNO(fstat(userns_fd, &st)); + ASSERT_OK(userns_restrict_reset_by_inode(bpf_obj, st.st_ino)); +} + TEST(userns_restrict_overlayfs) { _cleanup_close_ int userns_fd = -EBADF, idmapped_upper = -EBADF, bad_userns_fd = -EBADF, bad_upper = -EBADF; diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c index 2f274c3bb2c..1c29e3b3322 100644 --- a/src/nsresourced/userns-restrict.c +++ b/src/nsresourced/userns-restrict.c @@ -68,6 +68,12 @@ int userns_restrict_install( if (!obj) return log_error_errno(errno, "Failed to open userns_restrict BPF object: %m"); + /* Kernels without the compat ioctl hook cannot load a program attached to it. */ + r = sym_bpf_program__set_autoload(obj->progs.userns_restrict_file_ioctl_compat, + bpf_kernel_has_kfunc("bpf_lsm_file_ioctl_compat")); + if (r < 0) + return log_error_errno(r, "Failed to select compat ioctl BPF program: %m"); + if (pin) { struct bpf_map *map; @@ -159,6 +165,9 @@ int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin) { bool linked = false; const char *e; + if (sym_bpf_program__fd(*ps->prog) < 0) + continue; + e = startswith(ps->name, "userns_restrict_"); assert(e); diff --git a/src/shared/bpf-util.c b/src/shared/bpf-util.c index ad855522b99..a29cae820d6 100644 --- a/src/shared/bpf-util.c +++ b/src/shared/bpf-util.c @@ -50,6 +50,7 @@ DLSYM_PROTOTYPE(bpf_object__pin_maps) = NULL; DLSYM_PROTOTYPE(bpf_program__attach) = NULL; DLSYM_PROTOTYPE(bpf_program__attach_cgroup) = NULL; DLSYM_PROTOTYPE(bpf_program__attach_lsm) = NULL; +DLSYM_PROTOTYPE(bpf_program__fd) = NULL; DLSYM_PROTOTYPE(bpf_program__name) = NULL; DLSYM_PROTOTYPE(bpf_program__set_autoload) = NULL; static int missing_bpf_token_create(int bpffs_fd, struct bpf_token_create_opts *opts) { @@ -128,6 +129,7 @@ int dlopen_bpf(int log_level) { DLSYM_ARG_FORCE(bpf_program__attach_cgroup), DLSYM_ARG_FORCE(bpf_program__attach_lsm), #endif + DLSYM_ARG(bpf_program__fd), DLSYM_ARG(bpf_program__name), DLSYM_ARG(bpf_program__set_autoload), DLSYM_ARG(libbpf_get_error), diff --git a/src/shared/bpf-util.h b/src/shared/bpf-util.h index 6a3fc5ad73b..3b349e543eb 100644 --- a/src/shared/bpf-util.h +++ b/src/shared/bpf-util.h @@ -62,6 +62,7 @@ extern DLSYM_PROTOTYPE(bpf_object__pin_maps); extern DLSYM_PROTOTYPE(bpf_program__attach); extern DLSYM_PROTOTYPE(bpf_program__attach_cgroup); extern DLSYM_PROTOTYPE(bpf_program__attach_lsm); +extern DLSYM_PROTOTYPE(bpf_program__fd); extern DLSYM_PROTOTYPE(bpf_program__name); extern DLSYM_PROTOTYPE(bpf_program__set_autoload); extern DLSYM_PROTOTYPE(bpf_token_create);