git: 35a226dded99 - main - tests/sys/kern: stress FP registers across sigreturn on powerpc64

From: Piotr Kubaj <pkubaj_at_FreeBSD.org>
Date: Thu, 15 Oct 2026 14:29:38 UTC
The branch main has been updated by pkubaj:

URL: https://cgit.FreeBSD.org/src/commit/?id=35a226dded9934c6beb5945a982a705712454be1

commit 35a226dded9934c6beb5945a982a705712454be1
Author:     Piotr Kubaj <pkubaj@FreeBSD.org>
AuthorDate: 2026-10-15 14:28:00 +0000
Commit:     Piotr Kubaj <pkubaj@FreeBSD.org>
CommitDate: 2026-10-15 14:28:00 +0000

    tests/sys/kern: stress FP registers across sigreturn on powerpc64
    
    This is the program that found the races fixed in 1f1043d550ea. One
    thread keeps a pattern in f14-f31 across a signal, while helpers pinned
    to the same CPU keep different patterns in the real registers and get
    woken up from another CPU to preempt it at random points. If the kernel
    ever hands it somebody else's registers, the pattern doesn't match.
    
    The register loads, the thr_kill() and the handshake with the helper are
    in assembly so that nothing touches the FP registers between sigreturn
    and the check. It runs for ten seconds and is skipped with fewer than
    two CPUs.
    
    Requested by:   adrian
    Approved by:    jhibbits
    MFC after:      1 week
    Differential Revision:  https://reviews.freebsd.org/D60411
---
 tests/sys/kern/Makefile                 |   6 +
 tests/sys/kern/sigreturn_fpregs.c       | 233 ++++++++++++++++++++++++++++++++
 tests/sys/kern/sigreturn_fpregs_ppc64.S | 175 ++++++++++++++++++++++++
 3 files changed, 414 insertions(+)

diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index c193e7108dc0..a59168094c58 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -45,6 +45,12 @@ ATF_TESTS_C+=	resolve_beneath_test
 ATF_TESTS_C+=	sched_affinity
 ATF_TESTS_C+=	shutdown_dgram
 ATF_TESTS_C+=	sigaltstack
+.if ${MACHINE_ARCH:Mpowerpc64*} != ""
+ATF_TESTS_C+=	sigreturn_fpregs
+SRCS.sigreturn_fpregs=	sigreturn_fpregs.c sigreturn_fpregs_ppc64.S
+LIBADD.sigreturn_fpregs+=	pthread
+TEST_METADATA.sigreturn_fpregs+=	is_exclusive="true"
+.endif
 ATF_TESTS_C+=	sigwait
 ATF_TESTS_C+=	socket_accept
 ATF_TESTS_C+=	socket_accf
