* [PATCH 0/2] x86/shstk: don't leak a shadow stack when clone TLS fails
@ 2026-10-15 2:31 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
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Iván Ezequiel Rodriguez @ 2026-10-15 2:31 UTC (permalink / raw)
To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86
Cc: H. Peter Anvin, Shuah Khan, Christian Brauner, Nikolay Borisov,
Li RongQing, Sohil Mehta, HyeongJun An, Rick Edgecombe,
linux-kernel, linux-kselftest
copy_thread() maps a shadow stack and set_new_tls() can still fail.
That error goes to bad_fork_cleanup_io and skips exit_thread().
bad_fork_cleanup_thread calls exit_thread() only after copy_thread()
has returned 0.
commit 748c90c69336 ("x86/shstk: Remove useless clone error handling")
removed the shstk_free() on the set_new_tls() failure. Restoring it
only there puts back the single free. The allocation-error return
still aliases the parent, and the exit_thread() path is left alone,
so the double unmap that commit avoided does not come back.
The selftest assumes /proc is mounted. A clone3() with a valid TLS
must succeed before EPERM counts as the TLS failure. No CPUID support,
no kernel support, or a locked feature skips the test. Any other
ARCH_SHSTK_ENABLE error fails it.
Sixteen clone3() calls with CLONE_VM|CLONE_SETTLS and an out-of-range
TLS leaked 16 shadow-stack VMAs without the fix. With it, the count
stayed unchanged and call/return on the parent still used the
original shadow stack. Tested with user shadow stack on the host and
in a one-vCPU KVM guest (-cpu host), same configuration with and
without the fix.
The diagnosis and the selftest were drafted with Grok 4.7.
Iván Ezequiel Rodriguez (2):
x86/shstk: free shadow stack when copy_thread() fails
selftests/x86: shadow stack must survive a bad TLS
arch/x86/kernel/process.c | 12 +-
tools/testing/selftests/x86/Makefile | 3 +-
tools/testing/selftests/x86/shstk_clone_tls.c | 364 ++++++++++++++++++
3 files changed, 376 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/x86/shstk_clone_tls.c
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH 1/2] x86/shstk: free shadow stack when copy_thread() fails 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 ` 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 2 siblings, 1 reply; 7+ messages in thread From: Iván Ezequiel Rodriguez @ 2026-10-15 2:31 UTC (permalink / raw) To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86 Cc: H. Peter Anvin, Shuah Khan, Christian Brauner, Nikolay Borisov, Li RongQing, Sohil Mehta, HyeongJun An, Rick Edgecombe, linux-kernel, linux-kselftest copy_thread() maps a shadow stack for a CLONE_VM child and can still fail in set_new_tls(). That error returns through bad_fork_cleanup_io, which does not call exit_thread(). bad_fork_cleanup_thread does, but only after copy_thread() has returned 0. commit 748c90c69336 ("x86/shstk: Remove useless clone error handling") removed the shstk_free() on this path, assuming exit_thread() would run. It does not. Each rejected clone leaves one shadow-stack VMA in the parent's mm until the mm dies: one map_count slot, pages not populated. The size follows the clone stack, or RLIMIT_STACK when that size is zero. Free only after set_new_tls() fails. The allocation error returns earlier, while the child's base still aliases the parent. shstk_free() ignores a cleared base and a different mm, so CLONE_VFORK and !CLONE_VM stay no-ops. The exit_thread() path is left alone, so the double unmap that commit avoided does not come back. The io bitmap is still shared only when TLS setup succeeds. Tested with user shadow stack enabled, on one config with and without this change. Sixteen clone3() calls with CLONE_VM|CLONE_SETTLS and an out-of-range TLS leaked 16 shadow-stack VMAs without the fix. With it, the VMA count was unchanged and call/return on the parent still used the original shadow stack. Fixes: 748c90c69336 ("x86/shstk: Remove useless clone error handling") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com> --- arch/x86/kernel/process.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c index 346c438ac880..be840652d055 100644 --- a/arch/x86/kernel/process.c +++ b/arch/x86/kernel/process.c @@ -265,11 +265,19 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) /* Set a new TLS for the child thread? */ if (clone_flags & CLONE_SETTLS) ret = set_new_tls(p, tls); + if (ret) { + /* + * copy_process() skips exit_thread() when copy_thread() + * fails. Free the shadow stack mapped before this error. + */ + shstk_free(p); + return ret; + } - if (!ret && unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) + if (unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) io_bitmap_share(p); - return ret; + return 0; } static void pkru_flush_thread(void) -- 2.43.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 1/2] x86/shstk: free shadow stack when copy_thread() fails 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 0 siblings, 0 replies; 7+ messages in thread From: Edgecombe, Rick P @ 2026-10-21 19:26 UTC (permalink / raw) To: x86, ivanrwcm25, tglx, mingo, bp, dave.hansen Cc: brauner, sammiee5311, lirongqing, hpa, linux-kernel, shuah, Mehta, Sohil, nik.borisov, linux-kselftest By inspection the bug looks real to me. On Wed, 2026-10-14 at 23:31 -0300, Iván Ezequiel Rodriguez wrote: > diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c > index 346c438ac880..be840652d055 100644 > --- a/arch/x86/kernel/process.c > +++ b/arch/x86/kernel/process.c > @@ -265,11 +265,19 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) > /* Set a new TLS for the child thread? */ > if (clone_flags & CLONE_SETTLS) > ret = set_new_tls(p, tls); > + if (ret) { Having the error check happen even if !CLONE_SETTLS makes it not clear that this condition could only happen that way. I'd maybe nest the check inside, like is done in several cases in in copy_process(). Like (untested): diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c index 346c438ac8801..e40f83a02fdb1 100644 --- a/arch/x86/kernel/process.c +++ b/arch/x86/kernel/process.c @@ -176,7 +176,7 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) struct fork_frame *fork_frame; struct pt_regs *childregs; unsigned long new_ssp; - int ret = 0; + int ret; childregs = task_pt_regs(p); fork_frame = container_of(childregs, struct fork_frame, regs); @@ -263,13 +263,19 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) } /* Set a new TLS for the child thread? */ - if (clone_flags & CLONE_SETTLS) + if (clone_flags & CLONE_SETTLS) { ret = set_new_tls(p, tls); + if (ret) + goto shstk_free; + } - if (!ret && unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) + if (unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) io_bitmap_share(p); return ret; +shstk_free: + shstk_free(p); + return ret; } static void pkru_flush_thread(void) > + /* > + * copy_process() skips exit_thread() when copy_thread() > + * fails. Free the shadow stack mapped before this error. > + */ I'm not in favor of this comment. It is describing behavior far away. And the idea that you cleanup what you allocated in on a later error condition is a pretty common pattern. I tried to write something and nothing seems non-obvious. Maybe just drop it. > + shstk_free(p); > + return ret; > + } > > - if (!ret && unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) > + if (unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) > io_bitmap_share(p); > > - return ret; > + return 0; > } ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/2] selftests/x86: shadow stack must survive a bad TLS 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-15 2:31 ` 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 2 siblings, 0 replies; 7+ messages in thread From: Iván Ezequiel Rodriguez @ 2026-10-15 2:31 UTC (permalink / raw) To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86 Cc: H. Peter Anvin, Shuah Khan, Christian Brauner, Nikolay Borisov, Li RongQing, Sohil Mehta, HyeongJun An, Rick Edgecombe, linux-kernel, linux-kselftest 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. Enable user shadow stack, reject 16 such clones, and require the shadow-stack VMA count to stay unchanged. The original mapping must remain, and a call/return on that stack must succeed, so the cleanup cannot be unmapping the parent's shadow stack. A clone3 with the same flags and a valid TLS has to succeed first. EPERM from the rejected calls is not evidence on its own: a policy can reject every clone3(). The child exits with a raw syscall because it resumes on an empty shadow stack. Skip when CPUID has no shadow stack, when the kernel does not provide the feature, or when it is locked. Any other ARCH_SHSTK_ENABLE error fails the test. The test expects /proc already mounted, as in a normal kselftest run. 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 | 364 ++++++++++++++++++ 2 files changed, 366 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..6b2f1001b084 --- /dev/null +++ b/tools/testing/selftests/x86/shstk_clone_tls.c @@ -0,0 +1,364 @@ +// 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. + * + * Pass: shadow-stack VMA count is unchanged, the original mapping is + * still there, and a call/return on this thread still works. + * Fail: one new shadow-stack VMA per rejected clone. + * + * Enable shadow stack with an inlined syscall. A normal function return + * across that point has no address on the shadow stack, so main() must + * not return after a successful enable. + */ +#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> + +#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 /* bit 0 */ +#endif + +/* Same value as KSFT_SKIP in kselftest.h. */ +#define KSFT_SKIP 4 + +#define NCLONE 16 +#define STACK_SIZE (64 * 1024) + +/* CPUID.(EAX=07H, ECX=0):ECX[7], CET shadow stack. */ +#define CPUID_SHSTK 0x80 + +#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; \ +}) + +#define get_ssp() \ +({ \ + unsigned long _ssp = 0; \ + \ + asm volatile("rdsspq %0" : "+r" (_ssp)); \ + _ssp; \ +}) + +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) +{ + static const char msg[] = "[FAIL]\tparent shadow stack faulted\n"; + + (void)sig; + if (write(2, msg, sizeof(msg) - 1) < 0) + _exit(2); + _exit(2); +} + +struct ss_span { + unsigned long start; + unsigned long end; +}; + +static void ss_scan(int *vmas, int *kb, unsigned long addr, struct ss_span *hit, + const struct ss_span *want, int *kept) +{ + FILE *f; + char line[256]; + unsigned long start = 0, end = 0; + int size = 0; + int saw_size = 0; + + *vmas = 0; + *kb = 0; + if (hit) { + hit->start = 0; + hit->end = 0; + } + if (kept) + *kept = 0; + f = fopen("/proc/self/smaps", "r"); + if (!f) { + perror("smaps"); + _exit(1); + } + while (fgets(line, sizeof(line), f)) { + unsigned long a, b; + + if (sscanf(line, "%lx-%lx", &a, &b) == 2) { + start = a; + end = b; + saw_size = 0; + continue; + } + if (sscanf(line, "Size: %d kB", &size) == 1) { + saw_size = 1; + continue; + } + if (strncmp(line, "VmFlags:", 8) || !saw_size) + continue; + if (!strstr(line, " ss")) { + saw_size = 0; + continue; + } + (*vmas)++; + *kb += size; + /* Ordinary membership is [start, end). */ + if (hit && addr >= start && addr < end) { + hit->start = start; + hit->end = end; + } + /* + * An empty shadow stack leaves SSP at the exclusive end. + * That address is not inside [start, end). + */ + if (hit && addr == end) { + hit->start = start; + hit->end = end; + } + if (kept && want && start == want->start && end == want->end) + *kept = 1; + saw_size = 0; + } + fclose(f); +} + +/* + * clone3 carries tls in the struct. 0 is a valid FS base. -1 is rejected + * by ARCH_SET_FS before any FS write. No CLONE_THREAD: a created child is + * a separate process and _exit() does not tear this one down. + * + * Returns 0 if the child exited 0, -errno if clone3 failed, 1 otherwise. + */ +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; + + /* + * Inline syscall. The child resumes here on an empty shadow stack, + * so it must not return into the libc wrapper. + */ + 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; +} + +/* + * GCC 13 turns a visible recursive body into "return n + 1". Hide the + * callee in an asm output so each level stays a real indirect call/ret. + */ +static int depth(int n); +static int (*depth_fn)(int) = 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 int skip_enable(long rc) +{ + if (rc == -EOPNOTSUPP) { + printf("[SKIP]\tkernel has no user shadow stack\n"); + } else if (rc == -EINVAL) { + printf("[SKIP]\tkernel built without user shadow stack\n"); + } else if (rc == -EPERM) { + printf("[SKIP]\tuser shadow stack is locked\n"); + } else { + printf("[FAIL]\tARCH_SHSTK_ENABLE returned %ld\n", rc); + return 1; + } + return KSFT_SKIP; +} + +int main(void) +{ + struct sigaction sa; + struct ss_span parent; + void *stack; + unsigned long ssp, ssp_mid; + long rc; + int vmas0, kb0, vmas1, kb1; + int i, kept, d, cret; + + setbuf(stdout, NULL); + + if (!cpu_has_shstk()) { + printf("[SKIP]\tCPUID has no shadow stack\n"); + return KSFT_SKIP; + } + + stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) { + perror("mmap"); + return 1; + } + + /* + * rdsspq #UDs while CET is off, so enable before the first read. + * Already enabled is a no-op. Locked-and-enabled returns EPERM + * before that no-op; ARCH_SHSTK_STATUS tells the two apart. + */ + rc = ARCH_PRCTL(ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK); + if (rc == -EPERM) { + unsigned long status = 0; + + if (ARCH_PRCTL(ARCH_SHSTK_STATUS, &status) || + !(status & ARCH_SHSTK_SHSTK)) + return skip_enable(rc); + } else if (rc) { + return skip_enable(rc); + } + if (!get_ssp()) { + printf("[FAIL]\tenable left SSP at 0\n"); + _exit(1); + } + + /* + * Same flags as the rejected calls, with a TLS that set_new_tls() + * accepts. EPERM from the rejected calls is only meaningful if this + * one is allowed: a policy can reject every clone3(). + */ + cret = clone_tls(stack, STACK_SIZE, 0); + if (cret < 0) { + printf("[SKIP]\tclone3 with a valid TLS failed: %s\n", + strerror(-cret)); + _exit(KSFT_SKIP); + } + if (cret) { + printf("[FAIL]\tclone3 with a valid TLS did not exit 0\n"); + _exit(1); + } + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_fault; + sigaction(SIGSEGV, &sa, NULL); + sigaction(SIGBUS, &sa, NULL); + + ssp = get_ssp(); + ss_scan(&vmas0, &kb0, ssp, &parent, NULL, NULL); + if (!parent.start) { + printf("[FAIL]\tSSP %lx matches no shadow-stack VMA\n", ssp); + _exit(1); + } + + for (i = 0; i < NCLONE; i++) { + cret = clone_tls(stack, STACK_SIZE, (unsigned long)-1); + if (cret == -EPERM) + continue; + if (cret < 0) + printf("[FAIL]\tbad TLS errno %d, want EPERM\n", -cret); + else + printf("[FAIL]\tbad TLS was accepted\n"); + _exit(1); + } + + ss_scan(&vmas1, &kb1, 0, NULL, &parent, &kept); + ssp_mid = get_ssp(); + d = -1; + if (kept && ssp_mid) + d = depth(16); + ssp = get_ssp(); + + printf("ss_vmas=%d->%d delta=%d ss_kb=%d->%d\n", + vmas0, vmas1, vmas1 - vmas0, kb0, kb1); + printf("parent=%lx-%lx kept=%d ssp_before_depth=%lx depth=%d ssp=%lx\n", + parent.start, parent.end, kept, ssp_mid, d, ssp); + + if (vmas1 != vmas0 || kb1 != kb0) { + printf("[FAIL]\tleaked %d shadow-stack VMA(s), %d kB\n", + vmas1 - vmas0, kb1 - kb0); + _exit(1); + } + if (!kept) { + printf("[FAIL]\tparent shadow stack %lx-%lx was unmapped\n", + parent.start, parent.end); + _exit(2); + } + if (!ssp_mid) { + printf("[FAIL]\tparent SSP was 0 before call/return\n"); + _exit(2); + } + if (d != 17) { + printf("[FAIL]\tparent call/return returned %d, want 17\n", d); + _exit(2); + } + if (!ssp) { + printf("[FAIL]\tparent SSP is 0 after call/return\n"); + _exit(2); + } + + printf("[OK]\tno shadow-stack VMA leak; parent stack still works\n"); + _exit(0); +} -- 2.43.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v2 0/2] x86/shstk: don't leak a shadow stack when clone TLS fails 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-15 2:31 ` [PATCH 2/2] selftests/x86: shadow stack must survive a bad TLS Iván Ezequiel Rodriguez @ 2026-10-22 22:17 ` 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 ` [PATCH v2 2/2] selftests/x86: shadow stack must survive a bad TLS Iván Ezequiel Rodriguez 2 siblings, 2 replies; 7+ messages in thread From: Iván Ezequiel Rodriguez @ 2026-10-22 22:17 UTC (permalink / raw) To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86 Cc: H . Peter Anvin, Shuah Khan, Christian Brauner, Rick Edgecombe, linux-kernel, linux-kselftest, Iván Ezequiel Rodriguez copy_thread() maps a shadow stack and set_new_tls() can still fail. That error goes to bad_fork_cleanup_io and skips exit_thread(). bad_fork_cleanup_thread calls exit_thread() only after copy_thread() has returned 0. commit 748c90c69336 ("x86/shstk: Remove useless clone error handling") removed the shstk_free() on the set_new_tls() failure. Restoring it only there puts back the single free. The allocation-error return still aliases the parent, and the exit_thread() path is left alone, so the double unmap that commit avoided does not come back. The selftest uses kselftest.h / KTAP. Feature and clone3 availability skips run before ksft_set_plan(). Missing /proc/self/smaps is SKIP. Read errors, truncated smaps lines, and a VMA range without VmFlags are FAIL. A valid-TLS clone3 must succeed before EPERM on a bad TLS counts as the leak path; ENOSYS/EPERM/EACCES SKIP that control. Regression (CONFIG_X86_USER_SHADOW_STACK=y, host and one-vCPU KVM -cpu host,migratable=off): Scenario Without fix With fix 16 bad-TLS clone3 +16 ss VMAs +0 ss VMAs Parent shadow stack kept + call/ret OK kept + call/ret OK KTAP 2/3 (test 3 fail) 3/3 pass Changes in v2: - Nest the TLS error check under CLONE_SETTLS, as suggested by Rick Edgecombe. - Use a dedicated shadow-stack cleanup label. - Return zero explicitly on the successful path. - Remove the redundant cleanup comment. - Selftest: rewrite on kselftest.h / KTAP (plan 3). Fix RDSSPQ comment; align get_ssp() with test_shadow_stack; SKIP only ENOSYS/EPERM/EACCES on the valid-TLS control; FAIL on truncated or incomplete smaps records that would under-count ss VMAs. Link: https://lore.kernel.org/lkml/20261015023147.519257-1-ivanrwcm25@gmail.com/ Iván Ezequiel Rodriguez (2): x86/shstk: free shadow stack when copy_thread() fails selftests/x86: shadow stack must survive a bad TLS arch/x86/kernel/process.c | 13 +- tools/testing/selftests/x86/Makefile | 3 +- tools/testing/selftests/x86/shstk_clone_tls.c | 334 ++++++++++++++++++ 3 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 tools/testing/selftests/x86/shstk_clone_tls.c -- 2.43.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v2 1/2] x86/shstk: free shadow stack when copy_thread() fails 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 ` Iván Ezequiel Rodriguez 2026-10-22 22:17 ` [PATCH v2 2/2] selftests/x86: shadow stack must survive a bad TLS Iván Ezequiel Rodriguez 1 sibling, 0 replies; 7+ messages in thread From: Iván Ezequiel Rodriguez @ 2026-10-22 22:17 UTC (permalink / raw) To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86 Cc: H . Peter Anvin, Shuah Khan, Christian Brauner, Rick Edgecombe, linux-kernel, linux-kselftest, Iván Ezequiel Rodriguez, stable copy_thread() maps a shadow stack for a CLONE_VM child and can still fail in set_new_tls(). That error returns through bad_fork_cleanup_io, which does not call exit_thread(). bad_fork_cleanup_thread does, but only after copy_thread() has returned 0. commit 748c90c69336 ("x86/shstk: Remove useless clone error handling") removed the shstk_free() on this path, assuming exit_thread() would run. It does not. Each rejected clone leaves one shadow-stack VMA in the parent's mm until the mm dies: one map_count slot, pages not populated. The size follows the clone stack, or RLIMIT_STACK when that size is zero. Free only after set_new_tls() fails, via a dedicated cleanup label. Nest the TLS error check under CLONE_SETTLS so the successful path returns 0 explicitly. The allocation error returns earlier, while the child's base still aliases the parent. shstk_free() ignores a cleared base and a different mm, so CLONE_VFORK and !CLONE_VM stay no-ops. The exit_thread() path is left alone, so the double unmap that commit avoided does not come back. The io bitmap is still shared only when TLS setup succeeds. Tested with user shadow stack enabled, on one config with and without this change. Sixteen clone3() calls with CLONE_VM|CLONE_SETTLS and an out-of-range TLS leaked 16 shadow-stack VMAs without the fix. With it, the VMA count was unchanged and call/return on the parent still used the original shadow stack. Fixes: 748c90c69336 ("x86/shstk: Remove useless clone error handling") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com> --- arch/x86/kernel/process.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c index 346c438ac880..451594fa5481 100644 --- a/arch/x86/kernel/process.c +++ b/arch/x86/kernel/process.c @@ -176,7 +176,7 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) struct fork_frame *fork_frame; struct pt_regs *childregs; unsigned long new_ssp; - int ret = 0; + int ret; childregs = task_pt_regs(p); fork_frame = container_of(childregs, struct fork_frame, regs); @@ -263,12 +263,19 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) } /* Set a new TLS for the child thread? */ - if (clone_flags & CLONE_SETTLS) + if (clone_flags & CLONE_SETTLS) { ret = set_new_tls(p, tls); + if (ret) + goto shstk_free; + } - if (!ret && unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) + if (unlikely(test_tsk_thread_flag(current, TIF_IO_BITMAP))) io_bitmap_share(p); + return 0; + +shstk_free: + shstk_free(p); return ret; } -- 2.43.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v2 2/2] selftests/x86: shadow stack must survive a bad TLS 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 1 sibling, 0 replies; 7+ messages in thread From: Iván Ezequiel Rodriguez @ 2026-10-22 22:17 UTC (permalink / raw) To: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86 Cc: H . Peter Anvin, Shuah Khan, Christian Brauner, Rick Edgecombe, linux-kernel, linux-kselftest, Iván Ezequiel Rodriguez 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 ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-10-22 22:17 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 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 ` [PATCH v2 2/2] selftests/x86: shadow stack must survive a bad TLS Iván Ezequiel Rodriguez
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®