git: 9313f6b01485 - main - acpi: Add a pseudo-bus for APEI devices to manage resources

From: John Baldwin <jhb_at_FreeBSD.org>
Date: Tue, 07 Jul 2026 19:18:16 UTC
The branch main has been updated by jhb:

URL: https://cgit.FreeBSD.org/src/commit/?id=9313f6b01485ad9a0b7cc59b459f5714533587c3

commit 9313f6b01485ad9a0b7cc59b459f5714533587c3
Author:     John Baldwin <jhb@FreeBSD.org>
AuthorDate: 2026-07-07 18:13:51 +0000
Commit:     John Baldwin <jhb@FreeBSD.org>
CommitDate: 2026-07-07 18:13:51 +0000

    acpi: Add a pseudo-bus for APEI devices to manage resources
    
    Different APEI tables can reuse the same registers (and sometimes
    different views of the same register, e.g. 32- vs 64-bit mappings of
    the same register).  To enable this sharing, apei0 now acts as a bus
    device managing a pool of allocated resources and handing out mappings
    to child devices which handle individual tables.
    
    Most of the previous apei(4) driver has been moved into a new
    hest0 device that is a child of apei0.
    
    Reviewed by:    gallatin
    Sponsored by:   Netflix
    Differential Revision:  https://reviews.freebsd.org/D58024
---
 sys/conf/files                 |   2 +
 sys/dev/acpica/acpi_apei.c     | 159 ++++-----------
 sys/dev/acpica/acpi_apei_bus.c | 435 +++++++++++++++++++++++++++++++++++++++++
 sys/dev/acpica/apei_if.m       |  49 +++++
 sys/dev/acpica/apeivar.h       |  54 +++++
 5 files changed, 579 insertions(+), 120 deletions(-)

diff --git a/sys/conf/files b/sys/conf/files
index cc18ecbb9301..3635e6474708 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -759,6 +759,7 @@ dev/acpica/Osd/OsdTable.c	optional acpi
 dev/acpica/acpi.c		optional acpi
 dev/acpica/acpi_acad.c		optional acpi
 dev/acpica/acpi_apei.c		optional acpi
+dev/acpica/acpi_apei_bus.c	optional acpi
 dev/acpica/acpi_battery.c	optional acpi
 dev/acpica/acpi_button.c	optional acpi
 dev/acpica/acpi_cmbat.c		optional acpi
@@ -779,6 +780,7 @@ dev/acpica/acpi_throttle.c	optional acpi
 dev/acpica/acpi_video.c		optional acpi_video acpi
 dev/acpica/acpi_dock.c		optional acpi_dock acpi
 dev/acpica/acpi_spmc.c		optional acpi
+dev/acpica/apei_if.m		optional acpi
 dev/adlink/adlink.c		optional adlink
 dev/ae/if_ae.c			optional ae pci
 dev/age/if_age.c		optional age pci
diff --git a/sys/dev/acpica/acpi_apei.c b/sys/dev/acpica/acpi_apei.c
index 925558d585bf..09073207c06e 100644
--- a/sys/dev/acpica/acpi_apei.c
+++ b/sys/dev/acpica/acpi_apei.c
@@ -48,6 +48,7 @@
 #include <contrib/dev/acpica/include/actables.h>
 
 #include <dev/acpica/acpivar.h>
+#include <dev/acpica/apeivar.h>
 #include <dev/pci/pcireg.h>
 #include <dev/pci/pcivar.h>
 
@@ -56,12 +57,8 @@ struct apei_ge {
 		ACPI_HEST_GENERIC v1;
 		ACPI_HEST_GENERIC_V2 v2;
 	};
-	int		 res_type;
-	int		 res_rid;
-	struct resource	*res;
-	int		 res2_type;
-	int		 res2_rid;
-	struct resource	*res2;
+	struct resource_map *res;
+	struct resource_map *res2;
 	uint8_t		*buf, *copybuf;
 	TAILQ_ENTRY(apei_ge) link;
 	TAILQ_ENTRY(apei_ge) nlink;
@@ -85,7 +82,7 @@ struct apei_pges {
 	TAILQ_HEAD(, apei_ge) ges;
 };
 
-struct apei_softc {
+struct hest_softc {
 	TAILQ_HEAD(, apei_ge) ges;
 	struct apei_nges nges;
 	struct apei_iges iges;
@@ -128,26 +125,6 @@ struct apei_pcie_error {
 	uint8_t		AERInfo[96];
 };
 
-#ifdef __i386__
-static __inline uint64_t
-apei_bus_read_8(struct resource *res, bus_size_t offset)
-{
-	return (bus_read_4(res, offset) |
-	    ((uint64_t)bus_read_4(res, offset + 4)) << 32);
-}
-static __inline void
-apei_bus_write_8(struct resource *res, bus_size_t offset, uint64_t val)
-{
-	bus_write_4(res, offset, val);
-	bus_write_4(res, offset + 4, val >> 32);
-}
-#define	READ8(r, o)	apei_bus_read_8((r), (o))
-#define	WRITE8(r, o, v)	apei_bus_write_8((r), (o), (v))
-#else
-#define	READ8(r, o)	bus_read_8((r), (o))
-#define	WRITE8(r, o, v)	bus_write_8((r), (o), (v))
-#endif
-
 #define GED_SIZE(ged)	((ged)->Revision >= 0x300 ? \
     sizeof(ACPI_HEST_GENERIC_DATA_V300) : sizeof(ACPI_HEST_GENERIC_DATA))
 #define GED_DATA(ged)	((uint8_t *)(ged) + GED_SIZE(ged))
@@ -431,10 +408,10 @@ apei_ge_handler(struct apei_ge *ge, bool copy)
 	ges->BlockStatus = 0;
 	if (!copy && ge->v1.Header.Type == ACPI_HEST_TYPE_GENERIC_ERROR_V2 &&
 	    ge->res2) {
-		uint64_t val = READ8(ge->res2, 0);
+		uint64_t val = bus_read_8(ge->res2, 0);
 		val &= ge->v2.ReadAckPreserve;
 		val |= ge->v2.ReadAckWrite;
-		WRITE8(ge->res2, 0, val);
+		bus_write_8(ge->res2, 0, val);
 	}
 
 	/* If ACPI told the error is fatal -- make it so. */
@@ -484,10 +461,10 @@ apei_nmi_handler(void)
 		ges->BlockStatus = 0;
 		if (ge->v1.Header.Type == ACPI_HEST_TYPE_GENERIC_ERROR_V2 &&
 		    ge->res2) {
-			uint64_t val = READ8(ge->res2, 0);
+			uint64_t val = bus_read_8(ge->res2, 0);
 			val &= ge->v2.ReadAckPreserve;
 			val |= ge->v2.ReadAckWrite;
-			WRITE8(ge->res2, 0, val);
+			bus_write_8(ge->res2, 0, val);
 		}
 		handled = 1;
 	}
@@ -514,7 +491,7 @@ static void
 apei_notify_handler(ACPI_HANDLE h, UINT32 notify, void *context)
 {
 	device_t dev = context;
-	struct apei_softc *sc = device_get_softc(dev);
+	struct hest_softc *sc = device_get_softc(dev);
 	struct apei_ge *ge;
 
 	TAILQ_FOREACH(ge, &sc->iges.ges, nlink)
@@ -522,7 +499,7 @@ apei_notify_handler(ACPI_HANDLE h, UINT32 notify, void *context)
 }
 
 static int
-hest_parse_structure(struct apei_softc *sc, void *addr, int remaining)
+hest_parse_structure(struct hest_softc *sc, void *addr, int remaining)
 {
 	ACPI_HEST_HEADER *hdr = addr;
 	struct apei_ge *ge;
@@ -582,7 +559,7 @@ hest_parse_structure(struct apei_softc *sc, void *addr, int remaining)
 }
 
 static void
-hest_parse_table(ACPI_TABLE_HEST *hest, struct apei_softc *sc)
+hest_parse_table(ACPI_TABLE_HEST *hest, struct hest_softc *sc)
 {
 	char *cp;
 	int remaining, consumed;
@@ -598,95 +575,43 @@ hest_parse_table(ACPI_TABLE_HEST *hest, struct apei_softc *sc)
 	}
 }
 
-static char *apei_ids[] = { "PNP0C33", NULL };
-
-static ACPI_STATUS
-apei_find(ACPI_HANDLE handle, UINT32 level, void *context,
-    void **status)
-{
-	int *found = (int *)status;
-	char **ids;
-
-	for (ids = apei_ids; *ids != NULL; ids++) {
-		if (acpi_MatchHid(handle, *ids)) {
-			*found = 1;
-			break;
-		}
-	}
-	return (AE_OK);
-}
-
 static void
-apei_identify(driver_t *driver, device_t parent)
+hest_identify(driver_t *driver, device_t parent)
 {
 	device_t	child;
-	int		found;
 	ACPI_TABLE_HEADER *hest;
 	ACPI_STATUS	status;
 
-	if (acpi_disabled("apei"))
-		return;
-
 	/* Without HEST table we have nothing to do. */
 	status = AcpiGetTable(ACPI_SIG_HEST, 0, &hest);
 	if (ACPI_FAILURE(status))
 		return;
 	AcpiPutTable(hest);
 
-	/* Only one APEI device can exist. */
-	if (devclass_get_device(devclass_find("apei"), 0))
+	if (device_find_child(parent, "hest", DEVICE_UNIT_ANY) != NULL)
 		return;
 
-	/* Search for ACPI error device to be used. */
-	found = 0;
-	AcpiWalkNamespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT,
-	    100, apei_find, NULL, NULL, (void *)&found);
-	if (found)
-		return;
-
-	/* If not found - create a fake one. */
-	child = BUS_ADD_CHILD(parent, 2, "apei", 0);
+	child = BUS_ADD_CHILD(parent, 0, "hest", DEVICE_UNIT_ANY);
 	if (child == NULL)
 		printf("%s: can't add child\n", __func__);
 }
 
 static int
-apei_probe(device_t dev)
+hest_probe(device_t dev)
 {
-	ACPI_TABLE_HEADER *hest;
-	ACPI_STATUS	status;
-	int rv;
-
-	if (acpi_disabled("apei"))
-		return (ENXIO);
-
-	if (acpi_get_handle(dev) != NULL) {
-		rv = ACPI_ID_PROBE(device_get_parent(dev), dev, apei_ids, NULL);
-		if (rv > 0)
-			return (rv);
-	} else
-		rv = 0;
-
-	/* Without HEST table we have nothing to do. */
-	status = AcpiGetTable(ACPI_SIG_HEST, 0, &hest);
-	if (ACPI_FAILURE(status))
-		return (ENXIO);
-	AcpiPutTable(hest);
-
-	device_set_desc(dev, "ACPI Platform Error Interface");
-	return (rv);
+	device_set_desc(dev, "APEI Hardware Errors");
+	return (BUS_PROBE_NOWILDCARD);
 }
 
 static int
-apei_attach(device_t dev)
+hest_attach(device_t dev)
 {
-	struct apei_softc *sc = device_get_softc(dev);
+	struct hest_softc *sc = device_get_softc(dev);
 	ACPI_TABLE_HEADER *hest;
 	struct acpi_softc *acpi_sc;
 	struct apei_pges *pges;
 	struct apei_ge *ge;
 	ACPI_STATUS status;
-	int rid;
 
 	if (!apei_sysctl_tree) {
 		/* Install hw.acpi.apei sysctl tree */
@@ -717,21 +642,17 @@ apei_attach(device_t dev)
 	hest_parse_table((ACPI_TABLE_HEST *)hest, sc);
 	AcpiPutTable(hest);
 
-	rid = 0;
 	TAILQ_FOREACH(ge, &sc->ges, link) {
-		ge->res_rid = rid++;
-		acpi_bus_alloc_gas(dev, &ge->res_type, ge->res_rid,
-		    &ge->v1.ErrorStatusAddress, &ge->res, 0);
+		ge->res = apei_map_register(dev, &ge->v1.ErrorStatusAddress);
 		if (ge->res) {
-			ge->buf = pmap_mapdev_attr(READ8(ge->res, 0),
+			ge->buf = pmap_mapdev_attr(bus_read_8(ge->res, 0),
 			    ge->v1.ErrorBlockLength, VM_MEMATTR_WRITE_COMBINING);
 		} else {
 			device_printf(dev, "Can't allocate status resource.\n");
 		}
 		if (ge->v1.Header.Type == ACPI_HEST_TYPE_GENERIC_ERROR_V2) {
-			ge->res2_rid = rid++;
-			acpi_bus_alloc_gas(dev, &ge->res2_type, ge->res2_rid,
-			    &ge->v2.ReadAckRegister, &ge->res2, RF_SHAREABLE);
+			ge->res2 = apei_map_register(dev,
+			    &ge->v2.ReadAckRegister);
 			if (ge->res2 == NULL)
 				device_printf(dev, "Can't allocate ack resource.\n");
 		}
@@ -766,9 +687,9 @@ apei_attach(device_t dev)
 }
 
 static int
-apei_detach(device_t dev)
+hest_detach(device_t dev)
 {
-	struct apei_softc *sc = device_get_softc(dev);
+	struct hest_softc *sc = device_get_softc(dev);
 	struct apei_ge *ge;
 
 	apei_nmi = NULL;
@@ -787,12 +708,10 @@ apei_detach(device_t dev)
 	while ((ge = TAILQ_FIRST(&sc->ges)) != NULL) {
 		TAILQ_REMOVE(&sc->ges, ge, link);
 		if (ge->res) {
-			bus_release_resource(dev, ge->res_type,
-			    ge->res_rid, ge->res);
+			apei_unmap_register(dev, ge->res);
 		}
 		if (ge->res2) {
-			bus_release_resource(dev, ge->res2_type,
-			    ge->res2_rid, ge->res2);
+			apei_unmap_register(dev, ge->res2);
 		}
 		if (ge->v1.Notify.Type == ACPI_HEST_NOTIFY_POLLED) {
 			TAILQ_REMOVE(&sc->pges[PGE_ID(ge)].ges, ge, nlink);
@@ -812,23 +731,23 @@ apei_detach(device_t dev)
 	return (0);
 }
 
-static device_method_t apei_methods[] = {
+static device_method_t hest_methods[] = {
 	/* Device interface */
-	DEVMETHOD(device_identify, apei_identify),
-	DEVMETHOD(device_probe, apei_probe),
-	DEVMETHOD(device_attach, apei_attach),
-	DEVMETHOD(device_detach, apei_detach),
+	DEVMETHOD(device_identify, hest_identify),
+	DEVMETHOD(device_probe, hest_probe),
+	DEVMETHOD(device_attach, hest_attach),
+	DEVMETHOD(device_detach, hest_detach),
 	DEVMETHOD_END
 };
 
-static driver_t	apei_driver = {
-	"apei",
-	apei_methods,
-	sizeof(struct apei_softc),
+static driver_t	hest_driver = {
+	"hest",
+	hest_methods,
+	sizeof(struct hest_softc),
 };
 
 static int
-apei_modevent(struct module *mod __unused, int evt, void *cookie __unused)
+hest_modevent(struct module *mod __unused, int evt, void *cookie __unused)
 {
 	int err = 0;
 
@@ -845,5 +764,5 @@ apei_modevent(struct module *mod __unused, int evt, void *cookie __unused)
 	return (err);
 }
 
-DRIVER_MODULE(apei, acpi, apei_driver, apei_modevent, 0);
-MODULE_DEPEND(apei, acpi, 1, 1, 1);
+DRIVER_MODULE(hest, apei, hest_driver, hest_modevent, 0);
+MODULE_DEPEND(hest, acpi, 1, 1, 1);
diff --git a/sys/dev/acpica/acpi_apei_bus.c b/sys/dev/acpica/acpi_apei_bus.c
new file mode 100644
index 000000000000..c6c3a4f11a12
--- /dev/null
+++ b/sys/dev/acpica/acpi_apei_bus.c
@@ -0,0 +1,435 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alexander Motin <mav@FreeBSD.org>
+ * Copyright (c) 2025-2026 Netflix, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/module.h>
+#include <sys/rman.h>
+
+#include <contrib/dev/acpica/include/acpi.h>
+
+#include <dev/acpica/acpivar.h>
+#include <dev/acpica/apeivar.h>
+
+/*
+ * Different APEI tables can reuse the same registers (and sometimes
+ * different views of the same register, e.g. 32- vs 64-bit mappings
+ * of the same register).  To enable this sharing, apei0 acts as a bus
+ * device managing a pool of allocated resources and hands out
+ * mappings to child devices.  Child devices handle individual tables.
+ */
+
+struct apei_register {
+	struct resource *res;
+	TAILQ_ENTRY(apei_register) link;
+};
+
+struct apei_register_mapping {
+	struct resource_map map;
+	struct apei_register *reg;
+	TAILQ_ENTRY(apei_register_mapping) link;
+};
+
+struct apei_ivars {
+	TAILQ_HEAD(, apei_register_mapping) mappings;
+};
+
+TAILQ_HEAD(apei_register_list, apei_register);
+
+struct apei_softc {
+	device_t dev;
+	struct apei_register_list mem;
+	struct apei_register_list io;
+};
+
+static char *apei_ids[] = { "PNP0C33", NULL };
+
+static MALLOC_DEFINE(M_APEI, "apei", "ACPI platform error interface");
+
+static ACPI_STATUS
+apei_find(ACPI_HANDLE handle, UINT32 level, void *context,
+    void **status)
+{
+	int *found = (int *)status;
+	char **ids;
+
+	for (ids = apei_ids; *ids != NULL; ids++) {
+		if (acpi_MatchHid(handle, *ids)) {
+			*found = 1;
+			break;
+		}
+	}
+	return (AE_OK);
+}
+
+static void
+apei_identify(driver_t *driver, device_t parent)
+{
+	device_t	child;
+	int		found;
+
+	if (acpi_disabled("apei"))
+		return;
+
+	/* Only one APEI device can exist. */
+	if (device_find_child(parent, "apei", DEVICE_UNIT_ANY) != NULL)
+		return;
+
+	/* Search for ACPI error device to be used. */
+	found = 0;
+	AcpiWalkNamespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT,
+	    100, apei_find, NULL, NULL, (void *)&found);
+	if (found)
+		return;
+
+	/* If not found - create a fake one. */
+	child = BUS_ADD_CHILD(parent, 2, "apei", 0);
+	if (child == NULL)
+		printf("%s: can't add child\n", __func__);
+}
+
+static int
+apei_probe(device_t dev)
+{
+	int rv;
+
+	if (acpi_disabled("apei"))
+		return (ENXIO);
+
+	if (acpi_get_handle(dev) != NULL) {
+		rv = ACPI_ID_PROBE(device_get_parent(dev), dev, apei_ids, NULL);
+		if (rv > 0)
+			return (rv);
+	}
+
+	device_set_desc(dev, "ACPI Platform Error Interface");
+	return (0);
+}
+
+static int
+apei_attach(device_t dev)
+{
+	struct apei_softc *sc = device_get_softc(dev);
+
+	sc->dev = dev;
+	TAILQ_INIT(&sc->mem);
+	TAILQ_INIT(&sc->io);
+	bus_identify_children(dev);
+	bus_attach_children(dev);
+	return (0);
+}
+
+static device_t
+apei_add_child(device_t dev, u_int order, const char *name, int unit)
+{
+	struct apei_ivars *ai;
+	device_t child;
+
+	ai = malloc(sizeof(*ai), M_APEI, M_WAITOK);
+	TAILQ_INIT(&ai->mappings);
+	child = device_add_child_ordered(dev, order, name, unit);
+	if (child != NULL)
+		device_set_ivars(child, ai);
+	else
+		free(ai, M_APEI);
+	return (child);
+}
+
+static void
+apei_child_deleted(device_t dev, device_t child)
+{
+	struct apei_ivars *ai = device_get_ivars(child);
+
+	MPASS(TAILQ_EMPTY(&ai->mappings));
+	free(ai, M_APEI);
+}
+
+static void
+apei_child_detached(device_t dev, device_t child)
+{
+	struct apei_ivars *ai = device_get_ivars(child);
+
+	while (!TAILQ_EMPTY(&ai->mappings)) {
+		struct apei_register_mapping *m = TAILQ_FIRST(&ai->mappings);
+
+		device_printf(dev, "child leaked mapping of %s %#jx-%#jx\n",
+		    rman_get_type(m->reg->res) == SYS_RES_MEMORY ? "iomem" :
+		    "port", rman_get_start(m->reg->res),
+		    rman_get_end(m->reg->res));
+
+		(void) apei_unmap_register(child, &m->map);
+	}
+}
+
+static int
+apei_read_ivar(device_t dev, device_t child, int index, uintptr_t *result)
+{
+	switch (index) {
+	case ACPI_IVAR_HANDLE:
+		*(ACPI_HANDLE *)result = acpi_get_handle(dev);
+		break;
+	default:
+		return (ENOENT);
+	}
+
+	return (0);
+}
+
+static struct apei_register *
+apei_alloc_register(struct apei_softc *sc, int type, rman_res_t start,
+    rman_res_t count)
+{
+	struct apei_register_list *list;
+	struct apei_register *reg, *prev;
+	rman_res_t end;
+	const char *descr;
+	int error, next_rid;
+
+	switch (type) {
+	case SYS_RES_MEMORY:
+		list = &sc->mem;
+		descr = "iomem";
+		break;
+	case SYS_RES_IOPORT:
+		list = &sc->io;
+		descr = "port";
+		break;
+	default:
+		return (NULL);
+	}
+
+	end = start + (count - 1);
+
+	/* First, look for an existing resource. */
+	prev = NULL;
+	TAILQ_FOREACH(reg, list, link) {
+		/* Does the existing register overlap with this request? */
+		if (start > rman_get_end(reg->res) ||
+		    end < rman_get_start(reg->res)) {
+
+			/* prev will be the location of the first free rid */
+			if (prev == NULL)
+				next_rid = 0;
+			else
+				next_rid = rman_get_rid(prev->res) + 1;
+			if (rman_get_rid(reg->res) == next_rid)
+				prev = reg;
+			continue;
+		}
+
+		/* Do we need to extend the existing register? */
+		if (start < rman_get_start(reg->res) ||
+		    end > rman_get_end(reg->res)) {
+			rman_res_t new_end, new_start;
+
+			new_start = rman_get_start(reg->res);
+			if (start < new_start)
+				new_start = start;
+			new_end = rman_get_end(reg->res);
+			if (end > new_end)
+				new_end = end;
+			error = bus_adjust_resource(sc->dev, reg->res,
+			    new_start, new_end);
+			if (error != 0) {
+				if (bootverbose)
+					device_printf(sc->dev,
+			    "failed to grow %s %#jx-%#jx for %#jx-%#jx: %d\n",
+					    descr, rman_get_start(reg->res),
+					    rman_get_end(reg->res), start, end,
+					    error);
+				return (NULL);
+			}
+		}
+
+		return (reg);
+	}
+
+	/* Allocate a new resource. */
+	if (prev == NULL)
+		next_rid = 0;
+	else
+		next_rid = rman_get_rid(prev->res) + 1;
+	error = bus_set_resource(sc->dev, type, next_rid, start, count);
+	if (error != 0) {
+		if (bootverbose)
+			device_printf(sc->dev,
+			    "failed to add %s %#jx-%#jx: %d\n", descr, start,
+			    end, error);
+		return (NULL);
+	}
+
+	reg = malloc(sizeof(*reg), M_APEI, M_WAITOK);
+	reg->res = bus_alloc_resource_any(sc->dev, type, next_rid, RF_ACTIVE |
+	    RF_UNMAPPED);
+	if (reg->res == NULL) {
+		if (bootverbose)
+			device_printf(sc->dev,
+			    "failed to allocate %s %#jx-%#jx\n", descr, start,
+			    end);
+		free(reg, M_APEI);
+		bus_delete_resource(sc->dev, type, next_rid);
+		return (NULL);
+	}
+
+	if (prev == NULL)
+		TAILQ_INSERT_HEAD(list, reg, link);
+	else
+		TAILQ_INSERT_AFTER(list, prev, reg, link);
+	return (reg);
+}
+
+static struct resource_map *
+apei_map_register_method(device_t dev, device_t child, int type,
+    rman_res_t start, rman_res_t count)
+{
+	struct apei_softc *sc = device_get_softc(dev);
+	struct apei_ivars *ai = device_get_ivars(child);
+	struct apei_register_mapping *m;
+	struct apei_register *reg;
+	struct resource_map_request req;
+	int error;
+
+	reg = apei_alloc_register(sc, type, start, count);
+	if (reg == NULL)
+		return (NULL);
+
+	m = malloc(sizeof(*m), M_APEI, M_WAITOK | M_ZERO);
+	m->reg = reg;
+	resource_init_map_request(&req);
+	req.offset = start - rman_get_start(reg->res);
+	req.length = count;
+	error = bus_map_resource(dev, reg->res, &req, &m->map);
+	if (error != 0) {
+		if (bootverbose)
+			device_printf(dev, "failed to map %s %#jx-%#jx: %d\n",
+			    type == SYS_RES_MEMORY ? "iomem" : "port",
+			    start,
+			    start + (count - 1), error);
+		free(m, M_APEI);
+		return (NULL);
+	}
+	TAILQ_INSERT_TAIL(&ai->mappings, m, link);
+	return (&m->map);
+}
+
+static int
+apei_unmap_register_method(device_t dev, device_t child,
+    struct resource_map *map)
+{
+	struct apei_ivars *ai = device_get_ivars(child);
+	struct apei_register_mapping *m;
+	int error;
+
+	if (map == NULL)
+		return (0);
+
+	TAILQ_FOREACH(m, &ai->mappings, link) {
+		if (&m->map == map)
+			break;
+	}
+
+	if (m == NULL)
+		return (EINVAL);
+
+	TAILQ_REMOVE(&ai->mappings, m, link);
+	error = bus_unmap_resource(dev, m->reg->res, &m->map);
+	if (error != 0 && bootverbose)
+		device_printf(dev, "failed to unmap %s %#jx-%#jx: %d\n",
+		    rman_get_type(m->reg->res) == SYS_RES_MEMORY ? "iomem" :
+		    "port", rman_get_start(m->reg->res),
+		    rman_get_end(m->reg->res), error);
+	free(m, M_APEI);
+	return (0);
+}
+
+struct resource_map *
+apei_map_register(device_t dev, ACPI_GENERIC_ADDRESS *gas)
+{
+	int type;
+
+	if (gas->Address == 0 || gas->BitWidth == 0 || gas->BitWidth % 8 != 0)
+		return (NULL);
+
+	switch (gas->SpaceId) {
+	case ACPI_ADR_SPACE_SYSTEM_MEMORY:
+		type = SYS_RES_MEMORY;
+		break;
+	case ACPI_ADR_SPACE_SYSTEM_IO:
+		type = SYS_RES_IOPORT;
+		break;
+	default:
+		return (NULL);
+	}
+
+	return (APEI_MAP_REGISTER(device_get_parent(dev), dev, type,
+	    gas->Address, gas->BitWidth / 8));
+}
+
+struct resource_map *
+apei_map_memory(device_t dev, rman_res_t start, rman_res_t count)
+{
+	return (APEI_MAP_REGISTER(device_get_parent(dev), dev, SYS_RES_MEMORY,
+	    start, count));
+}
+
+int
+apei_unmap_register(device_t dev, struct resource_map *map)
+{
+	return (APEI_UNMAP_REGISTER(device_get_parent(dev), dev, map));
+}
+
+static device_method_t apei_methods[] = {
+	/* Device interface */
+	DEVMETHOD(device_identify,	apei_identify),
+	DEVMETHOD(device_probe,		apei_probe),
+	DEVMETHOD(device_attach,	apei_attach),
+
+	/* Bus interface */
+	DEVMETHOD(bus_add_child,	apei_add_child),
+	DEVMETHOD(bus_print_child,	bus_generic_print_child),
+	DEVMETHOD(bus_child_deleted,	apei_child_deleted),
+	DEVMETHOD(bus_child_detached,	apei_child_detached),
+	DEVMETHOD(bus_read_ivar,	apei_read_ivar),
+
+	/* APEI interface */
+	DEVMETHOD(apei_map_register,	apei_map_register_method),
+	DEVMETHOD(apei_unmap_register,	apei_unmap_register_method),
+
+	DEVMETHOD_END
+};
+
+static driver_t	apei_driver = {
+	"apei",
+	apei_methods,
+	sizeof(struct apei_softc),
+};
+
+DRIVER_MODULE(apei, acpi, apei_driver, NULL, NULL);
+MODULE_DEPEND(apei, acpi, 1, 1, 1);
diff --git a/sys/dev/acpica/apei_if.m b/sys/dev/acpica/apei_if.m
new file mode 100644
index 000000000000..1975fbde962f
--- /dev/null
+++ b/sys/dev/acpica/apei_if.m
@@ -0,0 +1,49 @@
+#-
+#  SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Netflix, Inc.
+# Written by: John Baldwin <jhb@FreeBSD.org>
+#
+
+#include <sys/bus.h>
+
+#include <contrib/dev/acpica/include/acpi.h>
+
+/**
+ * @defgroup APEI apei - KObj methods for ACPI platform error interface drivers
+ * @brief A pair of methods to allocate/deallocate registers for APEI
+ * @{
+ */
+INTERFACE apei;
+
+/**
+ * @brief Allocate a mapping for a resource.
+ *
+ * @param _bus	 parent device
+ * @param _dev	 device requesting the mapping
+ * @param _type	 resource type
+ * @param _start start address
+ * @param _count length
+ *
+ * @returns	resource mapping on success, or @c NULL on failure
+ */
+METHOD struct resource_map * map_register {
+	device_t	_bus;
+	device_t	_child;
+	int		_type;
+	rman_res_t	_start;
+	rman_res_t	_count;
+};
+
+/**
+ * @brief Deallocate a mapping for a resource.
+ *
+ * @param _bus	parent device
+ * @param _dev	device releasing the mapping
+ * @param _map	resource map
+ */
+METHOD int unmap_register {
+	device_t	_bus;
+	device_t	_child;
+	struct resource_map *_map;
+};
diff --git a/sys/dev/acpica/apeivar.h b/sys/dev/acpica/apeivar.h
new file mode 100644
index 000000000000..7820641aef1f
--- /dev/null
+++ b/sys/dev/acpica/apeivar.h
@@ -0,0 +1,54 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alexander Motin <mav@FreeBSD.org>
+ * Copyright (c) 2025 Netflix, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef __APEIVAR_H__
+#define	__APEIVAR_H__
+
+#include "apei_if.h"
+
+struct resource_map *apei_map_register(device_t dev, ACPI_GENERIC_ADDRESS *gas);
+struct resource_map *apei_map_memory(device_t dev, rman_res_t start,
+    rman_res_t len);
+int	apei_unmap_register(device_t dev, struct resource_map *map);
+
+#ifdef __i386__
+static __inline uint64_t
+bus_read_8(struct resource_map *res, bus_size_t offset)
+{
+	return (bus_read_4(res, offset) |
+	    ((uint64_t)bus_read_4(res, offset + 4)) << 32);
+}
+static __inline void
+bus_write_8(struct resource_map *res, bus_size_t offset, uint64_t val)
+{
+	bus_write_4(res, offset, val);
+	bus_write_4(res, offset + 4, val >> 32);
+}
+#endif
+
+#endif /* !__APEIVAR_H__ */