diff --git a/tests/sys/kern/sigreturn_fpregs.c b/tests/sys/kern/sigreturn_fpregs.c
new file mode 100644
index 000000000000..e99f60b154d3
--- /dev/null
+++ b/tests/sys/kern/sigreturn_fpregs.c
@@ -0,0 +1,233 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Piotr Kubaj <pkubaj@FreeBSD.org>
+ */
+
+/*
+ * Stress test for the lazy reload of the floating point registers after
+ * sigreturn(2) on powerpc64.
+ *
+ * sigreturn turns the FPU off for the thread; the next FP instruction takes
+ * the FP unavailable trap, which reloads the registers from the PCB.  The
+ * checker loads f14-f31 with a pattern, sends itself a signal and stores the
+ * registers again, so that store takes the trap.  Between the two it pings a
+ * helper thread pinned to its own CPU, so the hardware registers hold the
+ * helper's values when the reload runs; more helpers on the same CPU are
+ * woken from another CPU so that ULE preempts the checker at random points
+ * of the sequence (only a remote wakeup preempts immediately).  Any mismatch
+ * means the kernel handed the checker another thread's registers.  Before
+ * 1f1043d550ea ("powerpc: keep FP, VMX and VSX ownership changes atomic
+ * with preemption") a POWER9 saw about one mismatch per 3000 round trips.
+ */
+
+#include <sys/param.h>
+#include <sys/cpuset.h>
+#include <sys/thr.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/* sigreturn_fpregs_ppc64.S */
+void	fp_roundtrip(const double *in, double *out, long tid, long sig,
+	    long wfd, long rfd);
+void	fp_load_all(const double *pat);
+
+#define	DURATION	10	/* seconds */
+#define	NPOLL		4	/* 3 woken by the waker, #3 woken by the checker */
+#define	NREGS		18	/* f14-f31 */
+
+static double pollpat[NPOLL][32];
+static int pin_in[NPOLL][2], pin_out[NPOLL][2];
+static int checker_cpu, waker_cpu;
+
+static void
+xfer(int fd, int wr, char *c)
+{
+	ssize_t n;
+
+	do
+		n = wr ? write(fd, c, 1) : read(fd, c, 1);
+	while (n < 0 && errno == EINTR);
+	if (n != 1)
+		err(1, wr ? "write" : "read");
+}
+
+static void
+pin_thread(int cpu)
+{
+	cpuset_t cs;
+
+	CPU_ZERO(&cs);
+	CPU_SET(cpu, &cs);
+	if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, sizeof(cs),
+	    &cs) != 0)
+		err(1, "cpuset_setaffinity(%d)", cpu);
+}
+
+static void
+handler(int sig __unused)
+{
+}
+
+static void *
+polluter(void *arg)
+{
+	int i = (int)(intptr_t)arg;
+	char c;
+
+	pin_thread(checker_cpu);
+	if (i == NPOLL - 1)	/* the checker's helper: distinctive FPSCR */
+		__asm__ volatile("mtfsb1 31");
+	for (;;) {
+		xfer(pin_in[i][0], 0, &c);
+		fp_load_all(pollpat[i]);
+		xfer(pin_out[i][1], 1, &c);
+	}
+	return (NULL);
+}
+
+static void *
+waker(void *arg __unused)
+{
+	uint64_t until;
+	char c = 'x';
+	int i;
+
+	pin_thread(waker_cpu);
+	for (;;) {
+		for (i = 0; i < NPOLL - 1; i++) {
+			/*
+			 * Random 0-20 us delay so the preemption samples the
+			 * checker's loop at uniformly random phases.
+			 */
+			until = __builtin_ppc_get_timebase() + random() % 10240;
+			while (__builtin_ppc_get_timebase() < until)
+				;
+			xfer(pin_in[i][1], 1, &c);
+			xfer(pin_out[i][0], 0, &c);
+		}
+	}
+	return (NULL);
+}
+
+/* Pick two distinct CPUs this process may run on. */
+static int
+pick_cpus(void)
+{
+	cpuset_t cs;
+	int i, n;
+
+	CPU_ZERO(&cs);
+	ATF_REQUIRE_EQ(0, cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID,
+	    -1, sizeof(cs), &cs));
+	n = 0;
+	for (i = 0; i < CPU_SETSIZE; i++) {
+		if (!CPU_ISSET(i, &cs))
+			continue;
+		if (n == 0)
+			checker_cpu = i;
+		else if (n == 1)
+			waker_cpu = i;
+		n++;
+	}
+	return (n);
+}
+
+ATF_TC(sigreturn_fpregs);
+ATF_TC_HEAD(sigreturn_fpregs, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "FP registers reloaded after sigreturn "
+	    "are the thread's own under preemption");
+	atf_tc_set_md_var(tc, "timeout", "60");
+}
+ATF_TC_BODY(sigreturn_fpregs, tc)
+{
+	double in[NREGS], out[NREGS + 1];
+	struct sigaction sa;
+	sigset_t set;
+	pthread_t pt;
+	uint64_t iters, bad, v, first_exp, first_got;
+	time_t end;
+	long tid;
+	int first_reg, k, p, r;
+
+	if (pick_cpus() < 2)
+		atf_tc_skip("needs two CPUs");
+	pin_thread(checker_cpu);
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_handler = handler;
+	ATF_REQUIRE_EQ(0, sigaction(SIGUSR1, &sa, NULL));
+
+	/* The helpers never see the signal. */
+	sigemptyset(&set);
+	sigaddset(&set, SIGUSR1);
+	ATF_REQUIRE_EQ(0, pthread_sigmask(SIG_BLOCK, &set, NULL));
+	for (p = 0; p < NPOLL; p++) {
+		for (r = 0; r < 32; r++) {
+			v = 0xbad0000000000000ULL | ((uint64_t)(p + 1) << 32) |
+			    r;
+			memcpy(&pollpat[p][r], &v, sizeof(v));
+		}
+		ATF_REQUIRE_EQ(0, pipe(pin_in[p]));
+		ATF_REQUIRE_EQ(0, pipe(pin_out[p]));
+		ATF_REQUIRE_EQ(0, pthread_create(&pt, NULL, polluter,
+		    (void *)(intptr_t)p));
+	}
+	ATF_REQUIRE_EQ(0, pthread_create(&pt, NULL, waker, NULL));
+	ATF_REQUIRE_EQ(0, pthread_sigmask(SIG_UNBLOCK, &set, NULL));
+
+	for (r = 0; r < NREGS; r++) {
+		v = 0xc0de000000000000ULL | (uint64_t)(r + 14);
+		memcpy(&in[r], &v, sizeof(v));
+	}
+	thr_self(&tid);
+
+	iters = bad = 0;
+	first_reg = -1;
+	first_exp = first_got = 0;
+	end = time(NULL) + DURATION;
+	while (time(NULL) < end) {
+		for (k = 0; k < 2000; k++) {
+			memset(out, 0, sizeof(out));
+			fp_roundtrip(in, out, tid, SIGUSR1, pin_in[NPOLL - 1][1],
+			    pin_out[NPOLL - 1][0]);
+			iters++;
+			if (memcmp(in, out, sizeof(in)) == 0)
+				continue;
+			bad++;
+			if (first_reg >= 0)
+				continue;
+			for (r = 0; r < NREGS; r++) {
+				memcpy(&first_exp, &in[r], sizeof(first_exp));
+				memcpy(&first_got, &out[r], sizeof(first_got));
+				if (first_exp != first_got) {
+					first_reg = r + 14;
+					break;
+				}
+			}
+		}
+	}
+	if (bad != 0)
+		atf_tc_fail("%ju of %ju sigreturn round trips came back with "
+		    "another thread's FP registers (first: f%d expected %#jx "
+		    "got %#jx)", (uintmax_t)bad, (uintmax_t)iters, first_reg,
+		    (uintmax_t)first_exp, (uintmax_t)first_got);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, sigreturn_fpregs);
+
+	return (atf_no_error());
+}
diff --git a/tests/sys/kern/sigreturn_fpregs_ppc64.S b/tests/sys/kern/sigreturn_fpregs_ppc64.S
new file mode 100644
index 000000000000..f9942c3e0e5b
--- /dev/null
+++ b/tests/sys/kern/sigreturn_fpregs_ppc64.S
@@ -0,0 +1,175 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Piotr Kubaj <pkubaj@FreeBSD.org>
+ */
+
+/*
+ * Helpers for sigreturn_fpregs.c, kept in assembly so that nothing touches
+ * the FP registers between the sigreturn and the stores that take the lazy
+ * reload trap.
+ *
+ * fp_roundtrip(in, out, tid, sig, wfd, rfd): load f14-f31 from in, raise
+ * sig on tid with thr_kill(2), write a byte to wfd and read one from rfd
+ * (both raw system calls: they let the helper on this CPU run), then store
+ * f14-f31 and the FPSCR to out.
+ *
+ * fp_load_all(pat): load f0-f31 from pat.
+ */
+
+	.abiversion 2
+	.text
+/* void fp_roundtrip(const double *in, double *out, long tid, long sig, long wfd, long rfd) */
+	.globl fp_roundtrip
+	.type fp_roundtrip,@function
+fp_roundtrip:
+	mflr 0
+	std 0,16(1)
+	stdu 1,-320(1)
+	stfd 14,32(1)
+	stfd 15,40(1)
+	stfd 16,48(1)
+	stfd 17,56(1)
+	stfd 18,64(1)
+	stfd 19,72(1)
+	stfd 20,80(1)
+	stfd 21,88(1)
+	stfd 22,96(1)
+	stfd 23,104(1)
+	stfd 24,112(1)
+	stfd 25,120(1)
+	stfd 26,128(1)
+	stfd 27,136(1)
+	stfd 28,144(1)
+	stfd 29,152(1)
+	stfd 30,160(1)
+	stfd 31,168(1)
+	std 28,192(1)
+	std 29,200(1)
+	std 30,176(1)
+	std 31,184(1)
+	mr 30,3
+	mr 31,4
+	mr 28,7
+	mr 29,8
+	lfd 14,0(30)
+	lfd 15,8(30)
+	lfd 16,16(30)
+	lfd 17,24(30)
+	lfd 18,32(30)
+	lfd 19,40(30)
+	lfd 20,48(30)
+	lfd 21,56(30)
+	lfd 22,64(30)
+	lfd 23,72(30)
+	lfd 24,80(30)
+	lfd 25,88(30)
+	lfd 26,96(30)
+	lfd 27,104(30)
+	lfd 28,112(30)
+	lfd 29,120(30)
+	lfd 30,128(30)
+	lfd 31,136(30)
+	li 0,433		/* SYS_thr_kill */
+	mr 3,5
+	mr 4,6
+	sc
+	/* ping the helper on this CPU: it leaves ITS registers in the FPU */
+	li 0,4		/* SYS_write(wfd, buf, 1) */
+	mr 3,28
+	addi 4,1,208
+	li 5,1
+	sc
+	li 0,3		/* SYS_read(rfd, buf, 1) */
+	mr 3,29
+	addi 4,1,208
+	li 5,1
+	sc
+	/* first FP instruction since sigreturn: lazy reload happens here */
+	stfd 14,0(31)
+	stfd 15,8(31)
+	stfd 16,16(31)
+	stfd 17,24(31)
+	stfd 18,32(31)
+	stfd 19,40(31)
+	stfd 20,48(31)
+	stfd 21,56(31)
+	stfd 22,64(31)
+	stfd 23,72(31)
+	stfd 24,80(31)
+	stfd 25,88(31)
+	stfd 26,96(31)
+	stfd 27,104(31)
+	stfd 28,112(31)
+	stfd 29,120(31)
+	stfd 30,128(31)
+	stfd 31,136(31)
+	mffs 0		/* FPSCR, for diagnosis */
+	stfd 0,144(31)
+	lfd 14,32(1)
+	lfd 15,40(1)
+	lfd 16,48(1)
+	lfd 17,56(1)
+	lfd 18,64(1)
+	lfd 19,72(1)
+	lfd 20,80(1)
+	lfd 21,88(1)
+	lfd 22,96(1)
+	lfd 23,104(1)
+	lfd 24,112(1)
+	lfd 25,120(1)
+	lfd 26,128(1)
+	lfd 27,136(1)
+	lfd 28,144(1)
+	lfd 29,152(1)
+	lfd 30,160(1)
+	lfd 31,168(1)
+	ld 28,192(1)
+	ld 29,200(1)
+	ld 30,176(1)
+	ld 31,184(1)
+	addi 1,1,320
+	ld 0,16(1)
+	mtlr 0
+	blr
+	.size fp_roundtrip,.-fp_roundtrip
+
+/* void fp_load_all(const double *pat): leave pat[0..31] live in f0-f31 */
+	.globl fp_load_all
+	.type fp_load_all,@function
+fp_load_all:
+	lfd 0,0(3)
+	lfd 1,8(3)
+	lfd 2,16(3)
+	lfd 3,24(3)
+	lfd 4,32(3)
+	lfd 5,40(3)
+	lfd 6,48(3)
+	lfd 7,56(3)
+	lfd 8,64(3)
+	lfd 9,72(3)
+	lfd 10,80(3)
+	lfd 11,88(3)
+	lfd 12,96(3)
+	lfd 13,104(3)
+	lfd 14,112(3)
+	lfd 15,120(3)
+	lfd 16,128(3)
+	lfd 17,136(3)
+	lfd 18,144(3)
+	lfd 19,152(3)
+	lfd 20,160(3)
+	lfd 21,168(3)
+	lfd 22,176(3)
+	lfd 23,184(3)
+	lfd 24,192(3)
+	lfd 25,200(3)
+	lfd 26,208(3)
+	lfd 27,216(3)
+	lfd 28,224(3)
+	lfd 29,232(3)
+	lfd 30,240(3)
+	lfd 31,248(3)
+	blr
+	.size fp_load_all,.-fp_load_all
+	.section .note.GNU-stack,"",@progbits