[PATCH] x86/mm: Clear PUD entry before populating PMD table when splitting 1GB page in phys_pud_init

Phineas Su posted 1 patch 5 days, 8 hours ago
arch/x86/mm/init_64.c | 1 +
1 file changed, 1 insertion(+)
[PATCH] x86/mm: Clear PUD entry before populating PMD table when splitting 1GB page in phys_pud_init
Posted by Phineas Su 5 days, 8 hours ago
When phys_pud_init() splits an existing 1 GB huge page (pud_leaf(*pud))
into PMD table entries, it extracts the page protection attributes but
does not clear the existing PUD entry (*pud).

Subsequently, pud_populate_init() -> set_pud_safe() is invoked to link
the new PMD table. Because *pud is still populated with the 1 GB huge
page entry, set_pud_safe() detects a present entry that differs from the
new PMD table pointer, triggering a WARN_ON_ONCE call trace in dmesg:

  WARNING: CPU: 0 PID: 0 at arch/x86/mm/init_64.c:89 phys_pud_init+0x2d6/0x390

Call pud_clear(pud) right after extracting protection attributes when
splitting the 1 GB page so that set_pud_safe() can populate the PMD
table cleanly without issuing a warning.

Signed-off-by: Phineas Su <pohaosu@google.com>
---
 arch/x86/mm/init_64.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/arch/x86/mm/init_64.c b/arch/x86/mm/init_64.c
index d57f29ca23a5..fdfcf4a7b090 100644
--- a/arch/x86/mm/init_64.c
+++ b/arch/x86/mm/init_64.c
@@ -659,6 +659,7 @@ phys_pud_init(pud_t *pud_page, unsigned long paddr, unsigned long paddr_end,
 				continue;
 			}
 			prot = pte_pgprot(pte_clrhuge(*(pte_t *)pud));
