[PATCH v5 2/7] selftest: riscv: test vectorized user copy
Andy Chiu
tchiu at tenstorrent.com
Mon Aug 10 10:22:29 PDT 2026
Add a test that pushes a 1KiB pseudo-random stream through a pipe, so
write() and read() copy it in and out of the kernel on the vectorized
uaccess path, and compares the result against the original. Both buffers
shift every iteration to sweep all relative alignments, and 16 processes
per online CPU keep the vector unit contended. The compare is a byte at a
time loop through volatile pointers rather than memcmp(), keeping it scalar
and independent of the user vector state.
Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Andy Chiu <tchiu at tenstorrent.com>
---
Changelog v5:
- new patch since v5
---
tools/testing/selftests/riscv/vector/Makefile | 6 +-
.../selftests/riscv/vector/v_uaccess_stress.c | 221 ++++++++++++++++++
2 files changed, 226 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/riscv/vector/v_uaccess_stress.c
diff --git a/tools/testing/selftests/riscv/vector/Makefile b/tools/testing/selftests/riscv/vector/Makefile
index 7e0017b3fb8b..0ed09b69dd8a 100644
--- a/tools/testing/selftests/riscv/vector/Makefile
+++ b/tools/testing/selftests/riscv/vector/Makefile
@@ -2,7 +2,8 @@
# Copyright (C) 2021 ARM Limited
# Originally tools/testing/arm64/abi/Makefile
-TEST_GEN_PROGS := v_initval vstate_prctl vstate_ptrace validate_v_ptrace
+TEST_GEN_PROGS := v_initval vstate_prctl vstate_ptrace validate_v_ptrace \
+ v_uaccess_stress
TEST_GEN_PROGS_EXTENDED := vstate_exec_nolibc v_exec_initval_nolibc
TEST_GEN_LIBS := v_helpers.c sys_hwprobe.c
@@ -36,4 +37,7 @@ $(OUTPUT)/vstate_ptrace: vstate_ptrace.c $(OUTPUT)/sys_hwprobe.o $(OUTPUT)/v_hel
$(OUTPUT)/validate_v_ptrace: validate_v_ptrace.c $(OUTPUT)/sys_hwprobe.o $(OUTPUT)/v_helpers.o
$(CC) -static -o $@ $(CFLAGS) $(LDFLAGS) $^
+$(OUTPUT)/v_uaccess_stress: v_uaccess_stress.c
+ $(CC) -static -o $@ $(CFLAGS) $(LDFLAGS) $^
+
EXTRA_CLEAN += $(TEST_GEN_OBJ)
diff --git a/tools/testing/selftests/riscv/vector/v_uaccess_stress.c b/tools/testing/selftests/riscv/vector/v_uaccess_stress.c
new file mode 100644
index 000000000000..d70e0387e246
--- /dev/null
+++ b/tools/testing/selftests/riscv/vector/v_uaccess_stress.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stress the kernel's user copy path from several processes at once.
+ *
+ * Each child pushes a 1KiB pseudo-random byte stream through a pipe: write()
+ * copies it from user space into the kernel (copy_from_user()) and read()
+ * copies it back out (copy_to_user()). The round trip is repeated 512 times
+ * per child, and every iteration the buffers are shifted so that the copies
+ * cover all of the source/destination alignment combinations.
+ *
+ * On RISC-V those copies may be serviced by kernel-mode vector routines, so
+ * the payload is generated and verified with plain scalar loads and stores
+ * only. The comparison goes through volatile pointers, which keeps both GCC
+ * and clang from turning the loop into a vectorised compare and keeps the
+ * check independent of the user vector state the kernel is supposed to
+ * preserve. memcmp() is avoided for the same reason: the libc version is free
+ * to use vector instructions.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#include "kselftest_harness.h"
+
+#define ITERATIONS 32768
+#define STREAM_SIZE 1024
+#define MAX_SHIFT 16
+#define BUF_SIZE (STREAM_SIZE + MAX_SHIFT)
+
+#define CONCURRENCY 16
+
+/* xorshift64*, so the payload does not depend on any libc or kernel helper. */
+static unsigned long prng_state;
+
+static unsigned long prng_next(void)
+{
+ prng_state ^= prng_state >> 12;
+ prng_state ^= prng_state << 25;
+ prng_state ^= prng_state >> 27;
+
+ return prng_state * 2685821657736338717UL;
+}
+
+static void fill_random(unsigned char *buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ buf[i] = (unsigned char)(prng_next() >> 24);
+}
+
+/*
+ * Scalar, byte at a time comparison. Returns the offset of the first
+ * mismatching byte, or len when the two streams are identical.
+ */
+static size_t scalar_diff(const unsigned char *a, const unsigned char *b,
+ size_t len)
+{
+ const volatile unsigned char *va = a;
+ const volatile unsigned char *vb = b;
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ if (va[i] != vb[i])
+ break;
+ }
+
+ return i;
+}
+
+static ssize_t write_all(int fd, const unsigned char *buf, size_t len)
+{
+ size_t done = 0;
+
+ while (done < len) {
+ ssize_t ret = write(fd, buf + done, len - done);
+
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ done += ret;
+ }
+
+ return done;
+}
+
+static ssize_t read_all(int fd, unsigned char *buf, size_t len)
+{
+ size_t done = 0;
+
+ while (done < len) {
+ ssize_t ret = read(fd, buf + done, len - done);
+
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ if (ret == 0)
+ return -1;
+ done += ret;
+ }
+
+ return done;
+}
+
+/* Runs in the child. Returns the exit status to hand back to the parent. */
+static int child_loop(unsigned long seed)
+{
+ unsigned char src[BUF_SIZE], dst[BUF_SIZE];
+ int pipefd[2];
+ int i;
+
+ prng_state = seed;
+
+ if (pipe(pipefd)) {
+ fprintf(stderr, "pid %d: pipe() failed: %s\n",
+ getpid(), strerror(errno));
+ return 1;
+ }
+
+ for (i = 0; i < ITERATIONS; i++) {
+ /*
+ * Walk the source and destination through every relative
+ * alignment, including the case where both are equally
+ * misaligned.
+ */
+ unsigned char *in = src + (i % MAX_SHIFT);
+ unsigned char *out = dst + ((i / MAX_SHIFT) % MAX_SHIFT);
+ size_t off;
+
+ fill_random(in, STREAM_SIZE);
+ memset(out, 0, STREAM_SIZE);
+
+ if (write_all(pipefd[1], in, STREAM_SIZE) < 0) {
+ fprintf(stderr, "pid %d: iteration %d: write() failed: %s\n",
+ getpid(), i, strerror(errno));
+ return 1;
+ }
+
+ if (read_all(pipefd[0], out, STREAM_SIZE) < 0) {
+ fprintf(stderr, "pid %d: iteration %d: read() failed: %s\n",
+ getpid(), i, strerror(errno));
+ return 1;
+ }
+
+ off = scalar_diff(in, out, STREAM_SIZE);
+ if (off != STREAM_SIZE) {
+ fprintf(stderr,
+ "pid %d: iteration %d: mismatch at byte %zu of %d (in %p, out %p): expected 0x%02x, got 0x%02x\n",
+ getpid(), i, off, STREAM_SIZE, in, out,
+ in[off], out[off]);
+ return 1;
+ }
+ }
+
+ close(pipefd[0]);
+ close(pipefd[1]);
+
+ return 0;
+}
+
+static int nr_children(void)
+{
+ long online = sysconf(_SC_NPROCESSORS_ONLN);
+
+ return online * CONCURRENCY;
+}
+
+TEST(uaccess_round_trip)
+{
+ int nr = nr_children();
+ int failures = 0;
+ pid_t pids[nr];
+ int i;
+
+ ksft_print_msg("%d children, %d iterations of %d bytes each\n",
+ nr, ITERATIONS, STREAM_SIZE);
+
+ for (i = 0; i < nr; i++) {
+ pids[i] = fork();
+ ASSERT_LE(0, pids[i]) {
+ TH_LOG("fork() failed: %s", strerror(errno));
+ }
+
+ if (pids[i] == 0) {
+ /* A distinct, reproducible stream per child. */
+ _exit(child_loop(0x9e3779b97f4a7c15UL + i));
+ }
+ }
+
+ for (i = 0; i < nr; i++) {
+ int status;
+
+ if (waitpid(pids[i], &status, 0) < 0) {
+ TH_LOG("waitpid(%d) failed: %s", pids[i],
+ strerror(errno));
+ failures++;
+ continue;
+ }
+
+ if (WIFSIGNALED(status)) {
+ TH_LOG("child %d killed by signal %d", pids[i],
+ WTERMSIG(status));
+ failures++;
+ } else if (!WIFEXITED(status) || WEXITSTATUS(status)) {
+ TH_LOG("child %d failed", pids[i]);
+ failures++;
+ }
+ }
+
+ ASSERT_EQ(0, failures);
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
More information about the linux-riscv
mailing list