Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 20 Jul 2026 10:25:15 +0000
From:      Alexander Leidinger <netchild@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: 2cfd82f747c0 - main - hwpmc: add regression tests for counting-PMC counter wraparound
Message-ID:  <6a5df78b.314a2.2f5ef495@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by netchild:

URL: https://cgit.FreeBSD.org/src/commit/?id=2cfd82f747c04f68f679824ba627460e87ab3848

commit 2cfd82f747c04f68f679824ba627460e87ab3848
Author:     Alexander Leidinger <netchild@FreeBSD.org>
AuthorDate: 2026-07-19 08:08:48 +0000
Commit:     Alexander Leidinger <netchild@FreeBSD.org>
CommitDate: 2026-07-20 10:24:36 +0000

    hwpmc: add regression tests for counting-PMC counter wraparound
    
    Exercise a process-mode counting PMC whose accumulated count crosses,
    or already exceeds, the range of the underlying hardware counter.
    Before the previous commit, the first context switch after the
    hardware counter wrapped panicked INVARIANTS kernels with "negative
    increment" and silently corrupted the accumulated count on other
    kernels.
    
    The tests need a hardware counting event backed by a counter narrower
    than 64 bits and skip where none is available (hwpmc(4) not loaded,
    or a VM without a vPMU).
    
    Reviewed by:            adrian
    MFC after:              2 weeks
    Assisted-by:            Claude Code (Fable 5)
    Differential Revision:  https://reviews.freebsd.org/D58341
---
 etc/mtree/BSD.tests.dist      |   2 +
 tests/sys/Makefile            |   1 +
 tests/sys/pmc/Makefile        |   7 ++
 tests/sys/pmc/pmc_wrap_test.c | 216 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 226 insertions(+)

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 004be9536f1f..dec5bf53ebeb 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -971,6 +971,8 @@
             utimensat
             ..
         ..
+        pmc
+        ..
         posixshm
         ..
         ses
diff --git a/tests/sys/Makefile b/tests/sys/Makefile
index ccdc29886d23..7e7f35ded73f 100644
--- a/tests/sys/Makefile
+++ b/tests/sys/Makefile
@@ -30,6 +30,7 @@ TESTS_SUBDIRS+=		netlink
 TESTS_SUBDIRS+=		netmap
 TESTS_SUBDIRS+=		netpfil
 TESTS_SUBDIRS+=		opencrypto
+TESTS_SUBDIRS+=		pmc
 TESTS_SUBDIRS+=		posixshm
 TESTS_SUBDIRS+=		ses
 TESTS_SUBDIRS+=		sound
