tests: add a regression test for the orphaned IBS NMI

Toggle IBS op and fetch through /dev/cpuctl0 with hwpmc loaded and
check that every NMI hwpmc declined was claimed.  Without the fix both
cases fail on an EPYC 9654.

Reviewed by:	kib
MFC after:	2 weeks
Sponsored by:	AMD
Signed-off-by: Osvaldo Janeri Filho <[email protected]>
Differential Revision:	https://reviews.freebsd.org/D60766
This commit is contained in:
Osvaldo Janeri Filho authored and Mitchell Horne committed 2026-11-06 10:50:59 -04:00
1 parent f5304e44ff
commit fdfb611461
4 files changed
+326

No files matched your search

+2
View File
@@ -819,6 +819,8 @@
..
compat32
..
cpuctl
..
devrandom
..
dtrace
+5
View File
@@ -12,6 +12,7 @@ TESTS_SUBDIRS+= capsicum
TESTS_SUBDIRS+= ${_cddl}
# XXX: Currently broken in CI
#TESTS_SUBDIRS+= compat32
TESTS_SUBDIRS+= ${_cpuctl}
TESTS_SUBDIRS+= devrandom
TESTS_SUBDIRS+= fifo
TESTS_SUBDIRS+= file
@@ -47,6 +48,10 @@ _audit= audit
_cddl= cddl
.endif
.if ${MACHINE_CPUARCH} == "amd64" || ${MACHINE_CPUARCH} == "i386"
_cpuctl= cpuctl
.endif
.if ${MK_NETGRAPH} != "no"
_netgraph= netgraph
.endif
+9
View File
@@ -0,0 +1,9 @@
TESTSDIR= ${TESTSBASE}/sys/cpuctl
ATF_TESTS_C+= cpuctl_ibs_stray_nmi_test
TEST_METADATA.cpuctl_ibs_stray_nmi_test+= is_exclusive="true"
TEST_METADATA.cpuctl_ibs_stray_nmi_test+= required_kmods="cpuctl hwpmc"
TEST_METADATA.cpuctl_ibs_stray_nmi_test+= required_user="root"
.include <bsd.test.mk>
@@ -0,0 +1,310 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2026 Advanced Micro Devices, Inc.
*/
/*
* Every IBS NMI hwpmc declines must be claimed after the cpuctl(4) write.
* machdep.panic_on_nmi is cleared so that an unfixed kernel fails instead
* of panicking.
*/
#include <sys/param.h>
#include <sys/cpuctl.h>
#include <sys/cpuset.h>
#include <sys/ioctl.h>
#include <sys/sysctl.h>
#include <machine/specialreg.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <atf-c.h>
#define FETCH_EN (1ULL << 48)
#define OP_EN (1ULL << 17)
#define TOGGLES 20000
#define FETCH_MAXCNT 0x100ULL /* complete within the spin window */
#define OP_MAXCNT 0x1000ULL
#define MAX_SPIN 200000 /* TSC ticks */
#define STATE_FILE "ibs_stray_state"
struct stray_state {
int panic_on_nmi;
uint64_t fetch;
uint64_t op;
};
static int
msr_rw(int fd, u_long cmd, uint32_t msr, uint64_t *val)
{
cpuctl_msr_args_t args;
args.msr = msr;
args.data = *val;
if (ioctl(fd, cmd, &args) != 0)
return (errno);
if (cmd == CPUCTL_RDMSR)
*val = args.data;
return (0);
}
static uint64_t
intr_ignored(void)
{
uint64_t v;
size_t len;
len = sizeof(v);
ATF_REQUIRE_MSG(sysctlbyname("kern.hwpmc.stats.intr_ignored", &v, &len,
NULL, 0) == 0, "kern.hwpmc.stats.intr_ignored: %s",
strerror(errno));
return (v);
}
/* A kernel without the fix has no counter and claims nothing. */
static uint64_t
nmis_claimed(void)
{
u_long v;
size_t len;
len = sizeof(v);
if (sysctlbyname("machdep.ibs_ctl_write_nmis", &v, &len, NULL,
0) != 0) {
ATF_REQUIRE_MSG(errno == ENOENT,
"machdep.ibs_ctl_write_nmis: %s", strerror(errno));
return (0);
}
return (v);
}
static void
set_panic_on_nmi(int v)
{
ATF_REQUIRE_MSG(sysctlbyname("machdep.panic_on_nmi", NULL, NULL, &v,
sizeof(v)) == 0, "machdep.panic_on_nmi: %s", strerror(errno));
}
static int
setup(void)
{
cpuctl_cpuid_args_t ext, vendor;
struct stray_state st;
uint64_t v;
size_t len;
FILE *f;
int error, fd;
fd = open("/dev/cpuctl0", O_RDWR);
if (fd < 0)
atf_tc_skip("/dev/cpuctl0: %s", strerror(errno));
vendor.level = 0;
ext.level = 0x80000001;
error = 0;
if (ioctl(fd, CPUCTL_CPUID, &vendor) != 0 ||
ioctl(fd, CPUCTL_CPUID, &ext) != 0)
error = errno;
if (error != 0) {
close(fd);
atf_tc_fail("CPUCTL_CPUID: %s", strerror(error));
}
if (!(vendor.data[1] == 0x68747541 && vendor.data[3] == 0x69746e65 &&
vendor.data[2] == 0x444d4163) && /* AuthenticAMD */
!(vendor.data[1] == 0x6f677948 && vendor.data[3] == 0x6e65476e &&
vendor.data[2] == 0x656e6975)) { /* HygonGenuine */
close(fd);
atf_tc_skip("not an AMD or Hygon CPU");
}
if ((ext.data[2] & AMDID2_IBS) == 0) {
close(fd);
atf_tc_skip("CPU does not implement IBS");
}
len = sizeof(v);
if (sysctlbyname("kern.hwpmc.stats.intr_ignored", &v, &len,
NULL, 0) != 0) {
close(fd);
atf_tc_skip("hwpmc is not loaded (IBS NMIs stay masked)");
}
len = sizeof(st.panic_on_nmi);
if (sysctlbyname("machdep.panic_on_nmi", &st.panic_on_nmi, &len,
NULL, 0) != 0)
error = errno;
st.fetch = st.op = 0;
if (error == 0)
error = msr_rw(fd, CPUCTL_RDMSR, MSR_AMD_IBS_FETCH_CTL,
&st.fetch);
if (error == 0)
error = msr_rw(fd, CPUCTL_RDMSR, MSR_AMD_IBS_OP_CTL, &st.op);
if (error != 0) {
close(fd);
atf_tc_fail("reading the initial state: %s", strerror(error));
}
if ((st.fetch & FETCH_EN) != 0 || (st.op & OP_EN) != 0) {
close(fd);
atf_tc_skip("IBS sampling is already enabled on CPU 0 "
"(fetch 0x%jx, op 0x%jx); run in a reserved environment "
"with no other IBS users", (uintmax_t)st.fetch,
(uintmax_t)st.op);
}
f = fopen(STATE_FILE, "w");
if (f == NULL || fwrite(&st, sizeof(st), 1, f) != 1 ||
fclose(f) != 0) {
close(fd);
atf_tc_fail("cannot save the state to %s", STATE_FILE);
}
return (fd);
}
static void
toggle(const char *unit, uint32_t msr, uint64_t enable)
{
cpuset_t set;
uint64_t claimed, deadline, ignored, val;
int fd, i;
CPU_ZERO(&set);
CPU_SET(0, &set);
ATF_REQUIRE(cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1,
sizeof(set), &set) == 0);
fd = setup();
set_panic_on_nmi(0);
srandom((unsigned)__builtin_ia32_rdtsc());
ignored = intr_ignored();
claimed = nmis_claimed();
for (i = 0; i < TOGGLES; i++) {
val = enable;
ATF_REQUIRE_EQ(msr_rw(fd, CPUCTL_WRMSR, msr, &val), 0);
/* Let the disabling write land anywhere in the period. */
deadline = __builtin_ia32_rdtsc() +
(uint64_t)(random() % (MAX_SPIN + 1));
while (__builtin_ia32_rdtsc() < deadline)
;
val = 0;
ATF_REQUIRE_EQ(msr_rw(fd, CPUCTL_WRMSR, msr, &val), 0);
}
close(fd);
ignored = intr_ignored() - ignored;
claimed = nmis_claimed() - claimed;
ATF_CHECK_MSG(ignored == claimed,
"%ju IBS %s NMIs declined by hwpmc, %ju claimed after the cpuctl "
"write, after %d toggles", (uintmax_t)ignored, unit,
(uintmax_t)claimed, TOGGLES);
}
static void
restore_msr(int fd, const char *unit, uint32_t msr, uint64_t saved, int *bad)
{
uint64_t val;
int i;
for (i = 0; i < 3; i++) {
val = saved;
if (msr_rw(fd, CPUCTL_WRMSR, msr, &val) == 0 &&
msr_rw(fd, CPUCTL_RDMSR, msr, &val) == 0 && val == saved)
return;
}
warnx("cannot restore the IBS %s control of CPU 0", unit);
(*bad)++;
}
static void
cleanup(void)
{
struct stray_state st;
int bad, fd, i, v;
size_t len;
FILE *f;
f = fopen(STATE_FILE, "r");
if (f == NULL)
return; /* skipped before anything was changed */
if (fread(&st, sizeof(st), 1, f) != 1) {
fclose(f);
errx(EXIT_FAILURE, "cannot read the saved state");
}
fclose(f);
bad = 0;
fd = open("/dev/cpuctl0", O_RDWR);
if (fd < 0) {
warn("/dev/cpuctl0");
bad++;
} else {
restore_msr(fd, "fetch", MSR_AMD_IBS_FETCH_CTL, st.fetch, &bad);
restore_msr(fd, "op", MSR_AMD_IBS_OP_CTL, st.op, &bad);
close(fd);
}
for (i = 0; i < 3; i++) {
len = sizeof(v);
if (sysctlbyname("machdep.panic_on_nmi", NULL, NULL,
&st.panic_on_nmi, sizeof(st.panic_on_nmi)) == 0 &&
sysctlbyname("machdep.panic_on_nmi", &v, &len, NULL,
0) == 0 && v == st.panic_on_nmi)
break;
}
if (i == 3) {
warnx("cannot restore machdep.panic_on_nmi to %d",
st.panic_on_nmi);
bad++;
}
if (bad != 0)
errx(EXIT_FAILURE, "state not restored");
}
ATF_TC_WITH_CLEANUP(raw_op_toggle_claimed);
ATF_TC_HEAD(raw_op_toggle_claimed, tc)
{
atf_tc_set_md_var(tc, "descr",
"Toggling IBS op through cpuctl leaves no unclaimed NMI");
}
ATF_TC_BODY(raw_op_toggle_claimed, tc)
{
toggle("op", MSR_AMD_IBS_OP_CTL, OP_EN | OP_MAXCNT);
}
ATF_TC_CLEANUP(raw_op_toggle_claimed, tc)
{
cleanup();
}
ATF_TC_WITH_CLEANUP(raw_fetch_toggle_claimed);
ATF_TC_HEAD(raw_fetch_toggle_claimed, tc)
{
atf_tc_set_md_var(tc, "descr",
"Toggling IBS fetch through cpuctl leaves no unclaimed NMI");
}
ATF_TC_BODY(raw_fetch_toggle_claimed, tc)
{
toggle("fetch", MSR_AMD_IBS_FETCH_CTL, FETCH_EN | FETCH_MAXCNT);
}
ATF_TC_CLEANUP(raw_fetch_toggle_claimed, tc)
{
cleanup();
}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, raw_op_toggle_claimed);
ATF_TP_ADD_TC(tp, raw_fetch_toggle_claimed);
return (atf_no_error());
}