* [PATCH 1/3] sched/core: Skip the shadow stack end check when SCS is disabled
2026-10-27 6:36 [PATCH 0/3] Make the scheduler stack end check selectable at boot Chris Down
@ 2026-10-27 6:36 ` Chris Down
2026-10-27 10:08 ` Peter Zijlstra
2026-10-27 6:36 ` [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter Chris Down
2026-10-27 6:36 ` [PATCH 3/3] lkdtm/bugs: Add CORRUPT_STACK_END to test the scheduler stack end check Chris Down
2 siblings, 1 reply; 7+ messages in thread
From: Chris Down @ 2026-10-27 6:36 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Kees Cook
Cc: Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Ard Biesheuvel, Will Deacon, Sami Tolvanen, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Arnd Bergmann, Greg Kroah-Hartman,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti,
linux-arm-kernel, linux-riscv, linux-doc, linux-kselftest,
linux-kernel, kernel-team
With CONFIG_DYNAMIC_SCS, shadow call stacks are only used when the boot
CPU lacks pointer authentication. scs_release() already checks
scs_is_enabled() before it looks at the shadow stack since commit
9beccca09840 ("scs: add support for dynamic shadow call stacks"), but
schedule_debug() does not.
With CONFIG_SCHED_STACK_END_CHECK=y it therefore checks init_task's
unused shadow stack on every context switch. That is harmless right now,
but the check is redundant and costs a load from an unrelated cache line
in __schedule().
So, let's guard on scs_is_enabled(). It is a static branch, and it folds
to true when CONFIG_DYNAMIC_SCS is not set.
Signed-off-by: Chris Down <chris@chrisdown.name>
---
kernel/sched/core.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index d7c3b43158de..fc269fd10192 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6152,7 +6152,7 @@ static inline void schedule_debug(struct task_struct *prev, bool preempt)
if (task_stack_end_corrupted(prev))
panic("corrupted stack end detected inside scheduler\n");
- if (task_scs_end_corrupted(prev))
+ if (scs_is_enabled() && task_scs_end_corrupted(prev))
panic("corrupted shadow stack detected inside scheduler\n");
#endif
--
2.54.0
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 1/3] sched/core: Skip the shadow stack end check when SCS is disabled
2026-10-27 6:36 ` [PATCH 1/3] sched/core: Skip the shadow stack end check when SCS is disabled Chris Down
@ 2026-10-27 10:08 ` Peter Zijlstra
0 siblings, 0 replies; 7+ messages in thread
From: Peter Zijlstra @ 2026-10-27 10:08 UTC (permalink / raw)
To: Chris Down
Cc: Ingo Molnar, Kees Cook, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Ard Biesheuvel, Will Deacon,
Sami Tolvanen, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Arnd Bergmann, Greg Kroah-Hartman, Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, linux-arm-kernel, linux-riscv,
linux-doc, linux-kselftest, linux-kernel, kernel-team
On Tue, Oct 27, 2026 at 02:36:09PM +0800, Chris Down wrote:
> With CONFIG_DYNAMIC_SCS, shadow call stacks are only used when the boot
> CPU lacks pointer authentication. scs_release() already checks
> scs_is_enabled() before it looks at the shadow stack since commit
> 9beccca09840 ("scs: add support for dynamic shadow call stacks"), but
> schedule_debug() does not.
>
> With CONFIG_SCHED_STACK_END_CHECK=y it therefore checks init_task's
> unused shadow stack on every context switch. That is harmless right now,
> but the check is redundant and costs a load from an unrelated cache line
> in __schedule().
>
> So, let's guard on scs_is_enabled(). It is a static branch, and it folds
> to true when CONFIG_DYNAMIC_SCS is not set.
>
> Signed-off-by: Chris Down <chris@chrisdown.name>
> ---
> kernel/sched/core.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index d7c3b43158de..fc269fd10192 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -6152,7 +6152,7 @@ static inline void schedule_debug(struct task_struct *prev, bool preempt)
> if (task_stack_end_corrupted(prev))
> panic("corrupted stack end detected inside scheduler\n");
>
> - if (task_scs_end_corrupted(prev))
> + if (scs_is_enabled() && task_scs_end_corrupted(prev))
> panic("corrupted shadow stack detected inside scheduler\n");
How about you push that scs_is_enabled() into task_scs_end_corrupted(),
this makes less noise here, and also keeps the next patch saner, no?
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter
2026-10-27 6:36 [PATCH 0/3] Make the scheduler stack end check selectable at boot Chris Down
2026-10-27 6:36 ` [PATCH 1/3] sched/core: Skip the shadow stack end check when SCS is disabled Chris Down
@ 2026-10-27 6:36 ` Chris Down
2026-10-27 10:17 ` Peter Zijlstra
2026-10-27 23:34 ` Randy Dunlap
2026-10-27 6:36 ` [PATCH 3/3] lkdtm/bugs: Add CORRUPT_STACK_END to test the scheduler stack end check Chris Down
2 siblings, 2 replies; 7+ messages in thread
From: Chris Down @ 2026-10-27 6:36 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Kees Cook
Cc: Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Ard Biesheuvel, Will Deacon, Sami Tolvanen, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Arnd Bergmann, Greg Kroah-Hartman,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti,
linux-arm-kernel, linux-riscv, linux-doc, linux-kselftest,
linux-kernel, kernel-team
CONFIG_SCHED_STACK_END_CHECK makes schedule_debug() check the magic at
the end of the outgoing task's stack and panic if it has been
overwritten.
Right now this can only be configured by setting the Kconfig option at
build time. At Meta we have it on for some hosts and off for others,
since it is helpful but adds cost on context switching. That means today
we have to build separate kernels for each, which is less than ideal.
So let's make this configurable at boot time. In benchmarking sched pipe
and sched messaging performance is unaffected.
Signed-off-by: Chris Down <chris@chrisdown.name>
---
.../admin-guide/kernel-parameters.txt | 8 +++++
kernel/sched/core.c | 33 +++++++++++++++----
2 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index fbee7b544903..95911d33c0fe 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6811,6 +6811,14 @@ Kernel parameters
core priority to both SMT siblings, so this option
does not favor either sibling.
+ sched_stack_end_check= [KNL]
+ Format: <bool>
+ Check the magic value at the end of the outgoing
+ task's stack (and its shadow call stack, if any) on
+ every call to schedule(), and panic if it has been
+ overwritten.
+ Default set by CONFIG_SCHED_STACK_END_CHECK.
+
sched_verbose [KNL,EARLY] Enables verbose scheduler debug messages.
schedstats= [KNL,X86] Enable or disable scheduled statistics.
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index fc269fd10192..c1fb86b813bb 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6143,18 +6143,39 @@ static noinline void __schedule_bug(struct task_struct *prev)
add_taint(TAINT_WARN, LOCKDEP_STILL_OK);
}
+static DEFINE_STATIC_KEY_MAYBE_RO(CONFIG_SCHED_STACK_END_CHECK, sched_stack_end_check);
+
+static int __init setup_sched_stack_end_check(char *str)
+{
+ bool enable;
+
+ if (kstrtobool(str, &enable)) {
+ pr_warn("Unable to parse sched_stack_end_check=\n");
+ return 0;
+ }
+
+ if (enable)
+ static_branch_enable(&sched_stack_end_check);
+ else
+ static_branch_disable(&sched_stack_end_check);
+
+ return 1;
+}
+__setup("sched_stack_end_check=", setup_sched_stack_end_check);
+
/*
* Various schedule()-time debugging checks and statistics:
*/
static inline void schedule_debug(struct task_struct *prev, bool preempt)
{
-#ifdef CONFIG_SCHED_STACK_END_CHECK
- if (task_stack_end_corrupted(prev))
- panic("corrupted stack end detected inside scheduler\n");
+ if (static_branch_maybe(CONFIG_SCHED_STACK_END_CHECK,
+ &sched_stack_end_check)) {
+ if (task_stack_end_corrupted(prev))
+ panic("corrupted stack end detected inside scheduler\n");
- if (scs_is_enabled() && task_scs_end_corrupted(prev))
- panic("corrupted shadow stack detected inside scheduler\n");
-#endif
+ if (scs_is_enabled() && task_scs_end_corrupted(prev))
+ panic("corrupted shadow stack detected inside scheduler\n");
+ }
#ifdef CONFIG_DEBUG_ATOMIC_SLEEP
if (!preempt && READ_ONCE(prev->__state) && prev->non_block_count) {
--
2.54.0
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter
2026-10-27 6:36 ` [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter Chris Down
@ 2026-10-27 10:17 ` Peter Zijlstra
2026-10-27 23:34 ` Randy Dunlap
1 sibling, 0 replies; 7+ messages in thread
From: Peter Zijlstra @ 2026-10-27 10:17 UTC (permalink / raw)
To: Chris Down
Cc: Ingo Molnar, Kees Cook, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Ard Biesheuvel, Will Deacon,
Sami Tolvanen, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Arnd Bergmann, Greg Kroah-Hartman, Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, linux-arm-kernel, linux-riscv,
linux-doc, linux-kselftest, linux-kernel, kernel-team
On Tue, Oct 27, 2026 at 02:36:21PM +0800, Chris Down wrote:
> CONFIG_SCHED_STACK_END_CHECK makes schedule_debug() check the magic at
> the end of the outgoing task's stack and panic if it has been
> overwritten.
>
> Right now this can only be configured by setting the Kconfig option at
> build time. At Meta we have it on for some hosts and off for others,
> since it is helpful but adds cost on context switching. That means today
> we have to build separate kernels for each, which is less than ideal.
>
> So let's make this configurable at boot time. In benchmarking sched pipe
> and sched messaging performance is unaffected.
>
> Signed-off-by: Chris Down <chris@chrisdown.name>
> ---
> .../admin-guide/kernel-parameters.txt | 8 +++++
> kernel/sched/core.c | 33 +++++++++++++++----
> 2 files changed, 35 insertions(+), 6 deletions(-)
>
> diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
> index fbee7b544903..95911d33c0fe 100644
> --- a/Documentation/admin-guide/kernel-parameters.txt
> +++ b/Documentation/admin-guide/kernel-parameters.txt
> @@ -6811,6 +6811,14 @@ Kernel parameters
> core priority to both SMT siblings, so this option
> does not favor either sibling.
>
> + sched_stack_end_check= [KNL]
> + Format: <bool>
> + Check the magic value at the end of the outgoing
> + task's stack (and its shadow call stack, if any) on
> + every call to schedule(), and panic if it has been
> + overwritten.
> + Default set by CONFIG_SCHED_STACK_END_CHECK.
> +
> sched_verbose [KNL,EARLY] Enables verbose scheduler debug messages.
>
> schedstats= [KNL,X86] Enable or disable scheduled statistics.
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index fc269fd10192..c1fb86b813bb 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -6143,18 +6143,39 @@ static noinline void __schedule_bug(struct task_struct *prev)
> add_taint(TAINT_WARN, LOCKDEP_STILL_OK);
> }
>
> +static DEFINE_STATIC_KEY_MAYBE_RO(CONFIG_SCHED_STACK_END_CHECK, sched_stack_end_check);
> +
> +static int __init setup_sched_stack_end_check(char *str)
> +{
> + bool enable;
> +
> + if (kstrtobool(str, &enable)) {
> + pr_warn("Unable to parse sched_stack_end_check=\n");
> + return 0;
> + }
> +
> + if (enable)
> + static_branch_enable(&sched_stack_end_check);
> + else
> + static_branch_disable(&sched_stack_end_check);
> +
> + return 1;
> +}
> +__setup("sched_stack_end_check=", setup_sched_stack_end_check);
> +
> /*
> * Various schedule()-time debugging checks and statistics:
> */
> static inline void schedule_debug(struct task_struct *prev, bool preempt)
> {
> -#ifdef CONFIG_SCHED_STACK_END_CHECK
> - if (task_stack_end_corrupted(prev))
> - panic("corrupted stack end detected inside scheduler\n");
> + if (static_branch_maybe(CONFIG_SCHED_STACK_END_CHECK,
> + &sched_stack_end_check)) {
> + if (task_stack_end_corrupted(prev))
> + panic("corrupted stack end detected inside scheduler\n");
>
> - if (scs_is_enabled() && task_scs_end_corrupted(prev))
> - panic("corrupted shadow stack detected inside scheduler\n");
> -#endif
> + if (scs_is_enabled() && task_scs_end_corrupted(prev))
> + panic("corrupted shadow stack detected inside scheduler\n");
> + }
I just noticed that there's a number of task_stack_end_corrupted()
checks that:
- do not also do task_scs_end_corrupted(); should
task_scs_end_corrupted() be put inside task_stack_end_corrupted() to
make sure they're always done in tandem and the shadow stack check
isn't lost?
- should these other sites also respect this switch? It seems weird to
only have this one site affected.
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter
2026-10-27 6:36 ` [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter Chris Down
2026-10-27 10:17 ` Peter Zijlstra
@ 2026-10-27 23:34 ` Randy Dunlap
1 sibling, 0 replies; 7+ messages in thread
From: Randy Dunlap @ 2026-10-27 23:34 UTC (permalink / raw)
To: Chris Down, Peter Zijlstra, Ingo Molnar, Kees Cook
Cc: Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Ard Biesheuvel, Will Deacon, Sami Tolvanen, Jonathan Corbet,
Shuah Khan, Arnd Bergmann, Greg Kroah-Hartman, Paul Walmsley,
Palmer Dabbelt, Albert Ou, Alexandre Ghiti, linux-arm-kernel,
linux-riscv, linux-doc, linux-kselftest, linux-kernel,
kernel-team
On 10/26/26 11:36 PM, Chris Down wrote:
> +static int __init setup_sched_stack_end_check(char *str)
> +{
> + bool enable;
> +
> + if (kstrtobool(str, &enable)) {
> + pr_warn("Unable to parse sched_stack_end_check=\n");
> + return 0;
> + }
This error branch should return 1 since it handled the command
line parameter -- it just didn't like the "value."
Returning 0 causes the option string to be added to init's
environment, which is not wanted in this case.
--
~Randy
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 3/3] lkdtm/bugs: Add CORRUPT_STACK_END to test the scheduler stack end check
2026-10-27 6:36 [PATCH 0/3] Make the scheduler stack end check selectable at boot Chris Down
2026-10-27 6:36 ` [PATCH 1/3] sched/core: Skip the shadow stack end check when SCS is disabled Chris Down
2026-10-27 6:36 ` [PATCH 2/3] sched/core: Add sched_stack_end_check= boot parameter Chris Down
@ 2026-10-27 6:36 ` Chris Down
2 siblings, 0 replies; 7+ messages in thread
From: Chris Down @ 2026-10-27 6:36 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Kees Cook
Cc: Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
Ard Biesheuvel, Will Deacon, Sami Tolvanen, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Arnd Bergmann, Greg Kroah-Hartman,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti,
linux-arm-kernel, linux-riscv, linux-doc, linux-kselftest,
linux-kernel, kernel-team
There's no existing test for this, so let's add one.
Signed-off-by: Chris Down <chris@chrisdown.name>
---
drivers/misc/lkdtm/bugs.c | 21 +++++++++++++++++++++
tools/testing/selftests/lkdtm/tests.txt | 1 +
2 files changed, 22 insertions(+)
diff --git a/drivers/misc/lkdtm/bugs.c b/drivers/misc/lkdtm/bugs.c
index 3eca2ef64aff..929a60db00d6 100644
--- a/drivers/misc/lkdtm/bugs.c
+++ b/drivers/misc/lkdtm/bugs.c
@@ -215,6 +215,26 @@ static noinline void lkdtm_CORRUPT_STACK_STRONG(void)
__lkdtm_CORRUPT_STACK((void *)&data);
}
+static void lkdtm_CORRUPT_STACK_END(void)
+{
+ unsigned long *stack_end = end_of_stack(current);
+
+ if (READ_ONCE(*stack_end) != STACK_END_MAGIC) {
+ pr_err("FAIL: stack end magic is already corrupted\n");
+ return;
+ }
+
+ pr_info("Corrupting stack end magic and calling schedule() ...\n");
+ WRITE_ONCE(*stack_end, ~STACK_END_MAGIC);
+ schedule_timeout_uninterruptible(1);
+
+ /* Still alive, so let's restore the magic so nothing else trips over it. */
+ WRITE_ONCE(*stack_end, STACK_END_MAGIC);
+ pr_err("FAIL: corrupted stack end was not detected by the scheduler\n");
+ pr_expected_config_param(CONFIG_SCHED_STACK_END_CHECK,
+ "sched_stack_end_check");
+}
+
static pid_t stack_pid;
static unsigned long stack_addr;
@@ -854,6 +874,7 @@ static struct crashtype crashtypes[] = {
CRASHTYPE(EXHAUST_STACK),
CRASHTYPE(CORRUPT_STACK),
CRASHTYPE(CORRUPT_STACK_STRONG),
+ CRASHTYPE(CORRUPT_STACK_END),
CRASHTYPE(REPORT_STACK),
CRASHTYPE(REPORT_STACK_CANARY),
CRASHTYPE(UNALIGNED_LOAD_STORE_WRITE),
diff --git a/tools/testing/selftests/lkdtm/tests.txt b/tools/testing/selftests/lkdtm/tests.txt
index bec57a02913a..2d856ed77a64 100644
--- a/tools/testing/selftests/lkdtm/tests.txt
+++ b/tools/testing/selftests/lkdtm/tests.txt
@@ -10,6 +10,7 @@ EXCEPTION
#EXHAUST_STACK Corrupts memory on failure
#CORRUPT_STACK Crashes entire system on success
#CORRUPT_STACK_STRONG Crashes entire system on success
+#CORRUPT_STACK_END Crashes entire system on success
ARRAY_BOUNDS call trace:|UBSAN: array-index-out-of-bounds
FAM_BOUNDS call trace:|UBSAN: array-index-out-of-bounds
PTR_BOUNDS call trace:|UBSAN: array-index-out-of-bounds
--
2.54.0
^ permalink raw reply [flat|nested] 7+ messages in thread