From fdfb611461aeb2501a4e99fa143b6a274a28da78 Mon Sep 17 00:00:00 2001 From: Osvaldo Janeri Filho Date: Fri, 6 Nov 2026 10:47:33 -0400 Subject: [PATCH] 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 Differential Revision: https://reviews.freebsd.org/D60766 --- etc/mtree/BSD.tests.dist | 2 + tests/sys/Makefile | 5 + tests/sys/cpuctl/Makefile | 9 + tests/sys/cpuctl/cpuctl_ibs_stray_nmi_test.c | 310 +++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 tests/sys/cpuctl/Makefile create mode 100644 tests/sys/cpuctl/cpuctl_ibs_stray_nmi_test.c diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index cec7a9f82627..8c6d2dd458ad 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -819,6 +819,8 @@ .. compat32 .. + cpuctl + .. devrandom .. dtrace diff --git a/tests/sys/Makefile b/tests/sys/Makefile index f92ef36b2d40..9205e5749f68 100644 --- a/tests/sys/Makefile +++ b/tests/sys/Makefile @@ -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 diff --git a/tests/sys/cpuctl/Makefile b/tests/sys/cpuctl/Makefile new file mode 100644 index 000000000000..e0563470b6e3 --- /dev/null +++ b/tests/sys/cpuctl/Makefile @@ -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 diff --git a/tests/sys/cpuctl/cpuctl_ibs_stray_nmi_test.c b/tests/sys/cpuctl/cpuctl_ibs_stray_nmi_test.c new file mode 100644 index 000000000000..db793ced53f8 --- /dev/null +++ b/tests/sys/cpuctl/cpuctl_ibs_stray_nmi_test.c @@ -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 +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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()); +}