Page MenuHomeFreeBSD

D59375.id.diff
No OneTemporary

D59375.id.diff

diff --git a/tests/sys/pmc/Makefile b/tests/sys/pmc/Makefile
--- a/tests/sys/pmc/Makefile
+++ b/tests/sys/pmc/Makefile
@@ -5,6 +5,7 @@
ATF_TESTS_C+= pmc_detach_test
ATF_TESTS_C+= pmc_group_test
ATF_TESTS_C+= pmc_groupread_test
+ATF_TESTS_C+= pmc_sys_group_test
ATF_TESTS_C+= pmc_group_gate_test
ATF_TESTS_C+= pmc_fork_test
ATF_TESTS_C+= pmc_exec_test
diff --git a/tests/sys/pmc/pmc_sys_group_test.c b/tests/sys/pmc/pmc_sys_group_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_sys_group_test.c
@@ -0,0 +1,437 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ */
+
+/*
+ * Grouped system-mode placement must publish and honor per-CPU row
+ * occupancy. A physical counter must never be live for two owners at
+ * once. The alpha implementation started rows without setting
+ * phw_pmc and without checking it. So a second owner, grouped or
+ * ungrouped, could be placed on top of a live group, with no error and
+ * no race needed.
+ *
+ * An owner is per-process, so each test forks.
+ * The child allocates its own PMCs and so gets its own owner descriptor.
+ * The child reports through its exit status, not through ATF, whose
+ * machinery is not fork-safe.
+ * The test finds row capacity at runtime by shrinking a group until it
+ * commits. A commit-time fit check ignores occupancy, so this measures
+ * pure class capacity.
+ */
+
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/sysctl.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define TEST_EVENT "instructions"
+#define TEST_CPU 0
+
+/* Child exit codes. Anything else is a setup failure. */
+#define CHILD_OK 0 /* Second owner handled correctly. */
+#define CHILD_BUG_PLACED 10 /* Given a row already in use. */
+#define CHILD_BUG_REJECTED 11 /* Refused a free row. */
+#define CHILD_ERR_ALLOCATE 2
+#define CHILD_ERR_CREATE 3
+#define CHILD_ERR_ADD 4
+#define CHILD_ERR_COMMIT 5
+#define CHILD_ERR_START 6
+#define CHILD_ERR_UNGROUPED 7
+
+struct sys_group {
+ uint32_t sg_groupid;
+ pmc_id_t sg_ids[PMC_GROUP_MAX_MEMBERS];
+ u_int sg_nmembers;
+ bool sg_started;
+};
+
+enum group_build_operation {
+ GROUP_BUILD_NONE,
+ GROUP_BUILD_ALLOCATE,
+ GROUP_BUILD_CREATE,
+ GROUP_BUILD_ADD,
+ GROUP_BUILD_COMMIT
+};
+
+struct group_build_failure {
+ enum group_build_operation gbf_operation;
+ u_int gbf_member;
+ int gbf_errno;
+};
+
+static bool
+is_amd(void)
+{
+ char vendor[64];
+ size_t len;
+
+ len = sizeof(vendor);
+ if (sysctlbyname("kern.hwpmc.cpuid", vendor, &len, NULL, 0) != 0)
+ return (false);
+ return (strstr(vendor, "AuthenticAMD") != NULL ||
+ strstr(vendor, "HygonGenuine") != NULL);
+}
+
+static void
+require_hwpmc(void)
+{
+
+ if (geteuid() != 0)
+ atf_tc_skip("system-mode PMCs require root");
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ if (!is_amd())
+ atf_tc_skip("PMC grouping is supported only on AMD CPUs");
+}
+
+static void
+group_init(struct sys_group *g)
+{
+ u_int i;
+
+ memset(g, 0, sizeof(*g));
+ for (i = 0; i < nitems(g->sg_ids); i++)
+ g->sg_ids[i] = PMC_ID_INVALID;
+}
+
+/*
+ * Releasing a committed leader releases the whole group.
+ * So the remaining members are already gone.
+ * Release them anyway for the uncommitted case.
+ * Ignore the resulting stale-handle errors.
+ */
+static void
+group_teardown(struct sys_group *g)
+{
+ u_int i;
+
+ if (g->sg_started)
+ (void)pmc_stop(g->sg_ids[0]);
+ for (i = 0; i < g->sg_nmembers; i++) {
+ if (g->sg_ids[i] != PMC_ID_INVALID)
+ (void)pmc_release(g->sg_ids[i]);
+ }
+ group_init(g);
+}
+
+/*
+ * Build and commit a system-mode group of nmembers, bound to cpu.
+ * Member 0 is the leader. Return 0, or -1 with errno set.
+ */
+static int
+group_build(struct sys_group *g, int cpu, u_int nmembers,
+ struct group_build_failure *failure)
+{
+ u_int i;
+
+ failure->gbf_operation = GROUP_BUILD_NONE;
+ failure->gbf_member = 0;
+ failure->gbf_errno = 0;
+ group_init(g);
+ g->sg_nmembers = nmembers;
+ for (i = 0; i < nmembers; i++) {
+ if (pmc_allocate_group(TEST_EVENT, PMC_MODE_SC, 0, cpu,
+ &g->sg_ids[i], 0) != 0) {
+ failure->gbf_operation = GROUP_BUILD_ALLOCATE;
+ failure->gbf_member = i;
+ failure->gbf_errno = errno;
+ return (-1);
+ }
+ }
+ if (pmc_group_create(&g->sg_groupid) != 0) {
+ failure->gbf_operation = GROUP_BUILD_CREATE;
+ failure->gbf_errno = errno;
+ return (-1);
+ }
+ for (i = 0; i < nmembers; i++) {
+ if (pmc_group_add(g->sg_groupid, g->sg_ids[i], i == 0) != 0) {
+ failure->gbf_operation = GROUP_BUILD_ADD;
+ failure->gbf_member = i;
+ failure->gbf_errno = errno;
+ return (-1);
+ }
+ }
+ if (pmc_group_commit(g->sg_groupid) != 0) {
+ failure->gbf_operation = GROUP_BUILD_COMMIT;
+ failure->gbf_errno = errno;
+ return (-1);
+ }
+ return (0);
+}
+
+static const char *
+group_build_operation_name(enum group_build_operation operation)
+{
+
+ switch (operation) {
+ case GROUP_BUILD_ALLOCATE:
+ return ("pmc_allocate_group");
+ case GROUP_BUILD_CREATE:
+ return ("pmc_group_create");
+ case GROUP_BUILD_ADD:
+ return ("pmc_group_add");
+ case GROUP_BUILD_COMMIT:
+ return ("pmc_group_commit");
+ case GROUP_BUILD_NONE:
+ default:
+ return ("unknown operation");
+ }
+}
+
+static void
+format_group_build_failure(char *buffer, size_t size, const char *description,
+ const struct group_build_failure *failure)
+{
+ const char *operation;
+
+ operation = group_build_operation_name(failure->gbf_operation);
+ if (failure->gbf_operation == GROUP_BUILD_ALLOCATE ||
+ failure->gbf_operation == GROUP_BUILD_ADD) {
+ snprintf(buffer, size,
+ "%s failed at %s for member %u: errno %d (%s)",
+ description, operation, failure->gbf_member,
+ failure->gbf_errno, strerror(failure->gbf_errno));
+ } else {
+ snprintf(buffer, size, "%s failed at %s: errno %d (%s)",
+ description, operation, failure->gbf_errno,
+ strerror(failure->gbf_errno));
+ }
+}
+
+static int
+child_group_build_error(const struct group_build_failure *failure)
+{
+
+ switch (failure->gbf_operation) {
+ case GROUP_BUILD_ALLOCATE:
+ return (CHILD_ERR_ALLOCATE);
+ case GROUP_BUILD_CREATE:
+ return (CHILD_ERR_CREATE);
+ case GROUP_BUILD_ADD:
+ return (CHILD_ERR_ADD);
+ case GROUP_BUILD_COMMIT:
+ return (CHILD_ERR_COMMIT);
+ case GROUP_BUILD_NONE:
+ default:
+ return (CHILD_ERR_COMMIT);
+ }
+}
+
+/*
+ * Return the largest group that commits on cpu.
+ * This is the class row capacity.
+ * The commit-time fit check ignores occupancy, so this does not
+ * depend on what is currently placed.
+ * Return zero if system-mode groups are unavailable.
+ */
+static u_int
+probe_group_capacity(int cpu)
+{
+ struct group_build_failure failure;
+ struct sys_group probe;
+ char description[128], message[256];
+ u_int n;
+
+ for (n = PMC_GROUP_MAX_MEMBERS; n > 0; n--) {
+ if (group_build(&probe, cpu, n, &failure) == 0) {
+ group_teardown(&probe);
+ return (n);
+ }
+ group_teardown(&probe);
+ snprintf(description, sizeof(description),
+ "capacity probe for %u members", n);
+ if (failure.gbf_operation != GROUP_BUILD_COMMIT ||
+ failure.gbf_errno != ENOSPC) {
+ format_group_build_failure(message, sizeof(message),
+ description, &failure);
+ atf_tc_fail("%s; expected pmc_group_commit to fail with "
+ "ENOSPC while reducing the probe size", message);
+ }
+ }
+ return (0);
+}
+
+static void
+check_child(int status)
+{
+
+ ATF_REQUIRE_MSG(WIFEXITED(status), "second owner died on signal %d",
+ WIFSIGNALED(status) ? WTERMSIG(status) : 0);
+ switch (WEXITSTATUS(status)) {
+ case CHILD_OK:
+ break;
+ case CHILD_BUG_PLACED:
+ atf_tc_fail("second owner was placed on a row already live "
+ "for the first owner");
+ case CHILD_BUG_REJECTED:
+ atf_tc_fail("second owner was refused a free row");
+ case CHILD_ERR_ALLOCATE:
+ atf_tc_fail("second owner setup failed at pmc_allocate_group");
+ case CHILD_ERR_CREATE:
+ atf_tc_fail("second owner setup failed at pmc_group_create");
+ case CHILD_ERR_ADD:
+ atf_tc_fail("second owner setup failed at pmc_group_add");
+ case CHILD_ERR_COMMIT:
+ atf_tc_fail("second owner setup failed at pmc_group_commit; "
+ "commit must ignore current occupancy");
+ case CHILD_ERR_START:
+ atf_tc_fail("second owner pmc_start failed with an errno other "
+ "than ENOSPC");
+ case CHILD_ERR_UNGROUPED:
+ atf_tc_fail("ungrouped pmc_allocate failed with an errno other "
+ "than EINVAL");
+ default:
+ atf_tc_fail("second owner setup failed, exit %d",
+ WEXITSTATUS(status));
+ }
+}
+
+/* Claim the same rows the first owner is already running on. */
+static int
+child_full_collision(u_int cap)
+{
+ struct group_build_failure failure;
+ struct sys_group owner_b;
+ int rc;
+
+ if (group_build(&owner_b, TEST_CPU, cap, &failure) != 0) {
+ rc = child_group_build_error(&failure);
+ group_teardown(&owner_b);
+ return (rc);
+ }
+ errno = 0;
+ if (pmc_start(owner_b.sg_ids[0]) == 0) {
+ owner_b.sg_started = true;
+ rc = CHILD_BUG_PLACED;
+ } else
+ rc = errno == ENOSPC ? CHILD_OK : CHILD_ERR_START;
+ group_teardown(&owner_b);
+ return (rc);
+}
+
+/* Make the same claim through the ungrouped allocation path. */
+static int
+child_ungrouped_alloc(u_int cap __unused)
+{
+ pmc_id_t id;
+
+ errno = 0;
+ if (pmc_allocate(TEST_EVENT, PMC_MODE_SC, 0, TEST_CPU, &id, 0) != 0)
+ return (errno == EINVAL ? CHILD_OK : CHILD_ERR_UNGROUPED);
+ (void)pmc_release(id);
+ return (CHILD_BUG_PLACED);
+}
+
+/* Claim the one row the first owner left free on purpose. */
+static int
+child_single_member(u_int cap __unused)
+{
+ struct group_build_failure failure;
+ struct sys_group owner_b;
+ int rc;
+
+ if (group_build(&owner_b, TEST_CPU, 1, &failure) != 0)
+ rc = child_group_build_error(&failure);
+ else if (pmc_start(owner_b.sg_ids[0]) != 0)
+ rc = CHILD_BUG_REJECTED;
+ else {
+ owner_b.sg_started = true;
+ rc = CHILD_OK;
+ }
+ group_teardown(&owner_b);
+ return (rc);
+}
+
+/*
+ * Run fn as a second owner while the first owner holds nrows on
+ * TEST_CPU. Check how the kernel answered it.
+ */
+static void
+run_second_owner(u_int nrows, int (*fn)(u_int), u_int arg)
+{
+ struct group_build_failure failure;
+ struct sys_group owner_a;
+ char message[256];
+ pid_t pid;
+ int status;
+
+ if (group_build(&owner_a, TEST_CPU, nrows, &failure) != 0) {
+ format_group_build_failure(message, sizeof(message),
+ "first owner setup", &failure);
+ group_teardown(&owner_a);
+ atf_tc_fail("%s", message);
+ }
+ ATF_REQUIRE_MSG(pmc_start(owner_a.sg_ids[0]) == 0,
+ "first owner start failed: %s", strerror(errno));
+ owner_a.sg_started = true;
+
+ pid = fork();
+ ATF_REQUIRE(pid != -1);
+ if (pid == 0)
+ _exit(fn(arg));
+ ATF_REQUIRE(waitpid(pid, &status, 0) == pid);
+
+ group_teardown(&owner_a);
+ check_child(status);
+}
+
+ATF_TC_WITHOUT_HEAD(sys_two_owners_full_collision);
+ATF_TC_BODY(sys_two_owners_full_collision, tc)
+{
+ u_int cap;
+
+ require_hwpmc();
+ cap = probe_group_capacity(TEST_CPU);
+ if (cap == 0)
+ atf_tc_skip("no system-mode group capacity on CPU %d",
+ TEST_CPU);
+ run_second_owner(cap, child_full_collision, cap);
+}
+
+ATF_TC_WITHOUT_HEAD(sys_grouped_vs_ungrouped_alloc);
+ATF_TC_BODY(sys_grouped_vs_ungrouped_alloc, tc)
+{
+ u_int cap;
+
+ require_hwpmc();
+ cap = probe_group_capacity(TEST_CPU);
+ if (cap == 0)
+ atf_tc_skip("no system-mode group capacity on CPU %d",
+ TEST_CPU);
+ run_second_owner(cap, child_ungrouped_alloc, 0);
+}
+
+ATF_TC_WITHOUT_HEAD(sys_two_owners_disjoint_rows);
+ATF_TC_BODY(sys_two_owners_disjoint_rows, tc)
+{
+ u_int cap;
+
+ require_hwpmc();
+ cap = probe_group_capacity(TEST_CPU);
+ if (cap < 2)
+ atf_tc_skip("need at least two system-mode group rows on "
+ "CPU %d, have %u", TEST_CPU, cap);
+ run_second_owner(cap - 1, child_single_member, 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, sys_two_owners_full_collision);
+ ATF_TP_ADD_TC(tp, sys_grouped_vs_ungrouped_alloc);
+ ATF_TP_ADD_TC(tp, sys_two_owners_disjoint_rows);
+
+ return (atf_no_error());
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Sep 6, 10:21 AM (7 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38285974
Default Alt Text
D59375.id.diff (11 KB)

Event Timeline