git: 592ede65bd48 - main - libkvm: support powerpc64 radix minidumps

From: Minsoo Choo <mchoo_at_FreeBSD.org>
Date: Mon, 28 Sep 2026 15:54:55 UTC
The branch main has been updated by mchoo:

URL: https://cgit.FreeBSD.org/src/commit/?id=592ede65bd48f16e5c46a016e92b732ed90094d2

commit 592ede65bd48f16e5c46a016e92b732ed90094d2
Author:     Minsoo Choo <mchoo@FreeBSD.org>
AuthorDate: 2026-09-16 04:20:45 +0000
Commit:     Minsoo Choo <mchoo@FreeBSD.org>
CommitDate: 2026-09-28 15:54:20 +0000

    libkvm: support powerpc64 radix minidumps
    
    PowerPC64 radix minidumps use a 64KB root directory followed by three
    levels of 4KB page tables.  Add an MMU backend which walks those tables,
    handles large-page leaves, and decodes their always-big-endian entries
    on both powerpc64 and powerpc64le.
    
    Reject old radix minidumps whose pmap section is empty with a specific
    diagnostic.  Add a synthetic powerpc64le dump test which reads a page
    through both its kernel and direct-map addresses.
    
    PR:             298532
    Reviewed by:    jhb
    Approved by:    jhb (mentor)
    MFC after:      2 weeks
    Sponsored by:   FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D59743
---
 lib/libkvm/Makefile                       |   3 +-
 lib/libkvm/kvm_minidump_powerpc64.c       |   2 +
 lib/libkvm/kvm_minidump_powerpc64_radix.c | 270 ++++++++++++++++++++++++++++++
 lib/libkvm/kvm_powerpc64.h                |   2 +
 lib/libkvm/tests/kvm_open2_test.c         | 118 +++++++++++++
 5 files changed, 394 insertions(+), 1 deletion(-)

diff --git a/lib/libkvm/Makefile b/lib/libkvm/Makefile
index 94a1173010a4..725491e1edac 100644
--- a/lib/libkvm/Makefile
+++ b/lib/libkvm/Makefile
@@ -13,7 +13,8 @@ SRCS=	kvm.c kvm_cptime.c kvm_getloadavg.c \
 	kvm_i386.c kvm_minidump_i386.c \
 	kvm_powerpc.c kvm_powerpc64.c \
 	kvm_minidump_riscv.c \
-	kvm_minidump_powerpc64.c kvm_minidump_powerpc64_hpt.c
+	kvm_minidump_powerpc64.c kvm_minidump_powerpc64_hpt.c \
+	kvm_minidump_powerpc64_radix.c
 INCS=	kvm.h
 
 LIBADD=	elf
diff --git a/lib/libkvm/kvm_minidump_powerpc64.c b/lib/libkvm/kvm_minidump_powerpc64.c
index f39a6ed9393b..7de5d8c0d458 100644
--- a/lib/libkvm/kvm_minidump_powerpc64.c
+++ b/lib/libkvm/kvm_minidump_powerpc64.c
@@ -131,6 +131,8 @@ _powerpc64_minidump_initvtop(kvm_t *kd)
 	if (strcmp(mmu_name, PPC64_MMU_G5) == 0 ||
 	    strcmp(mmu_name, PPC64_MMU_PHYP) == 0)
 		PPC64_MMU_OPS(kd) = ppc64_mmu_ops_hpt;
