| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2023 ARM Limited. |
| */ |
| |
| #include <limits.h> |
| #include <stdbool.h> |
| |
| #include <linux/prctl.h> |
| |
| #include <sys/mman.h> |
| #include <asm/mman.h> |
| #include <asm/hwcap.h> |
| #include <linux/sched.h> |
| |
| #include "kselftest.h" |
| #include "gcs-util.h" |
| |
| /* nolibc doesn't have sysconf(), just hard code the maximum */ |
| static size_t page_size = 65536; |
| |
| static __attribute__((noinline)) void valid_gcs_function(void) |
| { |
| /* Do something the compiler can't optimise out */ |
| my_syscall1(__NR_prctl, PR_SVE_GET_VL); |
| } |
| |
| static inline int gcs_set_status(unsigned long mode) |
| { |
| bool enabling = mode & PR_SHADOW_STACK_ENABLE; |
| int ret; |
| unsigned long new_mode; |
| |
| /* |
| * The prctl takes 1 argument but we need to ensure that the |
| * other 3 values passed in registers to the syscall are zero |
| * since the kernel validates them. |
| */ |
| ret = my_syscall5(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, mode, |
| 0, 0, 0); |
| |
| if (ret == 0) { |
| ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, |
| &new_mode, 0, 0, 0); |
| if (ret == 0) { |
| if (new_mode != mode) { |
| ksft_print_msg("Mode set to %lx not %lx\n", |
| new_mode, mode); |
| ret = -EINVAL; |
| } |
| } else { |
| ksft_print_msg("Failed to validate mode: %d\n", ret); |
| } |
| |
| if (enabling != chkfeat_gcs()) { |
| ksft_print_msg("%senabled by prctl but %senabled in CHKFEAT\n", |
| enabling ? "" : "not ", |
| chkfeat_gcs() ? "" : "not "); |
| ret = -EINVAL; |
| } |
| } |
| |
| return ret; |
| } |
| |
| /* Try to read the status */ |
| static bool read_status(void) |
| { |
| unsigned long state; |
| int ret; |
| |
| ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, |
| &state, 0, 0, 0); |
| if (ret != 0) { |
| ksft_print_msg("Failed to read state: %d\n", ret); |
| return false; |
| } |
| |
| return state & PR_SHADOW_STACK_ENABLE; |
| } |
| |
| /* Just a straight enable */ |
| static bool base_enable(void) |
| { |
| int ret; |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE); |
| if (ret) { |
| ksft_print_msg("PR_SHADOW_STACK_ENABLE failed %d\n", ret); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* Check we can read GCSPR_EL0 when GCS is enabled */ |
| static bool read_gcspr_el0(void) |
| { |
| unsigned long *gcspr_el0; |
| |
| ksft_print_msg("GET GCSPR\n"); |
| gcspr_el0 = get_gcspr(); |
| ksft_print_msg("GCSPR_EL0 is %p\n", gcspr_el0); |
| |
| return true; |
| } |
| |
| /* Also allow writes to stack */ |
| static bool enable_writeable(void) |
| { |
| int ret; |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_WRITE); |
| if (ret) { |
| ksft_print_msg("PR_SHADOW_STACK_ENABLE writeable failed: %d\n", ret); |
| return false; |
| } |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE); |
| if (ret) { |
| ksft_print_msg("failed to restore plain enable %d\n", ret); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* Also allow writes to stack */ |
| static bool enable_push_pop(void) |
| { |
| int ret; |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_PUSH); |
| if (ret) { |
| ksft_print_msg("PR_SHADOW_STACK_ENABLE with push failed: %d\n", |
| ret); |
| return false; |
| } |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE); |
| if (ret) { |
| ksft_print_msg("failed to restore plain enable %d\n", ret); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* Enable GCS and allow everything */ |
| static bool enable_all(void) |
| { |
| int ret; |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE | PR_SHADOW_STACK_PUSH | |
| PR_SHADOW_STACK_WRITE); |
| if (ret) { |
| ksft_print_msg("PR_SHADOW_STACK_ENABLE with everything failed: %d\n", |
| ret); |
| return false; |
| } |
| |
| ret = gcs_set_status(PR_SHADOW_STACK_ENABLE); |
| if (ret) { |
| ksft_print_msg("failed to restore plain enable %d\n", ret); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool enable_invalid(void) |
| { |
| int ret = gcs_set_status(ULONG_MAX); |
| if (ret == 0) { |
| ksft_print_msg("GCS_SET_STATUS %lx succeeded\n", ULONG_MAX); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* Map a GCS */ |
| static bool map_guarded_stack(void) |
| { |
| int ret; |
| uint64_t *buf; |
| uint64_t expected_cap; |
| int elem; |
| bool pass = true; |
| |
| buf = (void *)my_syscall3(__NR_map_shadow_stack, 0, page_size, |
| SHADOW_STACK_SET_MARKER | |
| SHADOW_STACK_SET_TOKEN); |
| if (buf == MAP_FAILED) { |
| ksft_print_msg("Failed to map %lu byte GCS: %d\n", |
| page_size, errno); |
| return false; |
| } |
| ksft_print_msg("Mapped GCS at %p-%p\n", buf, |
| (void *)((uint64_t)buf + page_size)); |
| |
| /* The top of the newly allocated region should be 0 */ |
| elem = (page_size / sizeof(uint64_t)) - 1; |
| if (buf[elem]) { |
| ksft_print_msg("Last entry is 0x%llx not 0x0\n", buf[elem]); |
| pass = false; |
| } |
| |
| /* Then a valid cap token */ |
| elem--; |
| expected_cap = ((uint64_t)buf + page_size - 16); |
| expected_cap &= GCS_CAP_ADDR_MASK; |
| expected_cap |= GCS_CAP_VALID_TOKEN; |
| if (buf[elem] != expected_cap) { |
| ksft_print_msg("Cap entry is 0x%llx not 0x%llx\n", |
| buf[elem], expected_cap); |
| pass = false; |
| } |
| ksft_print_msg("cap token is 0x%llx\n", buf[elem]); |
| |
| /* The rest should be zeros */ |
| for (elem = 0; elem < page_size / sizeof(uint64_t) - 2; elem++) { |
| if (!buf[elem]) |
| continue; |
| ksft_print_msg("GCS slot %d is 0x%llx not 0x0\n", |
| elem, buf[elem]); |
| pass = false; |
| } |
| |
| ret = munmap(buf, page_size); |
| if (ret != 0) { |
| ksft_print_msg("Failed to unmap %ld byte GCS: %d\n", |
| page_size, errno); |
| pass = false; |
| } |
| |
| return pass; |
| } |
| |
| /* A fork()ed process can run */ |
| static bool test_fork(void) |
| { |
| unsigned long child_mode; |
| int ret, status; |
| pid_t pid; |
| bool pass = true; |
| |
| pid = fork(); |
| if (pid == -1) { |
| ksft_print_msg("fork() failed: %d\n", errno); |
| pass = false; |
| goto out; |
| } |
| if (pid == 0) { |
| /* In child, make sure we can call a function, read |
| * the GCS pointer and status and then exit */ |
| valid_gcs_function(); |
| get_gcspr(); |
| |
| ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, |
| &child_mode, 0, 0, 0); |
| if (ret == 0 && !(child_mode & PR_SHADOW_STACK_ENABLE)) { |
| ksft_print_msg("GCS not enabled in child\n"); |
| ret = -EINVAL; |
| } |
| |
| exit(ret); |
| } |
| |
| /* |
| * In parent, check we can still do function calls then block |
| * for the child. |
| */ |
| valid_gcs_function(); |
| |
| ksft_print_msg("Waiting for child %d\n", pid); |
| |
| ret = waitpid(pid, &status, 0); |
| if (ret == -1) { |
| ksft_print_msg("Failed to wait for child: %d\n", |
| errno); |
| return false; |
| } |
| |
| if (!WIFEXITED(status)) { |
| ksft_print_msg("Child exited due to signal %d\n", |
| WTERMSIG(status)); |
| pass = false; |
| } else { |
| if (WEXITSTATUS(status)) { |
| ksft_print_msg("Child exited with status %d\n", |
| WEXITSTATUS(status)); |
| pass = false; |
| } |
| } |
| |
| out: |
| |
| return pass; |
| } |
| |
| /* A vfork()ed process can run and exit */ |
| static bool test_vfork(void) |
| { |
| unsigned long child_mode; |
| int ret, status; |
| pid_t pid; |
| bool pass = true; |
| |
| pid = vfork(); |
| if (pid == -1) { |
| ksft_print_msg("vfork() failed: %d\n", errno); |
| pass = false; |
| goto out; |
| } |
| if (pid == 0) { |
| /* |
| * In child, make sure we can call a function, read |
| * the GCS pointer and status and then exit. |
| */ |
| valid_gcs_function(); |
| get_gcspr(); |
| |
| ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, |
| &child_mode, 0, 0, 0); |
| if (ret == 0 && !(child_mode & PR_SHADOW_STACK_ENABLE)) { |
| ksft_print_msg("GCS not enabled in child\n"); |
| ret = EXIT_FAILURE; |
| } |
| |
| _exit(ret); |
| } |
| |
| /* |
| * In parent, check we can still do function calls then check |
| * on the child. |
| */ |
| valid_gcs_function(); |
| |
| ksft_print_msg("Waiting for child %d\n", pid); |
| |
| ret = waitpid(pid, &status, 0); |
| if (ret == -1) { |
| ksft_print_msg("Failed to wait for child: %d\n", |
| errno); |
| return false; |
| } |
| |
| if (!WIFEXITED(status)) { |
| ksft_print_msg("Child exited due to signal %d\n", |
| WTERMSIG(status)); |
| pass = false; |
| } else if (WEXITSTATUS(status)) { |
| ksft_print_msg("Child exited with status %d\n", |
| WEXITSTATUS(status)); |
| pass = false; |
| } |
| |
| out: |
| |
| return pass; |
| } |
| |
| typedef bool (*gcs_test)(void); |
| |
| static struct { |
| char *name; |
| gcs_test test; |
| bool needs_enable; |
| } tests[] = { |
| { "read_status", read_status }, |
| { "base_enable", base_enable, true }, |
| { "read_gcspr_el0", read_gcspr_el0 }, |
| { "enable_writeable", enable_writeable, true }, |
| { "enable_push_pop", enable_push_pop, true }, |
| { "enable_all", enable_all, true }, |
| { "enable_invalid", enable_invalid, true }, |
| { "map_guarded_stack", map_guarded_stack }, |
| { "fork", test_fork }, |
| { "vfork", test_vfork }, |
| }; |
| |
| int main(void) |
| { |
| int i, ret; |
| unsigned long gcs_mode; |
| |
| ksft_print_header(); |
| |
| if (!(getauxval(AT_HWCAP) & HWCAP_GCS)) |
| ksft_exit_skip("SKIP GCS not supported\n"); |
| |
| ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, |
| &gcs_mode, 0, 0, 0); |
| if (ret != 0) |
| ksft_exit_fail_msg("Failed to read GCS state: %d\n", ret); |
| |
| if (!(gcs_mode & PR_SHADOW_STACK_ENABLE)) { |
| gcs_mode = PR_SHADOW_STACK_ENABLE; |
| ret = my_syscall5(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, |
| gcs_mode, 0, 0, 0); |
| if (ret != 0) |
| ksft_exit_fail_msg("Failed to enable GCS: %d\n", ret); |
| } |
| |
| ksft_set_plan(ARRAY_SIZE(tests)); |
| |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| ksft_test_result((*tests[i].test)(), "%s\n", tests[i].name); |
| } |
| |
| /* One last test: disable GCS, we can do this one time */ |
| ret = my_syscall5(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, 0, 0, 0, 0); |
| if (ret != 0) |
| ksft_print_msg("Failed to disable GCS: %d\n", ret); |
| |
| ksft_finished(); |
| |
| return 0; |
| } |