Re: [PATCH v3 9/9] x86: re-enable EXECMEM_ROX support
From: Pedro Falcato
Date: Wed Aug 12 2026 - 07:52:26 EST
On Wed, Aug 12, 2026 at 06:33:15AM +0000, Nikunj A Dadhania wrote:
> Hi,
>
> A kernel BUG in __text_poke() is observed inside a Ubuntu 26.04 guest
> (v7.0.0.28 and also with upstream v7.2-4c5). Host is running on v7.2-rc5.
>
> SETUP
> -----
>
> 512 QEMU Q35 guests (2 vCPUs, 1G RAM each) boot concurrently across 200
> iterations. The crash is seen during module loading in early boot.
> 2-7 guests out of ~100,000 boot attempts crash with the kernel BUG.
>
> Reproduced on:
> - AMD EPYC, QEMU Q35 guests
> - Intel Icelake, QEMU Q35 guests
>
> Apparently it was seen once in the non-guest Ubuntu 26.04 environment as well
> https://discourse.ubuntu.com/t/failed-boot-one-time-on-ubuntu-unity-26-04-educational-mission/85300
>
> SYMPTOMS
> --------
>
> Either of the below two crash signatures is observed:
>
> Signature 1 — BUG_ON(!pages[0]) in __text_poke():
>
> kernel BUG at arch/x86/kernel/alternative.c:2564!
> Oops: invalid opcode: 0000 [#1] SMP NOPTI
> CPU: 0 UID: 0 PID: 741 Comm: (udev-worker) Not tainted 7.0.0-28-generic #28-Ubuntu PREEMPT(lazy)
> Hardware name: QEMU Standard PC (Q35 + ICH9, 2009)
> RIP: 0010:__text_poke+0x2a4/0x400
> Call Trace:
> <TASK>
> smp_text_poke_batch_finish+0x25f/0x5b0
> smp_text_poke_single+0x13/0x30
> __static_call_transform+0x10e/0x210
> arch_static_call_transform+0x5c/0xc0
> __static_call_init+0x154/0x300
> static_call_module_notify+0x144/0x170
> notifier_call_chain+0x62/0xf0
> blocking_notifier_call_chain_robust+0x62/0xc0
> load_module+0x5ef/0x960
> Modules linked in: ppdev(+) kvm(+) irqbypass ...
>
> Signature 2 — BUG_ON(memcmp()) in __text_poke():
>
> kernel BUG at arch/x86/kernel/alternative.c:2632!
> Oops: invalid opcode: 0000 [#1] SMP NOPTI
> CPU: 0 UID: 0 PID: 742 Comm: (udev-worker) Not tainted 7.0.0-28-generic #28-Ubuntu PREEMPT(lazy)
> Hardware name: QEMU Standard PC (Q35 + ICH9, 2009)
> RIP: 0010:__text_poke+0x380/0x400
> Call Trace:
> <TASK>
> smp_text_poke_batch_finish+0x323/0x5b0
> smp_text_poke_single+0x13/0x30
> __static_call_transform+0x10e/0x210
> arch_static_call_transform+0x5c/0xc0
> __static_call_init+0x154/0x300
> static_call_module_notify+0x144/0x170
> notifier_call_chain+0x62/0xf0
> blocking_notifier_call_chain_robust+0x62/0xc0
> load_module+0x5ef/0x960
> Modules linked in: kvm(+) irqbypass ...
>
> INSTRUMENTATION
> ---------------
>
> The following instrumentation was applied to capture the page table state at
> the moment of failure:
>
> In mm/vmalloc.c::vmalloc_to_page()
>
> ptep = pte_offset_kernel(pmd, addr);
> pte = ptep_get(ptep);
> if (pte_present(pte))
> page = pte_page(pte);
> else
> pr_err("vmalloc_to_page: pte_not_present for addr=%lx "
> "PGD=%lx P4D=%lx PUD=%lx PMD=%lx PTE=%lx\n",
> addr, pgd_val(*pgd), p4d_val(*p4d), pud_val(*pud),
> pmd_val(*pmd), pte_val(pte));
>
> In arch/x86/kernel/alternative.c::__text_poke()
>
> /* BUG 1: pages[0] NULL */
> if (!pages[0] || ...) {
> pr_err("%s: vmalloc_to_page failed for addr=%pK pages[0]=%pK pages[1]=%pK cross=%d\n", ...);
> BUG();
> }
>
> ...
>
> /* BUG 2: memcmp mismatch */
> if (memcmp(addr, src, len)) {
> struct page *page_now = vmalloc_to_page(addr);
> pr_err("%s: memcmp failed at addr=%pK len=%zu\n", __func__, addr, len);
> pr_err("%s: expected (src): %*ph\n", __func__, (int)len, src);
> pr_err("%s: found (dst): %*ph\n", __func__, (int)len, (u8 *)addr);
> pr_err("%s: pfn at poke time (vmalloc_to_page): %lx\n", __func__, page_to_pfn(pages[0]));
> pr_err("%s: pfn at readback time (vmalloc_to_page): %lx\n", __func__, page_to_pfn(page_now));
> text_poke_dump_pagetable((unsigned long)addr); /* reads CR3 */
> BUG();
> }
>
> Signature 1 — vmalloc_to_page() returned NULL:
>
> vmalloc_to_page: pte_not_present for addr=ffffffffc0697979 PGD=28242067 P4D=28243067 PUD=28245067 PMD=3be001a1 PTE=0
> SMP alternatives: __text_poke: vmalloc_to_page failed for addr=ffffffffc0697979 pages[0]=0000000000000000 pages[1]=0000000000000000 cross=0
>
> PMD was not a leaf, and the code reached a point to check pte_present() and
> finds the PTE to be not present. But in the dump in else case, pmd_val(*pmd)
> shows that it is a PMD leaf (PSE bit set).
>
> Signature 2 — vmalloc_to_page() returned a wrong page:
>
> SMP alternatives: __text_poke: memcmp failed at addr=ffffffffc06fc4bd len=1
> SMP alternatives: __text_poke: expected (src): cc
> SMP alternatives: __text_poke: found (dst): e8
> SMP alternatives: __text_poke: pfn at poke time (vmalloc_to_page): 3884a
> SMP alternatives: __text_poke: pfn at readback time (vmalloc_to_page): 52fc
> SMP alternatives: __text_poke: page table dump for addr:
> PGD 1ba42067 P4D 1ba43067 PUD 1ba45067 PMD 52001a1
>
> vmalloc_to_page() returned pfn=0x3884a when pages[0] was queried. The write
> of 0xcc landed there. When vmalloc_to_page() was called again during the
> memcmp readback in the instrumentation code, the vmap mapping resolved through
> PMD=52001a1 to a different pfn=0x52fc, which contains 0xe8 (existing
> instruction bytes). This is silent memory corruption — 0xcc was written to
> the wrong physical page (pfn=0x3884a).
>
> BISECTION
> ---------
>
> The following experiments all ran 200 iterations of 512 concurrent guest boots
> with zero crashes.
>
> 1. Remove "select ARCH_HAS_EXECMEM_ROX" from arch/x86/Kconfig
>
> 2. Boot with "nohugevmalloc"
>
> 3. Remove VM_ALLOW_HUGE_VMAP from vm_flags in execmem_cache_populate()
>
> - unsigned long vm_flags = VM_ALLOW_HUGE_VMAP;
> + unsigned long vm_flags = 0;
>
> 4. Remove CPA_COLLAPSE from set_memory_rox()
>
> return change_page_attr_set_clr(&addr, numpages, __pgprot(0), clr, 0,
> - CPA_COLLAPSE, NULL);
> + 0, NULL);
>
> Without CPA_COLLAPSE, set_memory_rox() changes 4K PTE permissions
> in-place and the PMD stays permanently as a PTE-page pointer after
> the first split — no re-collapse, no future race window. This
> confirms that CPA_COLLAPSE is a necessary part of the race sequence.
>
> 5. Hold pgd_lock around vmalloc_to_page() in __text_poke()
>
> if (!core_kernel_text((unsigned long)addr)) {
> + spin_lock(&pgd_lock);
> pages[0] = vmalloc_to_page(addr);
> if (cross_page_boundary)
> pages[1] = vmalloc_to_page(addr + PAGE_SIZE);
> + spin_unlock(&pgd_lock);
> } else {
>
> __split_large_page() holds pgd_lock while installing the new PMD
> and populating all 512 4K PTEs. Holding pgd_lock around
> vmalloc_to_page() prevents CPA from modifying the PMD concurrently
> with the page table walk. This confirms the race is precisely
> between vmalloc_to_page() reading the PMD/PTE and
> __split_large_page() modifying it under pgd_lock.
>
> Note: this is a diagnostic experiment, not a production fix.
>
> The experiments point to a race between vmalloc_to_page() in
> __text_poke() and __split_large_page() modifying the shared PMD under
> pgd_lock. __split_large_page() holds pgd_lock while modifying the
> PMD; vmalloc_to_page() holds no lock and can observe the PMD in an
> intermediate state.
>
> What would be the correct fix for this issue?
>
> Happy to test any proposed fix or run further experiments.
https://lore.kernel.org/linux-mm/anCK3eWFMwZqq5ka@pedro-suse/
--
Pedro