mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


      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®