Subject: [RFC PATCH 10/13] selftests/powerpc: Add hashst/hashchk test

From: Benjamin Gray <[email-masked]>
Date: Mon, 28 Nov 2022 13:44:55 +1100

Test the kernel DEXCR[NPHIE] interface and hashchk exception handling.

Introduces with it a DEXCR utils library for common DEXCR operations.

Signed-off-by: Benjamin Gray <[email-masked]>
---
 tools/testing/selftests/powerpc/Makefile      |   1 +
 .../selftests/powerpc/dexcr/.gitignore        |   1 +
 .../testing/selftests/powerpc/dexcr/Makefile  |   9 +
 tools/testing/selftests/powerpc/dexcr/dexcr.c | 118 +++++++++
 tools/testing/selftests/powerpc/dexcr/dexcr.h |  52 ++++
 .../selftests/powerpc/dexcr/hashchk_test.c    | 229 ++++++++++++++++++
 tools/testing/selftests/powerpc/include/reg.h |   4 +
 7 files changed, 414 insertions(+)
 create mode 100644 tools/testing/selftests/powerpc/dexcr/.gitignore
 create mode 100644 tools/testing/selftests/powerpc/dexcr/Makefile
 create mode 100644 tools/testing/selftests/powerpc/dexcr/dexcr.c
 create mode 100644 tools/testing/selftests/powerpc/dexcr/dexcr.h
 create mode 100644 tools/testing/selftests/powerpc/dexcr/hashchk_test.c

diff --git a/tools/testing/selftests/powerpc/Makefile b/tools/testing/selftests/powerpc/Makefile
index 6ba95cd19e42..00dbd000ee01 100644
--- a/tools/testing/selftests/powerpc/Makefile
+++ b/tools/testing/selftests/powerpc/Makefile
@@ -17,6 +17,7 @@ SUB_DIRS = alignment		\
 	   benchmarks		\
 	   cache_shape		\
 	   copyloops		\
+	   dexcr		\
 	   dscr			\
 	   mm			\
 	   nx-gzip		\
