Re: [SECURITY] remap_file_pages() skips the SELinux backing-file checks
From: Stephen Smalley
Date: Tue Nov 10 2026 - 12:31:55 EST
On Tue, Nov 10, 2026 at 6:46 AM n3rd <18670333770@xxxxxxx> wrote:
>
>
> Hi,
>
> I am reporting a security bug in the Linux kernel.
>
> 1. Affected kernel versions
> Linux v7.3-rc7 (mainline; tested kernel 7.3.0-rc7, reproduced on two fresh boots), v7.3-rc6 (same code shape), and v7.2.9 (stable; mm/mmap.c:1143 is the same call that is at mm/mmap.c:1158 in v7.3-rc7, and the SELinux side has the same selinux_mmap_file_common() and file-has-perm helper shape). Only CONFIG_SECURITY_SELINUX=y plus CONFIG_OVERLAY_FS=y (which selects CONFIG_FS_STACK) are needed. Not fixed in v7.3-rc7 as of this report; no CVE assigned. The dynamic verification was done on v7.3-rc7 only; the v7.3-rc6 and v7.2.9 statements come from a line-by-line source comparison.
>
> 2. Problem description
> remap_file_pages() is emulated in mm/mmap.c (SYSCALL_DEFINE5 at line 1101 in v7.3-rc7). It does file = get_file(vma->vm_file) at line 1153 and then ret = security_mmap_file(file, prot, flags) at line 1158. On a stacked filesystem (overlayfs, erofs ishare, FUSE passthrough) vma->vm_file is not the file the caller opened: it is the FMODE_BACKING file that overlayfs opened with the mounter override credentials (fs/overlayfs/file.c:51, backing_file_open). The plain hook is therefore being handed a backing file. selinux_mmap_file() (security/selinux/hooks.c:4172) calls selinux_mmap_file_common() with mounter_check=false (line 4153), which reaches the internal file-has-perm helper with bf_user_file=false (line 1764) and takes tsid_fd = selinux_file(file)->sid at line 1791. That fsec->sid is current_sid() at file-open time (line 3853), which for a backing file is the mounter SID, so the fd use check at line 1797 runs from the caller SID to the mounter SID and is denied when the policy has no such allow rule. The inode that gets checked is file_inode(backing file), the real underlying inode, not the top-level stacked file; backing_mounters_has_perm() (line 4056) is not called for the intermediate layers; and fsnotify_mmap_perm() is not called either (its only call site in the tree is mm/util.c:577 in vm_mmap_pgoff). The sibling paths do handle this correctly, which is what makes this an omission rather than a design choice: the mmap path has the dedicated security_mmap_backing_file() hook (security/security.c:2599, called from backing_file_mmap() at fs/backing-file.c:344) and the SELinux implementation at hooks.c:4196 uses mounter_check=true; the mprotect path checks FMODE_BACKING explicitly in selinux_file_mprotect() (line 4215, check at line 4228) and then adds the intermediate-layer check plus a bf_user_file=true check that uses uf_sid (line 1783). The upstream fix family 82544d36b172, 9fe595fad54d, 8c0c602202b9 and 78fc54b934bf covers the mmap and mprotect VMA paths only; the remap emulation was never converted. Effect: with SELinux enforcing and a multi-domain policy loaded, a confined process that calls remap_file_pages(2) on its own shared mapping of an overlayfs file is refused with EACCES, while plain mmap and mprotect on the same file in the same domain succeed. In our run, permissive mode logged avc: denied { use } with scontext user_t, tcontext mounter_t and tclass fd while the call returned 0; after switching to enforcing the same denial was logged with permissive=0 and the call returned -1 with errno 13. The path also skips the top-level label recheck, the intermediate mounter recheck and the fanotify mmap permission event. Boundaries, stated honestly: this is an access-control semantics defect, not a memory-safety bug (prot is reconstructed from the existing VMA, so no new access right can be gained through remap), and KASAN, Oops, general protection and WARNING counts were all zero across both runs. We did not demonstrate an end-to-end bypass where mmap is denied and remap is allowed for the same object, because that needs different labels on the top-level and backing files.
>
> 3. Reproducer
> A single statically linked C program (poc/sec03_poc, source poc/sec03_poc.c) runs inside a QEMU guest as root. It mounts selinuxfs, loads a self-built minimal three-domain SELinux base policy generated from the target kernel's own security/selinux/include/classmap.h (97 classes, 27 initial SIDs, compiled with checkpolicy), mounts overlayfs with tmpfs lower and upper, registers kprobes on security_mmap_file (filtered with flags & 1 to exclude the process's own exec mappings) and security_mmap_backing_file, then dyntransitions to the confined domain and maps the overlay file. The reproducer is available on request; it is not included in this mail.
>
> 4. Conditions
> The file must live on a stacked filesystem and the caller must hold a MAP_SHARED mapping of it; the file has to be opened O_RDWR, because a read-only open makes do_mmap() clear VM_SHARED and the remap emulation then returns EINVAL at its VM_SHARED check. No capability and no user namespace are required: the whole chain is one unauthenticated local syscall on the caller's own VMA. SELinux must be enforcing with a policy that has more than one domain, and the kernel needs CONFIG_SECURITY_SELINUX=y, CONFIG_OVERLAY_FS=y and CONFIG_AUDIT=y to see the denial. The test setup was x86_64, QEMU 6.2.0 with TCG, 2 vCPU, 4096 MB, initramfs, kernel 7.3.0-rc7, KASAN enabled as a control only.
>
> 5. Suspected location
> mm/mmap.c: remap_file_pages() emulation, file = get_file(vma->vm_file) at line 1153 and ret = security_mmap_file(file, prot, flags) at line 1158 (v7.3-rc7); the same call is at mm/mmap.c:1143 in v7.2.9. security/selinux/hooks.c: selinux_mmap_file() at line 4172, selinux_mmap_file_common() at line 4153, the internal file-map-prot helper at line 4091, the internal file-has-perm helper at line 1764 with tsid_fd = fsec->sid at line 1791, fsec->sid = current_sid() at line 3853. Correct-semantics counterparts: selinux_mmap_backing_file() at line 4196, selinux_file_mprotect() at line 4215 with the FMODE_BACKING check at line 4228, backing_mounters_has_perm() at line 4056, selinux_backing_file_alloc() setting uf_sid at lines 3866 to 3875, and the bf_user_file=true branch using bfsec->uf_sid at line 1783. Generic layer: security_mmap_backing_file() at security/security.c:2599 (hook table include/linux/lsm_hook_defs.h:204), backing_file_mmap() and vma_set_file() at fs/backing-file.c:329 to 344, ovl_mmap() at fs/overlayfs/file.c:468 to 476, fs/erofs/ishare.c:145. The missing companion check is fsnotify_mmap_perm(), whose only call site is mm/util.c:577.
>
> 6. Suggested fix
> Do not hand a possibly FMODE_BACKING vma->vm_file to the plain security_mmap_file() hook from the remap emulation. Two options. In mm: reconstruct the user file via backing_file_user_path(file) before calling the hook, or add an entry point symmetric to security_mmap_backing_file() (that hook already takes the vma, so it can supply both prot and the user path). In SELinux, as a catch-all that also covers future paths handing vm_file to a plain hook: make selinux_mmap_file() take the backing-file semantics when file->f_mode has FMODE_BACKING, that is use bfsec->uf_sid as the fd owner, use the dentry from backing_file_user_path() as the checked inode, and add the backing_mounters_has_perm() intermediate-layer check. It would also be good to call fsnotify_mmap_perm() from the remap path so that fanotify permission events match the mmap path, and to add a self-test that asserts remap and mmap reach the same SELinux decision on an overlayfs file. No patch has been submitted by us.
>
> 7. Mitigation
> Until a fix lands, avoid calling remap_file_pages(2) on shared mappings of stacked filesystem files from confined SELinux domains; the syscall is deprecated and prints a pr_warn_once on first use, and mremap(MREMAP_FIXED) or a fresh mmap can be used instead. Switching SELinux to permissive only stops the denial from blocking the call, it does not fix the wrong object and SID, so it is not a recommended mitigation. Deployments that rely on fsnotify permission events for DLP or HSM range enforcement should be aware that this entry point generates no such event. No kernel configuration switch avoids the defect; /sys/fs/selinux/enforce only decides whether the denial blocks the call.
>
> 8. Disclosure
> Public. This report is handled as a public report because it was produced with AI assistance, following the kernel security process; no embargo is requested. The reproducer is not published here and is available on request.
>
> 9. AI-assistance note
> AI was used: an AI coding assistant helped with source review, with building and running the target kernel, and with drafting this report. Per the kernel security process, an AI-assisted report is treated as public.
Thank you for the bug report. For the SELinux fix, I would use the
existing mprotect handling as the reference: check the caller against
the saved opener SID and user-visible inode, and retain the mounter
checks on the backing layers. Calling security_mmap_backing_file()
alone wouldn't be sufficient, since the normal mmap path has already
checked the user-visible file before it reaches that hook.
I'd treat the missing fsnotify_map_perm() call as a separate
mm/fsnotify issue. It isn't specific to overlayfs and could be tested
on a regular file by having a fanotify pre-access listener deny a
fresh mapping of a page, then trying to remap to the same offset.
Could you provide Paul and me (and optionally Ondrej if interested)
with the reproducer source and test policy? Thank you.