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 bb375f3ba75..27189a16480 100644 --- a/src/nsresourced/userns-restrict.c +++ b/src/nsresourced/userns-restrict.c @@ -69,6 +69,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; @@ -160,6 +166,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 b1a8a250326..def8d6ea7f1 100644 --- a/src/shared/bpf-util.c +++ b/src/shared/bpf-util.c @@ -48,6 +48,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) { @@ -129,6 +130,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 42eb9332aed..987a3f97246 100644 --- a/src/shared/bpf-util.h +++ b/src/shared/bpf-util.h @@ -65,6 +65,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);