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 bdd20e5ff6)
This commit is contained in:
Daan De Meyer authored and Luca Boccassi committed 2026-11-04 21:15:56 +00:00
1 parent c043a92628
commit ebe9b22a27
5 files changed
+120

No files matched your search

+27
View File
@@ -15,6 +15,7 @@
#include "vmlinux.h"
#include <asm/ioctl.h>
#include <errno.h> /* IWYU pragma: keep */
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
@@ -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
+81
View File
@@ -1,13 +1,17 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <grp.h>
#include <linux/btrfs.h>
#include <linux/magic.h>
#include <sched.h>
#include <stdio.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
+9
View File
@@ -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);
+2
View File
@@ -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),
+1
View File
@@ -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);