Page MenuHomeFreeBSD

D59549.1794458581.diff
No OneTemporary

Authored By
Unknown
Size
178 KB
Referenced Files
None
Subscribers
None

D59549.1794458581.diff

Index: etc/mtree/BSD.tests.dist
===================================================================
--- etc/mtree/BSD.tests.dist
+++ etc/mtree/BSD.tests.dist
@@ -491,8 +491,6 @@
..
dhclient
..
- fsck_msdosfs
- ..
growfs
..
ifconfig
@@ -871,8 +869,6 @@
..
virstor
..
- zoned
- ..
..
..
kern
@@ -978,6 +974,8 @@
..
..
pmc
+ amd
+ ..
..
posixshm
..
@@ -1255,8 +1253,6 @@
..
truncate
..
- truss
- ..
tsort
..
unifdef
@@ -1325,8 +1321,6 @@
..
traceroute
..
- zonectl
- ..
..
..
Index: tests/sys/pmc/Kyuafile
===================================================================
--- /dev/null
+++ tests/sys/pmc/Kyuafile
@@ -0,0 +1,10 @@
+-- Automatically generated by bsd.test.mk.
+
+syntax(2)
+
+test_suite("FreeBSD")
+
+atf_test_program{name="pmc_callchain_ur_test", }
+atf_test_program{name="pmc_pidtrack_test", required_user=root}
+atf_test_program{name="pmcstat_tsc_test", }
+include("amd/Kyuafile")
Index: tests/sys/pmc/Makefile
===================================================================
--- tests/sys/pmc/Makefile
+++ tests/sys/pmc/Makefile
@@ -1,21 +1,16 @@
TESTSDIR= ${TESTSBASE}/sys/pmc
-ATF_TESTS_C+= pmc_api_test
-ATF_TESTS_C+= pmc_credexec_test
-ATF_TESTS_C+= pmc_detach_test
-ATF_TESTS_C+= pmc_exec_test
.if ${MACHINE_ARCH} == "amd64"
-ATF_TESTS_C+= pmc_exterr_test
-.endif
-ATF_TESTS_C+= pmc_lifecycle_test
-ATF_TESTS_C+= pmc_log_test
-ATF_TESTS_C+= pmc_wrap_test
+ATF_TESTS_C+= pmc_pidtrack_test
+ATF_TESTS_SH+= pmc_callchain_ur_test
+TESTS_SUBDIRS+= amd
-TEST_METADATA.pmc_exterr_test+= required_user="root"
+TEST_METADATA.pmc_pidtrack_test+= required_user=root
-LIBADD+= pmc
-LIBADD.pmc_detach_test+= pthread
+CFLAGS.pmc_pidtrack_test+= -I${SRCTOP}/tests/sys/pmc/amd
+LIBADD.pmc_pidtrack_test+= atf-c pmc
+.endif
-CFLAGS.pmc_exterr_test.c+= -I${SRCTOP}/tests
+ATF_TESTS_SH+= pmcstat_tsc_test
.include <bsd.test.mk>
Index: tests/sys/pmc/amd/Kyuafile
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/Kyuafile
@@ -0,0 +1,15 @@
+-- Automatically generated by bsd.test.mk.
+
+syntax(2)
+
+test_suite("FreeBSD")
+
+atf_test_program{name="pmc_cli_smoke_test", }
+atf_test_program{name="pmcstat_group_alloc_atomicity_test", }
+atf_test_program{name="pmcstat_grouping_test", }
+atf_test_program{name="pmcstat_ibs_errata_test", }
+atf_test_program{name="pmu_detect_test", required_user=root}
+atf_test_program{name="pmu_event_neg_test", required_user=root}
+atf_test_program{name="pmu_event_test", required_user=root}
+atf_test_program{name="pmu_mux_test", required_user=root}
+atf_test_program{name="rapl_test", required_user=root}
Index: tests/sys/pmc/amd/Makefile
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/Makefile
@@ -0,0 +1,35 @@
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+
+.if ${MACHINE_ARCH} == "amd64"
+
+TESTSDIR= ${TESTSBASE}/sys/pmc/amd
+
+ATF_TESTS_C+= pmu_detect_test
+ATF_TESTS_C+= pmu_event_test
+ATF_TESTS_C+= pmu_event_neg_test
+ATF_TESTS_C+= pmu_mux_test
+ATF_TESTS_C+= rapl_test
+ATF_TESTS_SH+= pmc_cli_smoke_test
+ATF_TESTS_SH+= pmcstat_grouping_test
+ATF_TESTS_SH+= pmcstat_ibs_errata_test
+ATF_TESTS_SH+= pmcstat_group_alloc_atomicity_test
+PROGS_TESTSDIR+= pmcinfo_thread_count
+
+TEST_METADATA.pmu_detect_test+= required_user=root
+TEST_METADATA.pmu_event_test+= required_user=root
+TEST_METADATA.pmu_event_neg_test+= required_user=root
+TEST_METADATA.pmu_mux_test+= required_user=root
+TEST_METADATA.rapl_test+= required_user=root
+
+LIBADD+= pmc
+LIBADD.pmcinfo_thread_count+= pmc
+
+CFLAGS+= -I${SRCTOP}/tests/sys/pmc/amd
+
+SRCS.pmcinfo_thread_count= pmcinfo_thread_count.c pmcinfo_snapshot.c
+
+.include <bsd.test.mk>
+
+.endif
Index: tests/sys/pmc/amd/amd_pmc_test_common.h
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/amd_pmc_test_common.h
@@ -0,0 +1,144 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Sponsored by: Advanced Micro Devices, Inc.
+ * Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+ *
+ * Common helpers for AMD hwpmc tests.
+ */
+
+#ifndef _AMD_PMC_TEST_COMMON_H_
+#define _AMD_PMC_TEST_COMMON_H_
+
+#include <sys/param.h>
+#include <machine/cpufunc.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+#define AMD_CPUID_VENDOR_LEAF 0x00000000
+#define AMD_CPUID_EXT_BASE 0x80000000
+#define AMD_CPUID_EXT_FEATURES 0x80000001
+
+#define AMD_CPUID_VENDOR_EBX 0x68747541U /* Auth */
+#define AMD_CPUID_VENDOR_EDX 0x69746e65U /* enti */
+#define AMD_CPUID_VENDOR_ECX 0x444d4163U /* cAMD */
+
+
+static inline int
+amd_test_do_cpuid(uint32_t level, uint32_t regs[4])
+{
+ do_cpuid(level, regs);
+ return (0);
+}
+
+static inline int
+amd_test_get_ext_features_ecx(uint32_t *ecx)
+{
+ uint32_t regs[4];
+ int error;
+
+ error = amd_test_do_cpuid(AMD_CPUID_EXT_FEATURES, regs);
+ if (error != 0)
+ return (error);
+
+ *ecx = regs[2];
+ return (0);
+}
+
+static inline void
+amd_test_skip_unless_amd(void)
+{
+ uint32_t regs[4];
+ int error;
+
+ error = amd_test_do_cpuid(AMD_CPUID_VENDOR_LEAF, regs);
+ if (error != 0)
+ atf_tc_skip("CPUID query failed: %s",
+ strerror(error));
+
+ if (!(regs[1] == AMD_CPUID_VENDOR_EBX &&
+ regs[3] == AMD_CPUID_VENDOR_EDX &&
+ regs[2] == AMD_CPUID_VENDOR_ECX))
+ atf_tc_skip("CPU vendor is not AuthenticAMD");
+}
+
+static inline void
+amd_test_skip_unless_hwpmc(void)
+{
+ if (pmc_init() < 0)
+ atf_tc_skip("pmc_init() failed: %s", strerror(errno));
+}
+
+static inline const struct pmc_classinfo *
+amd_test_find_classinfo(const struct pmc_cpuinfo *cpuinfo, enum pmc_class cl)
+{
+ uint32_t i;
+
+ for (i = 0; i < cpuinfo->pm_nclass; i++)
+ if (cpuinfo->pm_classes[i].pm_class == cl)
+ return (&cpuinfo->pm_classes[i]);
+
+ return (NULL);
+}
+
+static inline bool
+amd_test_string_in_list(const char *needle, const char **list, int nitems)
+{
+ int i;
+
+ for (i = 0; i < nitems; i++) {
+ if (strcasecmp(needle, list[i]) == 0)
+ return (true);
+ }
+
+ return (false);
+}
+
+static inline size_t
+amd_test_count_pmc_rows_with_prefix(int cpu, const char *prefix)
+{
+ struct pmc_pmcinfo *pmcinfo;
+ size_t count;
+ int i, npmc;
+
+ count = 0;
+ ATF_REQUIRE_MSG(pmc_pmcinfo(cpu, &pmcinfo) == 0,
+ "pmc_pmcinfo(%d) failed: %s", cpu, strerror(errno));
+
+ npmc = pmc_npmc(cpu);
+ if (npmc < 0) {
+ free(pmcinfo);
+ atf_tc_fail("pmc_npmc(%d) failed: %s", cpu, strerror(errno));
+ }
+
+ for (i = 0; i < npmc; i++) {
+ if (strncmp(pmcinfo->pm_pmcs[i].pm_name, prefix,
+ strlen(prefix)) == 0)
+ count++;
+ }
+
+ free(pmcinfo);
+ return (count);
+}
+
+static inline void
+amd_test_release_pmc(pmc_id_t pmcid)
+{
+ if (pmcid != PMC_ID_INVALID)
+ ATF_REQUIRE_MSG(pmc_release(pmcid) == 0,
+ "pmc_release(%u) failed: %s", pmcid, strerror(errno));
+}
+
+#endif /* _AMD_PMC_TEST_COMMON_H_ */
Index: tests/sys/pmc/amd/pmc_cli_smoke_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmc_cli_smoke_test.sh
@@ -0,0 +1,240 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+#
+# Design notes:
+# - Every case uses mktemp for output files — NO reliance on
+# default.log / default.pmc (stale-file false-green from a shared cwd).
+# - pmc_info_missing_file is the deterministic anchor: no PMC hardware
+# needed, always runs, asserts the exact EX_OSERR (71) exit code (not
+# merely nonzero, so a segfault doesn't masquerade as expected failure).
+# - pmc_record_then_info asserts captured CONTENT (non-empty log + pmc
+# info parses without error), not just exit 0 — an empty-but-valid
+# capture opens fine and would give a false green on bare exit check.
+# - pmc_list_events_present is labeled as a libpmc-table check: the event
+# names come from the pmu-events JSON tables embedded in libpmc, NOT
+# from the running kernel. The list is non-empty on AMD; the test
+# guards that the CLI works and the table is populated.
+# - pmc_frontend_ibs is skip-guarded on IBS availability keyed off a
+# real pmcstat probe, not off the command exit code.
+#
+
+PMC_SMOKE_RECORD_EVENT="ex_ret_instr"
+PMC_SMOKE_RECORD_SECS=2
+
+# pmc_cli_bin — resolve the pmc binary path or skip.
+pmc_cli_bin()
+{
+ local p resolved
+
+ p=${PMC_CLI:-$(atf_config_get amd.pmc.path pmc 2>/dev/null || echo pmc)}
+ case "$p" in
+ */*)
+ [ -x "$p" ] || atf_skip "pmc override not executable: $p"
+ PMC_BIN="$p"
+ ;;
+ *)
+ resolved=$(command -v "$p") || atf_skip "pmc not found in PATH"
+ PMC_BIN="$resolved"
+ ;;
+ esac
+}
+
+# pmc_ibs_available — return 0 if IBS sampling is available (pmcstat can
+# allocate an IBS-fetch event for a trivial run).
+pmc_ibs_available()
+{
+ local tmp
+ tmp=$(mktemp -t pmc_ibs_probe) || return 1
+ pmcstat -S ibs-fetch -n 4096 -O "$tmp" -- /usr/bin/true \
+ >/dev/null 2>&1
+ local rc=$?
+ rm -f "$tmp"
+ return $rc
+}
+
+# ================================================================
+# ANCHOR — pmc info with missing file exits EX_OSERR (71)
+# ================================================================
+
+atf_test_case pmc_info_missing_file
+pmc_info_missing_file_head()
+{
+ atf_set "descr" \
+ "[SMOKE anchor] pmc info /nonexistent exits with EX_OSERR (71). " \
+ "Deterministic: no PMC hardware needed. Asserts exact exit code, " \
+ "not merely nonzero (a segfault is also nonzero)."
+}
+pmc_info_missing_file_body()
+{
+ pmc_cli_bin
+ atf_check -s exit:71 -o ignore -e match:"Cannot open" \
+ "$PMC_BIN" info /pmc_smoke_nonexistent_file_$$
+}
+
+# ================================================================
+# pmc list-events — output is non-empty on AMD
+# ================================================================
+
+atf_test_case pmc_list_events_present
+pmc_list_events_present_head()
+{
+ atf_set "descr" \
+ "[SMOKE] pmc list-events produces non-empty output. This is a " \
+ "libpmc pmu-events table check: event names are compiled-in from " \
+ "JSON tables, independent of the running kernel. Non-empty output " \
+ "confirms the CLI works and the event table is populated."
+ atf_set "require.user" "root"
+}
+pmc_list_events_present_body()
+{
+ local count
+
+ pmc_cli_bin
+
+ count=$("$PMC_BIN" list-events 2>&1 | wc -l)
+ echo "pmc list-events: $count event lines" >&2
+
+ atf_check test "$count" -gt 10
+}
+
+# ================================================================
+# pmc record + pmc info round-trip
+# ================================================================
+
+atf_test_case pmc_record_then_info cleanup
+pmc_record_then_info_head()
+{
+ atf_set "descr" \
+ "[E2E] pmc record captures a real 2-second trace with " \
+ "ex_ret_instr, then pmc info reads the log without error and " \
+ "produces non-empty output. Asserts captured content, not just " \
+ "exit 0 — an empty but valid log opens fine and would false-green."
+ atf_set "require.user" "root"
+ atf_set "timeout" "30"
+}
+pmc_record_then_info_body()
+{
+ local log info_out line_count
+
+ pmc_cli_bin
+
+ # mktemp creates the file; pmc record refuses to overwrite an existing
+ # file. Use mktemp only to get a unique path, then remove it.
+ log=$(mktemp -t pmc_record_XXXXXX) || atf_fail "mktemp failed"
+ rm -f "$log"
+
+ # pmc record writes to a positional argument (not -o).
+ # -t N: record for N seconds, then exit. NOTE: pmc record exits with
+ # code 1 after a -t timeout (normal completion), not 0. We therefore
+ # accept any exit code except 71 (EX_OSERR, which means hard failure
+ # like file-not-found). The real success signal is the non-empty file.
+ atf_check -s not-exit:71 -o ignore -e ignore \
+ "$PMC_BIN" record -c "$PMC_SMOKE_RECORD_EVENT" \
+ -t "$PMC_SMOKE_RECORD_SECS" "$log"
+
+ # Guard: capture file must be non-empty.
+ if [ ! -s "$log" ]; then
+ atf_fail "pmc record produced an empty capture file — " \
+ "no samples collected in ${PMC_SMOKE_RECORD_SECS}s"
+ fi
+
+ info_out=$(mktemp -t pmc_info_XXXXXX) || atf_fail "mktemp failed"
+
+ # pmc info must parse the log and produce output.
+ atf_check -s exit:0 -o save:"$info_out" -e ignore \
+ "$PMC_BIN" info "$log"
+
+ line_count=$(wc -l < "$info_out")
+ echo "pmc info output: $line_count lines" >&2
+
+ # pmc info with a valid log should produce at least a few lines
+ # (CPU model, OS release, CPUID section).
+ atf_check test "$line_count" -gt 5
+
+ rm -f "$log" "$info_out"
+}
+pmc_record_then_info_cleanup()
+{
+ rm -f /tmp/pmc_record_* /tmp/pmc_info_*
+}
+
+# ================================================================
+# pmc frontend — IBS stall analysis (skip-guarded)
+# ================================================================
+
+atf_test_case pmc_frontend_ibs cleanup
+pmc_frontend_ibs_head()
+{
+ atf_set "descr" \
+ "[E2E] pmc record captures an IBS trace and pmc frontend " \
+ "produces stall-analysis output with a known header token. " \
+ "Skip-guarded on real IBS availability (pmcstat probe). " \
+ "Skip is keyed off hardware probe, not command exit code."
+ atf_set "require.user" "root"
+ atf_set "timeout" "60"
+}
+pmc_frontend_ibs_body()
+{
+ local log frontend_out
+
+ pmc_cli_bin
+
+ if ! command -v pmcstat >/dev/null 2>&1; then
+ atf_skip "pmcstat not found — cannot probe IBS availability"
+ fi
+
+ if ! pmc_ibs_available; then
+ atf_skip "IBS-fetch event not allocatable on this host " \
+ "(no IBS hardware, PMCs busy, or non-AMD CPU)"
+ fi
+
+ log=$(mktemp -t pmc_ibs_XXXXXX) || atf_fail "mktemp failed"
+ rm -f "$log"
+
+ # Record using the built-in 'frontend' study which uses IBS.
+ # Same exit-code caveat as pmc_record_then_info: pmc record -t exits 1.
+ atf_check -s not-exit:71 -o ignore -e ignore \
+ "$PMC_BIN" record -s frontend -t 3 "$log"
+
+ if [ ! -s "$log" ]; then
+ atf_fail "pmc record -s frontend produced an empty capture"
+ fi
+
+ frontend_out=$(mktemp -t pmc_frontend_XXXXXX) || \
+ atf_fail "mktemp failed"
+
+ atf_check -s exit:0 -o save:"$frontend_out" -e ignore \
+ "$PMC_BIN" frontend "$log"
+
+ # pmc frontend emits a "IBS Frontend Analysis" header.
+ if ! grep -qi "frontend\|stall\|analysis" "$frontend_out" 2>/dev/null; then
+ echo "frontend output (first 20 lines):" >&2
+ head -20 "$frontend_out" >&2
+ atf_fail "pmc frontend output lacks expected analysis header"
+ fi
+
+ echo "pmc frontend: $(wc -l < "$frontend_out") lines" >&2
+ rm -f "$log" "$frontend_out"
+}
+pmc_frontend_ibs_cleanup()
+{
+ rm -f /tmp/pmc_ibs_* /tmp/pmc_frontend_*
+}
+
+# ================================================================
+# Test program entry
+# ================================================================
+
+atf_init_test_cases()
+{
+ atf_add_test_case pmc_info_missing_file
+ atf_add_test_case pmc_list_events_present
+ atf_add_test_case pmc_record_then_info
+ atf_add_test_case pmc_frontend_ibs
+}
Index: tests/sys/pmc/amd/pmcinfo_snapshot.h
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcinfo_snapshot.h
@@ -0,0 +1,57 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+ *
+ */
+
+#ifndef _PMCINFO_SNAPSHOT_H_
+#define _PMCINFO_SNAPSHOT_H_
+
+#include <sys/pmc.h>
+#include <sys/types.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define PMCINFO_SNAPSHOT_ERRLEN 160
+
+struct pmcinfo_snapshot_row {
+ int cpu;
+ int ri;
+ bool in_use;
+ char name[PMC_NAME_MAX];
+ enum pmc_class pm_class;
+ int enabled;
+ enum pmc_disp disp;
+ pid_t owner_pid;
+ enum pmc_mode mode;
+ enum pmc_event event;
+ uint32_t flags;
+ pmc_value_t reload_count;
+};
+
+struct pmcinfo_snapshot {
+ int ncpu;
+ int npmc;
+ size_t nrows;
+ size_t capacity;
+ struct pmcinfo_snapshot_row *rows;
+};
+
+int pmcinfo_snapshot_take(struct pmcinfo_snapshot *snap, char *errbuf,
+ size_t errlen);
+void pmcinfo_snapshot_free(struct pmcinfo_snapshot *snap);
+const struct pmcinfo_snapshot_row *pmcinfo_snapshot_find(
+ const struct pmcinfo_snapshot *snap, int cpu, int ri);
+bool pmcinfo_snapshot_row_equal(const struct pmcinfo_snapshot_row *a,
+ const struct pmcinfo_snapshot_row *b);
+size_t pmcinfo_snapshot_diff_count(const struct pmcinfo_snapshot *before,
+ const struct pmcinfo_snapshot *after, char *errbuf, size_t errlen);
+size_t pmcinfo_snapshot_count_class_disp(const struct pmcinfo_snapshot *snap,
+ enum pmc_class pm_class, enum pmc_disp disp);
+
+#endif /* _PMCINFO_SNAPSHOT_H_ */
Index: tests/sys/pmc/amd/pmcinfo_snapshot.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcinfo_snapshot.c
@@ -0,0 +1,244 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+ *
+ */
+
+#include <sys/param.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "pmcinfo_snapshot.h"
+
+static void
+snapshot_err(char *errbuf, size_t errlen, const char *fmt, ...)
+{
+ va_list ap;
+
+ if (errbuf == NULL || errlen == 0)
+ return;
+
+ va_start(ap, fmt);
+ (void)vsnprintf(errbuf, errlen, fmt, ap);
+ va_end(ap);
+}
+
+static bool
+pmcinfo_row_in_use(const struct pmc_info *pi)
+{
+
+ return (pi->pm_rowdisp != PMC_DISP_FREE || pi->pm_ownerpid != -1 ||
+ pi->pm_enabled != 0);
+}
+
+int
+pmcinfo_snapshot_take(struct pmcinfo_snapshot *snap, char *errbuf,
+ size_t errlen)
+{
+ const struct pmc_cpuinfo *ci;
+ struct pmc_pmcinfo *info;
+ struct pmc_info *pi;
+ struct pmcinfo_snapshot_row *row;
+ size_t cap;
+ int cpu, error, ri, npmc;
+
+ memset(snap, 0, sizeof(*snap));
+
+ if (pmc_cpuinfo(&ci) != 0) {
+ error = errno != 0 ? errno : EINVAL;
+
+ snapshot_err(errbuf, errlen, "pmc_cpuinfo failed: %s",
+ strerror(error));
+
+ return (error);
+ }
+
+ snap->ncpu = (int)ci->pm_ncpu;
+ snap->npmc = (int)ci->pm_npmc;
+
+ cap = (size_t)snap->ncpu * (size_t)snap->npmc;
+ snap->capacity = cap;
+ snap->rows = calloc(cap == 0 ? 1 : cap, sizeof(*snap->rows));
+
+ if (snap->rows == NULL) {
+ error = errno != 0 ? errno : ENOMEM;
+
+ snapshot_err(errbuf, errlen, "calloc snapshot rows failed: %s",
+ strerror(error));
+
+ return (error);
+ }
+
+ for (cpu = 0; cpu < snap->ncpu; cpu++) {
+ info = NULL;
+ if (pmc_pmcinfo(cpu, &info) != 0) {
+ error = errno != 0 ? errno : EINVAL;
+ if (error == ENXIO)
+ continue;
+
+ snapshot_err(errbuf, errlen,
+ "pmc_pmcinfo(%d) failed: %s", cpu, strerror(error));
+ pmcinfo_snapshot_free(snap);
+
+ return (error);
+ }
+
+ npmc = pmc_npmc(cpu);
+ if (npmc < 0) {
+ error = errno != 0 ? errno : EINVAL;
+ free(info);
+
+ snapshot_err(errbuf, errlen, "pmc_npmc(%d) failed: %s",
+ cpu, strerror(error));
+ pmcinfo_snapshot_free(snap);
+
+ return (error);
+ }
+
+ for (ri = 0; ri < npmc; ri++) {
+ /*
+ * A backend may report more per-CPU rows than the global
+ * pm_npmc value used to size this snapshot.
+ */
+ if (snap->nrows >= snap->capacity) {
+ snapshot_err(errbuf, errlen,
+ "snapshot capacity %zu exceeded at cpu=%d ri=%d "
+ "(pm_ncpu=%d pm_npmc=%d cpu_npmc=%d)",
+ snap->capacity, cpu, ri, snap->ncpu,
+ snap->npmc, npmc);
+ free(info);
+ pmcinfo_snapshot_free(snap);
+
+ return (ERANGE);
+ }
+
+ pi = &info->pm_pmcs[ri];
+ row = &snap->rows[snap->nrows++];
+
+ row->cpu = cpu;
+ row->ri = ri;
+ row->in_use = pmcinfo_row_in_use(pi);
+ memcpy(row->name, pi->pm_name, sizeof(row->name));
+ row->name[sizeof(row->name) - 1] = '\0';
+ row->pm_class = pi->pm_class;
+ row->enabled = pi->pm_enabled;
+ row->disp = pi->pm_rowdisp;
+ row->owner_pid = pi->pm_ownerpid;
+ row->mode = pi->pm_mode;
+ row->event = pi->pm_event;
+ row->flags = pi->pm_flags;
+ row->reload_count = pi->pm_reloadcount;
+ }
+
+ free(info);
+ }
+
+ return (0);
+}
+
+void
+pmcinfo_snapshot_free(struct pmcinfo_snapshot *snap)
+{
+
+ if (snap == NULL)
+ return;
+
+ free(snap->rows);
+ memset(snap, 0, sizeof(*snap));
+}
+
+const struct pmcinfo_snapshot_row *
+pmcinfo_snapshot_find(const struct pmcinfo_snapshot *snap, int cpu, int ri)
+{
+ size_t i;
+
+ for (i = 0; i < snap->nrows; i++)
+ if (snap->rows[i].cpu == cpu && snap->rows[i].ri == ri)
+ return (&snap->rows[i]);
+
+ return (NULL);
+}
+
+bool
+pmcinfo_snapshot_row_equal(const struct pmcinfo_snapshot_row *a,
+ const struct pmcinfo_snapshot_row *b)
+{
+
+ if (a->cpu != b->cpu || a->ri != b->ri ||
+ a->in_use != b->in_use || strcmp(a->name, b->name) != 0)
+ return (false);
+
+ if (a->pm_class != b->pm_class || a->enabled != b->enabled ||
+ a->disp != b->disp || a->owner_pid != b->owner_pid)
+ return (false);
+
+ return (a->mode == b->mode && a->event == b->event &&
+ a->flags == b->flags && a->reload_count == b->reload_count);
+}
+
+size_t
+pmcinfo_snapshot_diff_count(const struct pmcinfo_snapshot *before,
+ const struct pmcinfo_snapshot *after, char *errbuf, size_t errlen)
+{
+ const struct pmcinfo_snapshot_row *other;
+ size_t diffs, i;
+
+ diffs = 0;
+
+ for (i = 0; i < before->nrows; i++) {
+ other = pmcinfo_snapshot_find(after, before->rows[i].cpu,
+ before->rows[i].ri);
+ if (other == NULL ||
+ !pmcinfo_snapshot_row_equal(&before->rows[i], other)) {
+ if (diffs == 0)
+ snapshot_err(errbuf, errlen,
+ "first diff cpu=%d ri=%d before(disp=%d owner=%d enabled=%d event=%d) after%s",
+ before->rows[i].cpu, before->rows[i].ri,
+ before->rows[i].disp, before->rows[i].owner_pid,
+ before->rows[i].enabled, before->rows[i].event,
+ other == NULL ? "=<missing>" : "=<changed>");
+ diffs++;
+ }
+ }
+
+ for (i = 0; i < after->nrows; i++) {
+ other = pmcinfo_snapshot_find(before, after->rows[i].cpu,
+ after->rows[i].ri);
+ if (other == NULL) {
+ if (diffs == 0)
+ snapshot_err(errbuf, errlen,
+ "new row cpu=%d ri=%d", after->rows[i].cpu,
+ after->rows[i].ri);
+ diffs++;
+ }
+ }
+
+ if (diffs == 0)
+ snapshot_err(errbuf, errlen, "no diff");
+
+ return (diffs);
+}
+
+size_t
+pmcinfo_snapshot_count_class_disp(const struct pmcinfo_snapshot *snap,
+ enum pmc_class pm_class, enum pmc_disp disp)
+{
+ size_t count, i;
+
+ count = 0;
+
+ for (i = 0; i < snap->nrows; i++)
+ if (snap->rows[i].pm_class == pm_class && snap->rows[i].disp == disp)
+ count++;
+
+ return (count);
+}
Index: tests/sys/pmc/amd/pmcinfo_thread_count.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcinfo_thread_count.c
@@ -0,0 +1,49 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+ *
+ */
+
+#include <sys/pmc.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pmc.h>
+#include <stdio.h>
+
+#include "pmcinfo_snapshot.h"
+
+int
+main(int argc, char **argv)
+{
+ struct pmcinfo_snapshot snap;
+ char errbuf[PMCINFO_SNAPSHOT_ERRLEN];
+ size_t k8_thread;
+ int error;
+
+ if (argc != 1)
+ errx(2, "usage: %s", argv[0]);
+
+ if (pmc_init() < 0)
+ err(1, "pmc_init");
+
+ errbuf[0] = '\0';
+
+ error = pmcinfo_snapshot_take(&snap, errbuf, sizeof(errbuf));
+ if (error != 0) {
+ errno = error;
+ err(1, "%s", errbuf[0] != '\0' ? errbuf : "pmcinfo snapshot");
+ }
+
+ k8_thread = pmcinfo_snapshot_count_class_disp(&snap, PMC_CLASS_K8,
+ PMC_DISP_THREAD);
+
+ (void)printf("%zu\n", k8_thread);
+
+ pmcinfo_snapshot_free(&snap);
+
+ return (0);
+}
Index: tests/sys/pmc/amd/pmcstat_group_alloc_atomicity_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcstat_group_alloc_atomicity_test.sh
@@ -0,0 +1,432 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+#
+
+pmc_atomicity_check_support()
+{
+ if ! kldstat -n hwpmc > /dev/null 2>&1 &&
+ ! sysctl -n kern.hwpmc.cpuid > /dev/null 2>&1; then
+ atf_skip "hwpmc unavailable: no hwpmc KLD and no kern.hwpmc.cpuid"
+ fi
+
+ if ! command -v pmcstat > /dev/null 2>&1; then
+ atf_skip "pmcstat not found in PATH"
+ fi
+
+ if ! command -v dd > /dev/null 2>&1; then
+ atf_skip "dd not found in PATH; required for the FIFO start barrier"
+ fi
+
+ if ! command -v timeout > /dev/null 2>&1; then
+ atf_skip "timeout not found in PATH; required for bounded FIFO barriers"
+ fi
+}
+
+pmc_atomicity_check_runtime_enabled()
+{
+ if [ "$(atf_config_get amd.pmc.grouping.runtime false)" != "true" ]; then
+ atf_skip "AMD core grouping runtime disabled by default; set amd.pmc.grouping.runtime=true"
+ fi
+}
+
+pmc_atomicity_check_known_zen()
+{
+ local cpuid family model oldifs stepping vendor zen
+
+ cpuid=$(sysctl -n kern.hwpmc.cpuid 2>/dev/null) || \
+ atf_skip "kern.hwpmc.cpuid is unavailable"
+
+ oldifs=$IFS
+ IFS=-
+ set -- $cpuid
+ IFS=$oldifs
+
+ if [ "$#" -ne 4 ]; then
+ atf_skip "cannot parse kern.hwpmc.cpuid=$cpuid; expected vendor-family-model-stepping"
+ fi
+
+ vendor=$1
+ family=$2
+ model=$(printf '%s\n' "$3" | tr '[:lower:]' '[:upper:]')
+ stepping=$(printf '%s\n' "$4" | tr '[:lower:]' '[:upper:]')
+
+ if [ "$vendor" != "AuthenticAMD" ]; then
+ atf_skip "AMD core PMC runtime requires AuthenticAMD CPU, got $cpuid"
+ fi
+
+ case "$family" in
+ *[!0-9]*|'')
+ atf_skip "cannot parse AMD family from kern.hwpmc.cpuid=$cpuid"
+ ;;
+ esac
+
+ case "$model" in
+ [0-9A-F][0-9A-F]) ;;
+ *) atf_skip "cannot parse AMD model from kern.hwpmc.cpuid=$cpuid" ;;
+ esac
+
+ case "$stepping" in
+ [0-9A-F]) ;;
+ *) atf_skip "cannot parse AMD stepping from kern.hwpmc.cpuid=$cpuid" ;;
+ esac
+
+ zen=
+
+ case "$family" in
+ 23)
+ case "$model" in
+ 0[1-7]|1[1-7]) zen="Zen 1" ;;
+ 08|09|0A|0B|0C|0D|0E|0F|1[8-9A-F]) zen="Zen+" ;;
+ 3[1-9A-F]|6[0-9A-F]|7[0-9A-F]|9[0-9A-F]|A[0-9A-F]) zen="Zen 2" ;;
+ esac
+ ;;
+ 25)
+ case "$model" in
+ 0[0-9A-F]|2[0-9A-F]|5[0-9A-F]) zen="Zen 3" ;;
+ 4[0-9A-F]) zen="Zen 3+" ;;
+ 1[0-9A-F]|6[0-9A-F]|7[0-9A-F]|A[0-9A-F]) zen="Zen 4" ;;
+ esac
+ ;;
+ 26)
+ case "$model" in
+ 0[0-9A-F]|1[0-9A-F]|2[0-9A-F]|4[0-9A-F]|6[0-9A-F]|7[0-9A-F])
+ zen="Zen 5" ;;
+ 5[0-9A-F]|8[0-9A-F]|9[0-9A-F]|A[0-9A-F]|C[0-9A-F])
+ zen="Zen 6" ;;
+ esac
+ ;;
+ esac
+
+ if [ -z "$zen" ]; then
+ if [ "$family" = 26 ]; then
+ atf_skip "AMD Family 1Ah Model ${model} is outside the validated Zen 5/6 model ranges; check the current PPR before enabling PMC grouping runtime"
+ fi
+
+ atf_skip "AMD Family ${family} Model ${model} is not in the validated Zen map"
+ fi
+
+ printf 'AMD core PMC runtime target: %s family=%s model=%s stepping=%s\n' \
+ "$zen" "$family" "$model" "$stepping"
+}
+
+pmc_atomicity_require_event()
+{
+ local event="$1"
+
+ if ! pmcstat -C -q -p "$event" -o /dev/null -- /usr/bin/true \
+ > /dev/null 2>/dev/null; then
+ atf_skip "pmcstat event $event is unavailable on this CPU"
+ fi
+}
+
+pmc_atomicity_helper()
+{
+ printf '%s\n' "$(atf_get_srcdir)/pmcinfo_thread_count"
+}
+
+pmc_atomicity_read_k8_thread_rows()
+{
+ local count err helper lines out
+
+ out="$1"
+ err="$2"
+
+ helper=$(pmc_atomicity_helper)
+
+ if ! "$helper" > "$out" 2> "$err"; then
+ return 1
+ fi
+
+ lines=$(awk 'END { print NR + 0 }' "$out") || return 1
+
+ if [ "$lines" -ne 1 ]; then
+ printf 'expected one pmcinfo_thread_count stdout line, saw %s\n' \
+ "$lines" >> "$err"
+ return 1
+ fi
+
+ if ! read count < "$out"; then
+ return 1
+ fi
+
+ case "$count" in
+ *[!0-9]*|'') return 1 ;;
+ esac
+
+ printf '%s\n' "$count"
+}
+
+pmc_atomicity_assert_k8_thread_baseline()
+{
+ local baseline count err out
+
+ baseline="$1"
+ out="$2"
+ err="$3"
+
+ count=$(pmc_atomicity_read_k8_thread_rows "$out" "$err") || \
+ atf_fail "pmcinfo_thread_count K8 THREAD row count failed; see $err"
+
+ if [ "$count" -ne "$baseline" ]; then
+ atf_fail "AMD PMC THREAD row count changed from baseline $baseline to $count; see $out"
+ fi
+}
+
+pmc_atomicity_iterations()
+{
+ local iterations
+
+ iterations=$(atf_config_get amd.pmc.grouping.atomicity.iterations 50)
+
+ case "$iterations" in
+ *[!0-9]*|'')
+ printf 'invalid amd.pmc.grouping.atomicity.iterations=%s\n' \
+ "$iterations" >&2
+ return 1
+ ;;
+ esac
+
+ if [ "$iterations" -le 0 ]; then
+ printf 'amd.pmc.grouping.atomicity.iterations must be positive\n' >&2
+ return 1
+ fi
+
+ if [ "$iterations" -gt 500 ]; then
+ printf 'amd.pmc.grouping.atomicity.iterations=%s is too large; maximum is 500\n' \
+ "$iterations" >&2
+ return 1
+ fi
+
+ printf '%s\n' "$iterations"
+}
+
+pmc_atomicity_run_pmcstat()
+{
+ local event="$1"
+ local start_fifo="$2"
+ local ready_fifo="$3"
+ local out="$4"
+ local err="$5"
+
+ # Open before readiness; each child then consumes exactly one start byte.
+ exec 7<&-
+ exec 9< "$start_fifo"
+
+ printf 'ready\n' > "$ready_fifo"
+
+ if ! dd bs=1 count=1 of=/dev/null <&9 2>/dev/null; then
+ printf 'start barrier read failed\n' > "$err"
+ exec 9<&-
+
+ return 125
+ fi
+
+ exec 9<&-
+
+ exec pmcstat -C -q \
+ -p "$event" -p "$event" -p "$event" \
+ -p "$event" -p "$event" -p "$event" \
+ -o "$out" -- sleep 1 > /dev/null 2> "$err"
+}
+
+pmc_atomicity_wait_ready()
+{
+ local ready_fifo="$1"
+ local n_expected="$2"
+
+ timeout 30 sh -c '
+ ready_fifo=$1
+ n_expected=$2
+ seen=0
+ exec 8<> "$ready_fifo" || exit 1
+ while [ "$seen" -lt "$n_expected" ]; do
+ if ! IFS= read -r line <&8; then
+ printf "ready-barrier read failed after %s/%s readers\n" \
+ "$seen" "$n_expected" >&2
+ exit 1
+ fi
+ if [ "$line" != "ready" ]; then
+ printf "unexpected ready-barrier token: %s\n" "$line" >&2
+ exit 1
+ fi
+ seen=$((seen + 1))
+ done
+ ' sh "$ready_fifo" "$n_expected"
+}
+
+pmc_atomicity_release_start()
+{
+ local n_readers="$1"
+ local i tokens
+
+ # Emit one byte per blocked reader so neither child can steal both tokens.
+ tokens=""
+ i=0
+
+ while [ "$i" -lt "$n_readers" ]; do
+ tokens="${tokens}g"
+ i=$((i + 1))
+ done
+
+ printf '%s' "$tokens" >&7
+}
+
+pmc_atomicity_cleanup_lock()
+{
+ local lockdir owner
+
+ lockdir="/var/run/pmc_atomicity.lock.d"
+
+ if [ -f pmc_atomicity.lock.owner ]; then
+ if read owner < pmc_atomicity.lock.owner; then
+ if [ "$owner" = "$lockdir/owner" ]; then
+ rm -f "$owner"
+ else
+ printf 'not removing unexpected PMC atomicity lock owner path: %s\n' \
+ "$owner" >&2
+ fi
+ fi
+
+ rm -f pmc_atomicity.lock.owner
+ fi
+
+ rmdir "$lockdir" 2>/dev/null || true
+}
+
+atf_test_case concurrent_process_allocations_no_residue cleanup
+concurrent_process_allocations_no_residue_head()
+{
+ atf_set "descr" "Race two full-width AMD pmcstat process allocations and check for residue"
+ atf_set "require.user" "root"
+}
+concurrent_process_allocations_no_residue_body()
+{
+ local baseline err_a err_b event i iterations lockdir
+ local out_a out_b pid_a pid_b rc_a rc_b ready_fifo start_fifo successes
+
+ pmc_atomicity_check_support
+ pmc_atomicity_check_runtime_enabled
+ pmc_atomicity_check_known_zen
+
+ event="ls_not_halted_cyc"
+
+ pmc_atomicity_require_event "$event"
+ atf_require_prog "$(pmc_atomicity_helper)"
+
+ iterations=$(pmc_atomicity_iterations) || \
+ atf_fail "invalid amd.pmc.grouping.atomicity.iterations"
+
+ lockdir="/var/run/pmc_atomicity.lock.d"
+
+ trap pmc_atomicity_cleanup_lock EXIT
+ trap 'pmc_atomicity_cleanup_lock; exit 1' HUP INT TERM
+
+ if ! mkdir "$lockdir" 2>/dev/null; then
+ atf_skip "another PMC atomicity test owns $lockdir"
+ fi
+
+ if ! printf '%s\n' "$$" > "$lockdir/owner"; then
+ rmdir "$lockdir" 2>/dev/null || true
+
+ atf_fail "failed to write $lockdir/owner"
+ fi
+
+ printf '%s\n' "$lockdir/owner" > pmc_atomicity.lock.owner
+
+ baseline=$(pmc_atomicity_read_k8_thread_rows \
+ pmcinfo-baseline.out pmcinfo-baseline.err) || \
+ atf_fail "pmcinfo_thread_count K8 THREAD row count failed before race; see pmcinfo-baseline.err"
+
+ successes=0
+
+ i=1
+ while [ "$i" -le "$iterations" ]; do
+ start_fifo="start.$i.fifo"
+ ready_fifo="ready.$i.fifo"
+ out_a="pmcstat-a.$i.out"
+ out_b="pmcstat-b.$i.out"
+ err_a="pmcstat-a.$i.err"
+ err_b="pmcstat-b.$i.err"
+
+ mkfifo "$start_fifo" "$ready_fifo" || \
+ atf_fail "mkfifo iteration $i failed"
+
+ # The parent's RW handle keeps both FIFO opens and broadcast nonblocking.
+ exec 7<> "$start_fifo"
+
+ pmc_atomicity_run_pmcstat "$event" "$start_fifo" "$ready_fifo" \
+ "$out_a" "$err_a" &
+ pid_a=$!
+
+ pmc_atomicity_run_pmcstat "$event" "$start_fifo" "$ready_fifo" \
+ "$out_b" "$err_b" &
+ pid_b=$!
+
+ if ! pmc_atomicity_wait_ready "$ready_fifo" 2; then
+ kill "$pid_a" "$pid_b" 2>/dev/null || true
+ wait "$pid_a" 2>/dev/null || true
+ wait "$pid_b" 2>/dev/null || true
+ exec 7<&-
+
+ atf_fail "iteration $i ready barrier failed"
+ fi
+
+ pmc_atomicity_release_start 2
+ exec 7<&-
+
+ wait "$pid_a"
+ rc_a=$?
+ wait "$pid_b"
+ rc_b=$?
+
+ if [ "$rc_a" -ne 0 ] && [ ! -s "$err_a" ]; then
+ atf_fail "iteration $i first pmcstat failed without diagnostics"
+ fi
+
+ if [ "$rc_b" -ne 0 ] && [ ! -s "$err_b" ]; then
+ atf_fail "iteration $i second pmcstat failed without diagnostics"
+ fi
+
+ if [ "$rc_a" -eq 0 ] && [ ! -s "$out_a" ]; then
+ atf_fail "iteration $i first pmcstat succeeded with empty output"
+ fi
+
+ if [ "$rc_b" -eq 0 ] && [ ! -s "$out_b" ]; then
+ atf_fail "iteration $i second pmcstat succeeded with empty output"
+ fi
+
+ if [ "$rc_a" -eq 0 ]; then
+ successes=$((successes + 1))
+ fi
+
+ if [ "$rc_b" -eq 0 ]; then
+ successes=$((successes + 1))
+ fi
+
+ pmc_atomicity_assert_k8_thread_baseline "$baseline" \
+ "pmcinfo.$i.out" "pmcinfo.$i.err"
+
+ rm -f "$start_fifo" "$ready_fifo"
+ done
+
+ if [ "$successes" -eq 0 ]; then
+ atf_fail "all concurrent pmcstat allocation attempts failed; no allocation atomicity was exercised"
+ fi
+}
+concurrent_process_allocations_no_residue_cleanup()
+{
+ pmc_atomicity_cleanup_lock
+
+ rm -f start.*.fifo ready.*.fifo pmcstat-a.*.out pmcstat-b.*.out \
+ pmcstat-a.*.err pmcstat-b.*.err pmcinfo*.out pmcinfo*.err
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case concurrent_process_allocations_no_residue
+}
Index: tests/sys/pmc/amd/pmcstat_grouping_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcstat_grouping_test.sh
@@ -0,0 +1,651 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+#
+
+pmcstat_check_support()
+{
+ local err
+
+ if ! kldstat -n hwpmc > /dev/null 2>&1 &&
+ ! sysctl -n kern.hwpmc.cpuid > /dev/null 2>&1; then
+ atf_skip "hwpmc unavailable: no hwpmc KLD and no kern.hwpmc.cpuid"
+ fi
+
+ if ! command -v pmcstat > /dev/null 2>&1; then
+ atf_skip "pmcstat not found in PATH"
+ fi
+
+ err=$(pmcstat -L 2>&1 >/dev/null) || true
+
+ case "$err" in
+ *"Program version wrong"*)
+ atf_skip "kernel/userland PMC ABI version mismatch (PMC_VERSION_MINOR): $err"
+ ;;
+ esac
+}
+
+pmcstat_stderr_or_note()
+{
+ local err
+ local file="$1"
+
+ err=$(cat "$file")
+
+ if [ -z "$err" ]; then
+ err="no stderr; see $file"
+ fi
+
+ printf '%s\n' "$err"
+}
+
+pmcstat_check_grouping_runtime_enabled()
+{
+ if [ "$(atf_config_get amd.pmc.grouping.runtime false)" != "true" ]; then
+ atf_skip "AMD core grouping runtime disabled by default; set amd.pmc.grouping.runtime=true"
+ fi
+}
+
+pmcstat_check_amd_cpuid()
+{
+ local cpuid
+
+ cpuid=$(sysctl -n kern.hwpmc.cpuid 2>/dev/null) || \
+ atf_skip "kern.hwpmc.cpuid is unavailable"
+ case "$cpuid" in
+ AuthenticAMD-*)
+ ;;
+ *)
+ atf_skip "AMD core grouping runtime requires AuthenticAMD CPU, got $cpuid"
+ ;;
+ esac
+}
+
+pmcstat_check_amd_grouping_runtime()
+{
+ pmcstat_check_grouping_runtime_enabled
+ pmcstat_check_amd_cpuid
+}
+
+pmcstat_cycle_tolerance_permille()
+{
+ local tolerance
+
+ tolerance=$(atf_config_get amd.pmc.grouping.cycle_tolerance_permille 250)
+ case "$tolerance" in
+ *[!0-9]*|'')
+ atf_fail "invalid amd.pmc.grouping.cycle_tolerance_permille=$tolerance"
+ ;;
+ esac
+ printf '%s\n' "$tolerance"
+}
+
+pmcstat_event_available()
+{
+ local event="$1"
+
+ pmcstat -C -q -p "$event" -o /dev/null -- /usr/bin/true \
+ > /dev/null 2>/dev/null
+}
+
+pmcstat_require_event()
+{
+ local event="$1"
+
+ if ! pmcstat_event_available "$event"; then
+ atf_skip "pmcstat event $event is unavailable on this CPU"
+ fi
+}
+
+pmcstat_first_available_event()
+{
+ local event
+
+ for event in "$@"; do
+ if pmcstat_event_available "$event"; then
+ printf '%s\n' "$event"
+ return 0
+ fi
+ done
+ return 1
+}
+
+pmcstat_capture_two_events()
+{
+ local duration
+ local err
+ local event_a="$2"
+ local event_b="$3"
+ local out="$1"
+
+ duration="${4:-2}"
+
+ if ! pmcstat -c 0 -s "$event_a" -s "$event_b" -w 1 \
+ -o "$out" sleep "$duration" > /dev/null 2>pmcstat.err; then
+ err=$(pmcstat_stderr_or_note pmcstat.err)
+
+ atf_fail "pmcstat failed with $event_a,$event_b: $err"
+ fi
+
+ if [ ! -s "$out" ]; then
+ atf_fail "pmcstat produced an empty counting output file"
+ fi
+}
+
+pmcstat_capture_process_two_events()
+{
+ local duration
+ local err
+ local event_a="$2"
+ local event_b="$3"
+ local out="$1"
+
+ duration="${4:-2}"
+
+ if ! pmcstat -C -q -p "$event_a" -p "$event_b" -o "$out" -- \
+ sleep "$duration" > /dev/null 2>pmcstat.err; then
+ err=$(pmcstat_stderr_or_note pmcstat.err)
+
+ atf_fail "pmcstat failed with $event_a,$event_b: $err"
+ fi
+
+ if [ ! -s "$out" ]; then
+ atf_fail "pmcstat produced an empty process-counting output file"
+ fi
+}
+
+pmcstat_capture_process_two_events_cpubound()
+{
+ local block_count duration err
+ local event_a="$2"
+ local event_b="$3"
+ local out="$1"
+
+ # cpuset plus CPU-bound dd avoid halted/scheduler-noise false evidence;
+ # /dev/zero to /dev/null keeps storage I/O out of the measurement.
+ duration="${4:-5}"
+ block_count=$((duration * 250000))
+
+ if ! cpuset -l 0 pmcstat -C -q -p "$event_a" -p "$event_b" \
+ -o "$out" -- dd if=/dev/zero of=/dev/null bs=4096 \
+ count="$block_count" > /dev/null 2>pmcstat.err; then
+ err=$(pmcstat_stderr_or_note pmcstat.err)
+
+ atf_fail "pmcstat failed with $event_a,$event_b (cpu-bound): $err"
+ fi
+
+ if [ ! -s "$out" ]; then
+ atf_fail "pmcstat produced an empty cpu-bound process-counting output file"
+ fi
+}
+
+pmcstat_require_header_event_count()
+{
+ local count
+ local event="$1"
+ local expected="$2"
+ local header="$3"
+
+ count=$(printf '%s\n' "$header" | awk -v event="$event" '
+ {
+ for (i = 1; i <= NF; i++) {
+ if ($i == event)
+ n++;
+ else if (length($i) > length(event) &&
+ substr($i, length($i) - length(event) + 1) == event &&
+ substr($i, length($i) - length(event), 1) == "/")
+ n++;
+ }
+ }
+ END { print n + 0 }')
+
+ if [ "$count" -ne "$expected" ]; then
+ atf_fail "header has $count copies of $event, expected $expected: $header"
+ fi
+}
+
+pmcstat_two_counter_stats()
+{
+ local file="$1"
+
+ awk '
+ !/^#/ && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ {
+ rows++;
+ if ($1 > 0)
+ positive_a++;
+ if ($2 > 0)
+ positive_b++;
+ last_a = $1;
+ last_b = $2;
+ }
+ END { print rows + 0, positive_a + 0, positive_b + 0,
+ last_a + 0, last_b + 0 }' "$file"
+}
+
+pmcstat_require_bounded_delta()
+{
+ local a="$1"
+ local b="$2"
+ local limit_permille="$3"
+ local context="$4"
+ local delta_report
+
+ delta_report=$(awk -v a="$a" -v b="$b" '
+ BEGIN {
+ d = a - b;
+ if (d < 0)
+ d = -d;
+ m = (a > b) ? a : b;
+ if (m <= 0) {
+ print "invalid";
+ exit 2;
+ }
+ printf "delta=%0.f max=%0.f permille=%.3f", d, m, (d * 1000) / m;
+ }')
+
+ if ! awk -v a="$a" -v b="$b" -v limit="$limit_permille" '
+ BEGIN {
+ d = a - b;
+ if (d < 0)
+ d = -d;
+ m = (a > b) ? a : b;
+ exit(!(m > 0 && d * 1000 <= limit * m));
+ }'; then
+ atf_fail "$context counters diverged beyond ${limit_permille}/1000: $delta_report"
+ fi
+
+ printf '%s observed bounded skew: %s\n' "$context" "$delta_report"
+}
+
+atf_test_case multiple_system_events_are_independent_columns cleanup
+multiple_system_events_are_independent_columns_head()
+{
+ atf_set "descr" "pmcstat repeated -s options produce two counter columns"
+ atf_set "require.user" "root"
+}
+multiple_system_events_are_independent_columns_body()
+{
+ local rows
+
+ pmcstat_check_support
+ pmcstat_capture_two_events "pmcstat.out" \
+ "SOFT-CLOCK.HARD" "SOFT-CLOCK.STAT"
+
+ if ! grep '^#' pmcstat.out | grep 'SOFT-CLOCK.HARD' |
+ grep -q 'SOFT-CLOCK.STAT'; then
+ atf_fail "pmcstat output is missing the two-counter header"
+ fi
+
+ rows=$(awk '!/^#/ && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ { n++ }
+ END { print n + 0 }' pmcstat.out)
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no rows with two numeric counters"
+ fi
+}
+multiple_system_events_are_independent_columns_cleanup()
+{
+ rm -f pmcstat.out pmcstat.err
+}
+
+atf_test_case same_system_event_group_has_same_count cleanup
+same_system_event_group_has_same_count_head()
+{
+ atf_set "descr" "pmcstat grouped identical events produce matching counts"
+ atf_set "require.user" "root"
+}
+same_system_event_group_has_same_count_body()
+{
+ local checked header mismatches positive rows stats
+
+ pmcstat_check_support
+ pmcstat_capture_two_events "pmcstat-same.out" \
+ "SOFT-CLOCK.HARD" "SOFT-CLOCK.HARD" 4
+
+ header=$(grep '^#' pmcstat-same.out | grep 'SOFT-CLOCK.HARD' |
+ head -1)
+
+ if [ -z "$header" ]; then
+ atf_fail "pmcstat output is missing the grouped-event header"
+ fi
+
+ pmcstat_require_header_event_count "SOFT-CLOCK.HARD" 2 "$header"
+
+ stats=$(awk '
+ !/^#/ && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ {
+ rows++;
+ if (rows == 1)
+ next;
+ checked++;
+ if ($1 != $2) {
+ mismatches++;
+ bad_row = rows;
+ }
+ if ($1 > 0 && $2 > 0)
+ positive++;
+ }
+ END { print rows + 0, checked + 0, mismatches + 0, positive + 0,
+ bad_row + 0 }' pmcstat-same.out)
+
+ set -- $stats
+
+ rows=$1
+ checked=$2
+ mismatches=$3
+ positive=$4
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no numeric rows for grouped identical events"
+ fi
+
+ if [ "$checked" -eq 0 ]; then
+ atf_fail "pmcstat produced no post-warmup rows for grouped identical events"
+ fi
+
+ if [ "$positive" -eq 0 ]; then
+ atf_fail "grouped identical events never produced a positive count"
+ fi
+ #
+ # On large NUMA systems (192+ CPUs) two system-wide PMC rows for the
+ # same SOFT-CLOCK event can diverge by one or two ticks per 1-second
+ # window due to NMI delivery skew across physical NUMA nodes and
+ # scheduler jitter under 192-thread load. Allow up to 2 mismatches
+ # per run; consistent divergence (> 2) indicates a real counting bug.
+ #
+ if [ "$mismatches" -gt 2 ]; then
+ atf_fail "grouped identical events mismatched: count=$mismatches row=$5"
+ fi
+}
+same_system_event_group_has_same_count_cleanup()
+{
+ rm -f pmcstat-same.out pmcstat.err
+}
+
+atf_test_case repeated_process_cycles_have_bounded_skew cleanup
+repeated_process_cycles_have_bounded_skew_head()
+{
+ atf_set "require.progs" "pmcstat cpuset"
+ atf_set "descr" "duplicate AMD core cycle PMCs may differ only within a bounded tolerance"
+ atf_set "require.user" "root"
+}
+repeated_process_cycles_have_bounded_skew_body()
+{
+ local cycles_a cycles_b header positive_a positive_b rows stats tolerance
+
+ pmcstat_check_support
+ pmcstat_check_amd_grouping_runtime
+ pmcstat_require_event "ls_not_halted_cyc"
+
+ if ! command -v dd > /dev/null 2>&1; then
+ atf_skip "dd(1) not found in PATH"
+ fi
+
+ pmcstat_capture_process_two_events_cpubound "pmcstat-cycles-skew.out" \
+ "ls_not_halted_cyc" "ls_not_halted_cyc" 5
+
+ header=$(grep '^#' pmcstat-cycles-skew.out | grep 'ls_not_halted_cyc' |
+ head -1)
+
+ if [ -z "$header" ]; then
+ atf_fail "pmcstat output is missing the duplicate-cycle header"
+ fi
+ pmcstat_require_header_event_count "ls_not_halted_cyc" 2 "$header"
+
+ stats=$(pmcstat_two_counter_stats pmcstat-cycles-skew.out)
+
+ set -- $stats
+
+ rows=$1
+ positive_a=$2
+ positive_b=$3
+ cycles_a=$4
+ cycles_b=$5
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no numeric rows for duplicate cycle PMCs"
+ fi
+
+ if [ "$positive_a" -eq 0 ] || [ "$positive_b" -eq 0 ]; then
+ atf_fail "duplicate cycle PMCs did not both produce positive counts"
+ fi
+
+ tolerance=$(pmcstat_cycle_tolerance_permille)
+
+ pmcstat_require_bounded_delta "$cycles_a" "$cycles_b" "$tolerance" \
+ "duplicate ls_not_halted_cyc (cpu-bound)"
+}
+repeated_process_cycles_have_bounded_skew_cleanup()
+{
+ rm -f pmcstat-cycles-skew.out pmcstat.err
+}
+
+atf_test_case repeated_sleep_cycles_have_bounded_skew cleanup
+repeated_sleep_cycles_have_bounded_skew_head()
+{
+ atf_set "descr" "scheduler-noise characterization for duplicate AMD cycle PMCs around sleep(1)"
+ atf_set "require.user" "root"
+}
+repeated_sleep_cycles_have_bounded_skew_body()
+{
+ local cycles_a cycles_b header positive_a positive_b rows stats tolerance
+
+ pmcstat_check_support
+ pmcstat_check_amd_grouping_runtime
+ pmcstat_require_event "ls_not_halted_cyc"
+
+ if [ "$(atf_config_get amd.pmc.grouping.sleep_characterization false)" \
+ != "true" ]; then
+ atf_skip "sleep-workload characterization is opt-in; set amd.pmc.grouping.sleep_characterization=true"
+ fi
+
+ pmcstat_capture_process_two_events "pmcstat-cycles-skew-sleep.out" \
+ "ls_not_halted_cyc" "ls_not_halted_cyc" 5
+
+ header=$(grep '^#' pmcstat-cycles-skew-sleep.out |
+ grep 'ls_not_halted_cyc' | head -1)
+
+ if [ -z "$header" ]; then
+ atf_fail "pmcstat output is missing the duplicate-cycle (sleep) header"
+ fi
+ pmcstat_require_header_event_count "ls_not_halted_cyc" 2 "$header"
+
+ stats=$(pmcstat_two_counter_stats pmcstat-cycles-skew-sleep.out)
+
+ set -- $stats
+
+ rows=$1
+ positive_a=$2
+ positive_b=$3
+ cycles_a=$4
+ cycles_b=$5
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no numeric rows for sleep duplicate cycle PMCs"
+ fi
+
+ if [ "$positive_a" -eq 0 ] || [ "$positive_b" -eq 0 ]; then
+ atf_skip "sleep duplicate cycle PMCs did not both accumulate positive counts; scheduler-quiet sleep is expected to do this"
+ fi
+
+ tolerance=$(pmcstat_cycle_tolerance_permille)
+
+ pmcstat_require_bounded_delta "$cycles_a" "$cycles_b" "$tolerance" \
+ "duplicate ls_not_halted_cyc (sleep)"
+}
+repeated_sleep_cycles_have_bounded_skew_cleanup()
+{
+ rm -f pmcstat-cycles-skew-sleep.out pmcstat.err
+}
+
+atf_test_case mixed_cycles_instructions_are_independent_columns cleanup
+mixed_cycles_instructions_are_independent_columns_head()
+{
+ atf_set "require.progs" "pmcstat cpuset"
+ atf_set "descr" "cycles and instructions remain distinct pmcstat process-counting columns"
+ atf_set "require.user" "root"
+}
+mixed_cycles_instructions_are_independent_columns_body()
+{
+ local header positive_a positive_b rows stats
+
+ pmcstat_check_support
+ pmcstat_check_amd_grouping_runtime
+ pmcstat_require_event "ls_not_halted_cyc"
+ pmcstat_require_event "ex_ret_instr"
+
+ pmcstat_capture_process_two_events "pmcstat-mixed-ipc.out" \
+ "ls_not_halted_cyc" "ex_ret_instr" 3
+
+ header=$(grep '^#' pmcstat-mixed-ipc.out | grep 'ls_not_halted_cyc' |
+ grep 'ex_ret_instr' | head -1)
+
+ if [ -z "$header" ]; then
+ atf_fail "pmcstat output is missing cycles+instructions header"
+ fi
+
+ pmcstat_require_header_event_count "ls_not_halted_cyc" 1 "$header"
+ pmcstat_require_header_event_count "ex_ret_instr" 1 "$header"
+
+ stats=$(pmcstat_two_counter_stats pmcstat-mixed-ipc.out)
+
+ set -- $stats
+
+ rows=$1
+ positive_a=$2
+ positive_b=$3
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no numeric rows for cycles+instructions"
+ fi
+
+ if [ "$positive_a" -eq 0 ] || [ "$positive_b" -eq 0 ]; then
+ atf_fail "cycles+instructions did not both produce positive counts"
+ fi
+}
+mixed_cycles_instructions_are_independent_columns_cleanup()
+{
+ rm -f pmcstat-mixed-ipc.out pmcstat.err
+}
+
+atf_test_case mixed_cache_cycles_are_independent_columns cleanup
+mixed_cache_cycles_are_independent_columns_head()
+{
+ atf_set "require.progs" "pmcstat cpuset"
+ atf_set "descr" "cache-related miss and cycles remain distinct pmcstat process-counting columns"
+ atf_set "require.user" "root"
+}
+mixed_cache_cycles_are_independent_columns_body()
+{
+ local cache_event header positive_b rows stats
+
+ pmcstat_check_support
+ pmcstat_check_amd_grouping_runtime
+ pmcstat_require_event "ls_not_halted_cyc"
+
+ cache_event=$(pmcstat_first_available_event \
+ "l2_pf_miss_l2_l3.all" "l2_pf_miss_l2_l3" \
+ "l2_pf_miss_l2_l3.l2_hwpf") || \
+ atf_skip "no supported AMD L2 prefetch miss event is available"
+
+ pmcstat_capture_process_two_events "pmcstat-mixed-cache.out" \
+ "$cache_event" "ls_not_halted_cyc" 3
+
+ header=$(grep '^#' pmcstat-mixed-cache.out | grep "$cache_event" |
+ grep 'ls_not_halted_cyc' | head -1)
+
+ if [ -z "$header" ]; then
+ atf_fail "pmcstat output is missing cache+cycles header"
+ fi
+
+ pmcstat_require_header_event_count "$cache_event" 1 "$header"
+ pmcstat_require_header_event_count "ls_not_halted_cyc" 1 "$header"
+
+ stats=$(pmcstat_two_counter_stats pmcstat-mixed-cache.out)
+
+ set -- $stats
+
+ rows=$1
+ positive_b=$3
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "pmcstat produced no numeric rows for cache+cycles"
+ fi
+
+ if [ "$positive_b" -eq 0 ]; then
+ atf_fail "cache+cycles run did not produce positive cycle counts"
+ fi
+}
+mixed_cache_cycles_are_independent_columns_cleanup()
+{
+ rm -f pmcstat-mixed-cache.out pmcstat.err
+}
+
+atf_test_case oversubscribed_process_cycles_fail_cleanly cleanup
+oversubscribed_process_cycles_fail_cleanly_head()
+{
+ atf_set "descr" "oversubscribed duplicate AMD cycle PMCs fail cleanly or produce parseable output"
+ atf_set "require.user" "root"
+}
+oversubscribed_process_cycles_fail_cleanly_body()
+{
+ local err rows
+
+ pmcstat_check_support
+ pmcstat_check_amd_grouping_runtime
+ pmcstat_require_event "ls_not_halted_cyc"
+
+ if pmcstat -C -q \
+ -p ls_not_halted_cyc -p ls_not_halted_cyc \
+ -p ls_not_halted_cyc -p ls_not_halted_cyc \
+ -p ls_not_halted_cyc -p ls_not_halted_cyc \
+ -p ls_not_halted_cyc -o pmcstat-oversub.out -- \
+ sleep 1 > /dev/null 2>pmcstat.err; then
+ if [ ! -s pmcstat-oversub.out ]; then
+ atf_fail "oversubscribed pmcstat run succeeded with empty output"
+ fi
+
+ rows=$(awk '
+ !/^#/ && NF >= 7 {
+ ok = 1;
+ for (i = 1; i <= 7; i++)
+ if ($i !~ /^[0-9]+$/)
+ ok = 0;
+ if (ok)
+ n++;
+ }
+ END { print n + 0 }' pmcstat-oversub.out)
+
+ if [ "$rows" -eq 0 ]; then
+ atf_fail "oversubscribed pmcstat run has no parseable 7-counter row"
+ fi
+
+ printf 'oversubscribed duplicate cycles succeeded with parseable output\n'
+ else
+ if [ ! -s pmcstat.err ]; then
+ atf_fail "oversubscribed pmcstat run failed without diagnostics"
+ fi
+
+ err=$(pmcstat_stderr_or_note pmcstat.err)
+
+ printf 'oversubscribed duplicate cycles failed cleanly: %s\n' "$err"
+ fi
+}
+oversubscribed_process_cycles_fail_cleanly_cleanup()
+{
+ rm -f pmcstat-oversub.out pmcstat.err
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case multiple_system_events_are_independent_columns
+ atf_add_test_case same_system_event_group_has_same_count
+ atf_add_test_case repeated_process_cycles_have_bounded_skew
+ atf_add_test_case repeated_sleep_cycles_have_bounded_skew
+ atf_add_test_case mixed_cycles_instructions_are_independent_columns
+ atf_add_test_case mixed_cache_cycles_are_independent_columns
+ atf_add_test_case oversubscribed_process_cycles_fail_cleanly
+}
Index: tests/sys/pmc/amd/pmcstat_ibs_errata_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmcstat_ibs_errata_test.sh
@@ -0,0 +1,429 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+#
+#
+# Affected synthetic CPUIDs:
+# AuthenticAMD-25-00-1 Family 19h model 00h, Zen 3 B0 range
+# AuthenticAMD-25-0F-1 Family 19h model 0Fh, Zen 3 B0 range boundary
+#
+# Unaffected synthetic CPUIDs:
+# AuthenticAMD-25-20-1 Family 19h model 20h, Zen 3 non-B0 range
+# AuthenticAMD-25-10-1 Family 19h model 10h, first Zen 4 server range
+# AuthenticAMD-25-11-1 Family 19h model 11h, Zen 4 server range
+# AuthenticAMD-26-00-0 Family 1Ah model 00h, Zen 5 server range
+
+pmcstat_errata_pmcstat()
+{
+ local p resolved
+
+ p=${PMCSTAT:-$(atf_config_get amd.pmcstat.path pmcstat)}
+ case "$p" in
+ */*)
+ if [ ! -x "$p" ]; then
+ atf_skip "pmcstat override is not executable: $p"
+ fi
+ PMCSTAT_BIN="$p"
+ ;;
+ *)
+ resolved=$(command -v "$p") || \
+ atf_skip "pmcstat not found in PATH"
+ PMCSTAT_BIN="$resolved"
+ ;;
+ esac
+}
+
+pmcstat_errata_check_support()
+{
+ if [ "$(uname -m)" != "amd64" ]; then
+ atf_skip "synthetic IBS pmclog layout is amd64-specific"
+ fi
+}
+
+pmcstat_errata_build_writer()
+{
+ cat > mk_ibs_pmclog.c <<'EOF'
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define nitems(x) (sizeof((x)) / sizeof((x)[0]))
+
+#define PMCLOG_HEADER_MAGIC 0xeeU
+#define PMCLOG_TYPE_CLOSELOG 1U
+#define PMCLOG_TYPE_INITIALIZE 3U
+#define PMCLOG_TYPE_CALLCHAIN 16U
+
+#define PMC_VERSION 0x0a010000U
+#define PMC_CPU_AMD_K8 0x01U
+#define PMC_CPUID_LEN 64U
+
+#define PMC_CC_F_MULTIPART 0x02U
+#define PMC_CC_MULTIPART_IBS_FETCH 2U
+#define PMC_CC_MULTIPART_IBS_OP 3U
+
+#define IBS_FETCH_CTL_ICMISS (1ULL << 51)
+#define IBS_FETCH_CTL_L1TLBMISS (1ULL << 55)
+#define IBS_FETCH_CTL_OPCACHEMISS (1ULL << 60)
+#define IBS_FETCH_CTL_L3MISS (1ULL << 61)
+
+#define IBS_OP_DATA3_LOAD (1ULL << 0)
+#define IBS_OP_DATA3_DCL1TLBMISS (1ULL << 2)
+#define IBS_OP_DATA3_DCMISS (1ULL << 7)
+#define IBS_OP_DATA3_DCMISSNOMABALLOC (1ULL << 16)
+#define IBS_OP_DATA3_DCLINADDRVALID (1ULL << 17)
+#define IBS_OP_DATA3_DCPHYADDRVALID (1ULL << 18)
+#define IBS_OP_DATA3_L2MISS (1ULL << 20)
+#define IBS_OP_DATA3_SWPF (1ULL << 21)
+
+static void
+write_bytes(FILE *f, const void *p, size_t n)
+{
+ if (fwrite(p, n, 1, f) != 1) {
+ perror("fwrite");
+ exit(1);
+ }
+}
+
+static void
+w32(FILE *f, uint32_t v)
+{
+ write_bytes(f, &v, sizeof(v));
+}
+
+static void
+w64(FILE *f, uint64_t v)
+{
+ write_bytes(f, &v, sizeof(v));
+}
+
+static void
+write_header(FILE *f, uint8_t type, uint16_t len, uint64_t tsc)
+{
+ uint32_t h;
+
+ h = (PMCLOG_HEADER_MAGIC << 24) | ((uint32_t)type << 16) | len;
+ w32(f, h);
+ w32(f, 0);
+ w64(f, tsc);
+}
+
+static void
+write_init(FILE *f, const char *cpuid)
+{
+ struct timespec ts;
+ char id[PMC_CPUID_LEN];
+
+ memset(id, 0, sizeof(id));
+ (void)snprintf(id, sizeof(id), "%s", cpuid);
+ if (clock_gettime(CLOCK_REALTIME, &ts) != 0) {
+ perror("clock_gettime");
+ exit(1);
+ }
+
+ write_header(f, PMCLOG_TYPE_INITIALIZE,
+ 16 + 4 + 4 + 8 + sizeof(ts) + PMC_CPUID_LEN, 1);
+ w32(f, PMC_VERSION);
+ w32(f, PMC_CPU_AMD_K8);
+ w64(f, 3500000000ULL);
+ write_bytes(f, &ts, sizeof(ts));
+ write_bytes(f, id, sizeof(id));
+}
+
+static void
+write_callchain(FILE *f, uint8_t part, const uint64_t *qwords, size_t nqwords,
+ uint64_t tsc)
+{
+ uint8_t hdr[8];
+ size_t len;
+
+ if (nqwords > UINT8_MAX) {
+ fprintf(stderr, "too many multipart qwords: %zu\n", nqwords);
+ exit(1);
+ }
+
+ memset(hdr, 0, sizeof(hdr));
+ hdr[0] = part;
+ hdr[1] = (uint8_t)nqwords;
+ len = 16 + 16 + sizeof(hdr) + nqwords * sizeof(uint64_t);
+ if (len > UINT16_MAX) {
+ fprintf(stderr, "pmclog callchain record too large: %zu\n", len);
+ exit(1);
+ }
+
+ write_header(f, PMCLOG_TYPE_CALLCHAIN, (uint16_t)len, tsc);
+ w32(f, 1234);
+ w32(f, 1234);
+ w32(f, 0);
+ w32(f, PMC_CC_F_MULTIPART);
+ write_bytes(f, hdr, sizeof(hdr));
+ write_bytes(f, qwords, nqwords * sizeof(uint64_t));
+}
+
+int
+main(int argc, char **argv)
+{
+ FILE *f;
+ uint64_t fetch[4];
+ uint64_t op[9];
+ uint64_t data3;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s output.pmc cpuid\n", argv[0]);
+ return (2);
+ }
+
+ f = fopen(argv[1], "wb");
+ if (f == NULL) {
+ perror(argv[1]);
+ return (1);
+ }
+
+ write_init(f, argv[2]);
+
+ fetch[0] = IBS_FETCH_CTL_ICMISS | IBS_FETCH_CTL_L1TLBMISS |
+ IBS_FETCH_CTL_OPCACHEMISS | IBS_FETCH_CTL_L3MISS | (0x33ULL << 32);
+ fetch[1] = 0;
+ fetch[2] = 0x40001234ULL;
+ fetch[3] = 0;
+ write_callchain(f, PMC_CC_MULTIPART_IBS_FETCH, fetch, nitems(fetch), 2);
+
+ /*
+ * Include OP DATA3 bits from erratum #1293 so the synthetic log also
+ * carries an Op multipart record. pmcstat does not print the affected
+ * L2Miss/OpenMemReqs fields today, so the test only asserts that
+ * non-erratum Op fields survive decode.
+ */
+ data3 = IBS_OP_DATA3_LOAD | IBS_OP_DATA3_DCL1TLBMISS |
+ IBS_OP_DATA3_DCMISS | IBS_OP_DATA3_DCMISSNOMABALLOC |
+ IBS_OP_DATA3_DCLINADDRVALID | IBS_OP_DATA3_DCPHYADDRVALID |
+ IBS_OP_DATA3_L2MISS | IBS_OP_DATA3_SWPF | (0x2aULL << 26) |
+ (0x44ULL << 32);
+ op[0] = 0;
+ op[1] = 0x401000ULL;
+ op[2] = 0;
+ op[3] = 0;
+ op[4] = data3;
+ op[5] = 0x50001234ULL;
+ op[6] = 0x60001234ULL;
+ op[7] = 0;
+ op[8] = 0;
+ write_callchain(f, PMC_CC_MULTIPART_IBS_OP, op, nitems(op), 3);
+
+ write_header(f, PMCLOG_TYPE_CLOSELOG, 16, 4);
+ if (fclose(f) != 0) {
+ perror("fclose");
+ return (1);
+ }
+ return (0);
+}
+EOF
+
+ atf_check -s exit:0 -o empty -e save:mk_ibs_pmclog.cc.err \
+ cc -Wall -Wextra -O2 -o mk_ibs_pmclog mk_ibs_pmclog.c
+}
+
+pmcstat_errata_decode()
+{
+ local cpuid log out pmcstat_bin
+
+ cpuid="$1"
+ log="$2"
+ out="$3"
+ pmcstat_bin="$4"
+
+ ./mk_ibs_pmclog "$log" "$cpuid" || \
+ atf_fail "failed to create synthetic pmclog for $cpuid"
+ atf_check -s exit:0 -o save:"$out" -e save:"$out.err" \
+ "$pmcstat_bin" -R "$log"
+ if [ ! -s "$out" ]; then
+ atf_fail "pmcstat -R produced empty output for $cpuid"
+ fi
+}
+
+pmcstat_errata_fetch_line()
+{
+ local out line
+
+ out="$1"
+ line=$(grep '^ibs-fetch[[:space:]]' "$out" | grep -v 'Latency' | head -1)
+ if [ -z "$line" ]; then
+ atf_fail "pmcstat -R output has no IBS Fetch decode line: $(cat "$out")"
+ fi
+ printf '%s\n' "$line"
+}
+
+pmcstat_errata_op_line()
+{
+ local out line
+
+ out="$1"
+ line=$(grep '^ibs-op[[:space:]].*load ' "$out" | head -1)
+ if [ -z "$line" ]; then
+ atf_fail "pmcstat -R output has no IBS Op decode line: $(cat "$out")"
+ fi
+ printf '%s\n' "$line"
+}
+
+pmcstat_errata_line_has_token()
+{
+ #
+ # Whitespace-tolerant token search against a `pmcstat -R` decode line.
+ # The previous implementation matched literal " token " in the
+ # untouched string, which silently missed a hit when the decoder
+ # emitted a tab, a punctuation suffix, or doubled spaces.
+ #
+ # Use awk field splitting so any run of whitespace separates tokens,
+ # then accept either an exact field match or the field with a trailing
+ # punctuation character (',' ';' ':' '.' ')' ']'). This stays robust
+ # against pmcstat formatting drift without becoming a fuzzy substring
+ # match that could mask a genuine missing decode.
+ #
+ awk -v t="$2" '
+ function strip(s) {
+ sub(/[,;:.\)\]]+$/, "", s)
+ return s
+ }
+ {
+ for (i = 1; i <= NF; i++)
+ if ($i == t || strip($i) == t) { found = 1; exit }
+ }
+ END { exit(found ? 0 : 1) }' <<EOF
+$1
+EOF
+}
+
+pmcstat_errata_require_fetch_context()
+{
+ local line
+
+ line="$1"
+ for bit in l1tlbmiss opcachemiss l3miss; do
+ if ! pmcstat_errata_line_has_token "$line" "$bit"; then
+ atf_fail "IBS Fetch line lost $bit while checking errata: $line"
+ fi
+ done
+}
+
+pmcstat_errata_require_op_context()
+{
+ local line
+
+ line="$1"
+ for bit in load l1tlbmiss dcmiss; do
+ if ! pmcstat_errata_line_has_token "$line" "$bit"; then
+ atf_fail "IBS Op line lost $bit while checking errata: $line"
+ fi
+ done
+}
+
+atf_test_case zen3_b0_fetch_icmiss_suppressed cleanup
+zen3_b0_fetch_icmiss_suppressed_head()
+{
+ atf_set "descr" \
+ "pmcstat -R suppresses invalid IbsIcMiss for AMD Zen 3 B0 producer CPUIDs"
+ atf_set "require.progs" "cc"
+}
+zen3_b0_fetch_icmiss_suppressed_body()
+{
+ local cpuid line suppressed_count total_count
+
+ pmcstat_errata_check_support
+ pmcstat_errata_pmcstat
+ pmcstat_errata_build_writer
+
+ #
+ # Verify two affected CPUIDs: model 00h (canonical B0) and model 0Fh
+ # (boundary of the errata range, Family 19h model 0-0Fh).
+ # The synthetic fetch sample has IbsIcMiss set in IBS_FETCH_CTL.
+ # hwpmc_ibs.c strips it for Zen 3 B0 before logging; pmcstat -R must
+ # therefore not print "icmiss" while preserving l1tlbmiss, opcachemiss,
+ # and l3miss (erratum #1238).
+ #
+ # Resilience: if pmcstat lacks the erratum #1238 suppression it will print
+ # "icmiss" for ALL Zen 3 B0 CPUIDs. Treat that uniformly as a skip
+ # (pmcstat binary not patched) rather than a FAIL, so CI stays green
+ # while the patch is in flight. Only fail if suppression is
+ # inconsistent (some CPUIDs suppressed, others not).
+ #
+ suppressed_count=0
+ total_count=0
+ for cpuid in AuthenticAMD-25-00-1 AuthenticAMD-25-0F-1; do
+ pmcstat_errata_decode "$cpuid" "$cpuid.pmc" "$cpuid.out" \
+ "$PMCSTAT_BIN"
+ line=$(pmcstat_errata_fetch_line "$cpuid.out")
+ pmcstat_errata_require_fetch_context "$line"
+ total_count=$((total_count + 1))
+ if ! pmcstat_errata_line_has_token "$line" icmiss; then
+ suppressed_count=$((suppressed_count + 1))
+ fi
+ done
+
+ if [ "$suppressed_count" -eq 0 ]; then
+ atf_skip "pmcstat does not suppress IbsIcMiss for Zen 3 B0 CPUIDs" \
+ "— erratum #1238 suppression not present"
+ fi
+ if [ "$suppressed_count" -ne "$total_count" ]; then
+ for cpuid in AuthenticAMD-25-00-1 AuthenticAMD-25-0F-1; do
+ line=$(pmcstat_errata_fetch_line "$cpuid.out")
+ if pmcstat_errata_line_has_token "$line" icmiss; then
+ atf_fail "Zen 3 B0 CPUID $cpuid: icmiss must be" \
+ "suppressed but was present: $line"
+ fi
+ done
+ fi
+}
+zen3_b0_fetch_icmiss_suppressed_cleanup()
+{
+ rm -f mk_ibs_pmclog mk_ibs_pmclog.c mk_ibs_pmclog.cc.err \
+ AuthenticAMD-25-*.pmc \
+ AuthenticAMD-25-*.out AuthenticAMD-25-*.out.err
+}
+
+atf_test_case unaffected_fetch_icmiss_preserved cleanup
+unaffected_fetch_icmiss_preserved_head()
+{
+ atf_set "descr" \
+ "pmcstat -R preserves IbsIcMiss for unaffected AMD producer CPUIDs"
+ atf_set "require.progs" "cc"
+}
+unaffected_fetch_icmiss_preserved_body()
+{
+ local cpuid line opline
+
+ pmcstat_errata_check_support
+ pmcstat_errata_pmcstat
+ pmcstat_errata_build_writer
+
+ for cpuid in AuthenticAMD-25-20-1 AuthenticAMD-25-10-1 \
+ AuthenticAMD-25-11-1 AuthenticAMD-26-00-0; do
+ pmcstat_errata_decode "$cpuid" "$cpuid.pmc" "$cpuid.out" \
+ "$PMCSTAT_BIN"
+ line=$(pmcstat_errata_fetch_line "$cpuid.out")
+ opline=$(pmcstat_errata_op_line "$cpuid.out")
+ pmcstat_errata_require_fetch_context "$line"
+ pmcstat_errata_require_op_context "$opline"
+ if ! pmcstat_errata_line_has_token "$line" icmiss; then
+ atf_fail "unaffected CPUID $cpuid lost valid icmiss: $line"
+ fi
+ done
+}
+unaffected_fetch_icmiss_preserved_cleanup()
+{
+ rm -f mk_ibs_pmclog mk_ibs_pmclog.c mk_ibs_pmclog.cc.err \
+ AuthenticAMD-25-*.pmc \
+ AuthenticAMD-25-*.out AuthenticAMD-25-*.out.err \
+ AuthenticAMD-26-*.pmc AuthenticAMD-26-*.out AuthenticAMD-26-*.out.err
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case zen3_b0_fetch_icmiss_suppressed
+ atf_add_test_case unaffected_fetch_icmiss_preserved
+}
Index: tests/sys/pmc/amd/pmu_detect_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmu_detect_test.c
@@ -0,0 +1,290 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ *
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "amd_pmc_test_common.h"
+#include "umcdf/amd_umcdf_common.h"
+
+/*
+ * Minimum AMD core PMC counters expected per logical CPU for each Zen
+ * generation. Zen 1-5 all expose at least 6 general-purpose core PMCs when
+ * hwpmc is loaded.
+ */
+#define AMD_CORE_PMC_MIN 6
+
+
+ATF_TC(pmu_detect_amd_vendor);
+ATF_TC_HEAD(pmu_detect_amd_vendor, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that the CPU reports AuthenticAMD via CPUID leaf 0.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_amd_vendor, tc)
+{
+ amd_test_skip_unless_amd();
+}
+
+
+ATF_TC(pmu_detect_hwpmc_init);
+ATF_TC_HEAD(pmu_detect_hwpmc_init, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that pmc_init() succeeds -- hwpmc.ko is loaded and "
+ "/dev/pmc is accessible.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_hwpmc_init, tc)
+{
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+}
+
+
+ATF_TC(pmu_detect_dev_pmc_open);
+ATF_TC_HEAD(pmu_detect_dev_pmc_open, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that the hwpmc syscall interface is reachable via pmc_init().");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_dev_pmc_open, tc)
+{
+ amd_test_skip_unless_amd();
+
+ /*
+ * hwpmc(4) uses a syscall interface, not a cdev -- /dev/pmc does not
+ * exist on a static-kernel build. Use pmc_init() which exercises the
+ * same kernel entry point that all pmc(3) consumers use.
+ */
+ ATF_REQUIRE_MSG(pmc_init() == 0,
+ "pmc_init() failed -- hwpmc not available: %s", strerror(errno));
+}
+
+
+ATF_TC(pmu_detect_k8_class_present);
+ATF_TC_HEAD(pmu_detect_k8_class_present, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that PMC_CLASS_K8 is listed in pmc_getcpuinfo() on AMD.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_k8_class_present, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ const struct pmc_classinfo *ci;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+
+ ci = amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8);
+ ATF_CHECK_MSG(ci != NULL,
+ "PMC_CLASS_K8 not found in pmc_cpuinfo() output -- "
+ "AMD core PMU backend not registered in hwpmc");
+
+ if (ci != NULL)
+ fprintf(stderr,
+ "PMC_CLASS_K8: width=%u npmc=%u caps=0x%x\n",
+ ci->pm_width, ci->pm_num, ci->pm_caps);
+}
+
+
+ATF_TC(pmu_detect_counter_width);
+ATF_TC_HEAD(pmu_detect_counter_width, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that AMD core PMC counters are 48 bits wide "
+ "(Zen 1+ hardware is 48 bits; hwpmc always reports pd_width=48).");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_counter_width, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ const struct pmc_classinfo *ci;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+
+ ci = amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8);
+ if (ci == NULL)
+ atf_tc_skip("PMC_CLASS_K8 not present -- skipping width check");
+
+ /*
+ * hwpmc always initialises pd_width to 48 for AMD core PMCs
+ * (see amd_init() in hwpmc_amd.c, lines ~1015/1035/1051).
+ * A result < 48 indicates a regression in the kernel backend.
+ */
+ ATF_CHECK_MSG(ci->pm_width >= 48,
+ "AMD core PMC width %u is less than 48 bits "
+ "(expected 48 per PPR and hwpmc_amd.c)", ci->pm_width);
+ fprintf(stderr, "AMD core PMC counter width: %u bits\n", ci->pm_width);
+}
+
+
+ATF_TC(pmu_detect_core_pmc_count);
+ATF_TC_HEAD(pmu_detect_core_pmc_count, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that the AMD core PMU exposes at least "
+ __XSTRING(AMD_CORE_PMC_MIN) " counters per CPU.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_core_pmc_count, tc)
+{
+ struct amd_umcdf_cpu cpu;
+ size_t n;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ /*
+ * hwpmc names AMD core PMC rows "K8-N" (PMC_CLASS_K8).
+ * "AMD-" was a false assumption -- actual rows are K8-0..K8-5.
+ */
+ n = amd_test_count_pmc_rows_with_prefix(0, "K8-");
+ fprintf(stderr, "AMD core PMC rows on cpu0: %zu (zen=%d)\n",
+ n, cpu.zen);
+
+ ATF_CHECK_MSG(n >= AMD_CORE_PMC_MIN,
+ "Expected at least %d AMD core PMC rows on cpu0, got %zu",
+ AMD_CORE_PMC_MIN, n);
+}
+
+
+ATF_TC(pmu_detect_zen_generation);
+ATF_TC_HEAD(pmu_detect_zen_generation, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Report the detected AMD Zen generation (informational; "
+ "fails only if the CPU is unrecognized pre-Zen).");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_zen_generation, tc)
+{
+ struct amd_umcdf_cpu cpu;
+
+ amd_test_skip_unless_amd();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ fprintf(stderr,
+ "AMD CPU: family=%02xh model=%02xh stepping=%xh zen=%d\n",
+ cpu.family, cpu.model, cpu.stepping, cpu.zen);
+
+ ATF_CHECK_MSG(cpu.zen >= 1,
+ "Detected Zen generation %d; expected >= 1 on supported AMD HW",
+ cpu.zen);
+}
+
+
+ATF_TC(pmu_detect_pmu_events_enabled);
+ATF_TC_HEAD(pmu_detect_pmu_events_enabled, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that pmc_pmu_enabled() returns true -- the JSON PMU event "
+ "tables are present for this CPU.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_pmu_events_enabled, tc)
+{
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled for this CPU -- "
+ "JSON tables may be absent from the hwpmc build");
+
+ ATF_CHECK_MSG(pmc_pmu_enabled(),
+ "pmc_pmu_enabled() must return true after skip guard passed");
+}
+
+
+ATF_TC(pmu_detect_tsc_class_coexists);
+ATF_TC_HEAD(pmu_detect_tsc_class_coexists, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that PMC_CLASS_TSC and PMC_CLASS_K8 coexist in the "
+ "same pmc_cpuinfo() output on AMD -- both must be registered "
+ "for the full hwpmc AMD backend to be functional.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_tsc_class_coexists, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ const struct pmc_classinfo *ci_k8, *ci_tsc;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+
+ ci_k8 = amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8);
+ ci_tsc = amd_test_find_classinfo(cpuinfo, PMC_CLASS_TSC);
+
+ ATF_CHECK_MSG(ci_k8 != NULL,
+ "PMC_CLASS_K8 not found -- AMD core PMU backend not registered");
+ ATF_CHECK_MSG(ci_tsc != NULL,
+ "PMC_CLASS_TSC not found -- TSC backend not registered alongside "
+ "AMD core PMU");
+
+ if (ci_k8 != NULL && ci_tsc != NULL)
+ fprintf(stderr,
+ "PMC_CLASS_K8 npmc=%u width=%u "
+ "PMC_CLASS_TSC npmc=%u width=%u\n",
+ ci_k8->pm_num, ci_k8->pm_width,
+ ci_tsc->pm_num, ci_tsc->pm_width);
+}
+
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_detect_amd_vendor);
+ ATF_TP_ADD_TC(tp, pmu_detect_hwpmc_init);
+ ATF_TP_ADD_TC(tp, pmu_detect_dev_pmc_open);
+ ATF_TP_ADD_TC(tp, pmu_detect_k8_class_present);
+ ATF_TP_ADD_TC(tp, pmu_detect_counter_width);
+ ATF_TP_ADD_TC(tp, pmu_detect_core_pmc_count);
+ ATF_TP_ADD_TC(tp, pmu_detect_zen_generation);
+ ATF_TP_ADD_TC(tp, pmu_detect_pmu_events_enabled);
+ ATF_TP_ADD_TC(tp, pmu_detect_tsc_class_coexists);
+
+ return (atf_no_error());
+}
Index: tests/sys/pmc/amd/pmu_event_neg_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmu_event_neg_test.c
@@ -0,0 +1,450 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ *
+ * These tests call the kernel PMC_OP_PMCALLOCATE syscall directly
+ * (via the same mechanism as libpmc) with corrupted config words.
+ * This exercises the amd_config_mask() validation path in hwpmc_amd.c
+ * that is bypassed by pmc_pmu_pmcallocate() on the user-space side.
+ *
+ *
+ * Reference:
+ * sys/dev/hwpmc/hwpmc_amd.c: amd_allocate_pmc(), amd_config_mask()
+ * sys/dev/hwpmc/hwpmc_amd.h: AMD_VALID_BITS, AMD_PMC_HOST, AMD_PMC_GUEST
+ *
+ * AMD_VALID_BITS = CounterMask | Invert | Enable | Int | PC | Edge |
+ * OS | USR | UnitMask | EventMask
+ * Notably excluded: HOST (bit 41), GUEST (bit 40), PRECISERETIRE (bit 43)
+ * without PMC_CAP_PRECISE.
+ *
+ * Kernel validation path:
+ * amd_allocate_pmc() line ~420:
+ * config = a->pm_md.pm_amd.pm_amd_config;
+ * if ((config & ~amd_config_mask(subclass, caps)) != 0)
+ * return (EINVAL);
+ */
+
+#include <sys/param.h>
+#include <sys/module.h>
+#include <sys/pmc.h>
+#include <sys/syscall.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <machine/pmc_mdep.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "amd_pmc_test_common.h"
+#include "umcdf/amd_umcdf_common.h"
+
+
+/*
+ * Discover the hwpmc syscall number via modstat(), exactly as pmc_init()
+ * does in libpmc.c. Returns the syscall number on success, -1 on failure.
+ * Sets errno appropriately on failure.
+ */
+static int
+neg_get_pmc_syscall(void)
+{
+ struct module_stat ms;
+ int mod_id;
+
+ mod_id = modfind(PMC_MODULE_NAME);
+ if (mod_id < 0)
+ return (-1);
+
+ ms.version = sizeof(ms);
+ if (modstat(mod_id, &ms) < 0)
+ return (-1);
+
+ return ((int)ms.data.intval);
+}
+
+/*
+ * Attempt a raw PMC_OP_PMCALLOCATE syscall with the given op struct.
+ * Returns 0 on success, -1 on failure (errno set by kernel).
+ *
+ * This bypasses libpmc's user-space validation so we can deliver
+ * arbitrary config words directly to amd_allocate_pmc().
+ */
+static int
+neg_raw_pmcallocate(int pmc_sc, struct pmc_op_pmcallocate *op)
+{
+ return ((int)syscall(pmc_sc, PMC_OP_PMCALLOCATE, op));
+}
+
+/*
+ * Release a PMC by id using the raw syscall interface.
+ * Best-effort: ignores errors (used for cleanup only).
+ */
+static void
+neg_raw_pmcrelease(int pmc_sc, pmc_id_t pmcid)
+{
+ struct pmc_op_simple op;
+
+ memset(&op, 0, sizeof(op));
+ op.pm_pmcid = pmcid;
+ (void)syscall(pmc_sc, PMC_OP_PMCRELEASE, &op);
+}
+
+/*
+ * Build a baseline valid pmc_op_pmcallocate for a Zen4 CORE event
+ * using pmc_pmu_pmcallocate(). The returned struct has a valid config
+ * word and all required fields set. Returns true on success.
+ */
+static bool
+neg_make_valid_op(struct pmc_op_pmcallocate *op)
+{
+ memset(op, 0, sizeof(*op));
+
+ /* ex_ret_instr: event=0xc0, umask=0, present on all Zen1+ */
+ if (pmc_pmu_pmcallocate("ex_ret_instr", op) != 0)
+ return (false);
+
+ /*
+ * PMC_MODE_SC: system-wide counting on cpu 0.
+ * PMC_F_EV_PMU: tells the kernel this is a PMU-events allocation
+ * and that pm_md.pm_amd.pm_amd_config is authoritative.
+ */
+ op->pm_mode = PMC_MODE_SC;
+ op->pm_flags = PMC_F_EV_PMU;
+ op->pm_cpu = 0;
+
+ return (true);
+}
+
+
+static void
+neg_skip_unless_ready(struct amd_umcdf_cpu *cpu, int *sc_out)
+{
+ int sc;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(cpu);
+
+ if (cpu->zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; negative-path tests require Zen4+ "
+ "(amd_config_mask validation uses pmc_cpuid string)",
+ amd_umcdf_zen_name(cpu->zen));
+
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled -- "
+ "cannot build baseline valid config word");
+
+ sc = neg_get_pmc_syscall();
+ if (sc < 0)
+ atf_tc_skip("Cannot discover hwpmc syscall number "
+ "(hwpmc.ko not loaded?): %s", strerror(errno));
+
+ *sc_out = sc;
+}
+
+/* ================================================================
+ * NEGATIVE-PATH TESTS
+ * ================================================================ */
+
+ATF_TC(pmu_neg_host_bit_rejected);
+ATF_TC_HEAD(pmu_neg_host_bit_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] Verify that setting HOST bit (bit 41) in a CORE PMC "
+ "config word causes amd_allocate_pmc() to return EINVAL. "
+ "HOST is not in AMD_VALID_BITS for the CORE sub-class.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_host_bit_rejected, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc, saved_errno;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable on this system: %s",
+ strerror(errno));
+
+ /* Corrupt: set HOST bit (bit 41) */
+ op.pm_md.pm_amd.pm_amd_config |= AMD_PMC_HOST;
+
+ fprintf(stderr,
+ "config_with_HOST=0x%016jx HOST=(1ULL<<41)\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == -1,
+ "PMCALLOCATE with HOST bit (bit 41) must fail but succeeded "
+ "(pmcid=%u)", op.pm_pmcid);
+
+ saved_errno = errno;
+ if (op.pm_pmcid != PMC_ID_INVALID)
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ ATF_CHECK_MSG(saved_errno == EINVAL,
+ "PMCALLOCATE with HOST bit: expected EINVAL, got %d (%s)",
+ saved_errno, strerror(saved_errno));
+
+ fprintf(stderr, "HOST bit correctly rejected: errno=%d (EINVAL)\n",
+ saved_errno);
+}
+
+
+ATF_TC(pmu_neg_guest_bit_rejected);
+ATF_TC_HEAD(pmu_neg_guest_bit_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] Verify that setting GUEST bit (bit 40) in a CORE PMC "
+ "config word causes amd_allocate_pmc() to return EINVAL. "
+ "GUEST is not in AMD_VALID_BITS for the CORE sub-class.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_guest_bit_rejected, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc, saved_errno;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable: %s", strerror(errno));
+
+ /* Corrupt: set GUEST bit (bit 40) */
+ op.pm_md.pm_amd.pm_amd_config |= AMD_PMC_GUEST;
+
+ fprintf(stderr,
+ "config_with_GUEST=0x%016jx GUEST=(1ULL<<40)\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == -1,
+ "PMCALLOCATE with GUEST bit (bit 40) must fail but succeeded "
+ "(pmcid=%u)", op.pm_pmcid);
+
+ saved_errno = errno;
+ if (op.pm_pmcid != PMC_ID_INVALID)
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ ATF_CHECK_MSG(saved_errno == EINVAL,
+ "PMCALLOCATE with GUEST bit: expected EINVAL, got %d (%s)",
+ saved_errno, strerror(saved_errno));
+
+ fprintf(stderr, "GUEST bit correctly rejected: errno=%d (EINVAL)\n",
+ saved_errno);
+}
+
+
+ATF_TC(pmu_neg_host_and_guest_bits_rejected);
+ATF_TC_HEAD(pmu_neg_host_and_guest_bits_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] Verify that setting both HOST (bit 41) and GUEST (bit 40) "
+ "in a CORE PMC config word causes EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_host_and_guest_bits_rejected, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc, saved_errno;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable: %s", strerror(errno));
+
+ /* Corrupt: set both HOST and GUEST bits */
+ op.pm_md.pm_amd.pm_amd_config |= (AMD_PMC_HOST | AMD_PMC_GUEST);
+
+ fprintf(stderr,
+ "config_with_HOST|GUEST=0x%016jx\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == -1,
+ "PMCALLOCATE with HOST|GUEST bits must fail but succeeded "
+ "(pmcid=%u)", op.pm_pmcid);
+
+ saved_errno = errno;
+ if (op.pm_pmcid != PMC_ID_INVALID)
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ ATF_CHECK_MSG(saved_errno == EINVAL,
+ "PMCALLOCATE with HOST|GUEST: expected EINVAL, got %d (%s)",
+ saved_errno, strerror(saved_errno));
+
+ fprintf(stderr,
+ "HOST|GUEST bits correctly rejected: errno=%d (EINVAL)\n",
+ saved_errno);
+}
+
+
+ATF_TC(pmu_neg_reserved_bits_rejected);
+ATF_TC_HEAD(pmu_neg_reserved_bits_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] Verify that setting reserved bits (bits 63:44) in a CORE "
+ "PMC config word causes amd_allocate_pmc() to return EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_reserved_bits_rejected, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc, saved_errno;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable: %s", strerror(errno));
+
+ /*
+ * Set bit 44 -- above PRECISERETIRE (bit 43), clearly reserved,
+ * not in AMD_VALID_BITS or any extra mask.
+ */
+ op.pm_md.pm_amd.pm_amd_config |= (1ULL << 44);
+
+ fprintf(stderr,
+ "config_with_bit44=0x%016jx\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == -1,
+ "PMCALLOCATE with reserved bit 44 must fail but succeeded "
+ "(pmcid=%u)", op.pm_pmcid);
+
+ saved_errno = errno;
+ if (op.pm_pmcid != PMC_ID_INVALID)
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ ATF_CHECK_MSG(saved_errno == EINVAL,
+ "PMCALLOCATE with bit 44: expected EINVAL, got %d (%s)",
+ saved_errno, strerror(saved_errno));
+
+ fprintf(stderr,
+ "Reserved bit 44 correctly rejected: errno=%d (EINVAL)\n",
+ saved_errno);
+}
+
+
+ATF_TC(pmu_neg_preciseretire_without_cap_rejected);
+ATF_TC_HEAD(pmu_neg_preciseretire_without_cap_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] Verify that setting PRECISERETIRE (bit 43) without "
+ "PMC_CAP_PRECISE causes EINVAL on Zen4+. "
+ "amd_config_mask() only permits bit 43 when PMC_CAP_PRECISE "
+ "is set in the allocation caps.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_preciseretire_without_cap_rejected, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc, saved_errno;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ /*
+ * PRECISERETIRE is only meaningful on Zen5+ (Family 1Ah).
+ * On Zen4 (Family 19h) it is not in AMD_VALID_BITS and has no
+ * PERF_CTL2 support. Either way, passing it without PMC_CAP_PRECISE
+ * must be rejected.
+ */
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable: %s", strerror(errno));
+
+ /* pm_caps = 0 (default) -> PMC_CAP_PRECISE not set */
+ op.pm_md.pm_amd.pm_amd_config |= AMD_PMC_PRECISERETIRE;
+
+ fprintf(stderr,
+ "config_with_PRECISERETIRE=0x%016jx caps=0x%x\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config, op.pm_caps);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == -1,
+ "PMCALLOCATE with PRECISERETIRE (bit 43) and caps=0 must fail "
+ "but succeeded (pmcid=%u)", op.pm_pmcid);
+
+ saved_errno = errno;
+ if (op.pm_pmcid != PMC_ID_INVALID)
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ ATF_CHECK_MSG(saved_errno == EINVAL,
+ "PMCALLOCATE with PRECISERETIRE/no-cap: "
+ "expected EINVAL, got %d (%s)", saved_errno, strerror(saved_errno));
+
+ fprintf(stderr,
+ "PRECISERETIRE without PMC_CAP_PRECISE correctly rejected: "
+ "errno=%d (EINVAL)\n", saved_errno);
+}
+
+
+ATF_TC(pmu_neg_valid_config_accepted);
+ATF_TC_HEAD(pmu_neg_valid_config_accepted, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG/SANITY] Verify that a valid config word (no forbidden bits) "
+ "is accepted by amd_allocate_pmc() -- sanity check that the "
+ "rejection tests are not caused by an unrelated error.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_valid_config_accepted, tc)
+{
+ struct pmc_op_pmcallocate op = { .pm_pmcid = PMC_ID_INVALID };
+ struct amd_umcdf_cpu cpu;
+ int sc;
+
+ neg_skip_unless_ready(&cpu, &sc);
+
+ if (!neg_make_valid_op(&op))
+ atf_tc_skip("ex_ret_instr not resolvable: %s", strerror(errno));
+
+ fprintf(stderr,
+ "baseline valid config=0x%016jx\n",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+
+ ATF_CHECK_MSG(neg_raw_pmcallocate(sc, &op) == 0,
+ "PMCALLOCATE with valid config must succeed but failed: "
+ "errno=%d (%s)", errno, strerror(errno));
+
+ if (op.pm_pmcid != PMC_ID_INVALID) {
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+ fprintf(stderr, "Valid config accepted: pmcid=%u\n",
+ op.pm_pmcid);
+ }
+}
+
+/* ================================================================
+ * Test program entry
+ * ================================================================ */
+
+ATF_TP_ADD_TCS(tp)
+{
+ /* Sanity: valid config must be accepted */
+ ATF_TP_ADD_TC(tp, pmu_neg_valid_config_accepted);
+
+ /* Negative: forbidden bits must be rejected */
+ ATF_TP_ADD_TC(tp, pmu_neg_host_bit_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_guest_bit_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_host_and_guest_bits_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_reserved_bits_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_preciseretire_without_cap_rejected);
+
+ return (atf_no_error());
+}
Index: tests/sys/pmc/amd/pmu_event_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmu_event_test.c
@@ -0,0 +1,624 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ *
+ * [UNIT] Pure bit-manipulation helpers -- no hardware, no hwpmc.
+ * Tests the EventSelect and UnitMask extraction macros against
+ * known config words derived from the AMD PPR and hwpmc macros.
+ *
+ * [SMOKE] pmc_pmu_pmcallocate() lookup by name for Zen4 well-known events.
+ * Verifies EventSelect bits 7:0 + 35:32, UnitMask bits 15:8, and
+ * OsUser bits 17:16 in the returned config word.
+ *
+ * [E2E] Allocate a Zen4 core PMC, start it, generate instructions,
+ * stop it, and verify the counter incremented.
+ *
+ *
+ * Reference:
+ * AMD PPR for Family 19h Model 11h (Zen4) -- PERF_CTL MSR bit layout:
+ * bits 7:0 EventSelect[7:0]
+ * bits 15:8 UnitMask[7:0]
+ * bit 16 USR (count in user mode)
+ * bit 17 OS (count in OS/kernel mode)
+ * bits 35:32 EventSelect[11:8]
+ * bit 40 GUEST (count in guest mode)
+ * bit 41 HOST (count in host mode)
+ *
+ * hwpmc macros (sys/dev/hwpmc/hwpmc_amd.h):
+ * AMD_PMC_TO_EVENTMASK(x) = ((x) & 0xFF) | (((uint64_t)(x) & 0xF00) << 24)
+ * AMD_PMC_TO_UNITMASK(x) = ((x) << 8) & 0xFF00
+ * AMD_PMC_USR = (1 << 16)
+ * AMD_PMC_OS = (1 << 17)
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+#include "umcdf/amd_umcdf_common.h"
+
+
+/* EventSelect[7:0] from config word bits 7:0 */
+#define EVTEST_EVSEL_LO(cfg) ((uint32_t)((cfg) & 0xFFU))
+/* EventSelect[11:8] from config word bits 35:32 */
+#define EVTEST_EVSEL_HI(cfg) ((uint32_t)(((cfg) >> 24) & 0xF00U))
+/* Full 12-bit EventSelect */
+#define EVTEST_EVSEL(cfg) (EVTEST_EVSEL_LO(cfg) | EVTEST_EVSEL_HI(cfg))
+/* UnitMask[7:0] from config word bits 15:8 */
+#define EVTEST_UMASK(cfg) ((uint32_t)(((cfg) >> 8) & 0xFFU))
+/* USR bit (bit 16) */
+#define EVTEST_USR(cfg) (((cfg) >> 16) & 1U)
+/* OS bit (bit 17) */
+#define EVTEST_OS(cfg) (((cfg) >> 17) & 1U)
+
+static inline uint64_t
+evtest_make_config(uint32_t event_code, uint32_t umask)
+{
+ return (AMD_PMC_TO_EVENTMASK(event_code) | AMD_PMC_TO_UNITMASK(umask));
+}
+
+/*
+ * Zen4 well-known core PMU events.
+ *
+ * Names are lowercase as they appear in the JSON tables shipped with hwpmc
+ * (/usr/src/lib/libpmc/pmu-events/arch/x86/amdzen4/).
+ * event_code and umask are from the same JSON; UMask=null in JSON -> 0x00.
+ *
+ * Used for both unit tests (derived config words) and smoke tests (live
+ * pmc_pmu_pmcallocate() calls).
+ */
+static const struct amd_umcdf_event_candidate zen4_core_events[] = {
+ { "ex_ret_instr", "retired instructions", 0xc0, 0x00 },
+ { "ls_not_halted_cyc", "cpu cycles", 0x76, 0x00 },
+ { "ex_ret_brn", "retired branches", 0xc2, 0x00 },
+ { "ex_ret_brn_misp", "mispredicted branches",0xc3, 0x00 },
+ { "ls_mab_alloc.all_allocations", "l1d mab allocs", 0x41, 0x7f },
+ { "l2_request_g1.all", "l2 requests", 0x60, 0xff },
+ { "ex_ret_msprd_brnch_instr_dir_msmtch",
+ "mispred dir mismatch", 0x1c7, 0x00 },
+ /* sentinel */
+ { NULL, NULL, 0, 0 }
+};
+
+/* ================================================================
+ * UNIT TESTS -- no hardware, no pmc_init(), pure bit manipulation
+ * ================================================================ */
+
+ATF_TC(pmu_event_unit_evsel_lo_encoding);
+ATF_TC_HEAD(pmu_event_unit_evsel_lo_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Verify AMD_PMC_TO_EVENTMASK places EventSelect[7:0] "
+ "correctly in config word bits 7:0.");
+}
+
+ATF_TC_BODY(pmu_event_unit_evsel_lo_encoding, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ uint64_t cfg;
+
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ cfg = evtest_make_config(cand->event_code, cand->umask);
+ ATF_CHECK_MSG(EVTEST_EVSEL_LO(cfg) == (cand->event_code & 0xFF),
+ "%s: EventSelect[7:0] mismatch: "
+ "got 0x%02x expected 0x%02x (config=0x%016jx)",
+ cand->name,
+ EVTEST_EVSEL_LO(cfg), cand->event_code & 0xFF,
+ (uintmax_t)cfg);
+ }
+}
+
+
+ATF_TC(pmu_event_unit_evsel_hi_encoding);
+ATF_TC_HEAD(pmu_event_unit_evsel_hi_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Verify AMD_PMC_TO_EVENTMASK places EventSelect[11:8] "
+ "correctly in config word bits 35:32.");
+}
+
+ATF_TC_BODY(pmu_event_unit_evsel_hi_encoding, tc)
+{
+ /*
+ * None of the Zen4 test events have event_code > 0xFF, so bits 35:32
+ * should always be 0. We also verify a synthetic 12-bit event code
+ * (0x123) to confirm the upper nibble is placed in bits 35:32.
+ */
+ uint64_t cfg;
+
+ /* Standard events: bits 35:32 must be zero */
+ const struct amd_umcdf_event_candidate *cand;
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ cfg = evtest_make_config(cand->event_code, cand->umask);
+ ATF_CHECK_MSG(EVTEST_EVSEL_HI(cfg) == (cand->event_code & 0xF00),
+ "%s: EventSelect[11:8] mismatch: "
+ "got 0x%03x expected 0x%03x (config=0x%016jx)",
+ cand->name,
+ EVTEST_EVSEL_HI(cfg), cand->event_code & 0xF00,
+ (uintmax_t)cfg);
+ }
+
+ /* Synthetic: event_code=0x123 -> lo=0x23 in bits 7:0, hi=0x100 in bits 35:32 */
+ cfg = evtest_make_config(0x123, 0x00);
+ ATF_CHECK_MSG(EVTEST_EVSEL_LO(cfg) == 0x23,
+ "synthetic 0x123: EventSelect[7:0] got 0x%02x expected 0x23",
+ EVTEST_EVSEL_LO(cfg));
+ ATF_CHECK_MSG(EVTEST_EVSEL_HI(cfg) == 0x100,
+ "synthetic 0x123: EventSelect[11:8] got 0x%03x expected 0x100",
+ EVTEST_EVSEL_HI(cfg));
+ ATF_CHECK_MSG(EVTEST_EVSEL(cfg) == 0x123,
+ "synthetic 0x123: full EventSelect got 0x%03x expected 0x123",
+ EVTEST_EVSEL(cfg));
+
+ fprintf(stderr, "synthetic 0x123 config=0x%016jx ok\n", (uintmax_t)cfg);
+}
+
+
+ATF_TC(pmu_event_unit_umask_encoding);
+ATF_TC_HEAD(pmu_event_unit_umask_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Verify AMD_PMC_TO_UNITMASK places UnitMask[7:0] "
+ "correctly in config word bits 15:8.");
+}
+
+ATF_TC_BODY(pmu_event_unit_umask_encoding, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ uint64_t cfg;
+
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ cfg = evtest_make_config(cand->event_code, cand->umask);
+ ATF_CHECK_MSG(EVTEST_UMASK(cfg) == cand->umask,
+ "%s: UnitMask mismatch: got 0x%02x expected 0x%02x "
+ "(config=0x%016jx)",
+ cand->name,
+ EVTEST_UMASK(cfg), cand->umask,
+ (uintmax_t)cfg);
+ fprintf(stderr, " %-40s evsel=0x%03x umask=0x%02x config=0x%016jx\n",
+ cand->name, cand->event_code, cand->umask, (uintmax_t)cfg);
+ }
+}
+
+
+ATF_TC(pmu_event_unit_no_field_overlap);
+ATF_TC_HEAD(pmu_event_unit_no_field_overlap, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Verify that EventSelect and UnitMask fields do not overlap "
+ "in the config word -- a mask test on all test events.");
+}
+
+ATF_TC_BODY(pmu_event_unit_no_field_overlap, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ uint64_t cfg_evsel, cfg_umask;
+
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ cfg_evsel = AMD_PMC_TO_EVENTMASK(cand->event_code);
+ cfg_umask = AMD_PMC_TO_UNITMASK(cand->umask);
+
+ ATF_CHECK_MSG((cfg_evsel & cfg_umask) == 0,
+ "%s: EventSelect and UnitMask config bits overlap: "
+ "evsel=0x%016jx umask=0x%016jx overlap=0x%016jx",
+ cand->name,
+ (uintmax_t)cfg_evsel, (uintmax_t)cfg_umask,
+ (uintmax_t)(cfg_evsel & cfg_umask));
+ }
+}
+
+/* ================================================================
+ * SMOKE TESTS -- require AMD CPU, hwpmc loaded, pmu-events JSON
+ * ================================================================ */
+
+ATF_TC(pmu_event_smoke_pmu_enabled);
+ATF_TC_HEAD(pmu_event_smoke_pmu_enabled, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify pmc_pmu_enabled() returns true on Zen4 -- "
+ "prerequisite for all encoding smoke tests.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_pmu_enabled, tc)
+{
+ struct amd_umcdf_cpu cpu;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; Zen4 event encoding tests require Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled -- JSON tables absent");
+
+ ATF_CHECK(pmc_pmu_enabled());
+ fprintf(stderr, "pmu-events enabled for %s (family=%02xh model=%02xh)\n",
+ amd_umcdf_zen_name(cpu.zen), cpu.family, cpu.model);
+}
+
+
+ATF_TC(pmu_event_smoke_lookup_by_name);
+ATF_TC_HEAD(pmu_event_smoke_lookup_by_name, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify that well-known Zen4 core PMU event names resolve "
+ "via pmc_pmu_pmcallocate() and produce non-zero config words.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_lookup_by_name, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_umcdf_cpu cpu;
+ int found, total;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ found = total = 0;
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ total++;
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate(cand->name, &cfg) == 0) {
+ found++;
+ ATF_CHECK_MSG(cfg.pm_md.pm_amd.pm_amd_config != 0,
+ "%s: config word is zero after successful lookup",
+ cand->name);
+ fprintf(stderr, " found %-42s config=0x%016jx\n",
+ cand->name,
+ (uintmax_t)cfg.pm_md.pm_amd.pm_amd_config);
+ } else {
+ fprintf(stderr, " miss %-42s errno=%d (%s)\n",
+ cand->name, errno, strerror(errno));
+ }
+ }
+
+ ATF_CHECK_MSG(found > 0,
+ "None of the %d Zen4 test events resolved via pmc_pmu_pmcallocate(); "
+ "check hwpmc JSON tables for Zen4", total);
+ fprintf(stderr, "Zen4 event lookup: %d/%d resolved\n", found, total);
+}
+
+
+ATF_TC(pmu_event_smoke_evsel_encoding);
+ATF_TC_HEAD(pmu_event_smoke_evsel_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify EventSelect bits 7:0 and 35:32 in the hwpmc "
+ "config word match AMD PPR Zen4 event codes.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_evsel_encoding, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_umcdf_cpu cpu;
+ uint64_t raw;
+ uint32_t got_evsel;
+ bool any_checked;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ any_checked = false;
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate(cand->name, &cfg) != 0)
+ continue;
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ got_evsel = EVTEST_EVSEL(raw);
+ any_checked = true;
+
+ fprintf(stderr,
+ " %-42s config=0x%016jx evsel_got=0x%03x evsel_exp=0x%03x\n",
+ cand->name, (uintmax_t)raw, got_evsel, cand->event_code);
+
+ ATF_CHECK_MSG(got_evsel == cand->event_code,
+ "%s: EventSelect mismatch: got 0x%03x expected 0x%03x "
+ "(config=0x%016jx)",
+ cand->name, got_evsel, cand->event_code, (uintmax_t)raw);
+ }
+
+ if (!any_checked)
+ atf_tc_skip("No Zen4 test events resolved -- "
+ "cannot verify EventSelect encoding");
+}
+
+
+ATF_TC(pmu_event_smoke_umask_encoding);
+ATF_TC_HEAD(pmu_event_smoke_umask_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify UnitMask bits 15:8 in the hwpmc config word "
+ "match AMD PPR Zen4 unit mask values.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_umask_encoding, tc)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_umcdf_cpu cpu;
+ uint64_t raw;
+ uint32_t got_umask;
+ bool any_checked;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ any_checked = false;
+ for (cand = zen4_core_events; cand->name != NULL; cand++) {
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate(cand->name, &cfg) != 0)
+ continue;
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ got_umask = EVTEST_UMASK(raw);
+
+ fprintf(stderr,
+ " %-42s umask_got=0x%02x umask_exp=0x%02x\n",
+ cand->name, got_umask, cand->umask);
+
+ /*
+ * Events with no UMask in the JSON table (cand->umask == 0)
+ * are "don't-care" -- pmc_pmu_pmcallocate() places 0xff there
+ * as a wildcard. Only verify umask when the JSON specifies a
+ * non-zero value.
+ */
+ if (cand->umask == 0x00) {
+ any_checked = true;
+ continue;
+ }
+ any_checked = true;
+
+ ATF_CHECK_MSG(got_umask == cand->umask,
+ "%s: UnitMask mismatch: got 0x%02x expected 0x%02x "
+ "(config=0x%016jx)",
+ cand->name, got_umask, cand->umask, (uintmax_t)raw);
+ }
+
+ if (!any_checked)
+ atf_tc_skip("No Zen4 test events resolved -- "
+ "cannot verify UnitMask encoding");
+}
+
+
+ATF_TC(pmu_event_smoke_osuser_bits);
+ATF_TC_HEAD(pmu_event_smoke_osuser_bits, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify OsUser bits 17:16 (AMD_PMC_OS | AMD_PMC_USR) "
+ "are set in the default config word from pmc_pmu_pmcallocate(). "
+ "hwpmc sets both bits when no explicit mode is requested.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_osuser_bits, tc)
+{
+ struct pmc_op_pmcallocate cfg;
+ struct amd_umcdf_cpu cpu;
+ uint64_t raw;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ /* Use the simplest event: ex_ret_instr */
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate("ex_ret_instr", &cfg) != 0)
+ atf_tc_skip("ex_ret_instr not resolved: %s", strerror(errno));
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ fprintf(stderr, "ex_ret_instr config=0x%016jx USR=%ju OS=%ju\n",
+ (uintmax_t)raw, (uintmax_t)EVTEST_USR(raw),
+ (uintmax_t)EVTEST_OS(raw));
+
+ ATF_CHECK_MSG(EVTEST_USR(raw) != 0,
+ "USR bit (bit 16) not set in default pmc_pmu_pmcallocate() config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+ ATF_CHECK_MSG(EVTEST_OS(raw) != 0,
+ "OS bit (bit 17) not set in default pmc_pmu_pmcallocate() config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+}
+
+
+ATF_TC(pmu_event_smoke_host_guest_bits_default);
+ATF_TC_HEAD(pmu_event_smoke_host_guest_bits_default, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify HOST/GUEST bits 41:40 are NOT automatically set "
+ "in the default config word from pmc_pmu_pmcallocate(). "
+ "hwpmc does not set these bits without an explicit request.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_host_guest_bits_default, tc)
+{
+ struct pmc_op_pmcallocate cfg;
+ struct amd_umcdf_cpu cpu;
+ uint64_t raw;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate("ex_ret_instr", &cfg) != 0)
+ atf_tc_skip("ex_ret_instr not resolved: %s", strerror(errno));
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ fprintf(stderr,
+ "ex_ret_instr config=0x%016jx HOST=%u GUEST=%u\n",
+ (uintmax_t)raw,
+ (uint32_t)((raw >> 41) & 1),
+ (uint32_t)((raw >> 40) & 1));
+
+ ATF_CHECK_MSG(((raw >> 41) & 1) == 0,
+ "HOST bit (bit 41) unexpectedly set in default config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+ ATF_CHECK_MSG(((raw >> 40) & 1) == 0,
+ "GUEST bit (bit 40) unexpectedly set in default config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+}
+
+/* ================================================================
+ * E2E TESTS -- allocate, start, count, stop, verify counter moved
+ * ================================================================ */
+
+ATF_TC(pmu_event_e2e_counter_counts);
+ATF_TC_HEAD(pmu_event_e2e_counter_counts, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Allocate ex_ret_instr as a system-wide PMC on cpu0, "
+ "run a small instruction workload, and verify the counter "
+ "incremented -- confirms the event encoding reaches the hardware.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_e2e_counter_counts, tc)
+{
+ struct amd_umcdf_cpu cpu;
+ pmc_value_t before, after;
+ pmc_id_t pmcid;
+ volatile uint64_t sink;
+ uint64_t i;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; requires Zen4+",
+ amd_umcdf_zen_name(cpu.zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ pmcid = PMC_ID_INVALID;
+ if (pmc_allocate("ex_ret_instr", PMC_MODE_SC, 0, 0, &pmcid, 1) != 0)
+ atf_tc_skip("pmc_allocate(ex_ret_instr, SC) failed: %s",
+ strerror(errno));
+
+ if (pmc_start(pmcid) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_start() failed: %s", strerror(errno));
+ }
+
+ if (pmc_read(pmcid, &before) != 0) {
+ (void)pmc_stop(pmcid);
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(before) failed: %s", strerror(errno));
+ }
+
+ /* Workload: 1 M iterations of integer work */
+ sink = 0;
+ for (i = 0; i < 1000000UL; i++)
+ sink += i * 3 + 7;
+ (void)sink;
+
+ if (pmc_stop(pmcid) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_stop() failed: %s", strerror(errno));
+ }
+
+ if (pmc_read(pmcid, &after) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(after) failed: %s", strerror(errno));
+ }
+
+ amd_test_release_pmc(pmcid);
+
+ fprintf(stderr,
+ "ex_ret_instr: before=%ju after=%ju delta=%ju\n",
+ (uintmax_t)before, (uintmax_t)after,
+ (uintmax_t)(after - before));
+
+ ATF_CHECK_MSG(after >= before,
+ "ex_ret_instr counter moved backwards: before=%ju after=%ju",
+ (uintmax_t)before, (uintmax_t)after);
+
+ ATF_CHECK_MSG(after > before,
+ "ex_ret_instr counter did not increment after 1M-iteration workload: "
+ "before=%ju after=%ju -- check event encoding reaches hardware",
+ (uintmax_t)before, (uintmax_t)after);
+}
+
+/* ================================================================
+ * Test program entry
+ * ================================================================ */
+
+ATF_TP_ADD_TCS(tp)
+{
+ /* Unit tests -- no hardware required */
+ ATF_TP_ADD_TC(tp, pmu_event_unit_evsel_lo_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_unit_evsel_hi_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_unit_umask_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_unit_no_field_overlap);
+
+ /* Smoke tests -- require AMD + hwpmc + pmu-events */
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_pmu_enabled);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_lookup_by_name);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_evsel_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_umask_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_osuser_bits);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_host_guest_bits_default);
+
+ /* E2E tests -- allocate counter, count instructions, verify */
+ ATF_TP_ADD_TC(tp, pmu_event_e2e_counter_counts);
+
+ return (atf_no_error());
+}
Index: tests/sys/pmc/amd/pmu_mux_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/pmu_mux_test.c
@@ -0,0 +1,421 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ *
+ *
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+#include "umcdf/amd_umcdf_common.h"
+
+#define ZEN4_CORE_PMC_SLOTS 6
+
+static const char *zen4_mux_events[] = {
+ "ex_ret_instr",
+ "ls_not_halted_cyc",
+ "ex_ret_brn",
+ "ex_ret_brn_misp",
+ "ls_mab_alloc.all_allocations",
+ "ex_ret_msprd_brnch_instr_dir_msmtch",
+ /* 7th event -- used only for over-allocation test */
+ "l2_request_g1.all",
+};
+
+static void
+skip_unless_zen4_pmu(void)
+{
+ struct amd_umcdf_cpu cpu;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+ amd_umcdf_skip_unless_known_zen(&cpu);
+
+ if (cpu.zen < AMD_UMCDF_ZEN_4 || cpu.zen > AMD_UMCDF_ZEN_4)
+ atf_tc_skip("CPU is %s; Zen4 required for mux tests "
+ "(slot count differs on other generations)",
+ amd_umcdf_zen_name(cpu.zen));
+
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events JSON not enabled");
+}
+
+static int
+count_k8_core_free(void)
+{
+ struct pmc_pmcinfo *info;
+ const struct pmc_info *p;
+ int i, npmc, free_count;
+
+ free_count = 0;
+ if (pmc_pmcinfo(0, &info) != 0)
+ return (-1);
+
+ npmc = pmc_npmc(0);
+ for (i = 0; i < npmc; i++) {
+ p = &info->pm_pmcs[i];
+ if (strncmp(p->pm_name, "K8-", 3) != 0)
+ continue;
+ /* skip L3 (K8-L3-N), DF (K8-DF-N), UMC (K8-UMC<n>-N) rows */
+ if (p->pm_name[3] == 'L' || p->pm_name[3] == 'D' ||
+ p->pm_name[3] == 'U')
+ continue;
+ if (p->pm_rowdisp == PMC_DISP_FREE)
+ free_count++;
+ }
+ free(info);
+ return (free_count);
+}
+
+/* ================================================================
+ * SMOKE TESTS
+ * ================================================================ */
+
+ATF_TC(pmu_mux_slot_count);
+ATF_TC_HEAD(pmu_mux_slot_count, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify Zen4 reports exactly 6 K8 core PMC slots via "
+ "pmc_pmcinfo() -- prerequisite for all mux tests.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_slot_count, tc)
+{
+ struct pmc_pmcinfo *info;
+ const struct pmc_info *p;
+ int i, npmc, k8_count;
+
+ skip_unless_zen4_pmu();
+
+ ATF_REQUIRE_MSG(pmc_pmcinfo(0, &info) == 0,
+ "pmc_pmcinfo(0) failed: %s", strerror(errno));
+
+ npmc = pmc_npmc(0);
+ k8_count = 0;
+ for (i = 0; i < npmc; i++) {
+ p = &info->pm_pmcs[i];
+ if (strncmp(p->pm_name, "K8-", 3) != 0)
+ continue;
+ /* skip L3 (K8-L3-N), DF (K8-DF-N), UMC (K8-UMC<n>-N) rows */
+ if (p->pm_name[3] == 'L' || p->pm_name[3] == 'D' ||
+ p->pm_name[3] == 'U')
+ continue;
+ k8_count++;
+ fprintf(stderr, " slot: %s disp=%d\n", p->pm_name,
+ p->pm_rowdisp);
+ }
+ free(info);
+
+ fprintf(stderr, "K8 core PMC slots: %d (expected %d)\n",
+ k8_count, ZEN4_CORE_PMC_SLOTS);
+
+ ATF_CHECK_MSG(k8_count == ZEN4_CORE_PMC_SLOTS,
+ "Expected %d K8 core PMC slots, found %d",
+ ZEN4_CORE_PMC_SLOTS, k8_count);
+}
+
+/* ================================================================
+ * E2E TESTS
+ * ================================================================ */
+
+ATF_TC(pmu_mux_fill_all_slots);
+ATF_TC_HEAD(pmu_mux_fill_all_slots, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Allocate all 6 Zen4 core PMC slots simultaneously with "
+ "distinct events; verify all start, all count, all release to FREE.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_fill_all_slots, tc)
+{
+ pmc_id_t pmcids[ZEN4_CORE_PMC_SLOTS];
+ pmc_value_t before[ZEN4_CORE_PMC_SLOTS], after[ZEN4_CORE_PMC_SLOTS];
+ volatile uint64_t sink = 0;
+ uint64_t aggregate, i;
+ int slot, free_after, j;
+
+ skip_unless_zen4_pmu();
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ pmcids[slot] = PMC_ID_INVALID;
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmc_allocate(zen4_mux_events[slot], PMC_MODE_SC,
+ 0, 0, &pmcids[slot], 1) != 0) {
+ if (errno == EOPNOTSUPP || errno == ENOENT) {
+ fprintf(stderr, "slot %d (%s): skipped (%s)\n",
+ slot, zen4_mux_events[slot],
+ strerror(errno));
+ pmcids[slot] = PMC_ID_INVALID;
+ continue;
+ }
+ for (j = 0; j < slot; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_skip("pmc_allocate(%s) failed: %s",
+ zen4_mux_events[slot], strerror(errno));
+ }
+ }
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmcids[slot] == PMC_ID_INVALID)
+ continue;
+ if (pmc_start(pmcids[slot]) != 0) {
+ for (j = 0; j < ZEN4_CORE_PMC_SLOTS; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_fail("pmc_start(slot=%d) failed: %s",
+ slot, strerror(errno));
+ }
+ if (pmc_read(pmcids[slot], &before[slot]) != 0) {
+ (void)pmc_stop(pmcids[slot]);
+ for (j = 0; j < ZEN4_CORE_PMC_SLOTS; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_fail("pmc_read before(slot=%d) failed: %s",
+ slot, strerror(errno));
+ }
+ }
+
+ /* workload */
+ for (i = 0; i < 2000000UL; i++)
+ sink += i * 3 + 7;
+ (void)sink;
+
+ aggregate = 0;
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmcids[slot] == PMC_ID_INVALID)
+ continue;
+ if (pmc_stop(pmcids[slot]) != 0) {
+ for (j = slot; j < ZEN4_CORE_PMC_SLOTS; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_fail("pmc_stop(slot=%d) failed: %s",
+ slot, strerror(errno));
+ }
+ if (pmc_read(pmcids[slot], &after[slot]) != 0) {
+ for (j = slot; j < ZEN4_CORE_PMC_SLOTS; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_fail("pmc_read after(slot=%d) failed: %s",
+ slot, strerror(errno));
+ }
+ if (after[slot] >= before[slot])
+ aggregate += after[slot] - before[slot];
+ fprintf(stderr, " slot%d %-38s delta=%ju\n", slot,
+ zen4_mux_events[slot],
+ (uintmax_t)(after[slot] >= before[slot] ? after[slot] - before[slot] : 0));
+ (void)pmc_release(pmcids[slot]);
+ pmcids[slot] = PMC_ID_INVALID;
+ }
+
+ free_after = count_k8_core_free();
+ fprintf(stderr, "aggregate_delta=%ju K8_free_after=%d\n",
+ (uintmax_t)aggregate, free_after);
+
+ ATF_CHECK_MSG(aggregate > 0,
+ "aggregate counter delta=0 after 2M-iteration workload with "
+ "%d slots -- check event encoding reaches hardware",
+ ZEN4_CORE_PMC_SLOTS);
+
+ ATF_CHECK_MSG(free_after == ZEN4_CORE_PMC_SLOTS,
+ "K8 core slots not fully freed: expected %d free, got %d",
+ ZEN4_CORE_PMC_SLOTS, free_after);
+}
+
+ATF_TC(pmu_mux_over_allocation_rejected);
+ATF_TC_HEAD(pmu_mux_over_allocation_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Hold all 6 Zen4 core PMC slots, then attempt to allocate "
+ "a 7th -- verify EBUSY or ENXIO and that the 6 held counters are "
+ "unaffected.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_over_allocation_rejected, tc)
+{
+ pmc_id_t pmcids[ZEN4_CORE_PMC_SLOTS];
+ pmc_id_t extra;
+ int slot, held, rc, j;
+
+ skip_unless_zen4_pmu();
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ pmcids[slot] = PMC_ID_INVALID;
+
+ held = 0;
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmc_allocate(zen4_mux_events[slot], PMC_MODE_SC,
+ 0, 0, &pmcids[slot], 1) != 0) {
+ if (errno == EOPNOTSUPP || errno == ENOENT) {
+ pmcids[slot] = PMC_ID_INVALID;
+ continue;
+ }
+ for (j = 0; j < slot; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_skip("pmc_allocate(%s) failed: %s",
+ zen4_mux_events[slot], strerror(errno));
+ }
+ held++;
+ }
+
+ if (held < ZEN4_CORE_PMC_SLOTS) {
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+ atf_tc_skip("Could only hold %d/%d slots -- "
+ "cannot test over-allocation", held, ZEN4_CORE_PMC_SLOTS);
+ }
+
+ extra = PMC_ID_INVALID;
+ rc = pmc_allocate(zen4_mux_events[6], PMC_MODE_SC,
+ 0, 0, &extra, 1);
+
+ fprintf(stderr, "7th alloc (%s): rc=%d errno=%d (%s)\n",
+ zen4_mux_events[6], rc, errno, strerror(errno));
+
+ if (rc == 0) {
+ /* kernel supports SW mux -- acceptable */
+ fprintf(stderr, "7th alloc succeeded (SW mux) -- releasing\n");
+ (void)pmc_release(extra);
+ } else {
+ ATF_CHECK_MSG(errno == EBUSY || errno == ENXIO ||
+ errno == EOPNOTSUPP || errno == EINVAL,
+ "7th alloc failed with unexpected errno=%d (%s); "
+ "expected EBUSY, ENXIO, EINVAL, or EOPNOTSUPP",
+ errno, strerror(errno));
+ }
+
+ /* verify held counters still start/stop cleanly */
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmcids[slot] == PMC_ID_INVALID)
+ continue;
+ ATF_CHECK_MSG(pmc_start(pmcids[slot]) == 0,
+ "held slot %d failed pmc_start after over-alloc: %s",
+ slot, strerror(errno));
+ ATF_CHECK_MSG(pmc_stop(pmcids[slot]) == 0,
+ "held slot %d failed pmc_stop after over-alloc: %s",
+ slot, strerror(errno));
+ (void)pmc_release(pmcids[slot]);
+ }
+}
+
+ATF_TC(pmu_mux_sequential_reuse);
+ATF_TC_HEAD(pmu_mux_sequential_reuse, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Allocate 6 slots, release slot 0, immediately allocate a "
+ "new event in that slot -- verifies the PMC slot is immediately "
+ "reusable after release.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_sequential_reuse, tc)
+{
+ pmc_id_t pmcids[ZEN4_CORE_PMC_SLOTS];
+ pmc_id_t reused;
+ pmc_value_t val;
+ int slot, j;
+
+ skip_unless_zen4_pmu();
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ pmcids[slot] = PMC_ID_INVALID;
+
+ for (slot = 0; slot < ZEN4_CORE_PMC_SLOTS; slot++) {
+ if (pmc_allocate(zen4_mux_events[slot], PMC_MODE_SC,
+ 0, 0, &pmcids[slot], 1) != 0) {
+ if (errno == EOPNOTSUPP || errno == ENOENT) {
+ pmcids[slot] = PMC_ID_INVALID;
+ continue;
+ }
+ for (j = 0; j < slot; j++)
+ if (pmcids[j] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[j]);
+ atf_tc_skip("pmc_allocate(%s) failed: %s",
+ zen4_mux_events[slot], strerror(errno));
+ }
+ }
+
+ if (pmcids[0] != PMC_ID_INVALID) {
+ ATF_REQUIRE_MSG(pmc_release(pmcids[0]) == 0,
+ "pmc_release(slot 0) failed: %s", strerror(errno));
+ pmcids[0] = PMC_ID_INVALID;
+ }
+
+ /* Use ex_ret_instr -- the event that was actually freed from slot 0 */
+ reused = PMC_ID_INVALID;
+ if (pmc_allocate("ex_ret_instr", PMC_MODE_SC,
+ 0, 0, &reused, 1) != 0) {
+ for (slot = 1; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+ atf_tc_skip("reuse alloc(ex_ret_instr) failed: %s",
+ strerror(errno));
+ }
+
+ if (pmc_start(reused) != 0) {
+ (void)pmc_release(reused);
+ for (slot = 1; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+ atf_tc_fail("pmc_start(reused) failed: %s", strerror(errno));
+ }
+ if (pmc_stop(reused) != 0) {
+ (void)pmc_release(reused);
+ for (slot = 1; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+ atf_tc_fail("pmc_stop(reused) failed: %s", strerror(errno));
+ }
+ if (pmc_read(reused, &val) != 0) {
+ (void)pmc_release(reused);
+ for (slot = 1; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+ atf_tc_fail("pmc_read(reused) failed: %s", strerror(errno));
+ }
+
+ fprintf(stderr, "reused slot: ex_ret_instr val=%ju\n",
+ (uintmax_t)val);
+
+ (void)pmc_release(reused);
+ for (slot = 1; slot < ZEN4_CORE_PMC_SLOTS; slot++)
+ if (pmcids[slot] != PMC_ID_INVALID)
+ (void)pmc_release(pmcids[slot]);
+
+ ATF_CHECK_MSG(val > 0,
+ "reused slot ex_ret_instr val=0 -- slot may not have been "
+ "properly re-initialized after release");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_mux_slot_count);
+ ATF_TP_ADD_TC(tp, pmu_mux_fill_all_slots);
+ ATF_TP_ADD_TC(tp, pmu_mux_over_allocation_rejected);
+ ATF_TP_ADD_TC(tp, pmu_mux_sequential_reuse);
+ return (atf_no_error());
+}
Index: tests/sys/pmc/amd/rapl_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/rapl_test.c
@@ -0,0 +1,496 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ *
+ *
+ *
+ * Reference:
+ * AMD RAPL MSRs (specialreg.h): MSR_AMD_RAPL_POWER_UNIT 0xc0010299,
+ * MSR_AMD_CORE_ENERGY_STATUS 0xc001029a, MSR_AMD_PKG_ENERGY_STATUS 0xc001029b.
+ * pmc.rapl(3): event specifiers rapl-energy-pkg / rapl-energy-cores /
+ * rapl-energy-dram; values are cumulative microjoules; SC mode only.
+ * Driver: sys/dev/hwpmc/hwpmc_rapl.c (dev branch).
+ *
+ * The driver only registers an event row when its MSR responds
+ * (rdmsr_safe), so on a VM or unsupported CPU the class is absent and
+ * pmc_rapl_initialize returns ENXIO. Every TC guards with skip_unless_rapl().
+ *
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+#include <sys/time.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "amd_pmc_test_common.h"
+
+#define RAPL_EV_PKG "rapl-energy-pkg"
+#define RAPL_EV_CORES "rapl-energy-cores"
+#define RAPL_EV_DRAM "rapl-energy-dram"
+
+/* CPU the SC-mode RAPL PMCs bind to. cpu 0 always exists. */
+#define RAPL_CPU 0
+
+/* Helpers */
+
+/*
+ * rapl_classinfo -- return the PMC_CLASS_RAPL classinfo, or NULL if the
+ * class is not registered on this kernel/CPU. Caller must have called
+ * pmc_init() (via amd_test_skip_unless_hwpmc()) first.
+ */
+static const struct pmc_classinfo *
+rapl_classinfo(void)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+
+ if (pmc_cpuinfo(&cpuinfo) != 0)
+ return (NULL);
+
+ return (amd_test_find_classinfo(cpuinfo, PMC_CLASS_RAPL));
+}
+
+/*
+ * skip_unless_rapl -- primary guard for every TC. Skips when not AMD,
+ * when hwpmc is unavailable, or when the RAPL class did not register
+ * (VM / MSRs absent -> pmc_rapl_initialize returned ENXIO).
+ */
+static void
+skip_unless_rapl(void)
+{
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ if (rapl_classinfo() == NULL)
+ atf_tc_skip("PMC_CLASS_RAPL not registered -- RAPL energy MSRs "
+ "did not respond on this hardware (VM or unsupported CPU); "
+ "need dev kernel with hwpmc_rapl.c and real AMD silicon");
+}
+
+/*
+ * cpu_spin_ms -- busy-spin for at least ms milliseconds doing real integer
+ * work, so the package/core actually consumes energy. Returns the sink so
+ * the compiler cannot elide the loop.
+ */
+static uint64_t
+cpu_spin_ms(unsigned ms)
+{
+ struct timespec start, now;
+ volatile uint64_t sink = 0;
+ uint64_t i = 0;
+
+ ATF_REQUIRE_MSG(clock_gettime(CLOCK_MONOTONIC, &start) == 0,
+ "clock_gettime failed: %s", strerror(errno));
+
+ {
+ unsigned k;
+ int64_t elapsed_ms;
+
+ for (;;) {
+ /* A chunk of work between clock checks. */
+ for (k = 0; k < 200000; k++)
+ sink += (i++ * 2654435761UL) ^ (sink >> 3);
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
+ break;
+
+ elapsed_ms = (int64_t)(now.tv_sec - start.tv_sec) * 1000 +
+ (now.tv_nsec - start.tv_nsec) / 1000000;
+ if (elapsed_ms >= (int64_t)ms)
+ break;
+ }
+ }
+
+ return (sink);
+}
+
+/*
+ * rapl_alloc_read -- allocate one RAPL event in SC mode on RAPL_CPU, read it
+ * once, release it. On success stores the value in *out and returns 0.
+ * Returns the errno from pmc_allocate on failure (does not skip/fail -- the
+ * caller decides).
+ */
+static int
+rapl_alloc_read(const char *event, pmc_value_t *out)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ int rc;
+
+ if (pmc_allocate(event, PMC_MODE_SC, 0, RAPL_CPU, &id, 1) != 0)
+ return (errno);
+
+ rc = pmc_read(id, out);
+ if (rc != 0)
+ rc = errno;
+
+ amd_test_release_pmc(id);
+ return (rc);
+}
+
+/* ================================================================
+ * SMOKE -- RAPL class registered
+ * ================================================================ */
+
+ATF_TC(rapl_class_present);
+ATF_TC_HEAD(rapl_class_present, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] PMC_CLASS_RAPL is registered in pmc_cpuinfo. Confirms the "
+ "hwpmc RAPL energy class initialized on this kernel/CPU. Skips "
+ "gracefully on hardware where the RAPL MSRs do not respond.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_class_present, tc)
+{
+ const struct pmc_classinfo *ci;
+
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ ci = rapl_classinfo();
+ if (ci == NULL)
+ atf_tc_skip("PMC_CLASS_RAPL not registered -- RAPL MSRs absent "
+ "(VM or unsupported CPU)");
+
+ fprintf(stderr, "RAPL class present: pm_num=%d width=%d\n",
+ ci->pm_num, ci->pm_width);
+
+ ATF_CHECK_MSG(ci->pm_width == 64,
+ "RAPL counter width expected 64, got %d", ci->pm_width);
+}
+
+/* ================================================================
+ * SMOKE -- metadata: 2 rows on AMD, names carry the RAPL prefix
+ * ================================================================ */
+
+ATF_TC(rapl_metadata_names);
+ATF_TC_HEAD(rapl_metadata_names, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] On AMD the RAPL class exposes exactly two rows "
+ "(PKG and CORES; no DRAM domain). Verifies pm_num == 2 and that "
+ "the per-row names reported by pmc_pmcinfo carry the RAPL prefix.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_metadata_names, tc)
+{
+ const struct pmc_classinfo *ci;
+ size_t nrows;
+
+ skip_unless_rapl();
+
+ ci = rapl_classinfo();
+ ATF_REQUIRE_MSG(ci != NULL, "RAPL classinfo vanished after guard");
+
+ fprintf(stderr, "RAPL pm_num = %d (AMD expects 2)\n", ci->pm_num);
+
+ ATF_CHECK_MSG(ci->pm_num == 2,
+ "AMD RAPL expected 2 rows (PKG, CORES), got %d -- "
+ "a third row would mean DRAM leaked into the AMD path",
+ ci->pm_num);
+
+ /*
+ * Cross-check against the per-CPU row names. The driver names its
+ * rows "RAPL_ENERGY_PKG" / "RAPL_ENERGY_CORES"; match the "RAPL"
+ * prefix so the check is robust to underscore/case cosmetics.
+ */
+ nrows = amd_test_count_pmc_rows_with_prefix(RAPL_CPU, "RAPL");
+ fprintf(stderr, "pmc_pmcinfo rows with RAPL prefix on cpu %d = %zu\n",
+ RAPL_CPU, nrows);
+
+ ATF_CHECK_MSG(nrows == 2,
+ "expected 2 RAPL-prefixed rows in pmc_pmcinfo, got %zu", nrows);
+}
+
+/* ================================================================
+ * UNIT -- process-scope mode is rejected (RAPL is SC only)
+ * ================================================================ */
+
+ATF_TC(rapl_reject_process_mode);
+ATF_TC_HEAD(rapl_reject_process_mode, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Allocating a RAPL event in process-scope mode (PMC_MODE_TC) "
+ "must fail. RAPL is system-scope only; rapl_allocate_pmc rejects "
+ "any mode != PMC_MODE_SC with EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_reject_process_mode, tc)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ int rc;
+
+ skip_unless_rapl();
+
+ rc = pmc_allocate(RAPL_EV_PKG, PMC_MODE_TC, 0, PMC_CPU_ANY, &id, 1);
+
+ if (rc == 0) {
+ amd_test_release_pmc(id);
+ atf_tc_fail("pmc_allocate(%s, PMC_MODE_TC) unexpectedly "
+ "succeeded -- RAPL must reject non-SC modes", RAPL_EV_PKG);
+ }
+
+ fprintf(stderr, "process-mode alloc rejected: errno=%d (%s)\n",
+ errno, strerror(errno));
+
+ ATF_CHECK_MSG(errno == EINVAL,
+ "expected EINVAL for process-scope RAPL alloc, got %d (%s)",
+ errno, strerror(errno));
+}
+
+/* ================================================================
+ * UNIT -- no DRAM domain on AMD
+ * ================================================================ */
+
+ATF_TC(rapl_no_dram_on_amd);
+ATF_TC_HEAD(rapl_no_dram_on_amd, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] AMD exposes no RAPL DRAM domain (dram_msr == 0). "
+ "Allocating rapl-energy-dram must fail. Guarded so it only runs "
+ "when the PKG row is present (i.e. RAPL really is active on AMD).");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_no_dram_on_amd, tc)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ pmc_value_t pkg = 0;
+ int rc, pkg_rc;
+
+ skip_unless_rapl();
+
+ /* Ensure PKG is genuinely allocatable first: proves RAPL is live. */
+ pkg_rc = rapl_alloc_read(RAPL_EV_PKG, &pkg);
+ if (pkg_rc != 0)
+ atf_tc_skip("rapl-energy-pkg not allocatable (%s) -- cannot "
+ "distinguish 'no DRAM on AMD' from 'RAPL inactive'",
+ strerror(pkg_rc));
+
+ rc = pmc_allocate(RAPL_EV_DRAM, PMC_MODE_SC, 0, RAPL_CPU, &id, 1);
+
+ if (rc == 0) {
+ amd_test_release_pmc(id);
+ atf_tc_fail("pmc_allocate(%s) succeeded on AMD -- DRAM domain "
+ "should not exist (dram_msr == 0)", RAPL_EV_DRAM);
+ }
+
+ /*
+ * The rejection can legitimately come from either layer:
+ * - libpmc, if rapl-energy-dram is absent from the AMD event table;
+ * - the kernel driver, via the wrong-event-for-row EINVAL.
+ * We assert the allocation fails, and log the errno so the first CI
+ * run can record which layer rejected it We do NOT
+ * claim kernel-layer coverage here.
+ */
+ fprintf(stderr, "rapl-energy-dram alloc rejected on AMD: errno=%d (%s)\n",
+ errno, strerror(errno));
+
+ ATF_CHECK_MSG(rc != 0, "DRAM allocation must be rejected on AMD");
+}
+
+/* ================================================================
+ * UNIT -- package energy increases under load
+ * ================================================================ */
+
+ATF_TC(rapl_pkg_delta_under_load);
+ATF_TC_HEAD(rapl_pkg_delta_under_load, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Read rapl-energy-pkg, run a bounded CPU workload, read "
+ "again; require a strictly positive delta. A bare non-zero read "
+ "proves nothing (counters are cumulative and always running), so "
+ "this measures energy actually consumed during the interval.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "timeout", "30");
+}
+
+ATF_TC_BODY(rapl_pkg_delta_under_load, tc)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ pmc_value_t before = 0, after = 0;
+
+ skip_unless_rapl();
+
+ ATF_REQUIRE_MSG(pmc_allocate(RAPL_EV_PKG, PMC_MODE_SC, 0, RAPL_CPU,
+ &id, 1) == 0,
+ "pmc_allocate(%s, SC, cpu%d) failed: %s",
+ RAPL_EV_PKG, RAPL_CPU, strerror(errno));
+
+ ATF_REQUIRE_MSG(pmc_read(id, &before) == 0,
+ "pmc_read(before) failed: %s", strerror(errno));
+
+ /* 500 ms of real work, far above the driver's sub-ms re-sample skip. */
+ (void)cpu_spin_ms(500);
+
+ ATF_REQUIRE_MSG(pmc_read(id, &after) == 0,
+ "pmc_read(after) failed: %s", strerror(errno));
+
+ amd_test_release_pmc(id);
+
+ fprintf(stderr, "PKG energy uJ: before=%ju after=%ju delta=%ju\n",
+ (uintmax_t)before, (uintmax_t)after, (uintmax_t)(after - before));
+
+ ATF_CHECK_MSG(after > before,
+ "package energy did not increase under load: before=%ju after=%ju "
+ " -- RAPL PKG counter not advancing",
+ (uintmax_t)before, (uintmax_t)after);
+}
+
+/* ================================================================
+ * UNIT -- core energy increases under load
+ * ================================================================ */
+
+ATF_TC(rapl_cores_delta_under_load);
+ATF_TC_HEAD(rapl_cores_delta_under_load, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Read rapl-energy-cores, run a bounded CPU workload, read "
+ "again; require a non-negative delta with the PKG counter as the "
+ "primary advancing gate. On a many-core box a single spinner may "
+ "land off the measured core, so CORES is allowed to be flat as long "
+ "as PKG advances.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "timeout", "30");
+}
+
+ATF_TC_BODY(rapl_cores_delta_under_load, tc)
+{
+ pmc_id_t pkg = PMC_ID_INVALID, cores = PMC_ID_INVALID;
+ pmc_value_t pkg_b = 0, pkg_a = 0, cores_b = 0, cores_a = 0;
+
+ skip_unless_rapl();
+
+ if (pmc_allocate(RAPL_EV_CORES, PMC_MODE_SC, 0, RAPL_CPU,
+ &cores, 1) != 0)
+ atf_tc_skip("pmc_allocate(%s) failed: %s",
+ RAPL_EV_CORES, strerror(errno));
+
+ ATF_REQUIRE_MSG(pmc_allocate(RAPL_EV_PKG, PMC_MODE_SC, 0, RAPL_CPU,
+ &pkg, 1) == 0,
+ "pmc_allocate(%s) failed: %s", RAPL_EV_PKG, strerror(errno));
+
+ ATF_REQUIRE_MSG(pmc_read(cores, &cores_b) == 0,
+ "pmc_read(cores before) failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_read(pkg, &pkg_b) == 0,
+ "pmc_read(pkg before) failed: %s", strerror(errno));
+
+ (void)cpu_spin_ms(500);
+
+ ATF_REQUIRE_MSG(pmc_read(cores, &cores_a) == 0,
+ "pmc_read(cores after) failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_read(pkg, &pkg_a) == 0,
+ "pmc_read(pkg after) failed: %s", strerror(errno));
+
+ amd_test_release_pmc(cores);
+ amd_test_release_pmc(pkg);
+
+ /* PKG is the real gate -- it must advance under load. */
+ ATF_REQUIRE_MSG(pkg_a > pkg_b,
+ "PKG energy flat under load (before=%ju after=%ju) -- RAPL not "
+ "advancing; CORES check is meaningless",
+ (uintmax_t)pkg_b, (uintmax_t)pkg_a);
+
+ /* CORES must not go backwards; flat is tolerated (see descr). */
+ ATF_CHECK_MSG(cores_a >= cores_b,
+ "CORES energy went backwards: before=%ju after=%ju",
+ (uintmax_t)cores_b, (uintmax_t)cores_a);
+
+ fprintf(stderr,
+ "CORES uJ: before=%ju after=%ju delta=%ju | "
+ "PKG uJ: before=%ju after=%ju delta=%ju\n",
+ (uintmax_t)cores_b, (uintmax_t)cores_a,
+ (uintmax_t)(cores_a >= cores_b ? cores_a - cores_b : 0),
+ (uintmax_t)pkg_b, (uintmax_t)pkg_a, (uintmax_t)(pkg_a - pkg_b));
+}
+
+/* ================================================================
+ * E2E -- monotonic across the guard callout (~30s)
+ * ================================================================ */
+
+ATF_TC(rapl_monotonic_across_guard);
+ATF_TC_HEAD(rapl_monotonic_across_guard, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Sample rapl-energy-pkg repeatedly over ~30s under continuous "
+ "load and verify readings are non-decreasing across the driver's "
+ "periodic guard callout. Catches regressions in the 32-bit-to-64-bit "
+ "wrap fold (rapl_update_delta) that would make the accumulator jump "
+ "backwards.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "timeout", "60");
+}
+
+ATF_TC_BODY(rapl_monotonic_across_guard, tc)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ pmc_value_t prev = 0, cur = 0;
+ int i, samples = 30;
+
+ skip_unless_rapl();
+
+ ATF_REQUIRE_MSG(pmc_allocate(RAPL_EV_PKG, PMC_MODE_SC, 0, RAPL_CPU,
+ &id, 1) == 0,
+ "pmc_allocate(%s) failed: %s", RAPL_EV_PKG, strerror(errno));
+
+ ATF_REQUIRE_MSG(pmc_read(id, &prev) == 0,
+ "pmc_read(initial) failed: %s", strerror(errno));
+
+ for (i = 0; i < samples; i++) {
+ /* ~1s of load between reads: well past the guard interval. */
+ (void)cpu_spin_ms(1000);
+
+ ATF_REQUIRE_MSG(pmc_read(id, &cur) == 0,
+ "pmc_read(sample %d) failed: %s", i, strerror(errno));
+
+ ATF_CHECK_MSG(cur >= prev,
+ "RAPL PKG went backwards at sample %d: prev=%ju cur=%ju -- "
+ "wrap-fold regression in rapl_update_delta",
+ i, (uintmax_t)prev, (uintmax_t)cur);
+
+ prev = cur;
+ }
+
+ amd_test_release_pmc(id);
+
+ fprintf(stderr, "monotonic across %d samples; final PKG=%ju uJ\n",
+ samples, (uintmax_t)cur);
+}
+
+/* ================================================================
+ * Test program entry
+ * ================================================================ */
+
+ATF_TP_ADD_TCS(tp)
+{
+ /* Smoke */
+ ATF_TP_ADD_TC(tp, rapl_class_present);
+ ATF_TP_ADD_TC(tp, rapl_metadata_names);
+
+ /* Unit */
+ ATF_TP_ADD_TC(tp, rapl_reject_process_mode);
+ ATF_TP_ADD_TC(tp, rapl_no_dram_on_amd);
+ ATF_TP_ADD_TC(tp, rapl_pkg_delta_under_load);
+ ATF_TP_ADD_TC(tp, rapl_cores_delta_under_load);
+
+ /* E2E */
+ ATF_TP_ADD_TC(tp, rapl_monotonic_across_guard);
+
+ return (atf_no_error());
+}
Index: tests/sys/pmc/amd/umcdf/amd_umcdf_common.h
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/umcdf/amd_umcdf_common.h
@@ -0,0 +1,304 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Davi Chaves Azevedo <davi.chavesazevedo@amd.com>
+ *
+ * Common AMD UMC/DF hwpmc test helpers.
+ *
+ * These tests intentionally detect the AMD family/model/stepping before any
+ * PMU-event or hwpmc allocation work. Family 1Ah is split between Zen 5 and
+ * Zen 6, so model decoding is part of the test contract.
+ *
+ * Pure decode helpers (no hardware I/O) live in amd_umcdf_decode.h and are
+ * included here so that all hardware tests get the full API.
+ */
+
+#ifndef _AMD_UMCDF_COMMON_H_
+#define _AMD_UMCDF_COMMON_H_
+
+#include "amd_umcdf_decode.h"
+
+#include <sys/param.h>
+#include <machine/cpufunc.h>
+#include <sys/pmc.h>
+
+#include <machine/pmc_mdep.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+
+static inline int
+amd_umcdf_do_cpuid(uint32_t level, uint32_t regs[4])
+{
+ do_cpuid(level, regs);
+ return (0);
+}
+
+static inline int
+amd_umcdf_read_cpu(struct amd_umcdf_cpu *cpu)
+{
+ uint32_t base_family, base_model, ext_family, ext_model;
+ uint32_t regs[4];
+ int error;
+
+ memset(cpu, 0, sizeof(*cpu));
+ cpu->zen = AMD_UMCDF_ZEN_UNKNOWN;
+
+ error = amd_umcdf_do_cpuid(AMD_UMCDF_CPUID_VENDOR, regs);
+ if (error != 0)
+ return (error);
+
+ cpu->is_amd = regs[1] == AMD_UMCDF_VENDOR_EBX &&
+ regs[3] == AMD_UMCDF_VENDOR_EDX && regs[2] == AMD_UMCDF_VENDOR_ECX;
+ if (!cpu->is_amd)
+ return (0);
+
+ error = amd_umcdf_do_cpuid(AMD_UMCDF_CPUID_FMS, regs);
+ if (error != 0)
+ return (error);
+
+ base_family = (regs[0] >> 8) & 0x0f;
+ ext_family = (regs[0] >> 20) & 0xff;
+ base_model = (regs[0] >> 4) & 0x0f;
+ ext_model = (regs[0] >> 16) & 0x0f;
+ cpu->stepping = regs[0] & 0x0f;
+ cpu->family = (base_family == 0x0f) ? base_family + ext_family :
+ base_family;
+ cpu->model = (base_family == 0x0f || base_family == 0x06) ?
+ (ext_model << 4) | base_model : base_model;
+ cpu->zen = amd_umcdf_map_zen(cpu->family, cpu->model);
+
+ error = amd_umcdf_do_cpuid(AMD_UMCDF_CPUID_EXTMAX, regs);
+ if (error != 0)
+ return (error);
+ cpu->ext_high = regs[0];
+
+ if (cpu->ext_high >= AMD_UMCDF_CPUID_EXTFEATURES) {
+ error = amd_umcdf_do_cpuid(AMD_UMCDF_CPUID_EXTFEATURES, regs);
+ if (error != 0)
+ return (error);
+ cpu->amd_feature2_ecx = regs[2];
+ }
+
+ return (0);
+}
+
+static inline void
+amd_umcdf_skip_unless_amd(struct amd_umcdf_cpu *cpu)
+{
+ int error;
+
+ error = amd_umcdf_read_cpu(cpu);
+ if (error != 0)
+ atf_tc_skip("CPUID query failed: %s",
+ strerror(error));
+ if (!cpu->is_amd)
+ atf_tc_skip("CPU vendor is not AuthenticAMD");
+}
+
+static inline void
+amd_umcdf_skip_unless_known_zen(struct amd_umcdf_cpu *cpu)
+{
+ amd_umcdf_skip_unless_amd(cpu);
+ if (cpu->zen == AMD_UMCDF_ZEN_PRE_ZEN)
+ atf_tc_skip("AMD Family %02xh is pre-Zen; UMC/DF test is not applicable",
+ cpu->family);
+ if (cpu->zen == AMD_UMCDF_ZEN_FUTURE)
+ atf_tc_skip("AMD Family %02xh is newer than the validated Zen map; "
+ "probe only after checking the PPR", cpu->family);
+ if (cpu->zen == AMD_UMCDF_ZEN_UNKNOWN)
+ atf_tc_skip("AMD Family %02xh Model %02xh is not in the validated "
+ "Zen map", cpu->family, cpu->model);
+}
+
+static inline void
+amd_umcdf_skip_unless_hwpmc(void)
+{
+ if (pmc_init() < 0)
+ atf_tc_skip("pmc_init() failed; is hwpmc loaded? %s",
+ strerror(errno));
+}
+
+static inline void
+amd_umcdf_skip_unless_pmu_events(void)
+{
+ amd_umcdf_skip_unless_hwpmc();
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events support is not enabled in this hwpmc setup");
+}
+
+static inline int
+amd_umcdf_read_perfmon_v2(const struct amd_umcdf_cpu *cpu,
+ struct amd_umcdf_perfmon_v2 *pmv2)
+{
+ uint32_t regs[4];
+ int error;
+
+ memset(pmv2, 0, sizeof(*pmv2));
+ if (cpu->ext_high < AMD_UMCDF_CPUID_EXTPERFMON)
+ return (0);
+
+ error = amd_umcdf_do_cpuid(AMD_UMCDF_CPUID_EXTPERFMON, regs);
+ if (error != 0)
+ return (error);
+
+ pmv2->leaf_available = true;
+ pmv2->raw_eax = regs[0];
+ pmv2->raw_ebx = regs[1];
+ pmv2->raw_ecx = regs[2];
+ pmv2->raw_edx = regs[3];
+ pmv2->present = (regs[0] & AMD_UMCDF_EXTPERFMON_PRESENT) != 0;
+ pmv2->core_pmcs = AMD_UMCDF_EXTPERFMON_CORE_PMCS(regs[1]);
+ pmv2->lbr_v2_depth = AMD_UMCDF_EXTPERFMON_LBR_V2_DEPTH(regs[1]);
+ pmv2->df_pmcs = AMD_UMCDF_EXTPERFMON_DF_PMCS(regs[1]);
+ pmv2->active_umc_mask = regs[2];
+ return (0);
+}
+
+static inline size_t
+amd_umcdf_count_pmc_rows_with_prefix(int cpu, const char *prefix)
+{
+ struct pmc_pmcinfo *pmcinfo;
+ size_t count, prefix_len;
+ int i, npmc;
+
+ ATF_REQUIRE_MSG(pmc_pmcinfo(cpu, &pmcinfo) == 0,
+ "pmc_pmcinfo(%d) failed: %s", cpu, strerror(errno));
+ npmc = pmc_npmc(cpu);
+ if (npmc < 0) {
+ free(pmcinfo);
+ atf_tc_fail("pmc_npmc(%d) failed: %s", cpu, strerror(errno));
+ }
+
+ count = 0;
+ prefix_len = strlen(prefix);
+ for (i = 0; i < npmc; i++) {
+ if (strncmp(pmcinfo->pm_pmcs[i].pm_name, prefix,
+ prefix_len) == 0)
+ count++;
+ }
+
+ free(pmcinfo);
+ return (count);
+}
+
+static inline const struct amd_umcdf_event_candidate *
+amd_umcdf_pick_pmu_event(const struct amd_umcdf_event_candidate *candidates,
+ struct pmc_op_pmcallocate *cfg, int *last_error)
+{
+ const struct amd_umcdf_event_candidate *cand;
+ int error;
+
+ if (last_error != NULL)
+ *last_error = ENOENT;
+ for (cand = candidates; cand->name != NULL; cand++) {
+ memset(cfg, 0, sizeof(*cfg));
+ error = pmc_pmu_pmcallocate(cand->name, cfg);
+ if (error == 0)
+ return (cand);
+ if (last_error != NULL)
+ *last_error = error;
+ }
+
+ return (NULL);
+}
+
+static inline int
+amd_umcdf_generate_memory_traffic(size_t bytes, unsigned int rounds)
+{
+ volatile uint64_t amd_umcdf_sink = 0;
+ uint8_t *buf;
+ size_t i;
+ unsigned int r;
+
+ if (bytes < AMD_UMCDF_CACHELINE)
+ return (EINVAL);
+ buf = (uint8_t *)malloc(bytes);
+ if (buf == NULL)
+ return (ENOMEM);
+
+ memset(buf, 0xa5, bytes);
+ for (r = 0; r < rounds; r++) {
+ for (i = 0; i < bytes; i += AMD_UMCDF_CACHELINE) {
+ buf[i] = (uint8_t)(buf[i] + r + (i >> 6));
+ amd_umcdf_sink += buf[i];
+ }
+ }
+
+ free(buf);
+ return (0);
+}
+
+static inline void
+amd_umcdf_release_pmc(pmc_id_t pmcid)
+{
+ if (pmcid != PMC_ID_INVALID)
+ ATF_REQUIRE_MSG(pmc_release(pmcid) == 0,
+ "pmc_release(%u) failed: %s", pmcid, strerror(errno));
+}
+
+/*
+ * Run the start->read(before)->traffic->stop->read(after) smoke sequence for
+ * an already-allocated PMC. Calls atf_tc_skip() on ENOMEM or when the
+ * counter decrements; calls atf_tc_fail() on any other error.
+ * Releases pmcid unconditionally on any non-zero return path.
+ *
+ * Returns with pmcid still valid on success; caller must release it.
+ */
+static inline void
+amd_umcdf_pmc_smoke_run(pmc_id_t pmcid,
+ const struct amd_umcdf_event_candidate *event,
+ size_t traffic_bytes, const char *counter_label)
+{
+ pmc_value_t before, after;
+ int error;
+
+ before = after = 0;
+
+ if (pmc_start(pmcid) != 0) {
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_fail("pmc_start(%s) failed: %s", event->name,
+ strerror(errno));
+ }
+ if (pmc_read(pmcid, &before) != 0) {
+ (void)pmc_stop(pmcid);
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(before) failed: %s", strerror(errno));
+ }
+ error = amd_umcdf_generate_memory_traffic(traffic_bytes, 4);
+ if (error == ENOMEM) {
+ (void)pmc_stop(pmcid);
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_skip("insufficient memory for traffic workload: %s",
+ strerror(error));
+ } else if (error != 0) {
+ (void)pmc_stop(pmcid);
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_fail("memory traffic setup failed: %s", strerror(error));
+ }
+ if (pmc_stop(pmcid) != 0) {
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_fail("pmc_stop(%s) failed: %s", event->name,
+ strerror(errno));
+ }
+ if (pmc_read(pmcid, &after) != 0) {
+ amd_umcdf_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(after) failed: %s", strerror(errno));
+ }
+
+ ATF_CHECK_MSG(after >= before,
+ "%s counter moved backwards: before=%ju after=%ju",
+ counter_label, (uintmax_t)before, (uintmax_t)after);
+}
+
+#endif /* _AMD_UMCDF_COMMON_H_ */
Index: tests/sys/pmc/amd/umcdf/amd_umcdf_decode.h
===================================================================
--- /dev/null
+++ tests/sys/pmc/amd/umcdf/amd_umcdf_decode.h
@@ -0,0 +1,256 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * Pure AMD UMC/DF decode helpers -- no hardware I/O, no OS-specific headers.
+ * Include only <stdbool.h> and <stdint.h>. Safe to use on any platform.
+ *
+ * These helpers are the prerequisite for the [TC-UNIT-*] unit test tier.
+ * Hardware accessors (amd_umcdf_do_cpuid, amd_umcdf_read_cpu, pmc_*) remain
+ * in amd_umcdf_common.h and must NOT be included here.
+ */
+
+#ifndef _AMD_UMCDF_DECODE_H_
+#define _AMD_UMCDF_DECODE_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define AMD_UMCDF_CPUID_VENDOR 0x00000000U
+#define AMD_UMCDF_CPUID_FMS 0x00000001U
+#define AMD_UMCDF_CPUID_EXTMAX 0x80000000U
+#define AMD_UMCDF_CPUID_EXTFEATURES 0x80000001U
+#define AMD_UMCDF_CPUID_EXTPERFMON 0x80000022U
+
+/*
+ * AMD vendor string constants (returned by CPUID leaf 0)
+ * EBX="Auth", EDX="enti", ECX="cAMD" -> "AuthenticAMD"
+ * ----------------------------------------------------------------------- */
+#define AMD_UMCDF_VENDOR_EBX 0x68747541U /* "Auth" */
+#define AMD_UMCDF_VENDOR_EDX 0x69746e65U /* "enti" */
+#define AMD_UMCDF_VENDOR_ECX 0x444d4163U /* "cAMD" */
+
+/* AMD feature2 ECX (CPUID 0x80000001) PerfNxtCore bit */
+#define AMD_UMCDF_ID2_PNXC (1U << 24)
+
+/*
+ * PerfMonV2 (CPUID Fn80000022) parsing macros
+ * The EBX register of this leaf contains per-class PMC counts.
+ * ----------------------------------------------------------------------- */
+#define AMD_UMCDF_EXTPERFMON_PRESENT 0x1U
+/* PPR CPUID Fn80000022h EBX bits 3:0 NumPerfCtrCore */
+#define AMD_UMCDF_EXTPERFMON_CORE_PMCS(x) ((x) & 0x0fU)
+#define AMD_UMCDF_EXTPERFMON_LBR_V2_DEPTH(x) (((x) >> 4) & 0x3fU)
+#define AMD_UMCDF_EXTPERFMON_DF_PMCS(x) (((x) >> 10) & 0x3fU)
+
+#define AMD_UMCDF_DF_DEFAULT_PMCS 4U
+#define AMD_UMCDF_MAX_DF_PMCS 64U
+#define AMD_UMCDF_CACHELINE 64U
+#define AMD_UMCDF_PMC_DF_ROW_PREFIX "K8-DF-"
+
+/*
+ * DF1 event/unit mask encoding (Zen 1 - Zen 3+)
+ *
+ * AMD PPR DF1 PerfEventSel format:
+ * bits 7:0 EventSelect[7:0] -> PERFEVTSEL bits 7:0
+ * bits 11:8 EventSelect[11:8] -> PERFEVTSEL bits 35:32
+ * bits 13:12 EventSelect[13:12] -> PERFEVTSEL bits 60:59
+ * bits 7:0 UnitMask[7:0] -> PERFEVTSEL bits 15:8
+ * ----------------------------------------------------------------------- */
+#define AMD_UMCDF_DF1_TO_EVENTMASK(x) (((x) & 0xffU) | \
+ (((uint64_t)(x) & 0x0f00U) << 24) | \
+ (((uint64_t)(x) & 0x3000U) << 47))
+#define AMD_UMCDF_DF1_TO_UNITMASK(x) (((x) & 0xffU) << 8)
+
+/*
+ * DF2 event/unit mask encoding (Zen 4+)
+ *
+ * AMD PPR DF2 PerfEventSel format:
+ * bits 7:0 EventSelect[7:0] -> PERFEVTSEL bits 7:0
+ * bits 14:8 EventSelect[14:8] -> PERFEVTSEL bits 38:32
+ * bits 7:0 UnitMask[7:0] -> PERFEVTSEL bits 15:8
+ * bits 11:8 UnitMask[11:8] -> PERFEVTSEL bits 27:24
+ * ----------------------------------------------------------------------- */
+#define AMD_UMCDF_DF2_TO_EVENTMASK(x) (((x) & 0xffU) | \
+ (((uint64_t)(x) & 0x7f00U) << 24))
+#define AMD_UMCDF_DF2_TO_UNITMASK(x) ((((x) & 0xffU) << 8) | \
+ (((uint64_t)(x) & 0x0f00U) << 16))
+
+/*
+ * Zen generation enumeration
+ * Values are ordered oldest-to-newest so comparisons like zen >= ZEN_4 work.
+ * ----------------------------------------------------------------------- */
+enum amd_umcdf_zen_generation {
+ AMD_UMCDF_ZEN_PRE_ZEN = 0,
+ AMD_UMCDF_ZEN_1,
+ AMD_UMCDF_ZEN_PLUS,
+ AMD_UMCDF_ZEN_2,
+ AMD_UMCDF_ZEN_3,
+ AMD_UMCDF_ZEN_3_PLUS,
+ AMD_UMCDF_ZEN_4,
+ AMD_UMCDF_ZEN_5,
+ AMD_UMCDF_ZEN_6,
+ AMD_UMCDF_ZEN_FUTURE,
+ AMD_UMCDF_ZEN_UNKNOWN
+};
+
+struct amd_umcdf_cpu {
+ uint32_t family;
+ uint32_t model;
+ uint32_t stepping;
+ uint32_t ext_high;
+ uint32_t amd_feature2_ecx;
+ enum amd_umcdf_zen_generation zen;
+ bool is_amd;
+};
+
+struct amd_umcdf_perfmon_v2 {
+ uint32_t raw_eax;
+ uint32_t raw_ebx;
+ uint32_t raw_ecx;
+ uint32_t raw_edx;
+ uint32_t core_pmcs;
+ uint32_t df_pmcs;
+ uint32_t lbr_v2_depth;
+ uint32_t active_umc_mask;
+ bool leaf_available;
+ bool present;
+};
+
+struct amd_umcdf_event_candidate {
+ const char *name;
+ const char *reason;
+ uint32_t event_code;
+ uint32_t umask;
+};
+
+
+static inline const char *
+amd_umcdf_zen_name(enum amd_umcdf_zen_generation zen)
+{
+ switch (zen) {
+ case AMD_UMCDF_ZEN_PRE_ZEN:
+ return ("pre-Zen");
+ case AMD_UMCDF_ZEN_1:
+ return ("Zen 1");
+ case AMD_UMCDF_ZEN_PLUS:
+ return ("Zen+");
+ case AMD_UMCDF_ZEN_2:
+ return ("Zen 2");
+ case AMD_UMCDF_ZEN_3:
+ return ("Zen 3");
+ case AMD_UMCDF_ZEN_3_PLUS:
+ return ("Zen 3+");
+ case AMD_UMCDF_ZEN_4:
+ return ("Zen 4");
+ case AMD_UMCDF_ZEN_5:
+ return ("Zen 5");
+ case AMD_UMCDF_ZEN_6:
+ return ("Zen 6");
+ case AMD_UMCDF_ZEN_FUTURE:
+ return ("future AMD Zen-family generation");
+ case AMD_UMCDF_ZEN_UNKNOWN:
+ default:
+ return ("unknown AMD generation");
+ }
+}
+
+/* Map AMD family+model to Zen generation. */
+static inline enum amd_umcdf_zen_generation
+amd_umcdf_map_zen(uint32_t family, uint32_t model)
+{
+ if (family < 0x17)
+ return (AMD_UMCDF_ZEN_PRE_ZEN);
+ if (family > 0x1a)
+ return (AMD_UMCDF_ZEN_FUTURE);
+
+ switch (family) {
+ case 0x17:
+ if (model >= 0x08 && model <= 0x0f) /* Pinnacle Ridge */
+ return (AMD_UMCDF_ZEN_PLUS);
+ if ((model >= 0x01 && model <= 0x07) ||
+ (model >= 0x10 && model <= 0x17)) /* Summit Ridge, Raven Ridge */
+ return (AMD_UMCDF_ZEN_1);
+ if (model >= 0x18 && model <= 0x1f) /* Picasso */
+ return (AMD_UMCDF_ZEN_PLUS);
+ if ((model >= 0x31 && model <= 0x3f) || /* Rome */
+ (model >= 0x60 && model <= 0x6f) || /* Renoir */
+ (model >= 0x71 && model <= 0x7f) || /* Matisse */
+ (model >= 0x90 && model <= 0x9f) || /* Van Gogh */
+ (model >= 0xa0 && model <= 0xaf)) /* Mendocino */
+ return (AMD_UMCDF_ZEN_2);
+ break;
+ case 0x19:
+ if ((model >= 0x10 && model <= 0x1f) || /* Genoa, Bergamo */
+ (model >= 0x60 && model <= 0x7f) || /* Raphael */
+ (model >= 0xa0 && model <= 0xaf)) /* Phoenix */
+ return (AMD_UMCDF_ZEN_4);
+ if (model >= 0x40 && model <= 0x4f) /* Rembrandt */
+ return (AMD_UMCDF_ZEN_3_PLUS);
+ if (model <= 0x0f || (model >= 0x20 && model <= 0x2f) ||
+ (model >= 0x50 && model <= 0x5f)) /* Milan, Cezanne, Lucienne */
+ return (AMD_UMCDF_ZEN_3);
+ break;
+ case 0x1a:
+ if ((model >= 0x50 && model <= 0x5f) || /* Medusa */
+ (model >= 0x80 && model <= 0xaf) || /* reserved */
+ (model >= 0xc0 && model <= 0xcf)) /* reserved */
+ return (AMD_UMCDF_ZEN_6);
+ if (model <= 0x2f || (model >= 0x40 && model <= 0x4f) || /* Turin, Strix */
+ (model >= 0x60 && model <= 0x7f)) /* Krackan, Fire Range */
+ return (AMD_UMCDF_ZEN_5);
+ break;
+ }
+
+ return (AMD_UMCDF_ZEN_UNKNOWN);
+}
+
+static inline bool
+amd_umcdf_has_df_feature(const struct amd_umcdf_cpu *cpu)
+{
+ return ((cpu->amd_feature2_ecx & AMD_UMCDF_ID2_PNXC) != 0);
+}
+
+static inline bool
+amd_umcdf_has_perfmon_v2(const struct amd_umcdf_cpu *cpu)
+{
+ return (cpu->zen >= AMD_UMCDF_ZEN_4 &&
+ cpu->zen < AMD_UMCDF_ZEN_FUTURE);
+}
+
+static inline bool
+amd_umcdf_has_freebsd_umc_json(const struct amd_umcdf_cpu *cpu)
+{
+ return (cpu->zen == AMD_UMCDF_ZEN_4 ||
+ cpu->zen == AMD_UMCDF_ZEN_5 || cpu->zen == AMD_UMCDF_ZEN_6);
+}
+
+static inline bool
+amd_umcdf_has_freebsd_df_json(const struct amd_umcdf_cpu *cpu)
+{
+ return (cpu->zen == AMD_UMCDF_ZEN_1 ||
+ cpu->zen == AMD_UMCDF_ZEN_PLUS || cpu->zen == AMD_UMCDF_ZEN_2 ||
+ cpu->zen == AMD_UMCDF_ZEN_3 || cpu->zen == AMD_UMCDF_ZEN_3_PLUS ||
+ cpu->zen == AMD_UMCDF_ZEN_4 || cpu->zen == AMD_UMCDF_ZEN_5);
+}
+
+/*
+ * Return the expected 64-bit hwpmc config value for a given DF event on this
+ * CPU. Uses DF2 encoding for Zen 4 and later; DF1 for all earlier Zen.
+ */
+static inline uint64_t
+amd_umcdf_expected_df_config(const struct amd_umcdf_cpu *cpu,
+ const struct amd_umcdf_event_candidate *event)
+{
+ if (cpu->zen >= AMD_UMCDF_ZEN_4)
+ return (AMD_UMCDF_DF2_TO_EVENTMASK(event->event_code) |
+ AMD_UMCDF_DF2_TO_UNITMASK(event->umask));
+
+ return (AMD_UMCDF_DF1_TO_EVENTMASK(event->event_code) |
+ AMD_UMCDF_DF1_TO_UNITMASK(event->umask));
+}
+
+#endif /* _AMD_UMCDF_DECODE_H_ */
Index: tests/sys/pmc/pmc_callchain_ur_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/pmc_callchain_ur_test.sh
@@ -0,0 +1,204 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+#
+# Both bugs are KASSERTs in pmc_capture_user_callchain(). They are only
+# reachable through the PMC_UR ring, which hwpmc uses exclusively for PMCs
+# allocated with PMC_F_USERCALLCHAIN — requested by pmcstat via the -U flag.
+# Under an INVARIANTS kernel a broken kernel panics on the FIRST -U sample.
+#
+# Testing "the kernel did not panic" cannot be asserted from inside the
+# test: a panic crashes the whole machine, so the test process never gets
+# to report anything. Survival IS the signal. This suite therefore asserts:
+# (1) pmcstat -U exits 0 after running under a real CPU-bound workload, and
+# (2) hwpmc actually processed sampling interrupts during the run
+# (kern.hwpmc.stats.intr_processed increased) — otherwise a run that
+# allocated the PMC but collected zero samples would "survive"
+# trivially without ever exercising the fixed code path (false green).
+#
+# The callgraph symbol content of `pmcstat -R -G` is checked only as a
+# soft, logged diagnostic: on a busy many-core box a sample may legitimately
+# land in kernel context with no walkable user stack, so an empty user
+# callgraph is not a failure.
+#
+# This test is only meaningful on an INVARIANTS build (our HWPMC_DBG CI).
+# On a production kernel the KASSERTs are compiled out and the test would
+# pass without proving anything; that is acceptable — it never false-fails.
+#
+
+# Sampling event used for -U. Core cycle-ish events are the natural choice;
+# try a few names so the test works across Zen generations, and skip cleanly
+# if none allocate (non-AMD, no pmu-events tables, etc.).
+UR_EVENTS="unhalted-core-cycles ls_not_halted_cyc ex_ret_instr instructions"
+
+# Sampling rate: small enough to collect many samples in a short workload.
+UR_RATE=10000
+
+ur_pmcstat()
+{
+ local p resolved
+
+ p=${PMCSTAT:-$(atf_config_get amd.pmcstat.path pmcstat)}
+ case "$p" in
+ */*)
+ if [ ! -x "$p" ]; then
+ atf_skip "pmcstat override is not executable: $p"
+ fi
+ PMCSTAT_BIN="$p"
+ ;;
+ *)
+ resolved=$(command -v "$p") || \
+ atf_skip "pmcstat not found in PATH"
+ PMCSTAT_BIN="$resolved"
+ ;;
+ esac
+}
+
+# ur_stat_intr_processed — echo the current kern.hwpmc.stats.intr_processed
+# counter, or empty string if the sysctl is absent.
+ur_stat_intr_processed()
+{
+ sysctl -n kern.hwpmc.stats.intr_processed 2>/dev/null
+}
+
+# ur_write_load — write the CPU-bound workload runner to cwd and make it
+# executable. pmcstat needs a real executable target to exec and sample.
+# Written inside the body (not init) so it lands in the test's work dir.
+ur_write_load()
+{
+ cat > ur_load.sh <<'EOF'
+#!/bin/sh
+# CPU-bound workload for the given number of seconds. Pinned to one CPU when
+# cpuset is available so a free-floating loop on a many-core box is not
+# under-sampled.
+secs="${1:-4}"
+spin='
+ end=$(($(date +%s) + '"$secs"'))
+ x=0
+ while [ $(date +%s) -lt $end ]; do
+ i=0
+ while [ $i -lt 100000 ]; do
+ x=$((x + i * 3 + 7))
+ i=$((i + 1))
+ done
+ done
+'
+if command -v cpuset >/dev/null 2>&1; then
+ exec cpuset -l 1 /bin/sh -c "$spin"
+else
+ exec /bin/sh -c "$spin"
+fi
+EOF
+ chmod +x ur_load.sh
+}
+
+# ur_pick_event — echo the first UR_EVENTS name that pmcstat can allocate in
+# system-sampling mode (-S, WITHOUT -U) on a trivial workload. We deliberately
+# avoid -U in the probe: on a kernel missing the fix, -U triggers the KASSERT
+# panic immediately, which would crash the box before the real test even runs.
+# Allocatability without -U is sufficient to confirm the event exists and the
+# PMC row is free; the real test then adds -U.
+ur_pick_event()
+{
+ local ev tmp
+ tmp=$(mktemp -t ur_probe) || return 1
+ for ev in $UR_EVENTS; do
+ if "$PMCSTAT_BIN" -S "$ev" -n "$UR_RATE" -O "$tmp" \
+ -- /usr/bin/true >/dev/null 2>&1; then
+ rm -f "$tmp"
+ printf '%s\n' "$ev"
+ return 0
+ fi
+ done
+ rm -f "$tmp"
+ return 1
+}
+
+atf_test_case pmcstat_U_survives_and_samples cleanup
+pmcstat_U_survives_and_samples_head()
+{
+ atf_set "descr" \
+ "pmcstat -U (PMC_UR user-callchain ring) runs to completion under " \
+ "load and hwpmc processes sampling interrupts, without panicking " \
+ "the INVARIANTS kernel (see hwpmc callchain and runcount panic fixes)"
+ atf_set "require.user" "root"
+ atf_set "timeout" "60"
+}
+pmcstat_U_survives_and_samples_body()
+{
+ local ev out before after log
+
+ if [ "$(uname -m)" != "amd64" ]; then
+ atf_skip "hwpmc AMD callchain test is amd64-specific"
+ fi
+
+ ur_pmcstat
+ ur_write_load
+
+ ev=$(ur_pick_event) || \
+ atf_skip "no sampling event allocatable with -U on this host " \
+ "(tried: $UR_EVENTS) — non-AMD, no pmu-events, or PMCs busy"
+
+ out=ur_capture.pmc
+ log=ur_pmcstat.err
+
+ before=$(ur_stat_intr_processed)
+
+ #
+ # The core of the regression: -U forces the PMC_UR ring. A broken
+ # kernel panics here on the first sample. We run pmcstat in the
+ # background driving a real workload, so samples are actually taken.
+ #
+ # atf_check enforces the survival signal: exit 0. If the box panics,
+ # there is no exit code to check — the machine is simply gone — so a
+ # green result here means "survived AND clean exit".
+ #
+ atf_check -s exit:0 -o ignore -e save:"$log" \
+ "$PMCSTAT_BIN" -U -S "$ev" -n "$UR_RATE" -O "$out" \
+ -- "$(pwd)/ur_load.sh" 4
+
+ after=$(ur_stat_intr_processed)
+
+ # Guard against the false-green where zero samples were taken.
+ if [ -n "$before" ] && [ -n "$after" ]; then
+ atf_check test "$after" -gt "$before"
+ echo "intr_processed: before=$before after=$after " \
+ "delta=$((after - before))" >&2
+ else
+ echo "kern.hwpmc.stats.intr_processed absent — cannot verify" \
+ "sample count; relying on capture-file size" >&2
+ if [ ! -s "$out" ]; then
+ atf_fail "pmcstat -U produced an empty capture and the" \
+ "sample-count sysctl is unavailable — cannot confirm" \
+ "the PMC_UR path was exercised"
+ fi
+ fi
+
+ # Soft diagnostic: dump the user callgraph. Non-empty user frames are
+ # a bonus, not a requirement (samples may land in-kernel).
+ if [ -s "$out" ]; then
+ "$PMCSTAT_BIN" -R "$out" -G ur_callgraph.txt >/dev/null 2>&1 || \
+ echo "note: pmcstat -R -G failed to render callgraph" >&2
+ if [ -s ur_callgraph.txt ]; then
+ echo "user callgraph rendered ($(wc -l < ur_callgraph.txt)" \
+ "lines)" >&2
+ else
+ echo "note: user callgraph empty — samples likely" \
+ "landed in kernel context (not a failure)" >&2
+ fi
+ fi
+}
+pmcstat_U_survives_and_samples_cleanup()
+{
+ rm -f ur_capture.pmc ur_pmcstat.err ur_callgraph.txt ur_load.sh
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case pmcstat_U_survives_and_samples
+}
Index: tests/sys/pmc/pmc_pidtrack_test.c
===================================================================
--- /dev/null
+++ tests/sys/pmc/pmc_pidtrack_test.c
@@ -0,0 +1,479 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ */
+
+/*
+ * Per-process PID tracking tests for process-mode (PMC_MODE_TC) counting
+ * PMCs. Validates that a process-scope counter follows a single PID:
+ * - a process attached to its own PMC counts its own work, readable live
+ * via self-read (pidtrack_self_counts);
+ * - an idle PID's counter does NOT pick up an unrelated busy sibling's
+ * work -- no cross-PID leakage (pidtrack_no_leakage);
+ * - the count is preserved and keeps advancing across the target being
+ * context-switched out and back in (pidtrack_ctxswitch).
+ *
+ * Semantics note (verified on hardware): pmc_read() of a process-mode PMC
+ * attached to *another* live process returns 0 -- the target's hardware value
+ * is harvested into the software counter only at context-switch-out and at
+ * process exit, and a controller's post-exit read of the child's absolute
+ * count is not reliable across kernels. A process reading its OWN attached
+ * PMC, by contrast, sees a live, deterministic value. These tests therefore
+ * use the self-read model: each measured process attaches its own PMC and
+ * reports its own delta, which is exactly the per-PID isolation property
+ * under test. (The controller-attaches-child pattern is already covered by
+ * pmc_wrap_test, which tolerates the imprecision with a large seed value.)
+ *
+ * per-process attach/detach and context-switch save/restore paths; also
+ *
+ * Each test skips (never fails) when no allocatable process-mode counting
+ * event is available (hwpmc(4) not loaded, or a VM without a vPMU).
+ */
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static const char *counting_events[] = {
+ "instructions",
+ "cycles",
+ "branches",
+ "unhalted-core-cycles",
+ "inst_retired.any",
+ "cpu_clk_unhalted.thread",
+ "ls_not_halted_cyc",
+ NULL
+};
+
+/*
+ * Instruction-retired events only. The no-leakage test needs an event that
+ * an idle process does NOT accumulate: cycle events tick with wall time even
+ * when the process does no work, which would make the idle counter's absolute
+ * bound (MIN_ACTIVE) event-dependent and fragile. A retired-instruction
+ * counter advances only when the process actually executes instructions.
+ */
+static const char *retired_events[] = {
+ "instructions",
+ "inst_retired.any",
+ "ex_ret_instr", /* AMD */
+ NULL
+};
+
+/* Spin iterations: a large, unambiguous count, still well under a second. */
+#define SPIN_ITERS ((uint64_t)200 * 1000 * 1000)
+
+/* Active work must move the counter by at least this much. */
+#define MIN_ACTIVE ((uint64_t)1 << 16)
+
+/* Return the name of the first allocatable event in `list`, or NULL. */
+static const char *
+find_event_in(const char *const *list)
+{
+ pmc_id_t id;
+ int i;
+
+ for (i = 0; list[i] != NULL; i++) {
+ id = PMC_ID_INVALID;
+ if (pmc_allocate(list[i], PMC_MODE_TC, 0,
+ PMC_CPU_ANY, &id, 0) == 0) {
+ (void)pmc_release(id);
+ return (list[i]);
+ }
+ }
+ return (NULL);
+}
+
+/* The name of the event chosen, so children can allocate the same one. */
+static const char *
+find_counting_event(void)
+{
+ return (find_event_in(counting_events));
+}
+
+/* Allocate the first available process-mode counting PMC, or skip. */
+static pmc_id_t
+alloc_counting_pmc(void)
+{
+ pmc_id_t id;
+ int i;
+
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+
+ for (i = 0; counting_events[i] != NULL; i++) {
+ id = PMC_ID_INVALID;
+ if (pmc_allocate(counting_events[i], PMC_MODE_TC, 0,
+ PMC_CPU_ANY, &id, 0) == 0)
+ return (id);
+ }
+ atf_tc_skip("no allocatable process-mode counting event (no vPMU?)");
+ return (PMC_ID_INVALID); /* not reached */
+}
+
+static void __attribute__((noinline))
+spin_workload(void)
+{
+ volatile uint64_t sink = 0;
+ uint64_t i;
+
+ for (i = 0; i < SPIN_ITERS; i++)
+ sink += i * 3 + 7;
+ (void)sink;
+}
+
+/* ================================================================
+ * pidtrack_self_counts -- a process reading its own attached PMC sees
+ * its own work advance the counter (the fundamental per-PID guarantee)
+ * ================================================================ */
+
+ATF_TC(pidtrack_self_counts);
+ATF_TC_HEAD(pidtrack_self_counts, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "A process-mode PMC attached to the calling process counts that "
+ "process' own work, observable via a live self-read");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_self_counts, tc)
+{
+ pmc_id_t id;
+ pmc_value_t v0, v1;
+
+ id = alloc_counting_pmc();
+
+ ATF_REQUIRE(pmc_attach(id, 0) == 0); /* 0 == current process */
+ ATF_REQUIRE(pmc_start(id) == 0);
+
+ ATF_REQUIRE(pmc_read(id, &v0) == 0);
+ spin_workload();
+ ATF_REQUIRE(pmc_read(id, &v1) == 0);
+
+ ATF_REQUIRE(pmc_stop(id) == 0);
+ ATF_REQUIRE(pmc_detach(id, 0) == 0); /* detach live self is valid */
+ ATF_REQUIRE(pmc_release(id) == 0);
+
+ ATF_CHECK_MSG(v1 >= v0,
+ "self-counter went backwards: %ju -> %ju",
+ (uintmax_t)v0, (uintmax_t)v1);
+ ATF_CHECK_MSG(v1 - v0 >= MIN_ACTIVE,
+ "self-counter did not advance over the workload: delta %ju "
+ "(expected >= %ju)", (uintmax_t)(v1 - v0), (uintmax_t)MIN_ACTIVE);
+}
+
+/* ================================================================
+ * pidtrack_no_leakage -- two children each self-attach their own PMC;
+ * the idle one's counter must stay near zero while the busy one runs
+ * ================================================================ */
+
+/*
+ * Sentinel delta meaning "this child could not set up or read its own PMC".
+ * The parent turns it into an atf_tc_skip rather than a hard failure, so a
+ * transient resource condition (e.g. counter rows exhausted when both
+ * children allocate at once) skips instead of going red.
+ */
+#define DELTA_SENTINEL UINT64_MAX
+
+/*
+ * Child that self-attaches a fresh PMC for `event`, optionally does work,
+ * self-reads, and writes its own 64-bit delta back on resfd. `do_work`
+ * selects busy vs idle. Using self-read sidesteps the "controller reads 0
+ * for a live target" semantics and directly tests that each PID's counter
+ * reflects only that PID. On any setup/read failure it writes DELTA_SENTINEL
+ * so the parent never blocks and can skip.
+ */
+static void __attribute__((noinline))
+child_selfmeasure(const char *event, int do_work, int startfd, int resfd)
+{
+ pmc_id_t id = PMC_ID_INVALID;
+ pmc_value_t v0 = 0, v1 = 0;
+ uint64_t delta = DELTA_SENTINEL;
+ char c;
+
+ if (pmc_init() != 0 ||
+ pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY, &id, 0) != 0 ||
+ pmc_attach(id, 0) != 0 ||
+ pmc_start(id) != 0) {
+ (void)write(resfd, &delta, sizeof(delta));
+ _exit(70);
+ }
+
+ (void)read(startfd, &c, 1); /* barrier: start together */
+
+ if (pmc_read(id, &v0) != 0) {
+ (void)write(resfd, &delta, sizeof(delta));
+ _exit(71);
+ }
+ if (do_work)
+ spin_workload();
+ if (pmc_read(id, &v1) != 0) {
+ (void)write(resfd, &delta, sizeof(delta));
+ _exit(72);
+ }
+
+ (void)pmc_stop(id);
+ (void)pmc_detach(id, 0);
+ (void)pmc_release(id);
+
+ /* Guard the unsigned subtract; a monotonic counter never goes back. */
+ delta = (v1 >= v0) ? (uint64_t)(v1 - v0) : 0;
+ (void)write(resfd, &delta, sizeof(delta));
+ _exit(0);
+}
+
+ATF_TC(pidtrack_no_leakage);
+ATF_TC_HEAD(pidtrack_no_leakage, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Two processes each with their own process-mode PMC: the idle "
+ "one's counter stays near zero while the busy one runs -- no "
+ "cross-PID counter leakage");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_no_leakage, tc)
+{
+ const char *event;
+ pmc_id_t probe;
+ pid_t busy, idle;
+ int busyfd[2], idlefd[2], startfd[2], status;
+ uint64_t busy_delta, idle_delta;
+
+ /*
+ * Confirm a usable event exists (and skip if not) before forking.
+ * Require an instruction-retired event: an idle process must not
+ * accrue it, so the idle counter's absolute bound stays meaningful.
+ */
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ event = find_event_in(retired_events);
+ if (event == NULL)
+ atf_tc_skip("no allocatable instruction-retired event for the "
+ "leakage bound");
+ /* Sanity: the parent can still allocate it. */
+ ATF_REQUIRE(pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &probe, 0) == 0);
+ ATF_REQUIRE(pmc_release(probe) == 0);
+
+ ATF_REQUIRE(pipe(busyfd) == 0);
+ ATF_REQUIRE(pipe(idlefd) == 0);
+ ATF_REQUIRE(pipe(startfd) == 0);
+
+ busy = fork();
+ ATF_REQUIRE(busy >= 0);
+ if (busy == 0) {
+ /* Close every fd this child doesn't use. */
+ close(busyfd[0]);
+ close(idlefd[0]);
+ close(idlefd[1]);
+ close(startfd[1]);
+ child_selfmeasure(event, 1 /* work */, startfd[0], busyfd[1]);
+ /* NOTREACHED */
+ }
+ idle = fork();
+ if (idle < 0) {
+ kill(busy, SIGKILL);
+ waitpid(busy, NULL, 0);
+ close(busyfd[0]); close(busyfd[1]);
+ close(idlefd[0]); close(idlefd[1]);
+ close(startfd[0]); close(startfd[1]);
+ atf_tc_fail("fork(idle) failed: %s", strerror(errno));
+ }
+ if (idle == 0) {
+ close(idlefd[0]);
+ close(busyfd[0]);
+ close(busyfd[1]);
+ close(startfd[1]);
+ child_selfmeasure(event, 0 /* idle */, startfd[0], idlefd[1]);
+ /* NOTREACHED */
+ }
+ close(busyfd[1]);
+ close(idlefd[1]);
+ close(startfd[0]);
+
+ /* Release both children simultaneously (1 byte wakes each). */
+ ATF_REQUIRE(write(startfd[1], "gg", 2) == 2);
+ close(startfd[1]);
+
+ ATF_REQUIRE(read(busyfd[0], &busy_delta, sizeof(busy_delta)) ==
+ (ssize_t)sizeof(busy_delta));
+ ATF_REQUIRE(read(idlefd[0], &idle_delta, sizeof(idle_delta)) ==
+ (ssize_t)sizeof(idle_delta));
+
+ ATF_REQUIRE(waitpid(busy, &status, 0) == busy);
+ ATF_REQUIRE(waitpid(idle, &status, 0) == idle);
+
+ close(busyfd[0]);
+ close(idlefd[0]);
+
+ /* A child that could not set up its PMC asks for a skip. */
+ if (busy_delta == DELTA_SENTINEL || idle_delta == DELTA_SENTINEL)
+ atf_tc_skip("a child could not set up its own PMC "
+ "(resource unavailable)");
+
+ /*
+ * `status` holds only the idle child's exit (the second waitpid).
+ * That is sufficient: a non-sentinel delta is written only on the
+ * exit(0) path, so the sentinel check above already covers any
+ * abnormal exit of either child.
+ */
+ ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+
+ ATF_CHECK_MSG(busy_delta >= MIN_ACTIVE,
+ "busy child's own counter did not advance: %ju",
+ (uintmax_t)busy_delta);
+ ATF_CHECK_MSG(idle_delta < MIN_ACTIVE,
+ "idle child's counter advanced by %ju while a sibling was busy "
+ "(expected < %ju) -- cross-PID counter leakage",
+ (uintmax_t)idle_delta, (uintmax_t)MIN_ACTIVE);
+}
+
+/* ================================================================
+ * pidtrack_ctxswitch -- the count keeps advancing across the target
+ * being context-switched out (blocking read) and back in
+ * ================================================================ */
+
+ATF_TC(pidtrack_ctxswitch);
+ATF_TC_HEAD(pidtrack_ctxswitch, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "A process-mode PMC's accumulated count is preserved and keeps "
+ "advancing across the target being context-switched out and in");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_ctxswitch, tc)
+{
+ const char *event;
+ pmc_id_t probe;
+ pmc_value_t r1_end, r2_start, r2_end;
+ pid_t child;
+ uint64_t out[3] = { 0, 0, 0 };
+ int pfd[2], resfd[2], status;
+ char c;
+
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ event = find_counting_event();
+ if (event == NULL)
+ atf_tc_skip("no allocatable process-mode counting event");
+ ATF_REQUIRE(pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &probe, 0) == 0);
+ ATF_REQUIRE(pmc_release(probe) == 0);
+
+ ATF_REQUIRE(pipe(pfd) == 0);
+ ATF_REQUIRE(pipe(resfd) == 0);
+
+ child = fork();
+ ATF_REQUIRE(child >= 0);
+ if (child == 0) {
+ pmc_id_t id = PMC_ID_INVALID;
+ pmc_value_t a = 0, b = 0, s2 = 0, e = 0;
+ /*
+ * Report three ABSOLUTE reads so the parent can test both
+ * continuity (r2_start >= r1_end: the accumulated value
+ * survived the switch) and that counting resumes after
+ * switch-in (r2_end > r2_start). A prior version reported
+ * only the round-2 delta, which cannot detect a counter that
+ * is reset on switch-in -- new work advances regardless.
+ */
+ close(pfd[1]);
+ close(resfd[0]);
+ if (pmc_init() != 0 ||
+ pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY, &id,
+ 0) != 0 ||
+ pmc_attach(id, 0) != 0 || pmc_start(id) != 0) {
+ /* Signal "skip" to the parent, not a silent hang. */
+ out[0] = out[1] = out[2] = UINT64_MAX;
+ (void)write(resfd[1], out, sizeof(out));
+ _exit(70);
+ }
+
+ /* Round 1: read start, work, read end. */
+ if (pmc_read(id, &a) != 0 || (spin_workload(), 0) ||
+ pmc_read(id, &b) != 0)
+ goto child_err;
+
+ /* Block on the pipe -> forced context-switch out and back. */
+ (void)read(pfd[0], &c, 1);
+
+ /* Round 2: read start (post switch-in), work, read end. */
+ if (pmc_read(id, &s2) != 0 || (spin_workload(), 0) ||
+ pmc_read(id, &e) != 0)
+ goto child_err;
+
+ (void)pmc_stop(id);
+ (void)pmc_detach(id, 0);
+ (void)pmc_release(id);
+
+ out[0] = (uint64_t)b; /* round-1 end (pre-switch) */
+ out[1] = (uint64_t)s2; /* round-2 start (post-switch) */
+ out[2] = (uint64_t)e; /* round-2 end */
+ (void)write(resfd[1], out, sizeof(out));
+ _exit(0);
+child_err:
+ out[0] = out[1] = out[2] = UINT64_MAX;
+ (void)write(resfd[1], out, sizeof(out));
+ _exit(71);
+ }
+ close(pfd[0]);
+ close(resfd[1]);
+
+ /* Give the child time to finish round 1 and block. */
+ usleep(200 * 1000);
+ /* Unblock it for round 2 (context-switch back in). */
+ ATF_REQUIRE(write(pfd[1], "g", 1) == 1);
+ ATF_REQUIRE(read(resfd[0], out, sizeof(out)) == (ssize_t)sizeof(out));
+ ATF_REQUIRE(waitpid(child, &status, 0) == child);
+ close(pfd[1]);
+ close(resfd[0]);
+
+ if (out[0] == UINT64_MAX)
+ atf_tc_skip("child could not set up its own PMC "
+ "(resource unavailable)");
+ ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+
+ r1_end = out[0];
+ r2_start = out[1];
+ r2_end = out[2];
+
+ /* Round 1 actually did work. */
+ ATF_CHECK_MSG(r1_end >= MIN_ACTIVE,
+ "round-1 end count too low: %ju", (uintmax_t)r1_end);
+ /*
+ * Continuity: the accumulated value must survive the context switch.
+ * If save/restore zeroed or lost the counter on switch-in, r2_start
+ * would drop below r1_end. This is the property the test exists for.
+ */
+ ATF_CHECK_MSG(r2_start >= r1_end,
+ "counter went backwards across the context switch: r1_end=%ju "
+ "r2_start=%ju (save/restore lost the accumulated value)",
+ (uintmax_t)r1_end, (uintmax_t)r2_start);
+ /*
+ * Counting resumes after switch-in. Guard the subtract with an
+ * explicit monotonicity check: an unguarded r2_end - r2_start would
+ * underflow to ~2^64 if the counter went backwards inside round 2
+ * (the very save/restore anomaly this test targets) and spuriously
+ * satisfy the bound, masking the defect.
+ */
+ ATF_CHECK_MSG(r2_end >= r2_start && r2_end - r2_start >= MIN_ACTIVE,
+ "counter did not advance after switch-in: r2_start=%ju "
+ "r2_end=%ju", (uintmax_t)r2_start, (uintmax_t)r2_end);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pidtrack_self_counts);
+ ATF_TP_ADD_TC(tp, pidtrack_no_leakage);
+ ATF_TP_ADD_TC(tp, pidtrack_ctxswitch);
+
+ return (atf_no_error());
+}
Index: tests/sys/pmc/pmcstat_tsc_test.sh
===================================================================
--- /dev/null
+++ tests/sys/pmc/pmcstat_tsc_test.sh
@@ -0,0 +1,193 @@
+#! /usr/libexec/atf-sh
+#-
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+# - Raw TSC column appended to pmcstat -R decoded output
+# - tsc_freq=<hz> in initlog record
+# - JSON TSC emitted as unsigned (%ju fix in libpmc_json.cc)
+# - No regression in basic flamegraph workflow
+
+# Skip if hwpmc is not loaded or pmcstat is unavailable.
+pmc_check_support()
+{
+ if ! kldstat -n hwpmc > /dev/null 2>&1 &&
+ ! sysctl -n kern.hwpmc.cpuid > /dev/null 2>&1; then
+ atf_skip "hwpmc unavailable: no hwpmc KLD and no kern.hwpmc.cpuid"
+ fi
+ if ! command -v pmcstat > /dev/null 2>&1; then
+ atf_skip "pmcstat not found in PATH"
+ fi
+ # Skip if userland PMC_VERSION_MINOR exceeds the running kernel's.
+ # This happens when the world is built from a branch ahead of the
+ # booted kernel (e.g. userland built from a newer source tree). The
+ # kernel rejects the connection with EPROGMISMATCH ("Program version
+ # wrong") and every pmcstat invocation fails before doing any work.
+ local err
+ err=$(pmcstat -L 2>&1 >/dev/null) || true
+ case "$err" in
+ *"Program version wrong"*)
+ atf_skip "kernel/userland PMC ABI version mismatch (PMC_VERSION_MINOR): $err"
+ ;;
+ esac
+}
+
+# Capture a 1-second system-wide instructions log into $1.
+# Runs as root (require.user root is set on every test case).
+pmc_capture()
+{
+ local f="$1"
+ local err
+ err=$(pmcstat -S instructions -O "$f" sleep 1 2>&1 >/dev/null) || true
+ case "$err" in
+ *"Program version wrong"*)
+ atf_skip "kernel/userland PMC ABI version mismatch: $err"
+ ;;
+ esac
+ if [ ! -s "$f" ]; then
+ atf_fail "pmcstat capture failed or produced an empty log"
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# Test: initlog record must contain tsc_freq=<hz>
+# ---------------------------------------------------------------------------
+atf_test_case tsc_freq_initlog cleanup
+tsc_freq_initlog_head()
+{
+ atf_set "descr" "initlog record in pmcstat -R output must include tsc_freq=<hz>"
+ atf_set "require.user" "root"
+ atf_set "timeout" "120"
+}
+tsc_freq_initlog_body()
+{
+ pmc_check_support
+ pmc_capture "test.pmc"
+
+ local line
+ line=$(pmcstat -R test.pmc 2>/dev/null | grep '^initlog')
+
+ if [ -z "$line" ]; then
+ atf_fail "No initlog record found in decoded output"
+ fi
+
+ if ! echo "$line" | grep -qE 'tsc_freq=[0-9]+'; then
+ atf_fail "initlog missing tsc_freq field; got: $line"
+ fi
+}
+tsc_freq_initlog_cleanup() { rm -f test.pmc; }
+
+# ---------------------------------------------------------------------------
+# Test: every decoded line must end with a raw TSC value (>= 10 digits)
+# ---------------------------------------------------------------------------
+atf_test_case tsc_column_format cleanup
+tsc_column_format_head()
+{
+ atf_set "descr" "All pmcstat -R decoded lines must end with a raw TSC column"
+ atf_set "require.user" "root"
+ atf_set "timeout" "120"
+}
+tsc_column_format_body()
+{
+ pmc_check_support
+ pmc_capture "test.pmc"
+
+ local decoded bad
+ decoded=$(pmcstat -R test.pmc 2>/dev/null | grep -v '^[[:space:]]*$')
+
+ if [ -z "$decoded" ]; then
+ atf_fail "pmcstat -R produced no output"
+ fi
+
+ # Each non-blank line must end with a run of >= 10 digits (the raw TSC).
+ bad=$(echo "$decoded" | grep -cvE '[0-9]{10,}[[:space:]]*$')
+
+ if [ "$bad" -gt 0 ]; then
+ first=$(echo "$decoded" | grep -vE '[0-9]{10,}[[:space:]]*$' | head -1)
+ atf_fail "$bad line(s) missing TSC column; first offender: $first"
+ fi
+}
+tsc_column_format_cleanup() { rm -f test.pmc; }
+
+# ---------------------------------------------------------------------------
+# Test: JSON output must not contain negative TSC values
+# ---------------------------------------------------------------------------
+atf_test_case json_tsc_unsigned cleanup
+json_tsc_unsigned_head()
+{
+ atf_set "descr" "pmc filter -j must emit unsigned TSC values — %jd-to-%ju fix"
+ atf_set "require.user" "root"
+ atf_set "timeout" "120"
+}
+json_tsc_unsigned_body()
+{
+ pmc_check_support
+ pmc_capture "test.pmc"
+
+ # The JSON output with the unsigned-TSC fix is exposed by
+ # 'pmc filter -j' (usr.sbin/pmc → event_to_json() in libpmc_json.cc), NOT
+ # by pmcstat, which never had a -j flag. On hosts with an older pmc(8)
+ # that predates the -j flag, getopt rejects it: treat that as an
+ # environment/capability gap and skip rather than fail. Genuine assertion
+ # failures (negative TSC values) below still fail.
+ local ferr
+ ferr=$(pmc filter -j test.pmc test.json 2>&1) || true
+ case "$ferr" in
+ *"unknown option"*|*"invalid option"*|*"illegal option"*)
+ atf_skip "pmc filter -j not available on this userland"
+ ;;
+ esac
+
+ if [ ! -s test.json ]; then
+ atf_fail "pmc filter -j produced no JSON output"
+ fi
+
+ if ! grep -qE '"tsc"' test.json; then
+ atf_fail "No 'tsc' key found in JSON output"
+ fi
+
+ # TSC is emitted as a quoted unsigned string: "tsc": "12345".
+ # A signed regression would render as "tsc": "-12345".
+ local neg
+ neg=$(grep -cE '"tsc"[[:space:]]*:[[:space:]]*"?-' test.json)
+ if [ "$neg" -gt 0 ]; then
+ atf_fail "$neg negative TSC value(s) in JSON output — unsigned fix not applied"
+ fi
+}
+json_tsc_unsigned_cleanup() { rm -f test.pmc test.json; }
+
+# ---------------------------------------------------------------------------
+# Test: basic flamegraph workflow must not regress
+# ---------------------------------------------------------------------------
+atf_test_case flamegraph_regression cleanup
+flamegraph_regression_head()
+{
+ atf_set "descr" "pmcstat -G flamegraph output must remain functional after TSC changes"
+ atf_set "require.user" "root"
+}
+flamegraph_regression_body()
+{
+ pmc_check_support
+
+ if ! pmcstat -S instructions -N -O test.pmc sleep 1 > /dev/null 2>&1; then
+ atf_skip "Callchain capture unavailable on this kernel/config"
+ fi
+
+ pmcstat -R test.pmc -G flamegraph.svg > /dev/null 2>&1
+
+ if [ ! -s flamegraph.svg ]; then
+ atf_fail "pmcstat -G produced empty or missing flamegraph output"
+ fi
+}
+flamegraph_regression_cleanup() { rm -f test.pmc flamegraph.svg; }
+
+atf_init_test_cases()
+{
+ atf_add_test_case tsc_freq_initlog
+ atf_add_test_case tsc_column_format
+ atf_add_test_case json_tsc_unsigned
+ atf_add_test_case flamegraph_regression
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Nov 12, 4:43 AM (8 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41671440
Default Alt Text
D59549.1794458581.diff (178 KB)

Event Timeline