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:
@@ -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 ----
|
||||
|
||||
Reference in New Issue
Block a user