From: "Iván Ezequiel Rodriguez" <ivanrwcm25@gmail.com>
To: Thomas Gleixner <tglx@kernel.org>, Ingo Molnar <mingo@redhat.com>,
Borislav Petkov <bp@alien8.de>,
Dave Hansen <dave.hansen@linux.intel.com>,
x86@kernel.org
Cc: "H . Peter Anvin" <hpa@zytor.com>,
"Shuah Khan" <shuah@kernel.org>,
"Christian Brauner" <brauner@kernel.org>,
"Rick Edgecombe" <rick.p.edgecombe@intel.com>,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
"Iván Ezequiel Rodriguez" <ivanrwcm25@gmail.com>
Subject: [PATCH v2 2/2] selftests/x86: shadow stack must survive a bad TLS
Date: Thu, 22 Oct 2026 19:17:21 -0300 [thread overview]
Message-ID: <20261022221722.619233-3-ivanrwcm25@gmail.com> (raw)
In-Reply-To: <20261022221722.619233-1-ivanrwcm25@gmail.com>
clone3(CLONE_VM|CLONE_SETTLS) with an out-of-range TLS fails in
set_new_tls() after copy_thread() has mapped a shadow stack.
copy_process() does not call exit_thread() on that failure, so 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, that sixteen
bad-TLS clones return -EPERM, and that the shadow-stack VMA count is
unchanged while the parent's stack still supports call/return.
SKIP the valid-TLS control only for ENOSYS, EPERM, or EACCES. Missing
/proc/self/smaps (ENOENT) is SKIP. Read errors, truncated lines, and
VMA records without VmFlags are FAIL so under-counting cannot hide a
leak. Skip when CPUID has no shadow stack or when user SHSTK cannot
be enabled. The child exits with a raw syscall because it resumes on
an empty shadow stack.
The recursive call goes through an indirect call. A visible recursive
body is optimized into a closed form and does not execute ret.
Assisted-by: LLM
Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
---
tools/testing/selftests/x86/Makefile | 3 +-
tools/testing/selftests/x86/shstk_clone_tls.c | 334 ++++++++++++++++++
2 files changed, 336 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/x86/shstk_clone_tls.c
diff --git a/tools/testing/selftests/x86/Makefile b/tools/testing/selftests/x86/Makefile
index d478b13cc8d5..4f8a771b0839 100644
--- a/tools/testing/selftests/x86/Makefile
+++ b/tools/testing/selftests/x86/Makefile
@@ -19,7 +19,8 @@ TARGETS_C_32BIT_ONLY := entry_from_vm86 test_syscall_vdso unwind_vdso \
test_FCMOV test_FCOMI test_FISTTP \
vdso_restorer
TARGETS_C_64BIT_ONLY := fsgsbase sysret_rip syscall_numbering \
- corrupt_xstate_header amx lam test_shadow_stack avx apx
+ corrupt_xstate_header amx lam test_shadow_stack avx apx \
+ shstk_clone_tls
# Some selftests require 32bit support enabled also on 64bit systems
TARGETS_C_32BIT_NEEDED := ldt_gdt ptrace_syscall
diff --git a/tools/testing/selftests/x86/shstk_clone_tls.c b/tools/testing/selftests/x86/shstk_clone_tls.c
new file mode 100644
index 000000000000..5c9b5709b440
--- /dev/null
+++ b/tools/testing/selftests/x86/shstk_clone_tls.c
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * clone3(CLONE_VM|CLONE_SETTLS) with an out-of-range TLS must not leak a
+ * shadow-stack VMA. copy_thread() maps one before set_new_tls() can fail,
+ * and copy_process() does not call exit_thread() on that failure.
+ *
+ * KTAP via kselftest.h. Capability / clone3 skips run before
+ * ksft_set_plan(). After ARCH_SHSTK_ENABLE, exit only through ksft_exit_*
+ * (do not return from main).
+ *
+ * Relies on /proc/self/smaps (CONFIG_PROC_PAGE_MONITOR).
+ */
+#define _GNU_SOURCE
+
+#include <errno.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"
+
+#ifndef __NR_clone3
+#define __NR_clone3 435
+#endif
+
+#ifndef ARCH_SHSTK_ENABLE
+#define ARCH_SHSTK_ENABLE 0x5001
+#endif
+#ifndef ARCH_SHSTK_STATUS
+#define ARCH_SHSTK_STATUS 0x5005
+#endif
+#ifndef ARCH_SHSTK_SHSTK
+#define ARCH_SHSTK_SHSTK 1ULL
+#endif
+
+#define NCLONE 16
+#define STACK_SIZE (64 * 1024)
+#define CPUID_SHSTK 0x80 /* CPUID.(EAX=07H,ECX=0):ECX[7] */
+
+/*
+ * Inlined arch_prctl — same constraint as test_shadow_stack.c: after
+ * SHSTK is enabled, a libc syscall wrapper return has no shadow token.
+ */
+#define ARCH_PRCTL(arg1, arg2) \
+({ \
+ long _ret; \
+ register long _num asm("eax") = __NR_arch_prctl; \
+ register long _arg1 asm("rdi") = (long)(arg1); \
+ register long _arg2 asm("rsi") = (long)(arg2); \
+ \
+ asm volatile ( \
+ "syscall\n" \
+ : "=a"(_ret) \
+ : "r"(_arg1), "r"(_arg2), \
+ "0"(_num) \
+ : "rcx", "r11", "memory", "cc" \
+ ); \
+ _ret; \
+})
+
+static __always_inline unsigned long get_ssp(void)
+{
+ unsigned long ret = 0;
+
+ /* RDSSPQ is a NOP when SHSTK is off; keep destination cleared. */
+ asm volatile("xor %0, %0; rdsspq %0" : "=r" (ret));
+ return ret;
+}
+
+static int cpu_has_shstk(void)
+{
+ unsigned int eax, ebx, ecx, edx;
+
+ asm volatile("cpuid"
+ : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx)
+ : "a" (7), "c" (0));
+ return ecx & CPUID_SHSTK;
+}
+
+static void on_fault(int sig)
+{
+ (void)sig;
+ /* Async-signal-safe path; do not call into libc printf. */
+ _exit(KSFT_FAIL);
+}
+
+struct ss_span {
+ unsigned long start;
+ unsigned long end;
+};
+
+static FILE *open_smaps(void)
+{
+ FILE *f = fopen("/proc/self/smaps", "r");
+
+ if (f)
+ return f;
+ if (errno == ENOENT)
+ ksft_exit_skip("/proc/self/smaps unavailable\n");
+ ksft_exit_fail_msg("fopen /proc/self/smaps: %s\n", strerror(errno));
+}
+
+/*
+ * Count ss VMAs; optionally resolve SSP → span and check a known span.
+ * Empty stacks leave SSP at the exclusive end.
+ *
+ * Each address-range header must be followed by VmFlags before the next
+ * range (or EOF). Truncated lines and a dangling range are FAIL — silent
+ * skips would under-count leaked shadow stacks.
+ */
+static void ss_scan(int *vmas, unsigned long ssp, struct ss_span *hit,
+ const struct ss_span *want, int *kept)
+{
+ FILE *f = open_smaps();
+ char line[256];
+ unsigned long start = 0, end = 0;
+ int saw_range = 0;
+
+ *vmas = 0;
+ if (hit) {
+ hit->start = 0;
+ hit->end = 0;
+ }
+ if (kept)
+ *kept = 0;
+
+ while (fgets(line, sizeof(line), f)) {
+ unsigned long a, b;
+ size_t len = strlen(line);
+
+ if (len + 1 == sizeof(line) && line[len - 1] != '\n') {
+ fclose(f);
+ ksft_exit_fail_msg("smaps line truncated\n");
+ }
+
+ if (sscanf(line, "%lx-%lx", &a, &b) == 2) {
+ if (saw_range) {
+ fclose(f);
+ ksft_exit_fail_msg(
+ "smaps range %lx-%lx missing VmFlags\n",
+ start, end);
+ }
+ start = a;
+ end = b;
+ saw_range = 1;
+ continue;
+ }
+ if (strncmp(line, "VmFlags:", 8))
+ continue;
+ if (!saw_range) {
+ fclose(f);
+ ksft_exit_fail_msg("smaps VmFlags without range\n");
+ }
+ saw_range = 0;
+ if (!strstr(line, " ss"))
+ continue;
+
+ (*vmas)++;
+ if (hit && ((ssp >= start && ssp < end) || ssp == end)) {
+ hit->start = start;
+ hit->end = end;
+ }
+ if (kept && want && start == want->start && end == want->end)
+ *kept = 1;
+ }
+ if (ferror(f)) {
+ fclose(f);
+ ksft_exit_fail_msg("read /proc/self/smaps: %s\n",
+ strerror(errno));
+ }
+ if (saw_range) {
+ fclose(f);
+ ksft_exit_fail_msg("smaps range %lx-%lx missing VmFlags\n",
+ start, end);
+ }
+ fclose(f);
+}
+
+/*
+ * Returns 0 if the child exited 0, -errno if clone3 failed, 1 otherwise.
+ * Child uses a raw exit: it resumes on an empty shadow stack.
+ */
+static int clone_tls(void *stack, size_t stack_size, unsigned long tls)
+{
+ struct clone_args args = {
+ .flags = CLONE_VM | CLONE_SETTLS,
+ .exit_signal = SIGCHLD,
+ .stack = (uint64_t)(uintptr_t)stack,
+ .stack_size = stack_size,
+ .tls = tls,
+ };
+ long ret;
+ int st;
+
+ asm volatile("syscall"
+ : "=a" (ret)
+ : "0" (__NR_clone3),
+ "D" (&args),
+ "S" (sizeof(args))
+ : "rcx", "r11", "memory");
+ if (ret == 0) {
+ asm volatile("syscall"
+ :
+ : "a" (__NR_exit),
+ "D" (0)
+ : "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;
+}
+
+/*
+ * Indirect call so GCC cannot fold recursion into n+1 (call *%rax + ret).
+ */
+static int depth(int n);
+static int (*depth_fn)(int n) = 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 void enable_shstk(void)
+{
+ long rc;
+ unsigned long status = 0;
+
+ /*
+ * RDSSPQ leaves its destination unchanged when shadow stacks are
+ * disabled. Enable SHSTK before inspecting the SSP.
+ */
+ rc = ARCH_PRCTL(ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK);
+ if (rc == -EPERM) {
+ if (ARCH_PRCTL(ARCH_SHSTK_STATUS, &status) ||
+ !(status & ARCH_SHSTK_SHSTK))
+ ksft_exit_skip("user shadow stack is locked\n");
+ return;
+ }
+ if (rc == -EOPNOTSUPP || rc == -EINVAL)
+ ksft_exit_skip("user shadow stack unavailable: %ld\n", rc);
+ if (rc)
+ ksft_exit_fail_msg("ARCH_SHSTK_ENABLE: %ld\n", rc);
+}
+
+int main(void)
+{
+ struct sigaction sa = { 0 };
+ struct ss_span parent;
+ void *stack;
+ unsigned long ssp, ssp_mid;
+ int vmas0, vmas1;
+ int i, kept, d, cret, n_eperm = 0;
+
+ ksft_print_header();
+
+ if (!cpu_has_shstk())
+ ksft_exit_skip("CPUID has no shadow stack\n");
+
+ 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));
+
+ enable_shstk();
+ if (!get_ssp())
+ ksft_exit_fail_msg("enable left SSP at 0\n");
+
+ /*
+ * Positive control before the plan: SKIP only when clone3 is
+ * unavailable or denied; unexpected errno is reported as a failed
+ * test after ksft_set_plan().
+ */
+ cret = clone_tls(stack, STACK_SIZE, 0);
+ if (cret == -ENOSYS || cret == -EPERM || cret == -EACCES)
+ ksft_exit_skip("clone3 valid TLS unavailable: %s\n",
+ strerror(-cret));
+
+ ksft_set_plan(3);
+
+ ksft_test_result(cret == 0,
+ "clone3 valid TLS (rc=%d)\n", cret);
+
+ sa.sa_handler = on_fault;
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGBUS, &sa, NULL);
+
+ ssp = get_ssp();
+ ss_scan(&vmas0, ssp, &parent, NULL, NULL);
+ if (!parent.start)
+ ksft_exit_fail_msg("SSP %lx matches no shadow-stack VMA\n",
+ ssp);
+
+ for (i = 0; i < NCLONE; i++) {
+ cret = clone_tls(stack, STACK_SIZE, (unsigned long)-1);
+ if (cret == -EPERM)
+ n_eperm++;
+ }
+ ksft_test_result(n_eperm == NCLONE,
+ "clone3 bad TLS returns -EPERM x%d (got %d)\n",
+ NCLONE, n_eperm);
+
+ ss_scan(&vmas1, 0, NULL, &parent, &kept);
+ ssp_mid = get_ssp();
+ d = (kept && ssp_mid) ? depth(16) : -1;
+ ssp = get_ssp();
+
+ ksft_print_msg("ss_vmas=%d->%d parent=%lx-%lx kept=%d depth=%d ssp=%lx\n",
+ vmas0, vmas1, parent.start, parent.end, kept, d, ssp);
+
+ ksft_test_result(vmas1 == vmas0 && kept && d == 17 && ssp_mid && ssp,
+ "no shadow-stack VMA leak; parent stack works (%d->%d)\n",
+ vmas0, vmas1);
+
+ ksft_finished();
+}
--
2.43.0
prev parent reply other threads:[~2026-10-22 22:17 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 2:31 [PATCH 0/2] x86/shstk: don't leak a shadow stack when clone TLS fails Iván Ezequiel Rodriguez
2026-10-15 2:31 ` [PATCH 1/2] x86/shstk: free shadow stack when copy_thread() fails Iván Ezequiel Rodriguez
2026-10-21 19:26 ` Edgecombe, Rick P
2026-10-15 2:31 ` [PATCH 2/2] selftests/x86: shadow stack must survive a bad TLS Iván Ezequiel Rodriguez
2026-10-22 22:17 ` [PATCH v2 0/2] x86/shstk: don't leak a shadow stack when clone TLS fails Iván Ezequiel Rodriguez
2026-10-22 22:17 ` [PATCH v2 1/2] x86/shstk: free shadow stack when copy_thread() fails Iván Ezequiel Rodriguez
2026-10-22 22:17 ` Iván Ezequiel Rodriguez [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261022221722.619233-3-ivanrwcm25@gmail.com \
--to=ivanrwcm25@gmail.com \
--cc=bp@alien8.de \
--cc=brauner@kernel.org \
--cc=dave.hansen@linux.intel.com \
--cc=hpa@zytor.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=mingo@redhat.com \
--cc=rick.p.edgecombe@intel.com \
--cc=shuah@kernel.org \
--cc=tglx@kernel.org \
--cc=x86@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®