Re: [SECURITY] mm/ksm: replace_page() drops the uffd-wp/rwp PTE bit
From: Xu Xin (vivo)
Date: Tue Nov 10 2026 - 08:16:42 EST
On Tue, Nov 10, 2026 at 07:48:25PM +0800, n3rd wrote:
> Hello,
>
>
> I am reporting a security bug in the Linux kernel.
Repeat report: https://lore.kernel.org/all/20261020110758.960048-1-shawnq1@xxxxxxx/
We're wating for the final fix v3: https://lore.kernel.org/all/CAAbz3fTjWnZmHaWT+zsycEvnEM-ZYXH0jG_XoV+yidYUquutVg@xxxxxxxxxxxxxx/
>
>
> 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. The same statement is byte-identical in the mm/ksm.c of v7.2.9 and v7.3-rc6 according to the static comparison records of the workspace that produced this report, so both are affected as well; earlier releases were not compared one by one. There is no upstream fix as of v7.3-rc7 and no CVE has been assigned. Note that CONFIG_KSM is not part of x86_64_defconfig, so the test kernel was built with CONFIG_KSM=y added explicitly by the preset in the reproducer bundle.
>
>
> 2. Problem description
> replace_page() in mm/ksm.c rebuilds the target PTE from scratch when KSM merges a page: newpte = mk_pte(kpage, vma->vm_page_prot), followed by set_pte_at(). mk_pte() only consults vma->vm_page_prot, which for a private anonymous VMA is PAGE_COPY, so the uffd enforcement state of the PTE being replaced is lost. For MODE_WP the _PAGE_UFFD_WP bit set by pte_mkuffd() disappears, and for MODE_RWP both the protnone state and the uffd bit disappear. mm/ksm.c contains no reference to uffd at all, and none of the guards exclude a uffd armed VMA: ksm_compatible() only rejects VM_SHARED, VM_MAYSHARE, VM_HUGETLB, VM_DROPPABLE, DAX and VMA_SPECIAL_FLAGS, and that mask is only VMA_IO, VMA_DONTEXPAND, VMA_PFNMAP and VMA_MIXEDMAP, with no uffd flag in it; the reverse check vma_can_userfault() does not reject VM_MERGEABLE either; the KSM scanner only skips !pte_present(), and on x86 pte_present() returns true for _PAGE_PROTNONE, so uffd-rwp armed pages are scanned like any other; and write_protect_page() only calls pte_mkclean() and pte_wrprotect(), so the stable tree anchor twin keeps its armed PTE. The observable effect is a selective, silent disarm. The page that went through replace_page() takes a normal copy-on-write fault on write in the WP case and reads straight through in the RWP case, in both cases with zero uffd events, while the anchor twin in the very same VMA and the very same uffd context still delivers UFFD_EVENT_PAGEFAULT. A monitor therefore cannot distinguish no access from enforcement having been removed, and there is no global failure signal. The practical impact is that QEMU live migration dirty page tracking silently loses writes on the disarmed pages, so the migrated guest can end up inconsistent; incremental checkpointing and in-process write watchdogs have the same problem. This is a policy and enforcement bypass only: there is no memory corruption, no out-of-bounds access, no use-after-free and no privilege escalation primitive, and KASAN stayed silent across both rounds. Every other path in the same kernel that rewrites a PTE does preserve the bit, for example the fork PTE-copy helper in mm/memory.c, the fault paths, migration in mm/migrate.c, swapin in mm/swapfile.c, hugetlb, mremap and UFFDIO_MOVE; mm/ksm.c is the outlier.
>
>
> 3. Reproducer
> A single statically linked C program, mm05, with its source next to it, runs as root inside a QEMU guest. It maps two three page anonymous VMAs: wp_area is registered with UFFDIO_REGISTER_MODE_WP and armed with UFFDIO_WRITEPROTECT, rwp_area is registered with MODE_RWP and armed with UFFDIO_RWPROTECT. Pages 0 and 1 of each VMA hold identical content so ksmd can merge them, and page 2 holds unique content and serves as an in-VMA enforcement control. The control page is written and read first: the write is blocked with a PAGEFAULT carrying flags 0x3 and the read faults with flags 0x8, printed as JUDGE BASELINE with the verdict ARMED. MADV_MERGEABLE is then called on both armed VMAs and returns 0, ksmd is enabled, and pagemap bit 57 (PM_UFFD_WP) is read for all four merge pages, all 1. ksmd merges within one second: pages_shared goes from 0 to 2 and pages_sharing is 2. After the merge exactly one page of each pair has bit 57 cleared while the other keeps it, and the roles are derived from that bit rather than assumed. Writing the cleared WP page and reading the cleared RWP page both complete instantly with zero uffd events, while the same operations on the retained anchor page in the same VMA still deliver events; the program prints JUDGE KSM-WP with BUG-C CONFIRMED and JUDGE KSM-RWP with BUG-D CONFIRMED. Because replace_page() is inlined into try_to_merge_one_page() there is no replace_page symbol in System.map and a kprobe on it is rejected with Invalid argument, so kprobes are placed on try_to_merge_one_page and write_protect_page instead; they fire four times each during the round and the trace lines show the ksmd thread executing that merge path. The complete reproducer and the raw console logs are available on request; I am not posting them publicly.
>
>
> 4. Conditions
> The target VMA must already be registered and armed with UFFDIO_REGISTER_MODE_WP plus UFFDIO_WRITEPROTECT, or with UFFDIO_REGISTER_MODE_RWP plus UFFDIO_RWPROTECT. ksmd must be running on the host, which is a deployment decision the attacker cannot make alone. A page with identical content must exist to pair with, which guest RAM zero pages and template pages provide naturally. Triggering needs only MADV_MERGEABLE plus one ksmd scan period, so the sequence is deterministic and has no race window, and it does not depend on THP, SLUB layout or kernel randomisation. No capability is required on the attacker side: madvise(2) only acts on the caller's own mm, and although userfaultfd(2) needs root or CAP_SYS_PTRACE unless vm.unprivileged_userfaultfd is 1, the monitor in this threat model is the uffd holder itself, so the attacker never needs uffd access. The attacker is the monitored process itself and the victim is the uffd manager. Tested on x86_64 with QEMU 6.2.0 in TCG mode, two vCPUs, 4096 MB of RAM and an initramfs, with CONFIG_KSM, CONFIG_USERFAULTFD, CONFIG_USERFAULTFD_RWP and CONFIG_PTE_MARKER_UFFD_WP enabled and KASAN on as a control.
>
>
> 5. Suspected location
> mm/ksm.c: replace_page(), specifically the newpte = mk_pte(kpage, vma->vm_page_prot) assignment and the following set_pte_at(). Supporting gaps: ksm_compatible() does not reject a uffd armed VMA, the scanner ksm_next_page_pmd_entry() only filters !pte_present(), get_mergeable_page() uses folio_walk with the same pte_present() gate, and write_protect_page() only mkclean/wrprotects so the anchor keeps the bit. The reverse gate is vma_can_userfault() in mm/userfaultfd.c, which does not reject VM_MERGEABLE. Paths that do preserve the bit and can serve as templates: the fork PTE-copy helper in mm/memory.c, the fault paths in the same file, mm/migrate.c, mm/swapfile.c, mm/hugetlb.c, mm/mremap.c and UFFDIO_MOVE in mm/userfaultfd.c.
>
>
> 6. Suggested fix
> In replace_page(), after newpte is built, mirror the other PTE rewriting paths: if the VMA is VM_UFFD_WP and the source PTE carries the uffd bit, apply pte_mkuffd() to newpte; if the VMA is VM_UFFD_RWP and the source PTE carries the uffd bit, restore PAGE_NONE and apply pte_mkuffd() as well. Templates to copy are the fork PTE-copy helper in mm/memory.c, the migration path in mm/migrate.c, the swapin path in mm/swapfile.c and the hugetlb path in mm/hugetlb.c. A more conservative alternative is to make ksm_compatible() refuse a VMA that userfaultfd has registered or armed, at the cost of losing deduplication for those VMAs.
>
>
> 7. Mitigation
> Monitors should not rely on events alone: re-arm the range with UFFDIO_WRITEPROTECT or UFFDIO_RWPROTECT periodically and cross-check pagemap bit 57 (PM_UFFD_WP), re-arming and warning when an armed page loses the bit without the monitor clearing it. For QEMU and KVM, memory merging can be turned off (-machine memory-merge=off, or host /sys/kernel/mm/ksm/run set to 0) at the cost of losing memory overcommit. Applications should avoid MADV_MERGEABLE on a range registered with userfaultfd, or keep deduplication on separate, unregistered ranges.
>
>
> 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.
>