From 9bb5d21c151848bc5184c749efc2336b501853d8 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Tue, 25 Aug 2026 14:34:54 +0200 Subject: [PATCH] bpf-restrict-fsaccess: enforce W^X on file mappings Right now RestrictFileSystemAccess= checks that a file comes from a trusted dm-verity device, but it doesn't guard against mapping that file and making use of COW modifications. So a MAP_PRIVATE mapping of a trusted binary can be made writable, modified, and executed, while the vma still points to the trusted file and the pages are attacker-controlled anonymous copies. Enforce W^X: deny any mapping that is writable and executable at once, and refuse to make a private file mapping executable once it has been modified through copy-on-write. Signed-off-by: Christian Brauner (Amutable) --- src/bpf/restrict-fsaccess.bpf.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/bpf/restrict-fsaccess.bpf.c b/src/bpf/restrict-fsaccess.bpf.c index 97d7009161..512b80e293 100644 --- a/src/bpf/restrict-fsaccess.bpf.c +++ b/src/bpf/restrict-fsaccess.bpf.c @@ -16,8 +16,11 @@ * dm-verity signals signature validity * - bdev_free_security hook: removes devices from the map on teardown * - 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 + * - mmap_file: blocks PROT_EXEC mmap from untrusted sources, and + * any writable+executable (W^X-violating) mapping + * - file_mprotect: blocks writable+executable mprotect, and making + * executable a mapping from an untrusted source or + * one that was modified through copy-on-write * * On kernels providing the bpf_real_data_inode() kfunc (v7.2+), files on * union filesystems (overlayfs) are resolved to the layer hosting their @@ -39,6 +42,7 @@ #include #include +#define PROT_WRITE 0x2 #define PROT_EXEC 0x4 #define VM_EXEC 0x00000004 #define PTRACE_MODE_ATTACH 0x02 @@ -218,6 +222,10 @@ SEC("lsm.s/mmap_file") int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags) { + /* W^X: a mapping may never be writable and executable at once. */ + if ((prot & PROT_EXEC) && (prot & PROT_WRITE)) + return -EPERM; + /* Only enforce on executable mappings */ if (!(prot & PROT_EXEC)) return 0; @@ -245,17 +253,27 @@ int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma, { struct file *file; unsigned long vm_flags; + void *anon_vma; + + /* W^X: a mapping may never be writable and executable at once. */ + if ((prot & PROT_EXEC) && (prot & PROT_WRITE)) + return -EPERM; /* Only enforce when adding PROT_EXEC */ if (!(prot & PROT_EXEC)) return 0; - /* If VM_EXEC is already set, the mapping is already executable — this - * mprotect isn't granting new executable capability, allow */ + /* Already executable so this mprotect() grants no new capability. */ BPF_CORE_READ_INTO(&vm_flags, vma, vm_flags); if (vm_flags & VM_EXEC) return 0; + /* A private file mapping with an anon_vma has been written to + * (copy-on-write), so its pages may no longer match the trusted file. */ + BPF_CORE_READ_INTO(&anon_vma, vma, anon_vma); + if (anon_vma) + return -EPERM; + /* Anonymous executable mapping — no file backing, deny. Direct * 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