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.
Use kselftest.h / KTAP. Skip paths that cannot run the regression run before ksft_set_plan(). Then confirm a valid clone3 writes a pidfd, reject 16 bad-pidfd clones with -EFAULT, and require the shadow-stack VMA count (VmFlags " ss") to stay unchanged. ENOSYS, EINVAL, and EPERM from the valid clone skip. The rejected child exits with a raw ecall. The test expects /proc already mounted. The Makefile skips this binary when the compiler cannot target zicfiss/zicfilp. 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 | 173 ++++++++++++++++++ 2 files changed, 177 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..1f16d9c513e6 --- /dev/null +++ b/tools/testing/selftests/riscv/cfi/shstk-clone-pidfd.c @@ -0,0 +1,173 @@ +// 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. + * + * After enabling the shadow stack, do not return from main(): libc + * frames above that point have no shadow-stack entries. + */ +#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 "../../kselftest.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 NCLONE 16 +#define STACK_SIZE (64 * 1024) + +static int count_ss_vmas(void) +{ + FILE *f; + char line[256]; + int vmas = 0; + + f = fopen("/proc/self/smaps", "r"); + if (!f) + ksft_exit_fail_msg("fopen /proc/self/smaps: %s\n", + strerror(errno)); + + while (fgets(line, sizeof(line), f)) { + if (!strncmp(line, "VmFlags:", 8) && strstr(line, " ss")) + vmas++; + } + fclose(f); + return vmas; +} + +/* + * Raw ecall: the child resumes on an empty shadow stack, so a libc + * return after clone3 would fault. + */ +static long 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; +} + +int main(void) +{ + void *stack; + void *bad_pidfd; + unsigned long ss_mode = 0; + long rc; + int pidfd = -1; + int vmas0, vmas1; + int i, n_efault = 0; + + ksft_print_header(); + + stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) + ksft_exit_fail_msg("mmap stack: %s\n", strerror(errno)); + + bad_pidfd = mmap(NULL, 4096, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (bad_pidfd == MAP_FAILED) + ksft_exit_fail_msg("mmap pidfd page: %s\n", strerror(errno)); + + rc = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, &ss_mode, + 0, 0, 0); + if (rc == -ENOSYS || rc == -EINVAL || rc == -EOPNOTSUPP) + ksft_exit_skip("shadow stack not available: %ld\n", rc); + if (rc) + ksft_exit_fail_msg("PR_GET_SHADOW_STACK_STATUS: %ld\n", rc); + + 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 == -EOPNOTSUPP || rc == -EINVAL || rc == -EBUSY) + ksft_exit_skip("cannot enable shadow stack: %ld\n", + rc); + if (rc) + ksft_exit_fail_msg("PR_SET_SHADOW_STACK_STATUS: %ld\n", + rc); + } + + rc = clone_pidfd(stack, STACK_SIZE, &pidfd); + if (rc == -ENOSYS || rc == -EINVAL || rc == -EPERM) + ksft_exit_skip("clone3 valid pidfd unavailable: %s\n", + strerror(-rc)); + + ksft_set_plan(3); + + ksft_test_result(rc == 0 && pidfd >= 0, + "clone3 valid pidfd (rc=%ld pidfd=%d)\n", + rc, pidfd); + if (pidfd >= 0) + close(pidfd); + + vmas0 = count_ss_vmas(); + + for (i = 0; i < NCLONE; i++) { + rc = clone_pidfd(stack, STACK_SIZE, bad_pidfd); + if (rc == -EFAULT) + n_efault++; + } + ksft_test_result(n_efault == NCLONE, + "clone3 bad pidfd returns -EFAULT x%d\n", NCLONE); + + vmas1 = count_ss_vmas(); + ksft_test_result(vmas1 == vmas0, + "no shadow-stack VMA leak (%d -> %d)\n", + vmas0, vmas1); + + ksft_finished(); +} -- 2.43.0