+			pud_clear(pud);
 		}
 
 		if (page_size_mask & (1<<PG_LEVEL_1G)) {
-- 
2.55.0.229.g6434b31f56-goog
Re: [PATCH] x86/mm: Clear PUD entry before populating PMD table when splitting 1GB page in phys_pud_init
Posted by Dave Hansen 4 days, 14 hours ago
On 7/19/26 11:26, Phineas Su wrote:
> When phys_pud_init() splits an existing 1 GB huge page (pud_leaf(*pud))
> into PMD table entries, it extracts the page protection attributes but
> does not clear the existing PUD entry (*pud).
> 
> Subsequently, pud_populate_init() -> set_pud_safe() is invoked to link
> the new PMD table. Because *pud is still populated with the 1 GB huge
> page entry, set_pud_safe() detects a present entry that differs from the
> new PMD table pointer, triggering a WARN_ON_ONCE call trace in dmesg:
> 
>   WARNING: CPU: 0 PID: 0 at arch/x86/mm/init_64.c:89 phys_pud_init+0x2d6/0x390
> 
> Call pud_clear(pud) right after extracting protection attributes when
> splitting the 1 GB page so that set_pud_safe() can populate the PMD
> table cleanly without issuing a warning.

Why aren't we running in to this left and right? I wouldn't think
splitting a PUD would be _that_ rare.
Re: [PATCH] x86/mm: Clear PUD entry before populating PMD table when splitting 1GB page in phys_pud_init
Posted by Phineas Su 3 days, 9 hours ago
On Mon, Jul 20, 2026 at 9:08 PM Dave Hansen <dave.hansen@intel.com> wrote:
>
> On 7/19/26 11:26, Phineas Su wrote:
> > When phys_pud_init() splits an existing 1 GB huge page (pud_leaf(*pud))
> > into PMD table entries, it extracts the page protection attributes but
> > does not clear the existing PUD entry (*pud).
> >
> > Subsequently, pud_populate_init() -> set_pud_safe() is invoked to link
> > the new PMD table. Because *pud is still populated with the 1 GB huge
> > page entry, set_pud_safe() detects a present entry that differs from the
> > new PMD table pointer, triggering a WARN_ON_ONCE call trace in dmesg:
> >
> >   WARNING: CPU: 0 PID: 0 at arch/x86/mm/init_64.c:89 phys_pud_init+0x2d6/0x390
> >
> > Call pud_clear(pud) right after extracting protection attributes when
> > splitting the 1 GB page so that set_pud_safe() can populate the PMD
> > table cleanly without issuing a warning.
>
> Why aren't we running in to this left and right? I wouldn't think
> splitting a PUD would be _that_ rare.

I'm not sure how common or rare PUD splitting is overall, but here is how
we hit it in our setup:
1. A 1 GB physical memory block is mapped as a 1 GB leaf page (pud_large(*pud))
   during an initial top-down mapping step in memory_map_top_down().
2. A non-1GB-aligned physical memory reservation (such as the AMD SEV-SNP RMP
   table or CXL reserved regions) splits physical memory near that 1
GB boundary.
3. A subsequent pass in init_range_memory_mapping() maps a small usable RAM
   fragment adjacent to the reservation within that same 1 GB block.
   split_mem_range() strips PG_LEVEL_1G due to sub-1G alignment, forcing
   phys_pud_init() to split the 1 GB PUD entry into PMD table entries.
Without pud_clear(pud), *pud remains populated with the old 1 GB leaf entry
when pud_populate_init() -> set_pud_safe() is invoked, causing set_pud_safe()
to detect a present entry (pud_present(*pud) && !pud_same(*pud, new_pmd)) and
trigger WARN_ON_ONCE.

Also, after auditing the other phys_xxx_init() functions, phys_pmd_init() has
the exact same issue when splitting 2 MB pages into 4 KB PTEs. I will send v2
including both fixes.
[PATCH v2] x86/mm: Clear PUD/PMD entries when splitting huge pages
Posted by Phineas Su 3 days, 9 hours ago
When phys_pud_init() or phys_pmd_init() splits an existing huge page
(pud_leaf(*pud) or pmd_leaf(*pmd)) into lower-level table entries, it
extracts the page protection attributes but does not clear the existing
PUD/PMD entry (*pud or *pmd).

Subsequently, set_pud_safe() / set_pmd_safe() is invoked to link the new
PMD/PTE table. Because *pud / *pmd is still populated with the huge page
entry, set_pud_safe() / set_pmd_safe() detects a present entry that differs
from the new table pointer, triggering a WARN_ON_ONCE call trace:

  WARNING: CPU: 0 PID: 0 at arch/x86/mm/init_64.c:89 phys_pud_init+0x2d6/0x390

Call pud_clear(pud) and pmd_clear(pmd) right after extracting protection
attributes when splitting 1 GB / 2 MB huge pages so that set_pud_safe() and
set_pmd_safe() can populate lower-level tables cleanly without issuing a
warning.

Signed-off-by: Phineas Su <pohaosu@google.com>
---
v2:
 - Also add pmd_clear(pmd) in phys_pmd_init() to handle 2 MB huge page splitting.
 - Update commit message to cover both PUD and PMD splitting.

arch/x86/mm/init_64.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/arch/x86/mm/init_64.c b/arch/x86/mm/init_64.c
index d57f29ca23a5..55845c40c1ca 100644
--- a/arch/x86/mm/init_64.c
+++ b/arch/x86/mm/init_64.c
@@ -573,6 +573,7 @@ phys_pmd_init(pmd_t *pmd_page, unsigned long paddr, unsigned long paddr_end,
 				continue;
 			}
 			new_prot = pte_pgprot(pte_clrhuge(*(pte_t *)pmd));
+			pmd_clear(pmd);
 		}
 
 		if (page_size_mask & (1<<PG_LEVEL_2M)) {
@@ -659,6 +660,7 @@ phys_pud_init(pud_t *pud_page, unsigned long paddr, unsigned long paddr_end,
 				continue;
 			}
 			prot = pte_pgprot(pte_clrhuge(*(pte_t *)pud));
+			pud_clear(pud);
 		}
 
 		if (page_size_mask & (1<<PG_LEVEL_1G)) {
-- 
2.55.0.229.g6434b31f56-goog