bpf-restrict-fsaccess: resolve union filesystems to the device hosting the data

Execution from an overlayfs mount is currently denied even when every
layer sits on a signed dm-verity device. The hooks compare
file->f_inode->i_sb->s_dev against the map of trusted devices and for a
file on an overlay that's the overlay's own anonymous device, never the
backing block device.

Kernel v7.2 grew the bpf_real_data_inode() kfunc which resolves a file to
the inode hosting its data. Use it from bprm_check_security and mmap_file
which both get to see the union-level file. They become lsm.s/ programs
because the kfunc is sleepable. That doesn't raise our kernel
requirements, both have been in sleepable_lsm_hooks since v5.11.

The file_mprotect hook stays non-sleepable. It runs under mmap_write_lock
where sleeping in d_real() would invert the documented i_rwsem ->
mmap_lock order, and it doesn't need the kfunc anyway. By the time a vma
exists ovl_mmap() has already installed the backing file into
vma->vm_file.

The initramfs trust window keeps comparing the mount device. If the
initrd is itself a union mount then userspace only ever recorded the
union's anonymous s_dev. The verity map is keyed by block device so that
one gets compared against the resolved device.

The kfunc is declared __weak __ksym and every call is gated on a const
volatile .rodata flag that PID1 only sets when the kernel provides it. So
the object still loads on pre-7.2 kernels where files on union
filesystems are denied as before. The flag stays off until the loader
learns to set it in a followup.

