Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F177458393
D59549.1794458581.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
178 KB
Referenced Files
None
Subscribers
None
D59549.1794458581.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D59549: tests/sys/pmc: add pidtrack, callchain, pmc(8), RAPL and AMD tests
Attached
Detach File
Event Timeline