diff --git a/tools/testing/selftests/powerpc/dexcr/.gitignore b/tools/testing/selftests/powerpc/dexcr/.gitignore
new file mode 100644
index 000000000000..37adb7f47832
--- /dev/null
+++ b/tools/testing/selftests/powerpc/dexcr/.gitignore
@@ -0,0 +1 @@
+hashchk_user
diff --git a/tools/testing/selftests/powerpc/dexcr/Makefile b/tools/testing/selftests/powerpc/dexcr/Makefile
new file mode 100644
index 000000000000..4b4380d4d986
--- /dev/null
+++ b/tools/testing/selftests/powerpc/dexcr/Makefile
@@ -0,0 +1,9 @@
+TEST_GEN_PROGS := hashchk_test
+
+TEST_FILES := settings
+top_srcdir = ../../../../..
+include ../../lib.mk
+
+HASHCHK_TEST_CFLAGS = -no-pie $(call cc-option,-mno-rop-protect)
+
+$(TEST_GEN_PROGS): ../harness.c ../utils.c ./dexcr.c
diff --git a/tools/testing/selftests/powerpc/dexcr/dexcr.c b/tools/testing/selftests/powerpc/dexcr/dexcr.c
new file mode 100644
index 000000000000..3e7cb581d4a2
--- /dev/null
+++ b/tools/testing/selftests/powerpc/dexcr/dexcr.c
@@ -0,0 +1,118 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/capability.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+
+#include "dexcr.h"
+#include "reg.h"
+#include "utils.h"
+
+long sysctl_get_sbhe(void)
+{
+	long value;
+
+	FAIL_IF_EXIT_MSG(read_long(SYSCTL_DEXCR_SBHE, &value, 10),
+			 "failed to read " SYSCTL_DEXCR_SBHE);
+
+	return value;
+}
+
+void sysctl_set_sbhe(long value)
+{
+	FAIL_IF_EXIT_MSG(write_long(SYSCTL_DEXCR_SBHE, value, 10),
+			 "failed to write to " SYSCTL_DEXCR_SBHE);
+}
+
+unsigned int pr_aspect_to_dexcr_mask(unsigned long which)
+{
+	switch (which) {
+	case PR_PPC_DEXCR_SBHE:
+		return DEXCR_PRO_SBHE;
+	case PR_PPC_DEXCR_IBRTPD:
+		return DEXCR_PRO_IBRTPD;
+	case PR_PPC_DEXCR_SRAPD:
+		return DEXCR_PRO_SRAPD;
+	case PR_PPC_DEXCR_NPHIE:
+		return DEXCR_PRO_NPHIE;
+	default:
+		FAIL_IF_EXIT_MSG(true, "unknown PR aspect");
+	}
+}
+
+static inline unsigned int get_dexcr_pro(void)
+{
+	return mfspr(SPRN_DEXCR);
+}
+
+static inline unsigned int get_dexcr_enf(void)
+{
+	return mfspr(SPRN_HDEXCR);
+}
+
+static inline unsigned int get_dexcr_eff(void)
+{
+	return get_dexcr_pro() | get_dexcr_enf();
+}
+
+unsigned int get_dexcr(enum DexcrSource source)
+{
+	switch (source) {
+	case UDEXCR:
+		return get_dexcr_pro();
+	case ENFORCED:
+		return get_dexcr_enf();
+	case EFFECTIVE:
+		return get_dexcr_eff();
+	default:
+		FAIL_IF_EXIT_MSG(true, "bad DEXCR source");
+	}
+}
+
+bool pr_aspect_supported(unsigned long which)
+{
+	return prctl(PR_PPC_GET_DEXCR, which, 0, 0, 0) >= 0;
+}
+
+bool pr_aspect_editable(unsigned long which)
+{
+	int ret = prctl(PR_PPC_GET_DEXCR, which, 0, 0, 0);
+	return ret > 0 && (ret & PR_PPC_DEXCR_PRCTL) > 0;
+}
+
+bool pr_aspect_edit(unsigned long which, unsigned long ctrl)
+{
+	return prctl(PR_PPC_SET_DEXCR, which, ctrl, 0, 0) == 0;
+}
+
+bool pr_aspect_check(unsigned long which, enum DexcrSource source)
+{
+	unsigned int dexcr = get_dexcr(source);
+	unsigned int aspect = pr_aspect_to_dexcr_mask(which);
+	return (dexcr & aspect) != 0;
+}
+
+int pr_aspect_get(unsigned long pr_aspect)
+{
+	int ret = prctl(PR_PPC_GET_DEXCR, pr_aspect, 0, 0, 0);
+	FAIL_IF_EXIT_MSG(ret < 0, "prctl failed");
+	return ret;
+}
+
+bool dexcr_pro_check(unsigned int pro, enum DexcrSource source)
+{
+	return (get_dexcr(source) & pro) != 0;
+}
+
+void await_child_success(pid_t pid)
+{
+	int wstatus;
+
+	FAIL_IF_EXIT_MSG(pid == -1, "fork failed");
+	FAIL_IF_EXIT_MSG(waitpid(pid, &wstatus, 0) == -1, "wait failed");
+	FAIL_IF_EXIT_MSG(!WIFEXITED(wstatus), "child did not exit cleanly");
+	FAIL_IF_EXIT_MSG(WEXITSTATUS(wstatus) != 0, "child exit error");
+}
diff --git a/tools/testing/selftests/powerpc/dexcr/dexcr.h b/tools/testing/selftests/powerpc/dexcr/dexcr.h
new file mode 100644
index 000000000000..fb8007bf19f8
--- /dev/null
+++ b/tools/testing/selftests/powerpc/dexcr/dexcr.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * POWER Dynamic Execution Control Facility (DEXCR)
+ *
+ * This header file contains helper functions and macros
+ * required for all the DEXCR related test cases.
+ */
+#ifndef _SELFTESTS_POWERPC_DEXCR_DEXCR_H
+#define _SELFTESTS_POWERPC_DEXCR_DEXCR_H
+
+#include <stdbool.h>
+#include <sys/prctl.h>
+#include <unistd.h>
+
+#include "reg.h"
+#include "utils.h"
+
+#define DEXCR_PRO_MASK(aspect)	__MASK(63 - (32 + (aspect)))
+#define DEXCR_PRO_SBHE		DEXCR_PRO_MASK(0)
+#define DEXCR_PRO_IBRTPD	DEXCR_PRO_MASK(3)
+#define DEXCR_PRO_SRAPD		DEXCR_PRO_MASK(4)
+#define DEXCR_PRO_NPHIE		DEXCR_PRO_MASK(5)
+
+enum DexcrSource {
+	UDEXCR,		/* Userspace DEXCR value */
+	ENFORCED,	/* Enforced by hypervisor */
+	EFFECTIVE,	/* Bitwise OR of requested and enforced DEXCR bits */
+};
+
+unsigned int get_dexcr(enum DexcrSource source);
+
+bool pr_aspect_supported(unsigned long which);
+
+bool pr_aspect_editable(unsigned long which);
+
+bool pr_aspect_edit(unsigned long which, unsigned long ctrl);
+
+bool pr_aspect_check(unsigned long which, enum DexcrSource source);
+
+int pr_aspect_get(unsigned long which);
+
+unsigned int pr_aspect_to_dexcr_mask(unsigned long which);
+
+bool dexcr_pro_check(unsigned int pro, enum DexcrSource source);
+
+long sysctl_get_sbhe(void);
+
+void sysctl_set_sbhe(long value);
+
+void await_child_success(pid_t pid);
+
+#endif  /* _SELFTESTS_POWERPC_DEXCR_DEXCR_H */
diff --git a/tools/testing/selftests/powerpc/dexcr/hashchk_test.c b/tools/testing/selftests/powerpc/dexcr/hashchk_test.c
new file mode 100644
index 000000000000..3351bdbdaf13
--- /dev/null
+++ b/tools/testing/selftests/powerpc/dexcr/hashchk_test.c
@@ -0,0 +1,229 @@
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <unistd.h>
+
+#include "dexcr.h"
+#include "utils.h"
+
+static int require_nphie(void)
+{
+	SKIP_IF_MSG(!pr_aspect_supported(PR_PPC_DEXCR_NPHIE),
+		    "DEXCR[NPHIE] not supported");
+
+	if (dexcr_pro_check(DEXCR_PRO_NPHIE, EFFECTIVE))
+		return 0;
+
+	pr_aspect_edit(PR_PPC_DEXCR_NPHIE, PR_PPC_DEXCR_FORCE_SET_ASPECT);
+	FAIL_IF_EXIT_MSG(!dexcr_pro_check(DEXCR_PRO_NPHIE, EFFECTIVE),
+			 "failed to enable DEXCR[NPIHE]");
+
+	return 0;
+}
+
+static void sigill_handler_enabled(int signum, siginfo_t *info, void *context)
+{
+	SIGSAFE_FAIL_IF_EXIT_MSG(signum != SIGILL, "wrong signal received");
+	SIGSAFE_FAIL_IF_EXIT_MSG(info->si_code != ILL_ILLOPN, "wrong signal-code received");
+	exit(0);
+}
+
+static void do_bad_hashchk(void)
+{
+	unsigned long hash = 0;
+	void *hash_p = ((void *)&hash) + 8;	/* hash* offset must be at least -8 */
+
+	asm ("li 3, 0;"			/* set r3 (pretend LR) to known value */
+	     "hashst 3, -8(%1);"	/* compute good hash */
+	     "addi 3, 3, 1;"		/* modify hash */
+	     "hashchk 3, -8(%1);"	/* check bad hash */
+	     : "+m" (hash) : "r" (hash_p) : "r3");
+}
+
+/*
+ * Check that hashchk triggers when DEXCR[NPHIE] is enabled
+ * and is detected as such by the kernel exception handler
+ */
+static int hashchk_enabled_test(void)
+{
+	int err;
+	struct sigaction sa;
+
+	if ((err = require_nphie()))
+		return err;
+
+	sa.sa_sigaction = sigill_handler_enabled;
+	sigemptyset(&sa.sa_mask);
+	sa.sa_flags = SA_SIGINFO;
+	FAIL_IF_MSG(sigaction(SIGILL, &sa, NULL), "cannot install signal handler");
+
+	do_bad_hashchk();
+
+	FAIL_IF_MSG(true, "hashchk failed to trigger");
+}
+
+#define HASH_COUNT 8
+
+static unsigned long hash_values[HASH_COUNT + 1];
+
+static void fill_hash_values(void)
+{
+	for (unsigned long i = 0; i < HASH_COUNT; i++) {
+		void *hash_addr = ((void*)&hash_values[i]) + 8;
+
+		asm volatile ("hashst %2, -8(%1);"
+			      : "+m" (hash_values[i]) : "r" (hash_addr), "r" (i));
+	}
+
+	hash_values[HASH_COUNT] = (unsigned long)&hash_values;
+}
+
+static unsigned int count_hash_values_matches(void)
+{
+	unsigned long matches = 0;
+
+	FAIL_IF_EXIT_MSG(hash_values[HASH_COUNT] != (unsigned long)hash_values,
+			 "bad address check");
+
+	for (unsigned long i = 0; i < HASH_COUNT; i++) {
+		unsigned long orig_hash = hash_values[i];
+		void *hash_addr = ((void*)&hash_values[i]) + 8;
+
+		asm volatile ("hashst %2, -8(%1);"
+			      : "+m" (hash_values[i]) : "r" (hash_addr), "r" (i));
+
+		if (hash_values[i] == orig_hash)
+			matches++;
+	}
+
+	return matches;
+}
+
+static int hashchk_exec_child(void)
+{
+	ssize_t count;
+
+	fill_hash_values();
+
+	count = write(STDOUT_FILENO, hash_values, sizeof(hash_values));
+	return count == sizeof(hash_values) ? 0 : EOVERFLOW;
+}
+
+/*
+ * Check that new programs get different keys so a malicious process
+ * can't recreate a victim's hash values.
+ */
+static int hashchk_exec_random_key_test(void)
+{
+	pid_t pid;
+	int err;
+	int pipefd[2];
+
+	if ((err = require_nphie()))
+		return err;
+
+	FAIL_IF_MSG(pipe(pipefd), "failed to create pipe");
+
+	pid = fork();
+	if (pid == 0) {
+		char *args[] = { "hashchk_exec_child", NULL };
+
+		if (dup2(pipefd[1], STDOUT_FILENO) == -1)
+			_exit(errno);
+
+		execve("/proc/self/exe", args, NULL);
+		_exit(errno);
+	}
+
+	await_child_success(pid);
+	FAIL_IF_MSG(read(pipefd[0], hash_values, sizeof(hash_values)) != sizeof(hash_values),
+		    "missing expected child output");
+
+	/* If all hashes are the same it means (most likely) same key */
+	FAIL_IF_MSG(count_hash_values_matches() == HASH_COUNT, "shared key detected");
+
+	return 0;
+}
+
+/*
+ * Check that forks share the same key so that existing hash values
+ * remain valid.
+ */
+static int hashchk_fork_share_key_test(void)
+{
+	pid_t pid;
+	int err;
+
+	if ((err = require_nphie()))
+		return err;
+
+	fill_hash_values();
+
+	pid = fork();
+	if (pid == 0) {
+		if (count_hash_values_matches() != HASH_COUNT)
+			_exit(1);
+		_exit(0);
+	}
+
+	await_child_success(pid);
+	return 0;
+}
+
+#define STACK_SIZE (1024 * 1024)
+
+static int hashchk_clone_child_fn(void *args)
+{
+	fill_hash_values();
+	return 0;
+}
+
+/*
+ * Check that threads share the same key so that existing hash values
+ * remain valid.
+ */
+static int hashchk_clone_share_key_test(void)
+{
+	void *child_stack;
+	pid_t pid;
+	int err;
+
+	if ((err = require_nphie()))
+		return err;
+
+	child_stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE,
+			   MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+
+	FAIL_IF_MSG(child_stack == MAP_FAILED, "failed to map child stack");
+
+	pid = clone(hashchk_clone_child_fn, child_stack + STACK_SIZE, CLONE_VM | SIGCHLD, NULL);
+
+	await_child_success(pid);
+	FAIL_IF_MSG(count_hash_values_matches() != HASH_COUNT, "different key detected");
+
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	int err = 0;
+
+	if (argc >= 1 && !strcmp(argv[0], "hashchk_exec_child"))
+		return hashchk_exec_child();
+
+	err |= test_harness(hashchk_enabled_test, "hashchk_enabled");
+	err |= test_harness(hashchk_exec_random_key_test, "hashchk_exec_random_key");
+	err |= test_harness(hashchk_fork_share_key_test, "hashchk_fork_share_key");
+	err |= test_harness(hashchk_clone_share_key_test, "hashchk_clone_share_key");
+
+	return err;
+}
diff --git a/tools/testing/selftests/powerpc/include/reg.h b/tools/testing/selftests/powerpc/include/reg.h
index d5a547f72669..cbb5979cb3e2 100644
--- a/tools/testing/selftests/powerpc/include/reg.h
+++ b/tools/testing/selftests/powerpc/include/reg.h
@@ -19,6 +19,8 @@
 #define mb()		asm volatile("sync" : : : "memory");
 #define barrier()	asm volatile("" : : : "memory");
 
+#define SPRN_HDEXCR    455
+
 #define SPRN_MMCR2     769
 #define SPRN_MMCRA     770
 #define SPRN_MMCR0     779
@@ -47,6 +49,8 @@
 #define SPRN_SDAR      781
 #define SPRN_SIER      768
 
+#define SPRN_DEXCR     812
+
 #define SPRN_TEXASR     0x82    /* Transaction Exception and Status Register */
 #define SPRN_TFIAR      0x81    /* Transaction Failure Inst Addr    */
 #define SPRN_TFHAR      0x80    /* Transaction Failure Handler Addr */
-- 
2.38.1