[RFC][PATCH 17/24] x86/task: Context switch the kernel shadow stack register value
From: Dave Hansen
Date: Wed Oct 21 2026 - 15:53:32 EST
This is the spot where things go off the rails. As I look at this, I
think it may be a mistake to permit a shadow-stack kernel to have non
shadow-stack tasks. It would just be too easy to leverage turn the
ability to set kshstk_ssp=0 into a control flow hijack.
This is also *not* the way the architecture intended context switching
to go. Feedback here would be much appreciated.
I'll continue on describing what is here.
The basic problem set:
1. SSP needs to be context-switched just like RSP:
1a. The kernel SSP value is discrete from the user SSP value
1b. The kernel SSP value needs storage space
2. At *least* the init task does not have a shadow stack mapping. The
expectation is that there will be other tasks. For instance, a
"permissive" mode after a shadow stack violation where the kernel
warns and keeps on running.
== Solution ==
First, establish and maintain kernel SSP storage at:
thread_struct->kshstk_ssp
A 0 value means that the task does not use kernel shadow stacks.
Second, drive MSR_IA32_S_CET from that value. If it is 0, then
clear CET_SHSTK_EN|CET_WRSS_EN during the context switch. If it
is non-zero set those bits.
Third, save and restore the kernel SSP value. This is substantially
more complicated than RSP switching because SSP is not directly
accessible and needs to be loaded with RSTORSSP, which is picky.
It's not picky for no reason. It is *intentionally* picky to foil
stack pivoting attacks. Before it will write SSP, it requires an
in-memory "restore token" be present on the shadow stack.
Just like the regular stack, this switch *must* be done in
assembly.
== Optimizations ==
<<<
These are probably all silly to talk about at this point if a
over over to the more correct RSTORSSP/SAVEPREVSSP is made.
>>>
First, TASK_thread_kshstk_ssp() will always be 0 when kernel shadow
stacks are disabled. This could be ALTERNATIVE()'d out to a plain
jump without the MOV and TEST.
Second, the ENABLE/DISABLE macros could avoid the WRMSR when it is
not changing values.
It might be possible to move the assembly logic like the
MSR_IA32_S_CET or restore token calculations out to C.
---
b/arch/x86/entry/entry_64.S | 87 +++++++++++++++++++++++++++++++++++++
b/arch/x86/include/asm/processor.h | 4 +
b/arch/x86/include/asm/shstk.h | 3 +
b/arch/x86/kernel/asm-offsets.c | 3 +
4 files changed, 97 insertions(+)
diff -puN arch/x86/entry/entry_64.S~per-task-ssp-storage arch/x86/entry/entry_64.S
--- a/arch/x86/entry/entry_64.S~per-task-ssp-storage 2026-10-21 12:36:40.914886779 -0700
+++ b/arch/x86/entry/entry_64.S 2026-10-21 12:36:40.948886844 -0700
@@ -34,6 +34,7 @@
#include <asm/percpu.h>
#include <asm/asm.h>
#include <asm/smap.h>
+#include <asm/shstk.h>
#include <asm/pgtable_types.h>
#include <asm/frame.h>
#include <asm/trapnr.h>
@@ -169,6 +170,87 @@ SYM_INNER_LABEL(entry_SYSRETQ_end, SYM_L
int3
SYM_CODE_END(entry_SYSCALL_64)
+.macro SAVE_KERNEL_SHADOW_STACK
+#ifdef CONFIG_X86_KERN_SHADOW_STACK
+ /* kshstk_ssp==0 means no kernel shadow stack, skip handling */
+ cmpq $0, TASK_thread_kshstk_ssp(%rdi)
+ je .Lprev_no_shstk\@
+ rdsspq %rax
+ movq %rax, TASK_thread_kshstk_ssp(%rdi)
+.Lprev_no_shstk\@:
+#endif
+.endm
+
+.macro ENABLE_KERNEL_SHADOW_STACK
+ movl $MSR_IA32_S_CET, %ecx
+ rdmsr
+ orl $(CET_SHSTK_EN | CET_WRSS_EN), %eax
+ wrmsr
+.endm
+
+.macro DISABLE_KERNEL_SHADOW_STACK
+ movl $MSR_IA32_S_CET, %ecx
+ rdmsr
+ andl $~(CET_SHSTK_EN | CET_WRSS_EN), %eax
+ wrmsr
+.endm
+
+/*
+ * Clobbers rax, rcx, rdx, r8, r9. rdi/rsi must survive for the rest
+ * of __switch_to_asm.
+ *
+ * Dumb and slow. Always does WRMSR, regardless of if changes need to
+ * be made.
+ */
+.macro LOAD_KERNEL_SHADOW_STACK
+#ifdef CONFIG_X86_KERN_SHADOW_STACK
+ movq TASK_thread_kshstk_ssp(%rsi), %r8
+ testq %r8, %r8
+ jz .Lnext_no_shstk\@
+
+ ENABLE_KERNEL_SHADOW_STACK
+
+ /*
+ * This should be just a:
+ *
+ * rstorssp
+ * saveprevssp
+ *
+ * sequence. But, currently, the previous task might not
+ * have a valid SSP, like the init_task. This needs fixing.
+ * But, for now, just hack a Prev-SSP token in.
+ *
+ * The right fix is to give all tasks a valid SSP universally
+ * and then initialize the token once at task creation time,
+ * probably near the spot that ret_from_fork_asm is also
+ * written to the shadow stack.
+ */
+ leaq -8(%r8), %r9
+ movq %r8, %rax
+ /*
+ * Set type=0. Indicates a "Restore token" to satisfy RSTORSSP:
+ * Set mode=1. Indicates 64-bit mode.
+ * */
+ orq $SHSTK_TOKEN_MODE64, %rax
+ wrssq %rax, (%r9)
+
+ rstorssp (%r9)
+
+ /*
+ * Discard the Previous-SSP token left by RSTORSSP.
+ *
+ * This will go away once SAVEPREVSSP is being used properly.
+ */
+ movl $1, %eax
+ incsspq %rax
+ jmp .Lshstk_done\@
+
+.Lnext_no_shstk\@:
+ DISABLE_KERNEL_SHADOW_STACK
+.Lshstk_done\@:
+#endif
+.endm
+
/*
* %rdi: prev task
* %rsi: next task
@@ -191,6 +273,8 @@ SYM_FUNC_START(__switch_to_asm)
movq %rsp, TASK_threadsp(%rdi)
movq TASK_threadsp(%rsi), %rsp
+ SAVE_KERNEL_SHADOW_STACK
+
#ifdef CONFIG_STACKPROTECTOR
movq TASK_stack_canary(%rsi), %rbx
movq %rbx, PER_CPU_VAR(__stack_chk_guard)
@@ -205,6 +289,9 @@ SYM_FUNC_START(__switch_to_asm)
*/
FILL_RETURN_BUFFER %r12, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW
+ /* Note that this clobbers tons of stuff: */
+ LOAD_KERNEL_SHADOW_STACK
+
/* restore callee-saved registers */
popq %r15
popq %r14
diff -puN arch/x86/include/asm/processor.h~per-task-ssp-storage arch/x86/include/asm/processor.h
--- a/arch/x86/include/asm/processor.h~per-task-ssp-storage 2026-10-21 12:36:40.927886804 -0700
+++ b/arch/x86/include/asm/processor.h 2026-10-21 12:36:40.949886846 -0700
@@ -532,6 +532,10 @@ struct thread_struct {
struct thread_shstk shstk;
#endif
+#ifdef CONFIG_X86_KERN_SHADOW_STACK
+ /* Kernel shadow stack register: */
+ void *kshstk_ssp;
+#endif
};
#ifdef CONFIG_X86_DEBUG_FPU
diff -puN arch/x86/include/asm/shstk.h~per-task-ssp-storage arch/x86/include/asm/shstk.h
--- a/arch/x86/include/asm/shstk.h~per-task-ssp-storage 2026-10-21 12:36:40.943886835 -0700
+++ b/arch/x86/include/asm/shstk.h 2026-10-21 12:36:40.949886846 -0700
@@ -2,6 +2,9 @@
#ifndef _ASM_X86_SHSTK_H
#define _ASM_X86_SHSTK_H
+#define SHSTK_TOKEN_MODE64 BIT_ULL(0) /* Token created in 64-bit mode */
+#define SHSTK_TOKEN_TYPE BIT_ULL(1) /* Types: Restore: 0, Prev-SSP: 1 */
+
#ifndef __ASSEMBLER__
#include <linux/types.h>
diff -puN arch/x86/kernel/asm-offsets.c~per-task-ssp-storage arch/x86/kernel/asm-offsets.c
--- a/arch/x86/kernel/asm-offsets.c~per-task-ssp-storage 2026-10-21 12:36:40.945886838 -0700
+++ b/arch/x86/kernel/asm-offsets.c 2026-10-21 12:36:40.949886846 -0700
@@ -43,6 +43,9 @@ static void __used common(void)
BLANK();
OFFSET(TASK_threadsp, task_struct, thread.sp);
+#ifdef CONFIG_X86_KERN_SHADOW_STACK
+ OFFSET(TASK_thread_kshstk_ssp, task_struct, thread.kshstk_ssp);
+#endif
#ifdef CONFIG_STACKPROTECTOR
OFFSET(TASK_stack_canary, task_struct, stack_canary);
#endif
_