+	else if (strcmp(mmu_name, PPC64_MMU_RADIX) == 0)
+		PPC64_MMU_OPS(kd) = ppc64_mmu_ops_radix;
 	else {
 		_kvm_err(kd, kd->program, "unsupported MMU: %s", mmu_name);
 		goto failed;
diff --git a/lib/libkvm/kvm_minidump_powerpc64_radix.c b/lib/libkvm/kvm_minidump_powerpc64_radix.c
new file mode 100644
index 000000000000..1d187e0e55e5
--- /dev/null
+++ b/lib/libkvm/kvm_minidump_powerpc64_radix.c
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2026 FreeBSD Foundation
+ *
+ * This software was developed by Minsoo Choo under sponsorship from the
+ * FreeBSD Foundation.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/endian.h>
+#include <vm/vm.h>
+
+#include <kvm.h>
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "../../sys/powerpc/include/minidump.h"
+#include "kvm_private.h"
+#include "kvm_powerpc64.h"
+
+/* Radix tree geometry. */
+#define	RADIX_ROOT_SHIFT	16
+#define	RADIX_ROOT_SIZE		(1UL << RADIX_ROOT_SHIFT)
+#define	RADIX_ROOT_ENTRIES	(RADIX_ROOT_SIZE / sizeof(uint64_t))
+#define	RADIX_ENTRIES		(PPC64_PAGE_SIZE / sizeof(uint64_t))
+#define	RADIX_INDEX_MASK	(RADIX_ENTRIES - 1)
+
+#define	RADIX_L1_SHIFT		39
+#define	RADIX_L2_SHIFT		30
+#define	RADIX_L3_SHIFT		21
+
+/* Radix PTE fields.  Radix PTEs are stored in big-endian byte order. */
+#define	RPTE_VALID		0x8000000000000000ULL
+#define	RPTE_LEAF		0x4000000000000000ULL
+#define	RPTE_RPN_MASK		0x00fffffffffff000ULL
+#define	RPDE_NLB_MASK		0x00ffffffffffff00ULL
+#define	RPTE_EAA_R		0x0000000000000004ULL
+#define	RPTE_EAA_W		0x0000000000000002ULL
+#define	RPTE_EAA_X		0x0000000000000001ULL
+
+static int
+radix_root_entry(kvm_t *kd, u_long index, uint64_t *entry)
+{
+	uint64_t *p;
+
+	p = _kvm_pmap_get(kd, index, sizeof(*p));
+	if (p == NULL)
+		return (-1);
+	*entry = be64toh(*p);
+	return (0);
+}
+
+static int
+radix_table_entry(kvm_t *kd, uint64_t pa, u_long index, uint64_t *entry)
+{
+	uint64_t *table;
+
+	table = _kvm_map_get(kd, pa, PPC64_PAGE_SIZE);
+	if (table == NULL) {
+		_kvm_err(kd, kd->program,
+		    "radix page table page at 0x%jx was not dumped",
+		    (uintmax_t)pa);
+		return (-1);
+	}
+	*entry = be64toh(table[index]);
+	return (0);
+}
+
+static int
+radix_lookup(kvm_t *kd, kvaddr_t va, uint64_t *pap, uint64_t *entryp,
+    uint64_t *pageszp)
+{
+	static const int shifts[] = {
+		RADIX_L1_SHIFT, RADIX_L2_SHIFT, RADIX_L3_SHIFT,
+		PPC64_PAGE_SHIFT
+	};
+	uint64_t entry, pa, pagesz;
+	u_long index;
+	u_int level;
+
+	index = (va >> RADIX_L1_SHIFT) & (RADIX_ROOT_ENTRIES - 1);
+	if (radix_root_entry(kd, index, &entry) != 0)
+		return (-1);
+	for (level = 0; level < nitems(shifts); level++) {
+		if ((entry & RPTE_VALID) == 0)
+			return (0);
+		pagesz = 1ULL << shifts[level];
+		if ((entry & RPTE_LEAF) != 0) {
+			pa = (entry & RPTE_RPN_MASK) & ~(pagesz - 1);
+			*pap = pa | (va & (pagesz - 1));
+			*entryp = entry;
+			*pageszp = pagesz;
+			return (1);
+		}
+		if (level == nitems(shifts) - 1)
+			return (0);
+		pa = entry & RPDE_NLB_MASK;
+		index = (va >> shifts[level + 1]) & RADIX_INDEX_MASK;
+		if (radix_table_entry(kd, pa, index, &entry) != 0)
+			return (-1);
+	}
+	return (0);
+}
+
+static int
+ppc64mmu_radix_init(kvm_t *kd)
+{
+	struct minidumphdr *hdr;
+
+	hdr = &kd->vmst->hdr;
+	if (hdr->pmapsize == 0) {
+		_kvm_err(kd, kd->program,
+		    "radix minidump does not contain a page map");
+		return (-1);
+	}
+	if (hdr->pmapsize != RADIX_ROOT_SIZE) {
+		_kvm_err(kd, kd->program,
+		    "unexpected radix page map size: %u", hdr->pmapsize);
+		return (-1);
+	}
+	return (0);
+}
+
+static void
+ppc64mmu_radix_cleanup(kvm_t *kd __unused)
+{
+}
+
+static int
+ppc64mmu_radix_kvatop(kvm_t *kd, kvaddr_t va, off_t *offp)
+{
+	struct minidumphdr *hdr;
+	uint64_t entry, pa, pagesz;
+	off_t off;
+	int found;
+
+	hdr = &kd->vmst->hdr;
+	if (va < hdr->dmapbase)
+		va += hdr->startkernel - PPC64_KERNBASE;
+
+	if (va >= hdr->dmapbase && va <= hdr->dmapend)
+		pa = va - hdr->dmapbase;
+	else if (va >= hdr->kernbase) {
+		found = radix_lookup(kd, va, &pa, &entry, &pagesz);
+		if (found < 0)
+			return (0);
+		if (found == 0)
+			goto invalid;
+	} else
+		goto invalid;
+
+	off = _kvm_pt_find(kd, trunc_page(pa), PPC64_PAGE_SIZE);
+	if (off == -1)
+		goto invalid;
+	*offp = off + (pa & PPC64_PAGE_MASK);
+	return (PPC64_PAGE_SIZE - (pa & PPC64_PAGE_MASK));
+
+invalid:
+	_kvm_err(kd, 0, "invalid address (0x%jx)", (uintmax_t)va);
+	return (0);
+}
+
+#if ULONG_MAX > UINT32_MAX
+static vm_prot_t
+radix_entry_to_prot(uint64_t entry)
+{
+	vm_prot_t prot;
+
+	prot = 0;
+	if ((entry & (RPTE_EAA_R | RPTE_EAA_W)) != 0)
+		prot |= VM_PROT_READ;
+	if ((entry & RPTE_EAA_W) != 0)
+		prot |= VM_PROT_WRITE;
+	if ((entry & RPTE_EAA_X) != 0)
+		prot |= VM_PROT_EXECUTE;
+	return (prot);
+}
+
+static int
+radix_walk_table(kvm_t *kd, kvm_walk_pages_cb_t *cb, void *arg,
+    uint64_t table_pa, uint64_t va, u_int level)
+{
+	static const int shifts[] = {
+		RADIX_L2_SHIFT, RADIX_L3_SHIFT, PPC64_PAGE_SHIFT
+	};
+	struct minidumphdr *hdr;
+	uint64_t dva, entry, pa, pagesz, subva;
+	u_long i;
+
+	hdr = &kd->vmst->hdr;
+	for (i = 0; i < RADIX_ENTRIES; i++) {
+		if (radix_table_entry(kd, table_pa, i, &entry) != 0)
+			return (0);
+		if ((entry & RPTE_VALID) == 0)
+			continue;
+		subva = va | (i << shifts[level]);
+		if ((entry & RPTE_LEAF) == 0) {
+			if (level == nitems(shifts) - 1)
+				continue;
+			if (!radix_walk_table(kd, cb, arg,
+			    entry & RPDE_NLB_MASK, subva, level + 1))
+				return (0);
+			continue;
+		}
+		if (subva < hdr->kernbase)
+			continue;
+		pagesz = 1ULL << shifts[level];
+		pa = (entry & RPTE_RPN_MASK) & ~(pagesz - 1);
+		dva = hdr->dmapbase + pa;
+		if (!_kvm_visit_cb(kd, cb, arg, pa, subva, dva,
+		    radix_entry_to_prot(entry), pagesz, PPC64_PAGE_SIZE))
+			return (0);
+	}
+	return (1);
+}
+
+static int
+ppc64mmu_radix_walk_pages(kvm_t *kd, kvm_walk_pages_cb_t *cb, void *arg)
+{
+	struct minidumphdr *hdr;
+	uint64_t entry, pa, va;
+	u_long i;
+
+	hdr = &kd->vmst->hdr;
+	for (i = 0; i < RADIX_ROOT_ENTRIES; i++) {
+		if (radix_root_entry(kd, i, &entry) != 0)
+			return (0);
+		if ((entry & RPTE_VALID) == 0)
+			continue;
+		va = hdr->dmapbase | ((uint64_t)i << RADIX_L1_SHIFT);
+		if (va < hdr->kernbase)
+			continue;
+		if ((entry & RPTE_LEAF) != 0) {
+			pa = (entry & RPTE_RPN_MASK) &
+			    ~((1ULL << RADIX_L1_SHIFT) - 1);
+			if (!_kvm_visit_cb(kd, cb, arg,
+			    pa, va, hdr->dmapbase + pa,
+			    radix_entry_to_prot(entry),
+			    1ULL << RADIX_L1_SHIFT, PPC64_PAGE_SIZE))
+				return (0);
+			continue;
+		}
+		if (!radix_walk_table(kd, cb, arg, entry & RPDE_NLB_MASK,
+		    va, 0))
+			return (0);
+	}
+	return (1);
+}
+#else
+static int
+ppc64mmu_radix_walk_pages(kvm_t *kd, kvm_walk_pages_cb_t *cb __unused,
+    void *arg __unused)
+{
+
+	_kvm_err(kd, kd->program,
+	    "walking powerpc64 radix pages requires a 64-bit host");
+	return (0);
+}
+#endif
+
+static struct ppc64_mmu_ops ops = {
+	.init		= ppc64mmu_radix_init,
+	.cleanup	= ppc64mmu_radix_cleanup,
+	.kvatop		= ppc64mmu_radix_kvatop,
+	.walk_pages	= ppc64mmu_radix_walk_pages,
+};
+
+struct ppc64_mmu_ops *ppc64_mmu_ops_radix = &ops;
diff --git a/lib/libkvm/kvm_powerpc64.h b/lib/libkvm/kvm_powerpc64.h
index efd7ce0d3e7f..591e87364453 100644
--- a/lib/libkvm/kvm_powerpc64.h
+++ b/lib/libkvm/kvm_powerpc64.h
@@ -49,6 +49,7 @@
 
 #define	PPC64_MMU_G5		"mmu_g5"
 #define	PPC64_MMU_PHYP		"mmu_phyp"
+#define	PPC64_MMU_RADIX		"mmu_radix"
 
 /* MMU interface */
 #define	PPC64_MMU_OPS(kd)	(kd)->vmst->mmu.ops
@@ -75,5 +76,6 @@ struct vmstate {
 };
 
 extern struct ppc64_mmu_ops *ppc64_mmu_ops_hpt;
+extern struct ppc64_mmu_ops *ppc64_mmu_ops_radix;
 
 #endif /* !__KVM_POWERPC64_H__ */
diff --git a/lib/libkvm/tests/kvm_open2_test.c b/lib/libkvm/tests/kvm_open2_test.c
index 5f882578c1f2..52e5c2f21d0f 100644
--- a/lib/libkvm/tests/kvm_open2_test.c
+++ b/lib/libkvm/tests/kvm_open2_test.c
@@ -39,8 +39,18 @@
 
 #include <atf-c.h>
 
+#include "../../../sys/powerpc/include/minidump.h"
 #include "kvm_test_common.h"
 
+#define	PPC64_DMAP_BASE		0xc000000000000000ULL
+#define	PPC64_KERN_BASE		0xc008000000000000ULL
+#define	RADIX_ROOT_SIZE		(1UL << 16)
+#define	RADIX_ROOT_ENTRIES	(RADIX_ROOT_SIZE / sizeof(uint64_t))
+#define	RADIX_VALID		0x8000000000000000ULL
+#define	RADIX_LEAF		0x4000000000000000ULL
+#define	RADIX_READ		0x4
+#define	RADIX_WRITE		0x2
+
 static int
 unresolved_symbol(const char *name __unused, kvaddr_t *value __unused)
 {
@@ -90,6 +100,91 @@ create_invalid_powerpc64_minidump(const char *path)
 	ATF_REQUIRE(close(fd) == 0);
 }
 
+static void
+write_all(int fd, const void *buf, size_t len)
+{
+	const char *p;
+	ssize_t n;
+
+	p = buf;
+	while (len != 0) {
+		n = write(fd, p, len);
+		ATF_REQUIRE(n > 0);
+		p += n;
+		len -= n;
+	}
+}
+
+static void
+create_powerpc64le_radix_minidump(const char *path)
+{
+	struct minidumphdr hdr;
+	uint64_t entry, *dump_avail, *root;
+	uint8_t *page;
+	size_t root_index;
+	int fd, i;
+
+	dump_avail = calloc(1, PAGE_SIZE);
+	root = calloc(1, RADIX_ROOT_SIZE);
+	page = calloc(1, PAGE_SIZE);
+	ATF_REQUIRE(dump_avail != NULL);
+	ATF_REQUIRE(root != NULL);
+	ATF_REQUIRE(page != NULL);
+
+	memset(&hdr, 0, sizeof(hdr));
+	strlcpy(hdr.magic, MINIDUMP_MAGIC, sizeof(hdr.magic));
+	strlcpy(hdr.mmu_name, "mmu_radix", sizeof(hdr.mmu_name));
+	hdr.version = htole32(MINIDUMP_VERSION);
+	hdr.bitmapsize = htole32(PAGE_SIZE);
+	hdr.pmapsize = htole32(RADIX_ROOT_SIZE);
+	hdr.kernbase = htole64(PPC64_KERN_BASE);
+	hdr.kernend = htole64(PPC64_KERN_BASE + PAGE_SIZE);
+	hdr.dmapbase = htole64(PPC64_DMAP_BASE);
+	hdr.dmapend = htole64(PPC64_DMAP_BASE + 0x10000);
+	hdr.startkernel = htole64(PPC64_DMAP_BASE + 0x4000);
+	hdr.endkernel = htole64(PPC64_DMAP_BASE + 0x5000);
+	hdr.dumpavailsize = htole32(PAGE_SIZE);
+
+	dump_avail[0] = htole64(0);
+	dump_avail[1] = htole64(0x10000);
+
+	root_index = (PPC64_KERN_BASE >> 39) & (RADIX_ROOT_ENTRIES - 1);
+	root[root_index] = htobe64(RADIX_VALID | 0x1000 | 9);
+
+	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+	ATF_REQUIRE(fd != -1);
+	memset(page, 0, PAGE_SIZE);
+	memcpy(page, &hdr, sizeof(hdr));
+	write_all(fd, page, PAGE_SIZE);
+	write_all(fd, dump_avail, PAGE_SIZE);
+	memset(page, 0, PAGE_SIZE);
+	for (i = 1; i <= 4; i++)
+		page[i / NBBY] |= 1U << (i % NBBY);
+	write_all(fd, page, PAGE_SIZE);
+	write_all(fd, root, RADIX_ROOT_SIZE);
+
+	/* L2, L3, and PTE pages, followed by the mapped data page. */
+	for (i = 1; i <= 3; i++) {
+		memset(page, 0, PAGE_SIZE);
+		if (i != 3)
+			entry = htobe64(RADIX_VALID | ((i + 1) * 0x1000) |
+			    9);
+		else
+			entry = htobe64(RADIX_VALID | RADIX_LEAF |
+			    RADIX_READ | RADIX_WRITE | 0x4000);
+		memcpy(page, &entry, sizeof(entry));
+		write_all(fd, page, PAGE_SIZE);
+	}
+	memset(page, 0, PAGE_SIZE);
+	memcpy(page, "radix minidump", sizeof("radix minidump"));
+	write_all(fd, page, PAGE_SIZE);
+	ATF_REQUIRE(close(fd) == 0);
+
+	free(page);
+	free(root);
+	free(dump_avail);
+}
+
 ATF_TC_WITHOUT_HEAD(kvm_open2_negative_test_nonexistent_corefile);
 ATF_TC_BODY(kvm_open2_negative_test_nonexistent_corefile, tc)
 {
@@ -167,6 +262,28 @@ ATF_TC_BODY(kvm_open2_powerpc64le_minidump_probe, tc)
 	ATF_CHECK_MATCH("wrong minidump version", errbuf);
 }
 
+ATF_TC_WITHOUT_HEAD(kvm_open2_powerpc64le_radix_minidump);
+ATF_TC_BODY(kvm_open2_powerpc64le_radix_minidump, tc)
+{
+	char buf[sizeof("radix minidump")];
+	kvm_t *kd;
+
+	create_powerpc64le_kernel("kernel-radix");
+	create_powerpc64le_radix_minidump("vmcore-radix");
+	errbuf_clear();
+	kd = kvm_open2("kernel-radix", "vmcore-radix", O_RDONLY, errbuf,
+	    unresolved_symbol);
+	ATF_REQUIRE_MSG(kd != NULL, "kvm_open2 failed: %s", errbuf);
+	ATF_REQUIRE_EQ(kvm_read2(kd, PPC64_KERN_BASE, buf, sizeof(buf)),
+	    (ssize_t)sizeof(buf));
+	ATF_CHECK_STREQ(buf, "radix minidump");
+	memset(buf, 0, sizeof(buf));
+	ATF_REQUIRE_EQ(kvm_read2(kd, PPC64_DMAP_BASE + 0x4000, buf,
+	    sizeof(buf)), (ssize_t)sizeof(buf));
+	ATF_CHECK_STREQ(buf, "radix minidump");
+	ATF_REQUIRE(kvm_close(kd) == 0);
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 
@@ -175,6 +292,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, kvm_open2_negative_test_nonexistent_corefile);
 	ATF_TP_ADD_TC(tp, kvm_open2_negative_test_nonexistent_execfile);
 	ATF_TP_ADD_TC(tp, kvm_open2_powerpc64le_minidump_probe);
+	ATF_TP_ADD_TC(tp, kvm_open2_powerpc64le_radix_minidump);
 
 	return (atf_no_error());
 }