// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2023 ARM Limited. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "kselftest_harness.h" #include "gcs-util.h" #define my_syscall2(num, arg1, arg2) \ ({ \ register long _num __asm__ ("x8") = (num); \ register long _arg1 __asm__ ("x0") = (long)(arg1); \ register long _arg2 __asm__ ("x1") = (long)(arg2); \ register long _arg3 __asm__ ("x2") = 0; \ register long _arg4 __asm__ ("x3") = 0; \ register long _arg5 __asm__ ("x4") = 0; \ \ __asm__ volatile ( \ "svc #0\n" \ : "=r"(_arg1) \ : "r"(_arg1), "r"(_arg2), \ "r"(_arg3), "r"(_arg4), \ "r"(_arg5), "r"(_num) \ : "memory", "cc" \ ); \ _arg1; \ }) static noinline void gcs_recurse(int depth) { if (depth) gcs_recurse(depth - 1); /* Prevent tail call optimization so we actually recurse */ asm volatile("dsb sy" : : : "memory"); } /* Smoke test that a function call and return works*/ TEST(can_call_function) { gcs_recurse(0); } static void *gcs_test_thread(void *arg) { int ret; unsigned long mode; /* * Some libcs don't seem to fill unused arguments with 0 but * the kernel validates this so we supply all 5 arguments. */ ret = prctl(PR_GET_SHADOW_STACK_STATUS, &mode, 0, 0, 0); if (ret != 0) { ksft_print_msg("PR_GET_SHADOW_STACK_STATUS failed: %d\n", ret); return NULL; } if (!(mode & PR_SHADOW_STACK_ENABLE)) { ksft_print_msg("GCS not enabled in thread, mode is %lu\n", mode); return NULL; } /* Just in case... */ gcs_recurse(0); /* Use a non-NULL value to indicate a pass */ return &gcs_test_thread; } /* Verify that if we start a new thread it has GCS enabled */ TEST(gcs_enabled_thread) { pthread_t thread; void *thread_ret; int ret; ret = pthread_create(&thread, NULL, gcs_test_thread, NULL); ASSERT_TRUE(ret == 0); if (ret != 0) return; ret = pthread_join(thread, &thread_ret); ASSERT_TRUE(ret == 0); if (ret != 0) return; ASSERT_TRUE(thread_ret != NULL); } /* Read the GCS until we find the terminator */ TEST(gcs_find_terminator) { unsigned long *gcs, *cur; gcs = get_gcspr(); cur = gcs; while (*cur) cur++; ksft_print_msg("GCS in use from %p-%p\n", gcs, cur); /* * We should have at least whatever called into this test so * the two pointer should differ. */ ASSERT_TRUE(gcs != cur); } /* * We can access a GCS via ptrace * * This could usefully have a fixture but note that each test is * fork()ed into a new child whcih causes issues. Might be better to * lift at least some of this out into a separate, non-harness, test * program. */ TEST(ptrace_read_write) { pid_t child, pid; int ret, status; siginfo_t si; uint64_t val, rval, gcspr; struct user_gcs child_gcs; struct iovec iov, local_iov, remote_iov; child = fork(); if (child == -1) { ksft_print_msg("fork() failed: %d (%s)\n", errno, strerror(errno)); ASSERT_NE(child, -1); } if (child == 0) { /* * In child, make sure there's something on the stack and * ask to be traced. */ gcs_recurse(0); if (ptrace(PTRACE_TRACEME, -1, NULL, NULL)) ksft_exit_fail_msg("PTRACE_TRACEME %s", strerror(errno)); if (raise(SIGSTOP)) ksft_exit_fail_msg("raise(SIGSTOP) %s", strerror(errno)); return; } ksft_print_msg("Child: %d\n", child); /* Attach to the child */ while (1) { int sig; pid = wait(&status); if (pid == -1) { ksft_print_msg("wait() failed: %s", strerror(errno)); goto error; } /* * This should never happen but it's hard to flag in * the framework. */ if (pid != child) continue; if (WIFEXITED(status) || WIFSIGNALED(status)) ksft_exit_fail_msg("Child died unexpectedly\n"); if (!WIFSTOPPED(status)) goto error; sig = WSTOPSIG(status); if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &si)) { if (errno == ESRCH) { ASSERT_NE(errno, ESRCH); return; } if (errno == EINVAL) { sig = 0; /* bust group-stop */ goto cont; } ksft_print_msg("PTRACE_GETSIGINFO: %s\n", strerror(errno)); goto error; } if (sig == SIGSTOP && si.si_code == SI_TKILL && si.si_pid == pid) break; cont: if (ptrace(PTRACE_CONT, pid, NULL, sig)) { if (errno == ESRCH) { ASSERT_NE(errno, ESRCH); return; } ksft_print_msg("PTRACE_CONT: %s\n", strerror(errno)); goto error; } } /* Where is the child GCS? */ iov.iov_base = &child_gcs; iov.iov_len = sizeof(child_gcs); ret = ptrace(PTRACE_GETREGSET, child, NT_ARM_GCS, &iov); if (ret != 0) { ksft_print_msg("Failed to read child GCS state: %s (%d)\n", strerror(errno), errno); goto error; } /* We should have inherited GCS over fork(), confirm */ if (!(child_gcs.features_enabled & PR_SHADOW_STACK_ENABLE)) { ASSERT_TRUE(child_gcs.features_enabled & PR_SHADOW_STACK_ENABLE); goto error; } gcspr = child_gcs.gcspr_el0; ksft_print_msg("Child GCSPR 0x%lx, flags %llx, locked %llx\n", gcspr, child_gcs.features_enabled, child_gcs.features_locked); /* Ideally we'd cross check with the child memory map */ errno = 0; val = ptrace(PTRACE_PEEKDATA, child, (void *)gcspr, NULL); ret = errno; if (ret != 0) ksft_print_msg("PTRACE_PEEKDATA failed: %s (%d)\n", strerror(ret), ret); EXPECT_EQ(ret, 0); /* The child should be in a function, the GCSPR shouldn't be 0 */ EXPECT_NE(val, 0); /* Same thing via process_vm_readv() */ local_iov.iov_base = &rval; local_iov.iov_len = sizeof(rval); remote_iov.iov_base = (void *)gcspr; remote_iov.iov_len = sizeof(rval); ret = process_vm_readv(child, &local_iov, 1, &remote_iov, 1, 0); if (ret == -1) ksft_print_msg("process_vm_readv() failed: %s (%d)\n", strerror(errno), errno); EXPECT_EQ(ret, sizeof(rval)); EXPECT_EQ(val, rval); /* Write data via a peek */ ret = ptrace(PTRACE_POKEDATA, child, (void *)gcspr, NULL); if (ret == -1) ksft_print_msg("PTRACE_POKEDATA failed: %s (%d)\n", strerror(errno), errno); EXPECT_EQ(ret, 0); EXPECT_EQ(0, ptrace(PTRACE_PEEKDATA, child, (void *)gcspr, NULL)); /* Restore what we had before */ ret = ptrace(PTRACE_POKEDATA, child, (void *)gcspr, val); if (ret == -1) ksft_print_msg("PTRACE_POKEDATA failed: %s (%d)\n", strerror(errno), errno); EXPECT_EQ(ret, 0); EXPECT_EQ(val, ptrace(PTRACE_PEEKDATA, child, (void *)gcspr, NULL)); /* That's all, folks */ kill(child, SIGKILL); return; error: kill(child, SIGKILL); ASSERT_FALSE(true); } FIXTURE(map_gcs) { unsigned long *stack; }; FIXTURE_VARIANT(map_gcs) { size_t stack_size; unsigned long flags; }; FIXTURE_VARIANT_ADD(map_gcs, s2k_cap_marker) { .stack_size = 2 * 1024, .flags = SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s2k_cap) { .stack_size = 2 * 1024, .flags = SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s2k_marker) { .stack_size = 2 * 1024, .flags = SHADOW_STACK_SET_MARKER, }; FIXTURE_VARIANT_ADD(map_gcs, s2k) { .stack_size = 2 * 1024, .flags = 0, }; FIXTURE_VARIANT_ADD(map_gcs, s4k_cap_marker) { .stack_size = 4 * 1024, .flags = SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s4k_cap) { .stack_size = 4 * 1024, .flags = SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s3k_marker) { .stack_size = 4 * 1024, .flags = SHADOW_STACK_SET_MARKER, }; FIXTURE_VARIANT_ADD(map_gcs, s4k) { .stack_size = 4 * 1024, .flags = 0, }; FIXTURE_VARIANT_ADD(map_gcs, s16k_cap_marker) { .stack_size = 16 * 1024, .flags = SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s16k_cap) { .stack_size = 16 * 1024, .flags = SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s16k_marker) { .stack_size = 16 * 1024, .flags = SHADOW_STACK_SET_MARKER, }; FIXTURE_VARIANT_ADD(map_gcs, s16k) { .stack_size = 16 * 1024, .flags = 0, }; FIXTURE_VARIANT_ADD(map_gcs, s64k_cap_marker) { .stack_size = 64 * 1024, .flags = SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s64k_cap) { .stack_size = 64 * 1024, .flags = SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s64k_marker) { .stack_size = 64 * 1024, .flags = SHADOW_STACK_SET_MARKER, }; FIXTURE_VARIANT_ADD(map_gcs, s64k) { .stack_size = 64 * 1024, .flags = 0, }; FIXTURE_VARIANT_ADD(map_gcs, s128k_cap_marker) { .stack_size = 128 * 1024, .flags = SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s128k_cap) { .stack_size = 128 * 1024, .flags = SHADOW_STACK_SET_TOKEN, }; FIXTURE_VARIANT_ADD(map_gcs, s128k_marker) { .stack_size = 128 * 1024, .flags = SHADOW_STACK_SET_MARKER, }; FIXTURE_VARIANT_ADD(map_gcs, s128k) { .stack_size = 128 * 1024, .flags = 0, }; FIXTURE_SETUP(map_gcs) { self->stack = (void *)syscall(__NR_map_shadow_stack, 0, variant->stack_size, variant->flags); ASSERT_FALSE(self->stack == MAP_FAILED); ksft_print_msg("Allocated stack from %p-%p\n", self->stack, self->stack + variant->stack_size); } FIXTURE_TEARDOWN(map_gcs) { int ret; if (self->stack != MAP_FAILED) { ret = munmap(self->stack, variant->stack_size); ASSERT_EQ(ret, 0); } } /* The stack has a cap token */ TEST_F(map_gcs, stack_capped) { unsigned long *stack = self->stack; size_t cap_index; cap_index = (variant->stack_size / sizeof(unsigned long)); switch (variant->flags & (SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN)) { case SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN: cap_index -= 2; break; case SHADOW_STACK_SET_TOKEN: cap_index -= 1; break; case SHADOW_STACK_SET_MARKER: case 0: /* No cap, no test */ return; } ASSERT_EQ(stack[cap_index], GCS_CAP(&stack[cap_index])); } /* The top of the stack is 0 */ TEST_F(map_gcs, stack_terminated) { unsigned long *stack = self->stack; size_t term_index; if (!(variant->flags & SHADOW_STACK_SET_MARKER)) return; term_index = (variant->stack_size / sizeof(unsigned long)) - 1; ASSERT_EQ(stack[term_index], 0); } /* Writes should fault */ TEST_F_SIGNAL(map_gcs, not_writeable, SIGSEGV) { self->stack[0] = 0; } /* Put it all together, we can safely switch to and from the stack */ TEST_F(map_gcs, stack_switch) { size_t cap_index; cap_index = (variant->stack_size / sizeof(unsigned long)); unsigned long *orig_gcspr_el0, *pivot_gcspr_el0; /* Skip over the stack terminator and point at the cap */ switch (variant->flags & (SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN)) { case SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN: cap_index -= 2; break; case SHADOW_STACK_SET_TOKEN: cap_index -= 1; break; case SHADOW_STACK_SET_MARKER: case 0: /* No cap, no test */ return; } pivot_gcspr_el0 = &self->stack[cap_index]; /* Pivot to the new GCS */ ksft_print_msg("Pivoting to %p from %p, target has value 0x%lx\n", pivot_gcspr_el0, get_gcspr(), *pivot_gcspr_el0); gcsss1(pivot_gcspr_el0); orig_gcspr_el0 = gcsss2(); ksft_print_msg("Pivoted to %p from %p, target has value 0x%lx\n", get_gcspr(), orig_gcspr_el0, *pivot_gcspr_el0); ksft_print_msg("Pivoted, GCSPR_EL0 now %p\n", get_gcspr()); /* New GCS must be in the new buffer */ ASSERT_TRUE((unsigned long)get_gcspr() > (unsigned long)self->stack); ASSERT_TRUE((unsigned long)get_gcspr() <= (unsigned long)self->stack + variant->stack_size); /* We should be able to use all but 2 slots of the new stack */ ksft_print_msg("Recursing %zu levels\n", cap_index - 1); gcs_recurse(cap_index - 1); /* Pivot back to the original GCS */ gcsss1(orig_gcspr_el0); pivot_gcspr_el0 = gcsss2(); gcs_recurse(0); ksft_print_msg("Pivoted back to GCSPR_EL0 0x%p\n", get_gcspr()); } /* We fault if we try to go beyond the end of the stack */ TEST_F_SIGNAL(map_gcs, stack_overflow, SIGSEGV) { size_t cap_index; cap_index = (variant->stack_size / sizeof(unsigned long)); unsigned long *orig_gcspr_el0, *pivot_gcspr_el0; /* Skip over the stack terminator and point at the cap */ switch (variant->flags & (SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN)) { case SHADOW_STACK_SET_MARKER | SHADOW_STACK_SET_TOKEN: cap_index -= 2; break; case SHADOW_STACK_SET_TOKEN: cap_index -= 1; break; case SHADOW_STACK_SET_MARKER: case 0: /* No cap, no test but we need to SEGV to avoid a false fail */ orig_gcspr_el0 = get_gcspr(); *orig_gcspr_el0 = 0; return; } pivot_gcspr_el0 = &self->stack[cap_index]; /* Pivot to the new GCS */ ksft_print_msg("Pivoting to %p from %p, target has value 0x%lx\n", pivot_gcspr_el0, get_gcspr(), *pivot_gcspr_el0); gcsss1(pivot_gcspr_el0); orig_gcspr_el0 = gcsss2(); ksft_print_msg("Pivoted to %p from %p, target has value 0x%lx\n", pivot_gcspr_el0, orig_gcspr_el0, *pivot_gcspr_el0); ksft_print_msg("Pivoted, GCSPR_EL0 now %p\n", get_gcspr()); /* New GCS must be in the new buffer */ ASSERT_TRUE((unsigned long)get_gcspr() > (unsigned long)self->stack); ASSERT_TRUE((unsigned long)get_gcspr() <= (unsigned long)self->stack + variant->stack_size); /* Now try to recurse, we should fault doing this. */ ksft_print_msg("Recursing %zu levels...\n", cap_index + 1); gcs_recurse(cap_index + 1); ksft_print_msg("...done\n"); /* Clean up properly to try to guard against spurious passes. */ gcsss1(orig_gcspr_el0); pivot_gcspr_el0 = gcsss2(); ksft_print_msg("Pivoted back to GCSPR_EL0 0x%p\n", get_gcspr()); } FIXTURE(map_invalid_gcs) { }; FIXTURE_VARIANT(map_invalid_gcs) { size_t stack_size; }; FIXTURE_SETUP(map_invalid_gcs) { } FIXTURE_TEARDOWN(map_invalid_gcs) { } /* GCS must be larger than 16 bytes */ FIXTURE_VARIANT_ADD(map_invalid_gcs, too_small) { .stack_size = 8, }; /* GCS size must be 16 byte aligned */ FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_1) { .stack_size = 1024 + 1 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_2) { .stack_size = 1024 + 2 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_3) { .stack_size = 1024 + 3 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_4) { .stack_size = 1024 + 4 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_5) { .stack_size = 1024 + 5 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_6) { .stack_size = 1024 + 6 }; FIXTURE_VARIANT_ADD(map_invalid_gcs, unligned_7) { .stack_size = 1024 + 7 }; TEST_F(map_invalid_gcs, do_map) { void *stack; stack = (void *)syscall(__NR_map_shadow_stack, 0, variant->stack_size, 0); ASSERT_TRUE(stack == MAP_FAILED); if (stack != MAP_FAILED) munmap(stack, variant->stack_size); } FIXTURE(invalid_mprotect) { unsigned long *stack; size_t stack_size; }; FIXTURE_VARIANT(invalid_mprotect) { unsigned long flags; }; FIXTURE_SETUP(invalid_mprotect) { self->stack_size = sysconf(_SC_PAGE_SIZE); self->stack = (void *)syscall(__NR_map_shadow_stack, 0, self->stack_size, 0); ASSERT_FALSE(self->stack == MAP_FAILED); ksft_print_msg("Allocated stack from %p-%p\n", self->stack, self->stack + self->stack_size); } FIXTURE_TEARDOWN(invalid_mprotect) { int ret; if (self->stack != MAP_FAILED) { ret = munmap(self->stack, self->stack_size); ASSERT_EQ(ret, 0); } } FIXTURE_VARIANT_ADD(invalid_mprotect, exec) { .flags = PROT_EXEC, }; TEST_F(invalid_mprotect, do_map) { int ret; ret = mprotect(self->stack, self->stack_size, variant->flags); ASSERT_EQ(ret, -1); } TEST_F(invalid_mprotect, do_map_read) { int ret; ret = mprotect(self->stack, self->stack_size, variant->flags | PROT_READ); ASSERT_EQ(ret, -1); } int main(int argc, char **argv) { unsigned long gcs_mode; int ret; if (!(getauxval(AT_HWCAP) & HWCAP_GCS)) ksft_exit_skip("SKIP GCS not supported\n"); /* * Force shadow stacks on, our tests *should* be fine with or * without libc support and with or without this having ended * up tagged for GCS and enabled by the dynamic linker. We * can't use the libc prctl() function since we can't return * from enabling the stack. */ ret = my_syscall2(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, &gcs_mode); if (ret) { ksft_print_msg("Failed to read GCS state: %d\n", ret); return EXIT_FAILURE; } if (!(gcs_mode & PR_SHADOW_STACK_ENABLE)) { gcs_mode = PR_SHADOW_STACK_ENABLE; ret = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, gcs_mode); if (ret) { ksft_print_msg("Failed to configure GCS: %d\n", ret); return EXIT_FAILURE; } } /* Avoid returning in case libc doesn't understand GCS */ exit(test_harness_run(argc, argv)); }