Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [SECURITY] mm: khugepaged collapse_file() drops RWP PTE markers
@ 2026-11-10 11:54 n3rd
  2026-11-10 11:55 ` David Hildenbrand (Arm)
  0 siblings, 1 reply; 12+ messages in thread
From: n3rd @ 2026-11-10 11:54 UTC (permalink / raw)
  To: linux-mm@kvack.org, linux-kernel@vger.kernel.org
  Cc: akpm@linux-foundation.org, david@kernel.org, ljs@kernel.org,
	kas@kernel.org, ziy@nvidia.com, Mike Rapoport, peterx@redhat.com,
	security@kernel.org

[-- Attachment #1: Type: text/plain, Size: 6711 bytes --]



Hello,


I am reporting a security bug in the Linux kernel.


1. Affected kernel versions
Linux v7.3-rc7 (mainline, built and tested as 7.3.0-rc7) is affected and was reproduced on two fresh boots. v7.3-rc6 carries the same code and is affected as well. v7.2.9 does not have the RWP feature at all, because UFFDIO_RWPROTECT, UFFD_FEATURE_RWP and VM_UFFD_RWP were added in v7.3, so it is not affected. Triggering additionally requires CONFIG_TRANSPARENT_HUGEPAGE. There is no upstream fix as of v7.3-rc7 and no CVE has been assigned.


2. Problem description
collapse_file() in mm/khugepaged.c replaces 512 order-0 folios with one PMD order folio and, for each mapped small folio, calls try_to_unmap() to clear the user PTEs. try_to_unmap_one() in mm/rmap.c then calls cond_install_uffd_wp_ptes() to preserve the uffd tracking state, but that function bails out immediately for an RWP VMA, because its gate in mm/memory.c reads: if (vma_is_anonymous(vma) || !userfaultfd_wp(vma)) return false. WP and RWP cannot coexist on one VMA, as userfaultfd_register() in mm/userfaultfd.c rejects the combination with the comment that WP and RWP share the uffd PTE bit, so every RWP VMA has userfaultfd_wp(vma) equal to false and always takes that early return. No PTE_MARKER_UFFD_WP is installed and the per-PTE RWP state, a PAGE_NONE PTE carrying the uffd bit, is silently dropped. Measured on v7.3-rc7 with kprobes: one madvise(MADV_COLLAPSE) on an RWP-armed shmem range calls collapse_file() once, try_to_unmap() 512 times and cond_install_uffd_wp_ptes() 512 times, with all 512 calls returning false; the first read of the range afterwards completes immediately with zero uffd events, while an un-collapsed range in the same uffd context still delivers RWP events. The same flow with UFFDIO_REGISTER_MODE_WP plus UFFDIO_WRITEPROTECT still delivers a WP event after the collapse, so the hole is RWP specific. Page contents are preserved, so the monitor gets no signal and no data anomaly: working set tracking silently treats hot pages as cold, and any RWP based access interception loses enforcement permanently. No memory corruption is involved; KASAN, Oops, general protection and WARNING counts are all zero. This is a gap in the preservation family that the public series already describes: patch 07/15 of the series userfaultfd: working set tracking for VM guest memory, mm: preserve RWP marker across PTE rewrites, states that the uffd PTE bit must survive any kernel path that rewrites a PTE on a VM_UFFD_RWP VMA, and wires that preservation through swap, device-exclusive, migration, fork, mprotect, mremap, UFFDIO_MOVE and hugetlb, but not through the plain rmap unmap path that file collapse uses. Patch 08/15 of the same series caps folio_unmap_pte_batch() at one PTE for VM_UFFD_RWP precisely so that the restore path can handle each PTE with its own marker, which is exactly the path that returns false here.


3. Reproducer
Two statically linked C programs, mm06 and mm06phase, run as root inside a QEMU guest; mm06phase arms a memfd range with UFFDIO_REGISTER_MODE_RWP plus UFFDIO_RWPROTECT, optionally issues madvise(MADV_COLLAPSE), and prints verdict lines computed at run time from kprobe counters, /proc/vmstat and its own uffd event counts, for example PHASE JUDGE collapse: collapse_file=1 try_to_unmap=512 cond_install_uffd_wp_ptes calls=512 all_returned_false=1 rwp_post_events=0 => NO uffd marker installed, RWP ENFORCEMENT LOST. mm06 runs the full sequence in both RWP and WP mode and prints lines such as the BUG line stating that the post-collapse read of the armed page completed with zero uffd events, and the control line showing that an un-collapsed range still delivers RWP events. The complete reproducer, the build commands and the raw console logs are available on request; I am not posting them publicly.


4. Conditions
The target range must already be registered and armed with UFFDIO_REGISTER_MODE_RWP plus UFFDIO_RWPROTECT, and its pages must be populated, since RWP only applies to present PTEs. Triggering needs a single madvise(MADV_COLLAPSE), which is a synchronous and deterministic path with no race window, or a background khugepaged scan of the same shmem file. No capability is required: madvise(2) only acts on the caller's own mm, and userfaultfd(2) is always available to root or CAP_SYS_PTRACE, while unprivileged users need vm.unprivileged_userfaultfd=1. The attacker is the monitored process itself or code sharing its mm, and the victim is the RWP monitor. The kernel needs CONFIG_TRANSPARENT_HUGEPAGE, without which madvise(MADV_COLLAPSE) cannot reach collapse_file, plus CONFIG_USERFAULTFD, CONFIG_USERFAULTFD_RWP, CONFIG_PTE_MARKER_UFFD_WP and CONFIG_SHMEM. Tested on x86_64 with QEMU 6.2.0 in TCG mode, two vCPUs, 4096 MB of RAM and an initramfs, with KASAN on as a control.


5. Suspected location
mm/khugepaged.c: collapse_file(), the try_to_unmap() call in the folio_mapped branch. mm/rmap.c: try_to_unmap_one(), which calls cond_install_uffd_wp_ptes() after clearing the PTE. mm/memory.c: cond_install_uffd_wp_ptes(), whose VMA gate is the WP only check. mm/userfaultfd.c: userfaultfd_register(), which rejects the WP plus RWP combination.


6. Suggested fix
Either widen the VMA gate in cond_install_uffd_wp_ptes() in mm/memory.c from userfaultfd_wp(vma) to userfaultfd_protected(vma), matching the predicate that patch 08/15 of the RWP series already uses in folio_unmap_pte_batch(), or bail out of collapse_file() for userfaultfd_protected() VMAs while walking i_mmap, matching the SCAN_PTE_UFFD semantics of the anonymous path. The second option must happen before try_to_unmap(), otherwise the PTEs are already gone, which is what happens today: the madvise call returns -EINVAL after the data plane collapse has completed.


7. Mitigation
Monitors should not rely on events alone: re-arm the range with UFFDIO_RWPROTECT periodically and cross-check with PAGEMAP_SCAN PAGE_IS_ACCESSED or the accessed bits. Applications should avoid madvise(MADV_COLLAPSE) on a range that is registered with RWP and should keep such ranges out of khugepaged scans. Administrators can keep vm.unprivileged_userfaultfd at zero, although a root monitor still has to re-arm periodically.


8. Disclosure
Public. This report is sent to the public linux-mm list and LKML and no embargo is requested. The reproducer is not posted publicly; it is available on request.


9. AI-assistance note
This report was prepared with the help of an AI coding assistant, which was used for the source review, for building and running the test kernel, and for drafting this text. Per the kernel security process I treat the issue as public and I am not posting the reproducer publicly.

[-- Attachment #2: Type: text/html, Size: 7632 bytes --]

^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-11-11 21:59 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 11:54 [SECURITY] mm: khugepaged collapse_file() drops RWP PTE markers n3rd
2026-11-10 11:55 ` David Hildenbrand (Arm)
2026-11-11  3:19   ` Tal Zussman
2026-11-11  7:29     ` David Hildenbrand (Arm)
2026-11-11 18:53       ` Kiryl Shutsemau
2026-11-11 19:18         ` Linus Torvalds
2026-11-11 19:43         ` David Hildenbrand (Arm)
2026-11-11 20:55           ` Linus Torvalds
2026-11-11 21:35             ` David Hildenbrand (Arm)
2026-11-11 21:59               ` Linus Torvalds
2026-11-11 19:43       ` Jose A. Perez de Azpillaga
2026-11-11 19:53         ` Kiryl Shutsemau

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox