#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: apei_hest.c,v 1.8 2026/02/20 07:58:40 yamt Exp $");
#include <sys/types.h>
#include <sys/atomic.h>
#include <sys/kmem.h>
#include <sys/lock.h>
#include <sys/systm.h>
#include <dev/acpi/acpivar.h>
#include <dev/acpi/apei_cper.h>
#include <dev/acpi/apei_hestvar.h>
#include <dev/acpi/apei_hed.h>
#include <dev/acpi/apei_mapreg.h>
#include <dev/acpi/apeivar.h>
#if defined(__i386__) || defined(__x86_64__)
#include <x86/nmi.h>
#endif
#include "ioconf.h"
#define _COMPONENT ACPI_RESOURCE_COMPONENT
ACPI_MODULE_NAME ("apei")
static bool
apei_hest_ghes_handle(struct apei_softc *sc, struct apei_source *src)
{
ACPI_HEST_GENERIC *ghes = container_of(src->as_header,
ACPI_HEST_GENERIC, Header);
ACPI_HEST_GENERIC_STATUS *gesb = src->as_ghes.gesb;
char ctx[sizeof("error source 65535")];
uint32_t status;
bool fatal = false;
snprintf(ctx, sizeof(ctx), "error source %"PRIu16,
ghes->Header.SourceId);
status = apei_gesb_report(sc, src->as_ghes.gesb,
ghes->ErrorBlockLength, ctx, &fatal);
membar_release();
const uint32_t status1 = atomic_cas_32(&gesb->BlockStatus, status, 0);
if (status1 != status) {
device_printf(sc->sc_dev, "%s: status changed from"
" 0x%"PRIx32" to 0x%"PRIx32"\n",
ctx, status, status1);
}
if (fatal)
panic("fatal hardware error");
return status != 0;
}
static bool
apei_hest_ghes_v2_handle(struct apei_softc *sc, struct apei_source *src)
{
ACPI_HEST_GENERIC_V2 *ghes_v2 = container_of(src->as_header,
ACPI_HEST_GENERIC_V2, Header);
ACPI_HEST_GENERIC_STATUS *gesb = src->as_ghes.gesb;
char ctx[sizeof("error source 65535")];
uint64_t X;
uint32_t status;
bool fatal;
snprintf(ctx, sizeof(ctx), "error source %"PRIu16,
ghes_v2->Header.SourceId);
status = apei_gesb_report(sc, src->as_ghes.gesb,
ghes_v2->ErrorBlockLength, ctx, &fatal);
membar_release();
const uint32_t status1 = atomic_cas_32(&gesb->BlockStatus, status, 0);
if (status1 != status) {
device_printf(sc->sc_dev, "%s: status changed from"
" 0x%"PRIx32" to 0x%"PRIx32"\n",
ctx, status, status1);
}
X = apei_mapreg_read(&ghes_v2->ReadAckRegister,
src->as_ghes_v2.read_ack);
X &= ghes_v2->ReadAckPreserve;
X |= ghes_v2->ReadAckWrite;
apei_mapreg_write(&ghes_v2->ReadAckRegister,
src->as_ghes_v2.read_ack, X);
if (fatal)
panic("fatal hardware error");
return status != 0;
}
static void
apei_hest_ghes_poll(void *cookie)
{
struct apei_source *src = cookie;
struct apei_softc *sc = src->as_sc;
ACPI_HEST_GENERIC *ghes = container_of(src->as_header,
ACPI_HEST_GENERIC, Header);
(void)apei_hest_ghes_handle(sc, src);
callout_schedule(&src->as_ch,
MAX(1, mstohz(ghes->Notify.PollInterval)));
}
static void
apei_hest_ghes_v2_poll(void *cookie)
{
struct apei_source *src = cookie;
struct apei_softc *sc = src->as_sc;
ACPI_HEST_GENERIC_V2 *ghes_v2 = container_of(src->as_header,
ACPI_HEST_GENERIC_V2, Header);
(void)apei_hest_ghes_v2_handle(sc, src);
callout_schedule(&src->as_ch,
MAX(1, mstohz(ghes_v2->Notify.PollInterval)));
}
#if defined(__i386__) || defined(__x86_64__)
static __cpu_simple_lock_t apei_hest_nmi_lock = __SIMPLELOCK_UNLOCKED;
static int
apei_hest_ghes_nmi(const struct trapframe *tf, void *cookie)
{
struct apei_source *src = cookie;
struct apei_softc *sc = src->as_sc;
__cpu_simple_lock(&apei_hest_nmi_lock);
const bool mine = apei_hest_ghes_handle(sc, src);
__cpu_simple_unlock(&apei_hest_nmi_lock);
return mine;
}
static int
apei_hest_ghes_v2_nmi(const struct trapframe *tf, void *cookie)
{
struct apei_source *src = cookie;
struct apei_softc *sc = src->as_sc;
__cpu_simple_lock(&apei_hest_nmi_lock);
const bool mine = apei_hest_ghes_v2_handle(sc, src);
__cpu_simple_unlock(&apei_hest_nmi_lock);
return mine;
}
#endif
static void
apei_hest_attach_ghes(struct apei_softc *sc, ACPI_HEST_GENERIC *ghes,
uint32_t i)
{
struct apei_hest_softc *hsc = &sc->sc_hest;
struct apei_source *src = &hsc->hsc_source[i];
uint64_t addr;
ACPI_STATUS rv;
char ctx[sizeof("HEST[4294967295, Id=65535]")];
snprintf(ctx, sizeof(ctx), "HEST[%"PRIu32", Id=%"PRIu16"]",
i, ghes->Header.SourceId);
switch (ghes->Enabled) {
case 1:
break;
case 0:
aprint_debug_dev(sc->sc_dev, "%s: disabled\n", ctx);
return;
default:
aprint_error_dev(sc->sc_dev, "%s: unknown GHES Enabled state:"
" 0x%"PRIx8"\n", ctx, ghes->Enabled);
return;
}
if (ghes->ErrorStatusAddress.BitWidth > 64) {
aprint_error_dev(sc->sc_dev, "%s: excessive address bits:"
" %"PRIu8"\n", ctx, ghes->ErrorStatusAddress.BitWidth);
return;
}
rv = AcpiRead(&addr, &ghes->ErrorStatusAddress);
if (ACPI_FAILURE(rv)) {
aprint_error_dev(sc->sc_dev, "%s:"
" failed to read error status address: %s", ctx,
AcpiFormatException(rv));
return;
}
aprint_debug_dev(sc->sc_dev, "%s: error status @ 0x%"PRIx64"\n", ctx,
addr);
src->as_sc = sc;
src->as_header = &ghes->Header;
src->as_ghes.gesb = AcpiOsMapMemory(addr, ghes->ErrorBlockLength);
switch (ghes->Notify.Type) {
case ACPI_HEST_NOTIFY_POLLED:
if (ghes->Notify.PollInterval == 0)
break;
callout_init(&src->as_ch, CALLOUT_MPSAFE);
callout_setfunc(&src->as_ch, &apei_hest_ghes_poll, src);
callout_schedule(&src->as_ch, 0);
break;
case ACPI_HEST_NOTIFY_SCI:
case ACPI_HEST_NOTIFY_GPIO:
SIMPLEQ_INSERT_TAIL(&hsc->hsc_hed_list, src, as_entry);
break;
#if defined(__i386__) || defined(__x86_64__)
case ACPI_HEST_NOTIFY_NMI:
src->as_nmi = nmi_establish(&apei_hest_ghes_nmi, src);
break;
#endif
}
apei_hest_ghes_handle(sc, src);
}
static void
apei_hest_detach_ghes(struct apei_softc *sc, ACPI_HEST_GENERIC *ghes,
uint32_t i)
{
struct apei_hest_softc *hsc = &sc->sc_hest;
struct apei_source *src = &hsc->hsc_source[i];
switch (ghes->Notify.Type) {
case ACPI_HEST_NOTIFY_POLLED:
if (ghes->Notify.PollInterval == 0)
break;
callout_halt(&src->as_ch, NULL);
callout_destroy(&src->as_ch);
break;
case ACPI_HEST_NOTIFY_SCI:
case ACPI_HEST_NOTIFY_GPIO:
break;
#if defined(__i386__) || defined(__x86_64__)
case ACPI_HEST_NOTIFY_NMI:
nmi_disestablish(src->as_nmi);
src->as_nmi = NULL;
break;
#endif
}
AcpiOsUnmapMemory(src->as_ghes.gesb, ghes->ErrorBlockLength);
src->as_ghes.gesb = NULL;
src->as_header = NULL;
src->as_sc = NULL;
}
static void
apei_hest_attach_ghes_v2(struct apei_softc *sc, ACPI_HEST_GENERIC_V2 *ghes_v2,
uint32_t i)
{
struct apei_hest_softc *hsc = &sc->sc_hest;
struct apei_source *src = &hsc->hsc_source[i];
uint64_t addr;
struct apei_mapreg *read_ack;
ACPI_STATUS rv;
char ctx[sizeof("HEST[4294967295, Id=65535]")];
snprintf(ctx, sizeof(ctx), "HEST[%"PRIu32", Id=%"PRIu16"]",
i, ghes_v2->Header.SourceId);
switch (ghes_v2->Enabled) {
case 1:
break;
case 0:
aprint_debug_dev(sc->sc_dev, "%s: disabled\n", ctx);
return;
default:
aprint_error_dev(sc->sc_dev, "%s:"
" unknown GHESv2 Enabled state: 0x%"PRIx8"\n", ctx,
ghes_v2->Enabled);
return;
}
if (ghes_v2->ErrorStatusAddress.BitWidth > 64) {
aprint_error_dev(sc->sc_dev, "%s: excessive address bits:"
" %"PRIu8"\n", ctx, ghes_v2->ErrorStatusAddress.BitWidth);
return;
}
rv = AcpiRead(&addr, &ghes_v2->ErrorStatusAddress);
if (ACPI_FAILURE(rv)) {
aprint_error_dev(sc->sc_dev, "%s:"
" failed to read error status address: %s", ctx,
AcpiFormatException(rv));
return;
}
aprint_debug_dev(sc->sc_dev, "%s: error status @ 0x%"PRIx64"\n", ctx,
addr);
read_ack = apei_mapreg_map(&ghes_v2->ReadAckRegister);
if (read_ack == NULL) {
aprint_error_dev(sc->sc_dev, "%s:"
" unable to map Read Ack register\n", ctx);
return;
}
src->as_sc = sc;
src->as_header = &ghes_v2->Header;
src->as_ghes_v2.gesb = AcpiOsMapMemory(addr,
ghes_v2->ErrorBlockLength);
src->as_ghes_v2.read_ack = read_ack;
switch (ghes_v2->Notify.Type) {
case ACPI_HEST_NOTIFY_POLLED:
if (ghes_v2->Notify.PollInterval == 0)
break;
callout_init(&src->as_ch, CALLOUT_MPSAFE);
callout_setfunc(&src->as_ch, &apei_hest_ghes_v2_poll, src);
callout_schedule(&src->as_ch, 0);
break;
case ACPI_HEST_NOTIFY_SCI:
case ACPI_HEST_NOTIFY_GPIO:
SIMPLEQ_INSERT_TAIL(&hsc->hsc_hed_list, src, as_entry);
break;
#if defined(__i386__) || defined(__x86_64__)
case ACPI_HEST_NOTIFY_NMI:
src->as_nmi = nmi_establish(&apei_hest_ghes_v2_nmi, src);
break;
#endif
}
apei_hest_ghes_handle(sc, src);
}
static void
apei_hest_detach_ghes_v2(struct apei_softc *sc, ACPI_HEST_GENERIC_V2 *ghes_v2,
uint32_t i)
{
struct apei_hest_softc *hsc = &sc->sc_hest;
struct apei_source *src = &hsc->hsc_source[i];
switch (ghes_v2->Notify.Type) {
case ACPI_HEST_NOTIFY_POLLED:
if (ghes_v2->Notify.PollInterval == 0)
break;
callout_halt(&src->as_ch, NULL);
callout_destroy(&src->as_ch);
break;
case ACPI_HEST_NOTIFY_SCI:
case ACPI_HEST_NOTIFY_GPIO:
break;
#if defined(__i386__) || defined(__x86_64__)
case ACPI_HEST_NOTIFY_NMI:
nmi_disestablish(src->as_nmi);
src->as_nmi = NULL;
break;
#endif
}
AcpiOsUnmapMemory(src->as_ghes_v2.gesb, ghes_v2->ErrorBlockLength);
src->as_ghes_v2.gesb = NULL;
apei_mapreg_unmap(&ghes_v2->ReadAckRegister, src->as_ghes_v2.read_ack);
src->as_ghes_v2.read_ack = NULL;
src->as_header = NULL;
src->as_sc = NULL;
}
static ACPI_HEST_HEADER *
apei_hest_attach_source(struct apei_softc *sc, ACPI_HEST_HEADER *header,
uint32_t i, size_t maxlen)
{
char ctx[sizeof("HEST[4294967295, Id=65535]")];
snprintf(ctx, sizeof(ctx), "HEST[%"PRIu32", Id=%"PRIu16"]",
i, header->SourceId);
switch (header->Type) {
case ACPI_HEST_TYPE_IA32_CHECK: {
ACPI_HEST_IA_MACHINE_CHECK *const imc = container_of(header,
ACPI_HEST_IA_MACHINE_CHECK, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*imc))
return NULL;
maxlen -= sizeof(*imc);
ACPI_HEST_IA_ERROR_BANK *const bank = (void *)(imc + 1);
if (maxlen < imc->NumHardwareBanks*sizeof(*bank))
return NULL;
return (ACPI_HEST_HEADER *)(bank + imc->NumHardwareBanks);
}
case ACPI_HEST_TYPE_IA32_CORRECTED_CHECK: {
ACPI_HEST_IA_CORRECTED *const imcc = container_of(header,
ACPI_HEST_IA_CORRECTED, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*imcc))
return NULL;
maxlen -= sizeof(*imcc);
ACPI_HEST_IA_ERROR_BANK *const bank = (void *)(imcc + 1);
if (maxlen < imcc->NumHardwareBanks*sizeof(*bank))
return NULL;
return (ACPI_HEST_HEADER *)(bank + imcc->NumHardwareBanks);
}
case ACPI_HEST_TYPE_IA32_NMI: {
ACPI_HEST_IA_NMI *const ianmi = container_of(header,
ACPI_HEST_IA_NMI, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*ianmi))
return NULL;
return (ACPI_HEST_HEADER *)(ianmi + 1);
}
case ACPI_HEST_TYPE_AER_ROOT_PORT: {
ACPI_HEST_AER_ROOT *const aerroot = container_of(header,
ACPI_HEST_AER_ROOT, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*aerroot))
return NULL;
return (ACPI_HEST_HEADER *)(aerroot + 1);
}
case ACPI_HEST_TYPE_AER_ENDPOINT: {
ACPI_HEST_AER *const aer = container_of(header,
ACPI_HEST_AER, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*aer))
return NULL;
return (ACPI_HEST_HEADER *)(aer + 1);
}
case ACPI_HEST_TYPE_AER_BRIDGE: {
ACPI_HEST_AER_BRIDGE *const aerbridge = container_of(header,
ACPI_HEST_AER_BRIDGE, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*aerbridge))
return NULL;
return (ACPI_HEST_HEADER *)(aerbridge + 1);
}
case ACPI_HEST_TYPE_GENERIC_ERROR: {
ACPI_HEST_GENERIC *const ghes = container_of(header,
ACPI_HEST_GENERIC, Header);
if (maxlen < sizeof(*ghes))
return NULL;
apei_hest_attach_ghes(sc, ghes, i);
return (ACPI_HEST_HEADER *)(ghes + 1);
}
case ACPI_HEST_TYPE_GENERIC_ERROR_V2: {
ACPI_HEST_GENERIC_V2 *const ghes_v2 = container_of(header,
ACPI_HEST_GENERIC_V2, Header);
if (maxlen < sizeof(*ghes_v2))
return NULL;
apei_hest_attach_ghes_v2(sc, ghes_v2, i);
return (ACPI_HEST_HEADER *)(ghes_v2 + 1);
}
case ACPI_HEST_TYPE_IA32_DEFERRED_CHECK: {
ACPI_HEST_IA_DEFERRED_CHECK *const imdc = container_of(header,
ACPI_HEST_IA_DEFERRED_CHECK, Header);
aprint_error_dev(sc->sc_dev, "%s:"
" unimplemented type: 0x%04"PRIx16"\n", ctx, header->Type);
if (maxlen < sizeof(*imdc))
return NULL;
maxlen -= sizeof(*imdc);
ACPI_HEST_IA_ERROR_BANK *const bank = (void *)(imdc + 1);
if (maxlen < imdc->NumHardwareBanks*sizeof(*bank))
return NULL;
return (ACPI_HEST_HEADER *)(bank + imdc->NumHardwareBanks);
}
case ACPI_HEST_TYPE_NOT_USED3:
case ACPI_HEST_TYPE_NOT_USED4:
case ACPI_HEST_TYPE_NOT_USED5:
default:
aprint_error_dev(sc->sc_dev, "%s: unknown type:"
" 0x%04"PRIx16"\n", ctx, header->Type);
if (header->Type >= 12) {
struct {
UINT16 Type;
UINT16 Length;
} *const essh = (void *)header;
if (maxlen < sizeof(*essh) || maxlen < essh->Length)
return NULL;
return (ACPI_HEST_HEADER *)((char *)header +
essh->Length);
}
return NULL;
}
}
static void
apei_hest_detach_source(struct apei_softc *sc, ACPI_HEST_HEADER *header,
uint32_t i)
{
switch (header->Type) {
case ACPI_HEST_TYPE_GENERIC_ERROR: {
ACPI_HEST_GENERIC *ghes = container_of(header,
ACPI_HEST_GENERIC, Header);
apei_hest_detach_ghes(sc, ghes, i);
break;
}
case ACPI_HEST_TYPE_GENERIC_ERROR_V2: {
ACPI_HEST_GENERIC_V2 *ghes_v2 = container_of(header,
ACPI_HEST_GENERIC_V2, Header);
apei_hest_detach_ghes_v2(sc, ghes_v2, i);
break;
}
case ACPI_HEST_TYPE_IA32_CHECK:
case ACPI_HEST_TYPE_IA32_CORRECTED_CHECK:
case ACPI_HEST_TYPE_IA32_NMI:
case ACPI_HEST_TYPE_NOT_USED3:
case ACPI_HEST_TYPE_NOT_USED4:
case ACPI_HEST_TYPE_NOT_USED5:
case ACPI_HEST_TYPE_AER_ROOT_PORT:
case ACPI_HEST_TYPE_AER_ENDPOINT:
case ACPI_HEST_TYPE_AER_BRIDGE:
case ACPI_HEST_TYPE_IA32_DEFERRED_CHECK:
default:
break;
}
}
void
apei_hest_attach(struct apei_softc *sc)
{
ACPI_TABLE_HEST *hest = sc->sc_tab.hest;
struct apei_hest_softc *hsc = &sc->sc_hest;
ACPI_HEST_HEADER *header, *next;
uint32_t i, n;
size_t resid;
SIMPLEQ_INIT(&hsc->hsc_hed_list);
if (hest->Header.Length < sizeof(*hest)) {
aprint_error_dev(sc->sc_dev, "HEST: truncated table:"
" %"PRIu32" < %zu minimum bytes\n",
hest->Header.Length, sizeof(*hest));
return;
}
n = hest->ErrorSourceCount;
aprint_normal_dev(sc->sc_dev, "HEST: %"PRIu32
" hardware error source%s\n", n, n == 1 ? "" : "s");
if (n > MIN(SIZE_MAX, INT32_MAX)/sizeof(hsc->hsc_source[0])) {
aprint_error_dev(sc->sc_dev, "HEST: too many error sources\n");
return;
}
if (n > 0) {
hsc->hsc_source = kmem_zalloc(n * sizeof(hsc->hsc_source[0]),
KM_SLEEP);
}
header = (ACPI_HEST_HEADER *)(hest + 1);
resid = hest->Header.Length - sizeof(*hest);
for (i = 0; i < n && resid; i++, header = next) {
next = apei_hest_attach_source(sc, header, i, resid);
if (next == NULL) {
aprint_error_dev(sc->sc_dev, "truncated source:"
" %"PRIu32"\n", i);
break;
}
KASSERT(header < next);
KASSERT((size_t)((const char *)next - (const char *)header) <=
resid);
resid -= (const char *)next - (const char *)header;
}
if (resid) {
aprint_error_dev(sc->sc_dev, "HEST:"
" %zu bytes of trailing garbage after %"PRIu32" entries\n",
resid, n);
}
}
void
apei_hest_detach(struct apei_softc *sc)
{
ACPI_TABLE_HEST *hest = sc->sc_tab.hest;
struct apei_hest_softc *hsc = &sc->sc_hest;
uint32_t i, n;
if (hsc->hsc_source) {
n = hest->ErrorSourceCount;
for (i = 0; i < n; i++) {
struct apei_source *src = &hsc->hsc_source[i];
ACPI_HEST_HEADER *header = src->as_header;
if (src->as_header == NULL)
continue;
apei_hest_detach_source(sc, header, i);
}
kmem_free(hsc->hsc_source, n * sizeof(hsc->hsc_source[0]));
hsc->hsc_source = NULL;
}
}
void
apei_hed_notify(void)
{
device_t apei0;
struct apei_softc *sc;
struct apei_hest_softc *hsc;
struct apei_source *src;
if ((apei0 = device_lookup_acquire(&apei_cd, 0)) == NULL)
goto out;
sc = device_private(apei0);
if (sc->sc_tab.hest == NULL)
goto out;
hsc = &sc->sc_hest;
SIMPLEQ_FOREACH(src, &hsc->hsc_hed_list, as_entry) {
ACPI_HEST_HEADER *const header = src->as_header;
switch (header->Type) {
case ACPI_HEST_TYPE_GENERIC_ERROR:
apei_hest_ghes_handle(sc, src);
break;
case ACPI_HEST_TYPE_GENERIC_ERROR_V2:
apei_hest_ghes_v2_handle(sc, src);
break;
case ACPI_HEST_TYPE_IA32_CHECK:
case ACPI_HEST_TYPE_IA32_CORRECTED_CHECK:
case ACPI_HEST_TYPE_IA32_NMI:
case ACPI_HEST_TYPE_NOT_USED3:
case ACPI_HEST_TYPE_NOT_USED4:
case ACPI_HEST_TYPE_NOT_USED5:
case ACPI_HEST_TYPE_AER_ROOT_PORT:
case ACPI_HEST_TYPE_AER_ENDPOINT:
case ACPI_HEST_TYPE_AER_BRIDGE:
case ACPI_HEST_TYPE_IA32_DEFERRED_CHECK:
default:
break;
}
}
out: if (apei0) {
device_release(apei0);
apei0 = NULL;
}
}