diff --git a/tests/sys/pmc/Makefile b/tests/sys/pmc/Makefile
new file mode 100644
index 000000000000..403acd0311ce
--- /dev/null
+++ b/tests/sys/pmc/Makefile
@@ -0,0 +1,7 @@
+TESTSDIR=	${TESTSBASE}/sys/pmc
+
+ATF_TESTS_C=	pmc_wrap_test
+
+LIBADD+=	pmc
+
+.include <bsd.test.mk>
diff --git a/tests/sys/pmc/pmc_wrap_test.c b/tests/sys/pmc/pmc_wrap_test.c
new file mode 100644
index 000000000000..58e7b3ef416a
--- /dev/null
+++ b/tests/sys/pmc/pmc_wrap_test.c
@@ -0,0 +1,216 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Alexander Leidinger <netchild@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other advertising materials provided with the
+ *    distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Regression tests for process-mode counting PMCs whose accumulated
+ * count is close to, or beyond, the range of the underlying hardware
+ * counter.  Hardware counters are narrower than the 64-bit software
+ * counter (48 bits on modern x86), and the increment collected when a
+ * counter is read out (at context switch and at process exit) must be
+ * computed modulo the hardware width.
+ *
+ * Before the fix these tests panicked INVARIANTS kernels with
+ * "[pmc,...] negative increment" once the hardware counter wrapped,
+ * and silently corrupted the accumulated count on other kernels.
+ *
+ * The counter is attached to a short-lived child process which spins
+ * to advance it; the child exits before the PMC is released, so the
+ * accounting is torn down cleanly (the accumulation is collected on
+ * the exit path, which is one of the two sites the fix touches).
+ *
+ * The tests need a hardware counting event whose counter is narrower
+ * than 64 bits; they skip on systems without one (e.g. VMs without a
+ * vPMU, or with hwpmc(4) not loaded).
+ */
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/*
+ * High-rate events; at least one of these should be allocatable on
+ * any x86 or arm64 system with a vPMU.  All of them count while the
+ * child's spin loop runs.
+ */
+static const char *wrap_test_events[] = {
+	"instructions",
+	"cycles",
+	"branches",
+	"unhalted-core-cycles",
+	"inst_retired.any",
+	"cpu_clk_unhalted.thread",
+	"ls_not_halted_cyc",
+	NULL
+};
+
+/* The child generates well more than this many events past the seed. */
+#define	WRAP_MARGIN	((uint64_t)1 << 20)
+
+/* Spin iterations: comfortably more than WRAP_MARGIN events, still < 1s. */
+#define	SPIN_ITERS	((uint64_t)200 * 1000 * 1000)
+
+/* Upper bound on events a single test run can plausibly generate. */
+#define	SANITY_BOUND	((uint64_t)1 << 40)
+
+/*
+ * Child: wait for the parent to start the PMC (one byte on the pipe),
+ * spin to advance the counter across the end of its range, then exit.
+ */
+static void __attribute__((noinline))
+child_spin(int startfd)
+{
+	volatile uint64_t sink = 0;
+	uint64_t i;
+	char c;
+
+	(void)read(startfd, &c, 1);
+	for (i = 0; i < SPIN_ITERS; i++)
+		sink += i;
+	_exit((int)(sink & 0x7f));
+}
+
+/*
+ * Allocate a process-mode counting PMC for the first available event
+ * backed by a counter narrower than 64 bits, attach it to a child,
+ * seed it with the given value, let the child spin past the end of
+ * the counter range, and check that the accumulated count stayed sane.
+ */
+static void
+wrap_test(bool seed_beyond_width)
+{
+	pmc_id_t pmcid;
+	pmc_value_t final;
+	uint64_t seed;
+	uint32_t width;
+	pid_t child;
+	int i, pfd[2], status;
+
+	if (pmc_init() != 0)
+		atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+
+	width = 0;
+	pmcid = PMC_ID_INVALID;
+	for (i = 0; wrap_test_events[i] != NULL; i++) {
+		if (pmc_allocate(wrap_test_events[i], PMC_MODE_TC, 0,
+		    PMC_CPU_ANY, &pmcid, 0) != 0)
+			continue;
+		ATF_REQUIRE(pmc_width(pmcid, &width) == 0);
+		if (width >= 32 && width < 64)
+			break;
+		ATF_REQUIRE(pmc_release(pmcid) == 0);
+		pmcid = PMC_ID_INVALID;
+	}
+	if (pmcid == PMC_ID_INVALID)
+		atf_tc_skip("no allocatable counting event with a hardware "
+		    "counter narrower than 64 bits");
+
+	if (seed_beyond_width) {
+		/* Accumulated count that no longer fits the counter at all. */
+		seed = ((uint64_t)1 << width) + 12345;
+	} else {
+		/* Just below the end of the counter range; the child crosses it. */
+		seed = ((uint64_t)1 << width) - WRAP_MARGIN;
+	}
+
+	ATF_REQUIRE(pipe(pfd) == 0);
+	child = fork();
+	ATF_REQUIRE(child >= 0);
+	if (child == 0) {
+		close(pfd[1]);
+		child_spin(pfd[0]);
+		/* NOTREACHED */
+	}
+	close(pfd[0]);
+
+	ATF_REQUIRE(pmc_attach(pmcid, child) == 0);
+	ATF_REQUIRE(pmc_write(pmcid, seed) == 0);
+	ATF_REQUIRE(pmc_start(pmcid) == 0);
+
+	/* Release the child, which spins and exits. */
+	ATF_REQUIRE(write(pfd[1], "g", 1) == 1);
+	close(pfd[1]);
+	ATF_REQUIRE(waitpid(child, &status, 0) == child);
+
+	/*
+	 * The child is gone: its final increment was collected on the
+	 * exit path.  Read the accumulated 64-bit count and stop.
+	 */
+	ATF_REQUIRE(pmc_read(pmcid, &final) == 0);
+	(void)pmc_stop(pmcid);
+
+	ATF_CHECK_MSG(final >= seed,
+	    "accumulated count went backwards: seed 0x%jx, final 0x%jx "
+	    "(width %u)", (uintmax_t)seed, (uintmax_t)final, width);
+	ATF_CHECK_MSG(final - seed < SANITY_BOUND,
+	    "accumulated count jumped implausibly: seed 0x%jx, final 0x%jx "
+	    "(width %u)", (uintmax_t)seed, (uintmax_t)final, width);
+
+	ATF_REQUIRE(pmc_release(pmcid) == 0);
+}
+
+ATF_TC(counting_pmc_wraps_hardware_counter);
+ATF_TC_HEAD(counting_pmc_wraps_hardware_counter, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "A process-mode counting PMC survives its hardware counter "
+	    "wrapping around while the counted process runs");
+	atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(counting_pmc_wraps_hardware_counter, tc)
+{
+	wrap_test(false);
+}
+
+ATF_TC(counting_pmc_beyond_hardware_width);
+ATF_TC_HEAD(counting_pmc_beyond_hardware_width, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "A process-mode counting PMC keeps counting correctly once "
+	    "its accumulated count exceeds the hardware counter range");
+	atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(counting_pmc_beyond_hardware_width, tc)
+{
+	wrap_test(true);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, counting_pmc_wraps_hardware_counter);
+	ATF_TP_ADD_TC(tp, counting_pmc_beyond_hardware_width);
+
+	return (atf_no_error());
+}


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a5df78b.314a2.2f5ef495>