The file argument of bpf_lsm_mmap_file() is annotated __nullable since
v7.0 so the verifier won't let us hand it to the kfunc unless we branch
on it first.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-08-10 16:09:06 +02:00
parent 060628d332
commit 43914520c1
+91 -13
View File
@@ -18,6 +18,11 @@
* - bprm_check_security: blocks execve() from untrusted sources
* - mmap_file: blocks PROT_EXEC mmap from untrusted sources
* - file_mprotect: blocks W->X transitions from untrusted sources
*
* On kernels providing the bpf_real_data_inode() kfunc (v7.2+), files on
* union filesystems (overlayfs) are resolved to the layer hosting their
* data, so execution from overlays stacked on signed dm-verity devices
* works. Without the kfunc such files are denied, as before.
*/
/* If offsetof() is implemented via __builtin_offset() then it doesn't work on current compilers, since the
@@ -116,30 +121,88 @@ void BPF_PROG(restrict_fsaccess_bdev_free, struct block_device *bdev)
/* ---- Enforcement helpers ---- */
/* Check whether a file is from a trusted source.
* Returns 0 (allow) or -EPERM (deny). */
static __always_inline int check_trusted_file(struct file *file)
/* Set by PID1 between skeleton open and load (.rodata is read-only and
* frozen after load): true when the kernel provides the
* bpf_real_data_inode() kfunc (v7.2+). The verifier constant-folds the flag
* and removes the (then unresolvable) kfunc call on kernels without it. */
const volatile bool have_real_data_inode = false;
/* Returns the inode hosting a regular file's data: on union filesystems
* (overlayfs) the backing layer's inode rather than the union inode.
* Sleepable, hence only callable from lsm.s/ programs. Weak so that the
* object still loads on kernels that lack the kfunc. */
extern struct inode *bpf_real_data_inode(struct file *file) __weak __ksym;
/* Check whether a file's data lives on a trusted device.
* Returns 0 (allow) or -EPERM (deny).
*
* mnt_dev is the device of the filesystem the file was opened on, data_dev
* the device of the filesystem hosting its data; they differ only on union
* filesystems. The initramfs window compares mnt_dev: whether a file is on
* the initramfs is a question about the mounted filesystem — userspace
* recorded the mount's s_dev, which for a union-mount initrd is the union's
* anonymous device, not any backing device. The verity map is keyed by
* block device, so it is compared against data_dev. */
static __always_inline int check_trusted_dev(__u32 mnt_dev, __u32 data_dev)
{
__u32 s_dev;
__u8 *sig_valid;
BPF_CORE_READ_INTO(&s_dev, file, f_inode, i_sb, s_dev);
/* Check initramfs trust (active only during early boot) */
if (initramfs_s_dev != 0 && s_dev == initramfs_s_dev)
if (initramfs_s_dev != 0 && mnt_dev == initramfs_s_dev)
return 0;
/* Check verity device map */
sig_valid = bpf_map_lookup_elem(&verity_devices, &s_dev);
sig_valid = bpf_map_lookup_elem(&verity_devices, &data_dev);
if (sig_valid && *sig_valid)
return 0;
return -EPERM;
}
/* Check a file as the user sees it, resolving union filesystems to the
* layer hosting the data. May sleep — lsm.s/ programs only.
*
* The kfunc follows the data: a metacopy upper on an untrusted layer can
* still own the metadata (mode, ownership, setuid) of a binary whose data —
* and hence verdict — comes from the trusted lower device. */
static __always_inline int check_trusted_file(struct file *file)
{
struct inode *inode;
__u32 mnt_dev, data_dev;
BPF_CORE_READ_INTO(&mnt_dev, file, f_inode, i_sb, s_dev);
/* No kfunc: the mount device stands in, denying union filesystems. */
data_dev = mnt_dev;
if (have_real_data_inode) {
inode = bpf_real_data_inode(file);
if (!inode)
return -EPERM;
BPF_CORE_READ_INTO(&data_dev, inode, i_sb, s_dev);
}
return check_trusted_dev(mnt_dev, data_dev);
}
/* Check vma->vm_file. By the time a vma exists, stacking filesystems have
* already installed the backing file into it (ovl_mmap() →
* backing_file_mmap() → vma_set_file()), so no union resolution is needed —
* and unlike re-resolving, vm_file names the file the pages actually come
* from even after a later copy-up. */
static __always_inline int check_trusted_vm_file(struct file *file)
{
__u32 s_dev;
BPF_CORE_READ_INTO(&s_dev, file, f_inode, i_sb, s_dev);
return check_trusted_dev(s_dev, s_dev);
}
/* ---- Enforcement hooks ---- */
SEC("lsm/bprm_check_security")
SEC("lsm.s/bprm_check_security")
int BPF_PROG(restrict_fsaccess_bprm_check, struct linux_binprm *bprm)
{
/* Direct dereference (CO-RE-relocated via vmlinux.h's
@@ -148,7 +211,10 @@ int BPF_PROG(restrict_fsaccess_bprm_check, struct linux_binprm *bprm)
return check_trusted_file(bprm->file);
}
SEC("lsm/mmap_file")
/* Covers more than plain mmap(): binfmt_elf maps the PT_INTERP interpreter
* via vm_mmap(), so ld.so is only ever checked here, not by
* bprm_check_security. */
SEC("lsm.s/mmap_file")
int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
{
@@ -156,13 +222,23 @@ int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqpr
if (!(prot & PROT_EXEC))
return 0;
/* Anonymous executable mapping — no file backing, deny */
/* Anonymous executable mapping — no file backing, deny. The hook's
* file argument is __nullable since kernel v7.0 (94e948b7e684), so the
* verifier types it trusted-or-NULL and this check is mandatory:
* without it, handing file to bpf_real_data_inode() fails to load.
* Older kernels lack the annotation and drop the branch, but they lack
* the kfunc too, so it never sees a NULL file. */
if (!file)
return -EPERM;
return check_trusted_file(file);
}
/* Deliberately not sleepable: this hook runs under mmap_write_lock, where
* sleeping in d_real()/verity I/O could invert the documented i_rwsem →
* mmap_lock order (and bpf_lsm_file_mprotect is not in the kernel's
* sleepable_lsm_hooks set anyway). No resolution is needed here, see
* check_trusted_vm_file(). */
SEC("lsm/file_mprotect")
int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot)
@@ -181,12 +257,14 @@ int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma,
return 0;
/* Anonymous executable mapping — no file backing, deny. Direct
* dereference, see restrict_fsaccess_bprm_check(). */
* dereference, see restrict_fsaccess_bprm_check(). vm_file is in the
* verifier's BTF_TYPE_SAFE_TRUSTED_OR_NULL list, so this check is live
* and required. */
file = vma->vm_file;
if (!file)
return -EPERM;
return check_trusted_file(file);
return check_trusted_vm_file(file);
}
/* ---- PID1 ptrace protection ----