clone3(CLONE_VM|CLONE_PIDFD) with an unwritable pidfd pointer fails in put_user() after copy_thread() has mapped a shadow stack. copy_process() then calls exit_thread(). Without HAVE_EXIT_THREAD that free is a no-op and the VMA stays in the parent's mm.
Enable user shadow stack, reject 16 such clones, and require the shadow-stack VMA count to stay unchanged. The original mapping must remain, and a call/return on that stack must succeed, so the cleanup cannot be unmapping the parent's shadow stack. A clone3 with the same flags and a valid pidfd has to succeed first and write a pidfd. That child exits normally, so deactivate_mm() and exit_thread() both run. ENOSYS, EINVAL, and EPERM skip; any other error from that clone, or a success that leaves pidfd unset, fails the test. The rejected child's empty shadow stack is left with a raw exit syscall. Skip when the kernel returns EOPNOTSUPP or EINVAL, or when the feature is locked. The test expects /proc already mounted, as in a normal kselftest run. The toolchain CFLAGS already request zicfiss/zicfilp; the Makefile skips this binary when the compiler cannot target them. Assisted-by: LLM Signed-off-by: Iván Ezequiel Rodriguez <[email protected]> --- tools/testing/selftests/riscv/cfi/Makefile | 5 +- .../selftests/riscv/cfi/shstk-clone-pidfd.c | 317 ++++++++++++++++++ 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/riscv/cfi/shstk-clone-pidfd.c diff --git a/tools/testing/selftests/riscv/cfi/Makefile b/tools/testing/selftests/riscv/cfi/Makefile index 418b4b5325a5..dbecb1fd3b82 100644 --- a/tools/testing/selftests/riscv/cfi/Makefile +++ b/tools/testing/selftests/riscv/cfi/Makefile @@ -13,10 +13,13 @@ endif ifeq ($(shell $(CC) $(CFLAGS) -nostdlib -xc /dev/null -o /dev/null > /dev/null 2>&1; echo $$?),0) -TEST_GEN_PROGS := cfitests +TEST_GEN_PROGS := cfitests shstk-clone-pidfd $(OUTPUT)/cfitests: cfitests.c shadowstack.c $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $^ + +$(OUTPUT)/shstk-clone-pidfd: shstk-clone-pidfd.c + $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $^ else $(shell echo "Toolchain doesn't support CFI, skipping CFI kselftest." >&2) diff --git a/tools/testing/selftests/riscv/cfi/shstk-clone-pidfd.c b/tools/testing/selftests/riscv/cfi/shstk-clone-pidfd.c new file mode 100644 index 000000000000..3f9f6827bd4d --- /dev/null +++ b/tools/testing/selftests/riscv/cfi/shstk-clone-pidfd.c @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * clone3(CLONE_VM|CLONE_PIDFD) with an unwritable pidfd pointer must + * not leak a shadow-stack VMA. copy_thread() maps one, then put_user() + * fails and copy_process() calls exit_thread(). Without HAVE_EXIT_THREAD + * that free is a no-op and the VMA stays in the parent's mm. + * + * Enable shadow stack with an inlined prctl. A libc return across that + * point has no address on the shadow stack, so main() must not return + * after enable. + */ +#define _GNU_SOURCE + +#include <errno.h> +#include <linux/prctl.h> +#include <linux/sched.h> +#include <sched.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/syscall.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "cfi_rv_test.h" + +#ifndef __NR_clone3 +#define __NR_clone3 435 +#endif + +#ifndef PR_GET_SHADOW_STACK_STATUS +#define PR_GET_SHADOW_STACK_STATUS 74 +#endif +#ifndef PR_SET_SHADOW_STACK_STATUS +#define PR_SET_SHADOW_STACK_STATUS 75 +#endif +#ifndef PR_SHADOW_STACK_ENABLE +#define PR_SHADOW_STACK_ENABLE (1UL << 0) +#endif + +#define KSFT_SKIP 4 +#define NCLONE 16 +#define STACK_SIZE (64 * 1024) + +static void on_fault(int sig) +{ + static const char msg[] = "[FAIL]\tparent shadow stack faulted\n"; + + (void)sig; + if (write(2, msg, sizeof(msg) - 1) < 0) + _exit(2); + _exit(2); +} + +struct ss_span { + unsigned long start; + unsigned long end; +}; + +static void ss_scan(int *vmas, int *kb, unsigned long addr, struct ss_span *hit, + const struct ss_span *want, int *kept) +{ + FILE *f; + char line[256]; + unsigned long start = 0, end = 0; + int size = 0; + int saw_size = 0; + + *vmas = 0; + *kb = 0; + if (hit) { + hit->start = 0; + hit->end = 0; + } + if (kept) + *kept = 0; + f = fopen("/proc/self/smaps", "r"); + if (!f) { + perror("smaps"); + _exit(1); + } + while (fgets(line, sizeof(line), f)) { + unsigned long a, b; + + if (sscanf(line, "%lx-%lx", &a, &b) == 2) { + start = a; + end = b; + saw_size = 0; + continue; + } + if (sscanf(line, "Size: %d kB", &size) == 1) { + saw_size = 1; + continue; + } + if (strncmp(line, "VmFlags:", 8) || !saw_size) + continue; + if (!strstr(line, " ss")) { + saw_size = 0; + continue; + } + (*vmas)++; + *kb += size; + if (hit && addr == end) { + hit->start = start; + hit->end = end; + } else if (hit && !hit->start && addr >= start && addr < end) { + hit->start = start; + hit->end = end; + } + if (kept && want && start == want->start && end == want->end) + *kept = 1; + saw_size = 0; + } + fclose(f); +} + +static int clone_pidfd(void *stack, size_t stack_size, int *pidfd_ptr) +{ + struct clone_args args = { + .flags = CLONE_VM | CLONE_PIDFD, + .pidfd = (uint64_t)(uintptr_t)pidfd_ptr, + .exit_signal = SIGCHLD, + .stack = (uint64_t)(uintptr_t)stack, + .stack_size = stack_size, + }; + register long num __asm__("a7") = __NR_clone3; + register long ret __asm__("a0") = (long)&args; + register long sz __asm__("a1") = sizeof(args); + int st; + + asm volatile("ecall" + : "+r"(ret) + : "r"(num), "r"(sz) + : "memory", "cc"); + if (ret == 0) { + register long n __asm__("a7") = __NR_exit; + register long e __asm__("a0") = 0; + + asm volatile("ecall" : : "r"(n), "r"(e) : "memory"); + __builtin_unreachable(); + } + if (ret < 0) + return ret; + if (waitpid((pid_t)ret, &st, 0) < 0) + return -errno; + if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) + return 1; + return 0; +} + +static int depth(int n); +static int (*depth_fn)(int) = depth; + +static int depth(int n) +{ + int (*go)(int n); + + if (n <= 0) + return 1; + asm volatile("" : "=r"(go) : "0"(depth_fn) : "memory"); + return go(n - 1) + 1; +} + +static int skip_enable(long rc) +{ + if (rc == -EOPNOTSUPP || rc == -EINVAL) { + printf("[SKIP]\tkernel has no user shadow stack\n"); + return KSFT_SKIP; + } + if (rc == -EBUSY) { + printf("[SKIP]\tshadow stack is locked\n"); + return KSFT_SKIP; + } + printf("[FAIL]\tPR_SET_SHADOW_STACK_STATUS returned %ld\n", rc); + return 1; +} + +int main(void) +{ + struct sigaction sa; + struct ss_span parent; + void *stack; + void *bad_pidfd; + unsigned long ssp, ssp_mid; + unsigned long ss_mode; + long rc; + int pidfd; + int vmas0, kb0, vmas1, kb1; + int i, kept, d, cret; + + setbuf(stdout, NULL); + + stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) { + perror("mmap stack"); + return 1; + } + bad_pidfd = mmap(NULL, 4096, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (bad_pidfd == MAP_FAILED) { + perror("mmap pidfd"); + return 1; + } + + rc = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, + (long)&ss_mode, 0, 0, 0); + if (rc) { + if (rc == -EOPNOTSUPP || rc == -EINVAL) { + printf("[SKIP]\tkernel has no user shadow stack\n"); + return KSFT_SKIP; + } + printf("[FAIL]\tPR_GET_SHADOW_STACK_STATUS returned %ld\n", rc); + return 1; + } + if (!(ss_mode & PR_SHADOW_STACK_ENABLE)) { + rc = my_syscall5(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, + PR_SHADOW_STACK_ENABLE, 0, 0, 0); + if (rc) + return skip_enable(rc); + } + + ssp = csr_read(CSR_SSP); + if (!ssp) { + printf("[FAIL]\tenable left SSP at 0\n"); + _exit(1); + } + + pidfd = -1; + cret = clone_pidfd(stack, STACK_SIZE, &pidfd); + if (cret == -ENOSYS || cret == -EINVAL || cret == -EPERM) { + printf("[SKIP]\tclone3 with a valid pidfd failed: %s\n", + strerror(-cret)); + _exit(KSFT_SKIP); + } + if (cret < 0) { + printf("[FAIL]\tclone3 with a valid pidfd failed: %s\n", + strerror(-cret)); + _exit(1); + } + if (cret) { + printf("[FAIL]\tclone3 with a valid pidfd did not exit 0\n"); + _exit(1); + } + if (pidfd < 0) { + printf("[FAIL]\tclone3 succeeded but did not write a pidfd\n"); + _exit(1); + } + close(pidfd); + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_fault; + if (sigaction(SIGSEGV, &sa, NULL) || sigaction(SIGBUS, &sa, NULL)) { + perror("sigaction"); + _exit(1); + } + + ssp = csr_read(CSR_SSP); + ss_scan(&vmas0, &kb0, ssp, &parent, NULL, NULL); + if (!parent.start) { + printf("[FAIL]\tSSP %lx matches no shadow-stack VMA\n", ssp); + _exit(1); + } + + for (i = 0; i < NCLONE; i++) { + cret = clone_pidfd(stack, STACK_SIZE, bad_pidfd); + if (cret == -EFAULT) + continue; + if (cret < 0) + printf("[FAIL]\tbad pidfd errno %d, want EFAULT\n", + -cret); + else + printf("[FAIL]\tbad pidfd was accepted\n"); + _exit(1); + } + + ss_scan(&vmas1, &kb1, 0, NULL, &parent, &kept); + ssp_mid = csr_read(CSR_SSP); + d = -1; + if (kept && ssp_mid) + d = depth(16); + ssp = csr_read(CSR_SSP); + + printf("ss_vmas=%d->%d delta=%d ss_kb=%d->%d\n", + vmas0, vmas1, vmas1 - vmas0, kb0, kb1); + printf("parent=%lx-%lx kept=%d ssp_before_depth=%lx depth=%d ssp=%lx\n", + parent.start, parent.end, kept, ssp_mid, d, ssp); + + if (vmas1 != vmas0 || kb1 != kb0) { + printf("[FAIL]\tleaked %d shadow-stack VMA(s), %d kB\n", + vmas1 - vmas0, kb1 - kb0); + _exit(1); + } + if (!kept) { + printf("[FAIL]\tparent shadow stack %lx-%lx was unmapped\n", + parent.start, parent.end); + _exit(2); + } + if (!ssp_mid) { + printf("[FAIL]\tparent SSP was 0 before call/return\n"); + _exit(2); + } + if (d != 17) { + printf("[FAIL]\tparent call/return returned %d, want 17\n", d); + _exit(2); + } + if (!ssp) { + printf("[FAIL]\tparent SSP is 0 after call/return\n"); + _exit(2); + } + + printf("[OK]\tno shadow-stack VMA leak; parent stack still works\n"); + _exit(0); +} -- 2.43.0

