diff --git a/tools/testing/selftests/x86/Makefile b/tools/testing/selftests/x86/Makefile
index 0388c4d60af0..b7555ccdad72 100644
--- a/tools/testing/selftests/x86/Makefile
+++ b/tools/testing/selftests/x86/Makefile
@@ -18,7 +18,7 @@ TARGETS_C_32BIT_ONLY := entry_from_vm86 test_syscall_vdso unwind_vdso \
 			test_FCMOV test_FCOMI test_FISTTP \
 			vdso_restorer
 TARGETS_C_64BIT_ONLY := fsgsbase sysret_rip syscall_numbering \
-			corrupt_xstate_header amx
+			corrupt_xstate_header amx ffff8
 # Some selftests require 32bit support enabled also on 64bit systems
 TARGETS_C_32BIT_NEEDED := ldt_gdt ptrace_syscall
 
diff --git a/tools/testing/selftests/x86/ffff8.c b/tools/testing/selftests/x86/ffff8.c
new file mode 100644
index 000000000000..73f55d238d43
--- /dev/null
+++ b/tools/testing/selftests/x86/ffff8.c
@@ -0,0 +1,432 @@
+/*
+ * Copyright (c) 2022 Alexey Dobriyan <adobriyan@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+/* Test that kernel memory is unreadable from userspace. */
+#undef _GNU_SOURCE
+#define _GNU_SOURCE
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define BPL (8 * sizeof(unsigned long))
+#define PAGE_SIZE 4096
+
+static inline uint64_t rol64(uint64_t x, int n)
+{
+	return (x << n) | (x >> (64 - n));
+}
+
+/*
+ * xoroshiro128**
+ * Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org)
+ */
+static uint64_t next(uint64_t s[2])
+{
+	uint64_t s0 = s[0];
+	uint64_t s1 = s[1];
+	uint64_t rv = rol64(s0 * 5, 7) * 9;
+	s1 ^= s0;
+	s[0] = rol64(s0, 24) ^ s1 ^ (s1 << 16);
+	s[1] = rol64(s1, 37);
+	return rv;
+}
+
+static inline long sys_sched_getaffinity(pid_t pid, unsigned int len, unsigned long *m)
+{
+	return syscall(SYS_sched_getaffinity, pid, len, m);
+}
+
+static inline long sys_sched_setaffinity(pid_t pid, unsigned int len, const unsigned long *m)
+{
+	return syscall(SYS_sched_setaffinity, pid, len, m);
+}
+
+static inline long sys_getrandom(void *buf, size_t len, unsigned int flags)
+{
+#ifndef SYS_getrandom
+#define SYS_getrandom 318
+#endif
+	return syscall(SYS_getrandom, buf, len, flags);
+}
+
+static int x86_virt_bits(void)
+{
+	FILE *f = fopen("/proc/cpuinfo", "r");
+	if (!f) {
+		fprintf(stderr, "error: open /proc/cpuinfo, errno %d\n", errno);
+		exit(EXIT_FAILURE);
+	}
+	char *line = NULL;
+	size_t len = 0;
+	ssize_t n;
+	while ((n = getline(&line, &len, f)) != -1) {
+		if (strncmp(line, "address sizes\t:", strlen("address sizes\t:")) == 0) {
+			int rv = atoi(strchr(line, ',') + 2);
+			free(line);
+			fclose(f);
+			return rv;
+		}
+	}
+	__builtin_trap();
+}
+
+/*
+ * 0: vsyscall VMA doesn't exist	vsyscall=none
+ * 1: vsyscall VMA is --xp		vsyscall=xonly
+ * 2: vsyscall VMA is r-xp		vsyscall=emulate
+ */
+static int g_vsyscall;
+#define VSYSCALL_ADDR	0xffffffffff600000
+
+static void sigsegv_vsyscall(int _, siginfo_t *__, void *___)
+{
+        _exit(g_vsyscall);
+}
+
+/*
+ * vsyscall page can't be unmapped, probe it directly.
+ */
+static void vsyscall(void)
+{
+	pid_t pid = fork();
+	if (pid < 0) {
+		fprintf(stderr, "fork, errno %d\n", errno);
+		exit(EXIT_FAILURE);
+	}
+	if (pid == 0) {
+		setrlimit(RLIMIT_CORE, &(struct rlimit){});
+
+		struct sigaction act = {};
+		act.sa_flags = SA_SIGINFO;
+		act.sa_sigaction = sigsegv_vsyscall;
+		sigaction(SIGSEGV, &act, NULL);
+
+		g_vsyscall = 0;
+		/* gettimeofday(NULL, NULL); */
+		asm volatile (
+			"call %P0"
+			:
+			: "i" (VSYSCALL_ADDR), "D" (NULL), "S" (NULL)
+			: "rax", "rcx", "r11"
+		);
+
+		g_vsyscall = 1;
+		*(volatile int *)VSYSCALL_ADDR;
+
+		g_vsyscall = 2;
+		exit(g_vsyscall);
+	}
+
+	int wstatus;
+	wait(&wstatus);
+	if (WIFEXITED(wstatus)) {
+		g_vsyscall = WEXITSTATUS(wstatus);
+	} else {
+		fprintf(stderr, "error: vsyscall wstatus %08x\n", wstatus);
+		exit(EXIT_FAILURE);
+	}
+}
+
+static void sigalrm(int _, siginfo_t *__, void *___)
+{
+	exit(EXIT_SUCCESS);
+}
+
+struct thread_arg {
+	uint64_t min;
+	uint64_t max;
+	int id;
+	int cpu;
+	uint64_t s[2];
+};
+
+__attribute__((noreturn,used))
+static void error_exit(uint64_t addr, int _, const struct thread_arg *arg)
+{
+	if (arg->cpu >= 0) {
+		fprintf(stderr, "thread %d, cpu %d: error: kernel memory read at %016lx\n",
+			arg->id, arg->cpu, addr);
+	} else {
+		fprintf(stderr, "thread %d: error: kernel memory read at %016lx\n",
+			arg->id, addr);
+	}
+	exit(EXIT_FAILURE);
+}
+
+void f_seq(uint64_t min, uint64_t max, struct thread_arg *arg);
+asm (
+".global f_seq;"
+".type f_seq, @function;"
+"f_seq:"
+	"cmp	%rsi, %rdi;"
+	"je	1f;"
+	/*
+	 * Should fault and restart from the beginning of this function
+	 * with different address.
+	 */
+	"mov	(%rdi), %al;"
+	/* DEFCON 1: kernel memory is readable from userspace. */
+	"jmp	error_exit;"
+"1:"
+	"ret;"
+".size f_seq, .-f_seq;"
+);
+
+static void sigsegv_seq(int _, siginfo_t *__, void *uc_)
+{
+	ucontext_t *uc = uc_;
+
+	/* Idea stolen from Sandsifter by Christopher Domas. */
+	uint64_t rdi = uc->uc_mcontext.gregs[REG_RDI] + PAGE_SIZE;
+	if (rdi == VSYSCALL_ADDR && g_vsyscall == 2) {
+		rdi += PAGE_SIZE;
+	}
+	uc->uc_mcontext.gregs[REG_RDI] = rdi;
+
+	uc->uc_mcontext.gregs[REG_RIP] = (uintptr_t)&f_seq;
+	if (0) {
+		printf("%016lx\n", rdi);
+	}
+}
+
+void f_rand(uint64_t min, uint64_t max, struct thread_arg *arg);
+asm (
+".global f_rand;"
+".type f_rand, @function;"
+"f_rand:"
+	/*
+	 * Should fault and restart from the beginning of this function
+	 * with different address.
+	 */
+	"mov	(%rdi), %al;"
+	/* DEFCON 1: kernel memory is readable from userspace. */
+	"jmp	error_exit;"
+".size f_rand, .-f_rand;"
+);
+
+static uint64_t g_mask;
+
+static void sigsegv_rand(int _, siginfo_t *__, void *uc_)
+{
+	ucontext_t *uc = uc_;
+	struct thread_arg *arg = (struct thread_arg *)uc->uc_mcontext.gregs[REG_RDX];
+	uint64_t rdi;
+
+	do {
+		rdi = g_mask | next(arg->s);
+	} while ((rdi & ~(PAGE_SIZE - 1)) == VSYSCALL_ADDR && g_vsyscall == 2);
+	uc->uc_mcontext.gregs[REG_RDI] = rdi;
+	uc->uc_mcontext.gregs[REG_RIP] = (uintptr_t)&f_rand;
+	if (0) {
+		printf("%016lx\n", rdi);
+	}
+}
+
+static int g_mode = 'r';
+static unsigned int g_len = 0;
+
+static void *thread_fn(void *arg_)
+{
+	struct thread_arg *arg = arg_;
+
+	if (arg->cpu >= 0) {
+		unsigned long *m = calloc(1, g_len);
+		int bit = arg->cpu;
+		m[bit / BPL] |= 1UL << (bit % BPL);
+		sys_sched_setaffinity(0, g_len, m);
+	}
+
+	/*
+	 * xoroshiro128** requires non-zero seed.
+	 * getrandom(2) may or may not exist or return EAGAIN.
+	 */
+	arg->s[0] = (uintptr_t)arg;
+	arg->s[1] = 0;
+	if (g_mode == 'r') {
+		do {
+#ifndef GRND_NONBLOCK
+#define GRND_NONBLOCK 1
+#endif
+			sys_getrandom(arg->s, sizeof(arg->s), GRND_NONBLOCK);
+		} while (arg->s[0] == 0 && arg->s[1] == 0);
+	}
+
+	if (arg->cpu >= 0) {
+		printf("thread %d, cpu %d: min %016lx, max %016lx, seed %016lx %016lx\n",
+			arg->id, arg->cpu, arg->min, arg->max, arg->s[0], arg->s[1]);
+	} else {
+		printf("thread %d: min %016lx, max %016lx, seed %016lx %016lx\n",
+			arg->id, arg->min, arg->max, arg->s[0], arg->s[1]);
+	}
+
+	if (g_mode == 'f') {
+		f_seq(arg->min, arg->max, arg);
+	} else {
+		f_rand(arg->min, arg->max, arg);
+	}
+	return NULL;
+}
+
+int main(int argc, char *argv[])
+{
+	int option_N = 0;
+	int option_s = x86_virt_bits() - 1;
+	unsigned int option_t = 256;
+
+	int opt;
+	while ((opt = getopt(argc, argv, "fhn:rs:t:")) != -1) {
+		switch (opt) {
+		case 'f':
+			/* Full scan implies running for as long as it takes. */
+			g_mode = 'f';
+			option_t = 0;
+			break;
+
+		case 'r':
+			g_mode = 'r';
+			break;
+
+		case 'n':
+			option_N = atoi(optarg);
+			break;
+
+		case 's':
+			option_s = atoi(optarg);
+			break;
+
+		case 't':
+			option_t = atoi(optarg);
+			break;
+
+		case 'h':
+			printf("usage: %s [-f] [-r] [-n N] [-s S]\n", argv[0]);
+			printf("\t-f: sequential scan\n");
+			printf("\t-r: random scan (default)\n");
+			printf("\t-n: use N threads (default: $(nproc))\n");
+			printf("\t-s: lowest address shift (default: %d)\n", option_s);
+			printf("\t-t: time to run (default: %u seconds)\n", option_t);
+			return EXIT_FAILURE;
+		}
+	}
+	if (option_s < 12 || option_s > 63) {
+		fprintf(stderr, "error: -s %d\n", option_s);
+		return EXIT_FAILURE;
+	}
+	if (option_N < 0) {
+		fprintf(stderr, "error: -n %d\n", option_N);
+		return EXIT_FAILURE;
+	}
+
+	g_mask = (uint64_t)-1 << option_s;
+	printf("g_mask %016lx\n", g_mask);
+
+	vsyscall();
+	printf("vsyscall %d\n", g_vsyscall);
+
+	unsigned long *m = NULL;
+	do {
+		g_len += sizeof(unsigned long);
+		free(m);
+		m = malloc(g_len);
+	} while (sys_sched_getaffinity(0, g_len, m) == -1 && errno == EINVAL);
+
+	int N;
+	if (option_N > 0) {
+		N = option_N;
+	} else {
+		N = 0;
+		for (int i = 0; i < g_len / sizeof(unsigned long); i += 1) {
+			N += __builtin_popcountl(m[i]);
+		}
+	}
+	printf("N %d threads\n", N);
+	if (option_t > 0) {
+		printf("T %u seconds\n", option_t);
+	}
+
+	{
+		struct sigaction act = {};
+		sigemptyset(&act.sa_mask);
+		act.sa_flags = SA_SIGINFO;
+		if (g_mode == 'f') {
+			act.sa_sigaction = sigsegv_seq;
+		} else {
+			act.sa_sigaction = sigsegv_rand;
+		}
+		sigaction(SIGSEGV, &act, NULL);
+	}
+
+	pthread_t *pth = calloc(N, sizeof(pthread_t));
+	struct thread_arg *arg = calloc(N, sizeof(struct thread_arg));
+
+	uint64_t min_addr = g_mask;
+	uint64_t max_addr = 0;	/* exclusive */
+
+	uint64_t d = ((max_addr - min_addr) / N) & ~(PAGE_SIZE - 1);
+	uint64_t a = min_addr;
+	uint64_t b = a + d;
+
+	int bit = -1;
+	for (int i = 0; i < N; i += 1) {
+		arg[i].min = a;
+		/* No page left behind. */
+		arg[i].max = (i == N - 1) ? 0 : b;
+		arg[i].id = i;
+
+		if (option_N > 0) {
+			arg[i].cpu = -1;
+		} else {
+			do {
+				bit += 1;
+			} while ((m[bit / BPL] & (1UL << (bit % BPL))) == 0);
+			arg[i].cpu = bit;
+		}
+
+		a = b;
+		b += d;
+	}
+	free(m);
+
+	for (int i = 0; i < N; i += 1) {
+		int rv = pthread_create(&pth[i], NULL, thread_fn, &arg[i]);
+		if (rv != 0) {
+			fprintf(stderr, "error: pthread_create, rv %d\n", rv);
+			return EXIT_FAILURE;
+		}
+	}
+
+	if (option_t > 0) {
+		struct sigaction act = {};
+		sigemptyset(&act.sa_mask);
+		act.sa_sigaction = sigalrm;
+		sigaction(SIGALRM, &act, NULL);
+
+		alarm(option_t);
+	}
+
+	for (int i = 0; i < N; i += 1) {
+		pthread_join(pth[i], NULL);
+	}
+
+	return EXIT_SUCCESS;
+}
