arm64: cross-CPU NMI via SDEI
cover arm64: cross-CPU NMI via SDEI
From: "Kiryl Shutsemau (Meta)" <[email protected]>
A class of debug/observability features needs to interrupt a CPU that has its interrupts locally masked: hard-lockup detection, the all-CPU backtrace behind sysrq-l / RCU-stall / hung-task dumps, and crash_smp_send_stop() capturing a stuck CPU's state into the vmcore. On arm64 these need a mechanism that reaches a CPU spinning with DAIF masked, which a normal IPI cannot.
arm64 has two such mechanisms today:
- GICv3 pseudo-NMI (interrupt priority masking). This is the preferred path and what the perf-based hard-lockup detector (HAVE_HARDLOCKUP_DETECTOR_PERF) is built on. Its cost, however, is on the interrupt mask/unmask hot path: local_irq_enable() becomes an ICC_PMR_EL1 write plus a synchronising barrier, and exception entry/exit save and restore the PMR, paid on every CPU whether or not an NMI is ever delivered.
In our measurements, enabling pseudo-NMI costs up to ~5% on real workloads, and ~66% on a syscall-in-a-loop microbenchmark that maximises exception entry/exit (where pseudo-NMI adds the PMR save/restore). A fleet-wide ~5% regression is not acceptable, so these systems run with pseudo-NMI disabled — and therefore have no hard-lockup detector and degraded backtrace/crash-stop today.
- FEAT_NMI (Armv8.8) — the architectural fix, but absent from deployed silicon and from most of the fleet for years to come.
For deployments that do not run pseudo-NMI (to avoid that standing hot-path cost), the hard-lockup detector and the backtrace/crash paths are degraded: a plain IPI can't reach the masked CPU, so the lockup goes undetected, the backtrace of the CPU you care about comes back empty, and the kdump is missing the culprit's registers.
This series adds a third delivery backend that costs nothing on the hot
path: SDEI. Firmware delivers an SDEI event into a CPU regardless of its DAIF state, so interrupt masking stays the cheap PSTATE.DAIF operation and the firmware round-trip is paid only at the rare moment a CPU must be interrupted.
Mechanism
=========
It uses the standard SDEI software-signalled event (event 0) and the SDEI_EVENT_SIGNAL call (DEN0054) — a spec-defined cross-PE signal, not a vendor extension. The driver registers a handler for event 0 and pokes a target CPU with sdei_event_signal(0, target_mpidr); firmware makes event 0 pending on that PE and dispatches the handler NMI-like.
No firmware change is required beyond SDEI being enabled, which firmware-first RAS (APEI/GHES) deployments already have; the only SDEI-core addition is a thin sdei_event_signal() wrapper over the standard call.
Clean kdump when a CPU panics from inside the SDEI handler (the hard-lockup case) is handled by the already-merged sdei_handler_abort(), which crash_smp_send_stop() calls: it issues SDEI_EVENT_COMPLETE_AND_RESUME so the firmware-side priority is dropped before the capture kernel boots.
Prior SDEI watchdog work
========================
Out-of-tree SDEI hard-lockup watchdogs exist (e.g. in the openEuler and Anolis kernels). They take a different mechanism: they bind the secure physical timer as an SDEI event, so firmware delivers a periodic self-CPU tick that drives the detector. That requires a new SDEI interrupt-binding API, pushes the watchdog period (watchdog_thresh) into firmware, and adds secure-timer EOI handling on the kexec path.
This series instead uses only the standard software-signalled event 0:
the kernel keeps the timing (a per-CPU hrtimer with a buddy heartbeat check) and firmware does nothing but deliver the cross-CPU poke when a buddy looks stalled. The result is a smaller, far less firmware-coupled change — no secure-timer dependency, no new SDEI API, no period in firmware — and the same delivery primitive serves the backtrace and crash-stop users, not just the watchdog.
Testing
=======
Developed on QEMU (Trusted Firmware-A with SDEI enabled) and validated on NVIDIA Grace (Neoverse V2) hardware, under irqchip.gicv3_pseudo_nmi=0:
- hard lockup (LKDTM) caught by the SDEI watchdog and panicked, with the
stack pointing at the wedged code;
- sysrq-l backtrace of an interrupt-masked CPU returning its real stack;
- kdump via crash_smp_send_stop() with a wedged CPU, and via a watchdog panic from inside the event-0 handler — sdei_handler_abort() fires and the capture kernel boots to userspace on the formerly-wedged CPU, with its registers present in the vmcore.
Series
======
[1/4] firmware: arm_sdei: add SDEI_EVENT_SIGNAL support
Thin sdei_event_signal() wrapper over the standard call; NMI/crash safe (no locks).
[2/4] drivers/firmware: add SDEI cross-CPU NMI service for arm64
Register event 0; first user, arch_trigger_cpumask_backtrace().
[3/4] arm64: wire SDEI NMI into the hardlockup watchdog
HAVE_HARDLOCKUP_DETECTOR_ARCH backend; boot-time source selection with perf-NMI fallback.
[4/4] arm64: route crash_smp_send_stop() last resort through SDEI
SDEI as the final escalation rung for CPUs that ignored the normal and pseudo-NMI stop IPIs.
Also available at:
git://git.kernel.org/pub/scm/linux/kernel/git/kas/linux.git sdei-nmi
arch/arm64/Kconfig | 1 + arch/arm64/include/asm/nmi.h | 30 ++ arch/arm64/kernel/smp.c | 33 +++ drivers/firmware/Kconfig | 23 ++ drivers/firmware/Makefile | 1 + drivers/firmware/arm_sdei.c | 12 + drivers/firmware/sdei_nmi.c | 523 ++++++++++++++++++++++++++++++++++ include/linux/arm_sdei.h | 6 + include/uapi/linux/arm_sdei.h | 1 + 9 files changed, 630 insertions(+) create mode 100644 arch/arm64/include/asm/nmi.h create mode 100644 drivers/firmware/sdei_nmi.c
base-commit: e7ae89a0c97ce2b68b0983cd01eda67cf373517d
Signature · 1 line
2.54.0
PATCH 1/4 firmware: arm_sdei: add SDEI_EVENT_SIGNAL support
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Add sdei_event_signal(), a thin wrapper over the SDEI_EVENT_SIGNAL call (DEN0054) that makes the software-signalled event (event 0) pending on a target PE -- delivered NMI-like even when that PE has interrupts masked. It takes no locks, so it is safe to call from NMI / crash context.
--- drivers/firmware/arm_sdei.c | 12 ++++++++++++ include/linux/arm_sdei.h | 6 ++++++ include/uapi/linux/arm_sdei.h | 1 + 3 files changed, 19 insertions(+)
drivers/firmware/arm_sdei.c +12 -0
@@ -339,6 +339,18 @@ static void _ipi_unmask_cpu(void *ignored)sdei_unmask_local_cpu (); } +/* +* Signal the software-signalled event (event 0) to @mpidr. Does nothing +* but the SMC -- no locks, no event lookup -- so it is safe from NMI / +* crash context (e.g. the cross-CPU NMI service). +*/ +int sdei_event_signal (u32 event_num,u64 mpidr) +{ +return invoke_sdei_fn (SDEI_1_0_FN_SDEI_EVENT_SIGNAL , event_num, + mpidr,0 ,0 ,0 ,NULL ); +} +NOKPROBE_SYMBOL (sdei_event_signal); +static void _ipi_private_reset (void *ignored) {int err;
include/linux/arm_sdei.h +6 -0
@@ -37,6 +37,12 @@ int sdei_event_unregister(u32 event_num);int sdei_event_enable (u32 event_num);int sdei_event_disable (u32 event_num); +/* +* Signal the software-signalled event (event 0) to another PE, NMI-like. +* @mpidr is the target's MPIDR affinity. +*/ +int sdei_event_signal (u32 event_num,u64 mpidr); +/* GHES register/unregister helpers */ int sdei_register_ghes (struct ghes *ghes,sdei_event_callback *normal_cb,sdei_event_callback *critical_cb);
include/uapi/linux/arm_sdei.h +1 -0
@@ -22,6 +22,7 @@#define SDEI_1_0_FN_SDEI_PE_UNMASK SDEI_1_0_FN(0x0C)#define SDEI_1_0_FN_SDEI_INTERRUPT_BIND SDEI_1_0_FN(0x0D)#define SDEI_1_0_FN_SDEI_INTERRUPT_RELEASE SDEI_1_0_FN(0x0E) +#define SDEI_1_0_FN_SDEI_EVENT_SIGNAL SDEI_1_0_FN(0x0F)#define SDEI_1_0_FN_SDEI_PRIVATE_RESET SDEI_1_0_FN(0x11)#define SDEI_1_0_FN_SDEI_SHARED_RESET SDEI_1_0_FN(0x12)
Signature · 1 line
2.54.0
Re: [PATCH 1/4] firmware: arm_sdei: add SDEI_EVENT_SIGNAL support
Hi,
Quoted 8 lines · Kiryl Shutsemau
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Add
sdei_event_signal(), a thin wrapper over theSDEI_EVENT_SIGNALcall (DEN0054) that makes the software-signalled event (event 0) pending on a target PE -- delivered NMI-like even when that PE has interrupts masked. It takes no locks, so it is safe to call from NMI / crash context.
Quoted 6 lines
Signed-off-by Kiryl Shutsemau (Meta) <[email protected]>--- drivers/firmware/arm_sdei.c | 12 ++++++++++++ include/linux/arm_sdei.h | 6 ++++++ include/uapi/linux/arm_sdei.h | 1 + 3 files changed, 19 insertions(+)
I'd never looked at SDEI before this (so my review is probably not terribly strong), but this looks reasonable to me.
PATCH 2/4 drivers/firmware: add SDEI cross-CPU NMI service for arm64
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
Deliver an NMI-like event to an interrupt-masked arm64 CPU via the standard SDEI software-signalled event (event 0), without the pseudo-NMI hot-path cost: register a handler for event 0 and poke a target with sdei_event_signal(0, mpidr).
First user is arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, hung-task/soft-lockup dumps), which otherwise rides an IPI that can't reach a masked CPU. Falls back to the IPI path when SDEI is absent; no watchdog backend yet, so the stock detector is untouched.
--- arch/arm64/include/asm/nmi.h | 24 ++++++ arch/arm64/kernel/smp.c | 9 +++ drivers/firmware/Kconfig | 19 +++++ drivers/firmware/Makefile | 1 + drivers/firmware/sdei_nmi.c | 147 +++++++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 arch/arm64/include/asm/nmi.h create mode 100644 drivers/firmware/sdei_nmi.c
arch/arm64/include/asm/nmi.h new file +24 -0
@@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __ASM_NMI_H +#define __ASM_NMI_H + +#include <linux/cpumask.h> + +/* +* Cross-CPU NMI provider hooks, consulted by the arm64 arch code before +* its regular-IRQ / pseudo-NMI IPI paths. The SDEI provider in +* drivers/firmware/sdei_nmi.c implements them when active; a future +* FEAT_NMI provider could slot in here too. The stubs let callers stay +* unconditional when ARM_SDEI_NMI is off. +*/ +#ifdef CONFIG_ARM_SDEI_NMI +bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu); +#else +static inline bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask, +int exclude_cpu) +{ +return false; +} +#endif + +#endif /* __ASM_NMI_H */
arch/arm64/kernel/smp.c +9 -0
@@ -45,6 +45,7 @@#include <asm/daifflags.h> #include <asm/kvm_mmu.h> #include <asm/mmu_context.h> +#include <asm/nmi.h> #include <asm/numa.h> #include <asm/processor.h> #include <asm/smp_plat.h>
@@ -928,11 +929,19 @@ static void arm64_backtrace_ipi(cpumask_t *mask)void arch_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {/* +* Prefer the SDEI cross-CPU NMI provider when active: firmware +* dispatches the event out of EL3 and reaches CPUs that have +* interrupts locally masked, without the per-IRQ-mask cost that +* pseudo-NMI pays for the same reach. The plain IPI path below +* can't reach such a CPU unless pseudo-NMI is enabled. +* * NOTE: though nmi_trigger_cpumask_backtrace() has "nmi_" in the name, * nothing about it truly needs to be implemented using an NMI, it's * just that it's _allowed_ to work with NMIs. If ipi_should_be_nmi() * returned false our backtrace attempt will just use a regular IPI. */ +if (sdei_nmi_trigger_cpumask_backtrace (mask, exclude_cpu)) +return ;nmi_trigger_cpumask_backtrace (mask, exclude_cpu, arm64_backtrace_ipi); }
drivers/firmware/Kconfig +19 -0
@@ -36,6 +36,25 @@ config ARM_SDE_INTERFACE standard for registering callbacks from the platform firmware into the OS. This is typically used to implement RAS notifications. +config ARM_SDEI_NMI + bool "SDEI-based cross-CPU NMI service (arm64)" + depends on ARM64 && ARM_SDE_INTERFACE + help + Provides SDEI-based cross-CPU NMI delivery for hooks that need + to reach interrupt-masked CPUs on silicon that lacks FEAT_NMI: + + - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, + hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, + hung-task auxiliary dumps) + + The driver registers a handler for the SDEI software-signalled + event (event 0) and reaches a target CPU by signalling it with + SDEI_EVENT_SIGNAL. Firmware delivers the event out of EL3 + regardless of the target's PSTATE.DAIF -- forced delivery into a + CPU wedged with interrupts locally masked. + + If unsure, say N. + config EDD tristate "BIOS Enhanced Disk Drive calls determine boot disk" depends on X86
drivers/firmware/Makefile +1 -0
@@ -4,6 +4,7 @@ # obj-$(CONFIG_ARM_SCPI_PROTOCOL) += arm_scpi.o obj-$(CONFIG_ARM_SDE_INTERFACE) += arm_sdei.o +obj-$(CONFIG_ARM_SDEI_NMI) += sdei_nmi.o obj-$(CONFIG_DMI) += dmi_scan.o obj-$(CONFIG_DMI_SYSFS) += dmi-sysfs.o obj-$(CONFIG_EDD) += edd.o
drivers/firmware/sdei_nmi.c new file +147 -0
147 lines not on this page · raw message
Signature · 1 line
2.54.0
Re: [PATCH 2/4] drivers/firmware: add SDEI cross-CPU NMI service for arm64
Hi,
Quoted 9 lines · Kiryl Shutsemau
arch/arm64/kernel/smp.c +4
-0@@ -928,11 +929,19 @@ static void arm64_backtrace_ipi(cpumask_t *mask)void arch_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {/* +* Prefer the SDEI cross-CPU NMI provider when active: firmware +* dispatches the event out of EL3 and reaches CPUs that have +* interrupts locally masked, without the per-IRQ-mask cost that +* pseudo-NMI pays for the same reach. The plain IPI path below
arch/arm64/kernel/smp.c static void arm64_backtrace_ipi(cpumask_t *mask) · line 945
Quoted 10 lines
arch/arm64/kernel/smp.c +4
-0+* can't reach such a CPU unless pseudo-NMI is enabled. +* * NOTE: though nmi_trigger_cpumask_backtrace() has "nmi_" in the name, * nothing about it truly needs to be implemented using an NMI, it's * just that it's _allowed_ to work with NMIs. If ipi_should_be_nmi() * returned false our backtrace attempt will just use a regular IPI. */ +if (sdei_nmi_trigger_cpumask_backtrace (mask, exclude_cpu)) +return ;nmi_trigger_cpumask_backtrace (mask, exclude_cpu, arm64_backtrace_ipi);
nit: instead of one comment block, I would have broken it up in two. Like:
/*
* Prefer the SDEI ...
*/
if (sdei_nmi_trigger_cpumask_backtrace(mask, exclude_cpu))
return;
/*
* NOTE: though ...
*/nmi_trigger_cpumask_backtrace(...);
Quoted 18 lines
(quoted diff) +0
-0}drivers/firmware/Kconfig +8
-0@@ -36,6 +36,25 @@ config ARM_SDE_INTERFACE standard for registering callbacks from the platform firmware into the OS. This is typically used to implement RAS notifications. +config ARM_SDEI_NMI + bool "SDEI-based cross-CPU NMI service (arm64)" + depends on ARM64 && ARM_SDE_INTERFACE + help + Provides SDEI-based cross-CPU NMI delivery for hooks that need + to reach interrupt-masked CPUs on silicon that lacks FEAT_NMI: + + - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls,
drivers/firmware/Kconfig config ARM_SDE_INTERFACE · line 56
Quoted 10 lines
drivers/firmware/Kconfig +10
-0+ hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, + hung-task auxiliary dumps) + + The driver registers a handler for the SDEI software-signalled + event (event 0) and reaches a target CPU by signalling it with + SDEI_EVENT_SIGNAL. Firmware delivers the event out of EL3 + regardless of the target's PSTATE.DAIF -- forced delivery into a + CPU wedged with interrupts locally masked. + + If unsure, say N.
Is there some downside to this? It seems like anyone who has the SDE interface would want this. Not sure why you'd suggest people say "N".
Other than the nit, this looks reasoanble to me, though I'm a complete noob when it comes to SDEI...
Re: [PATCH 2/4] drivers/firmware: add SDEI cross-CPU NMI service for arm64
Quoted 31 lines · Doug Anderson
Hi,
Quoted 9 lines · Kiryl Shutsemau
arch/arm64/kernel/smp.c +4
-0@@ -928,11 +929,19 @@ static void arm64_backtrace_ipi(cpumask_t *mask)void arch_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {/* +* Prefer the SDEI cross-CPU NMI provider when active: firmware +* dispatches the event out of EL3 and reaches CPUs that have +* interrupts locally masked, without the per-IRQ-mask cost that +* pseudo-NMI pays for the same reach. The plain IPI path below arch/arm64/kernel/smp.c static void arm64_backtrace_ipi(cpumask_t *mask) · line 945
Quoted 10 lines
arch/arm64/kernel/smp.c +4
-0+* can't reach such a CPU unless pseudo-NMI is enabled. +* * NOTE: though nmi_trigger_cpumask_backtrace() has "nmi_" in the name, * nothing about it truly needs to be implemented using an NMI, it's * just that it's _allowed_ to work with NMIs. If ipi_should_be_nmi() * returned false our backtrace attempt will just use a regular IPI. */ +if (sdei_nmi_trigger_cpumask_backtrace (mask, exclude_cpu)) +return ;nmi_trigger_cpumask_backtrace (mask, exclude_cpu, arm64_backtrace_ipi);nit: instead of one comment block, I would have broken it up in two. Like:
/*
* Prefer the SDEI ...
*/
if (sdei_nmi_trigger_cpumask_backtrace(mask, exclude_cpu))
return;
Quoted 4 lines
/*
* NOTE: though ...
*/nmi_trigger_cpumask_backtrace(...);
Makes sense.
Quoted 23 lines
Quoted 23 lines
(quoted diff) +0
-0}drivers/firmware/Kconfig +13
-0@@ -36,6 +36,25 @@ config ARM_SDE_INTERFACE standard for registering callbacks from the platform firmware into the OS. This is typically used to implement RAS notifications. +config ARM_SDEI_NMI + bool "SDEI-based cross-CPU NMI service (arm64)" + depends on ARM64 && ARM_SDE_INTERFACE + help + Provides SDEI-based cross-CPU NMI delivery for hooks that need + to reach interrupt-masked CPUs on silicon that lacks FEAT_NMI: + + - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, + hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, + hung-task auxiliary dumps) + + The driver registers a handler for the SDEI software-signalled + event (event 0) and reaches a target CPU by signalling it with
Quoted 8 lines
drivers/firmware/Kconfig config ARM_SDE_INTERFACE · line 56
Quoted 5 lines
drivers/firmware/Kconfig +5
-0+ SDEI_EVENT_SIGNAL. Firmware delivers the event out of EL3 + regardless of the target's PSTATE.DAIF -- forced delivery into a + CPU wedged with interrupts locally masked. + + If unsure, say N.Is there some downside to this? It seems like anyone who has the SDE interface would want this. Not sure why you'd suggest people say "N".
No real downside -- without the software-signalled event the driver stays inert, and there is no cost until an event actually fires.
The "say N" is caution, not a technical limit: so far this has run on QEMU (TF-A) and one hardware platform, and the interesting paths depend on each vendor's SDEI implementation at EL3. I'm not sure vendors would care to run SDEI_EVENT_SIGNAL validation. Maybe we want to see more data points first?
But maybe I am too cautious. Happy to flip the recommendation (or add default y) in v2 if that the consensus.
Quoted 4 lines
Other than the nit, this looks reasoanble to me, though I'm a complete noob when it comes to SDEI...
Reviewed-by Douglas Anderson <[email protected]>
Thanks!
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
PATCH 3/4 arm64: wire SDEI NMI into the hardlockup watchdog
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
Select HAVE_HARDLOCKUP_DETECTOR_ARCH so the framework takes its backend from this driver. A per-CPU hrtimer checks its buddy's heartbeat and signals event 0 at a stalled CPU, which runs watchdog_hardlockup_check() NMI-like.
The source is chosen at boot: SDEI if firmware provides it, otherwise a perf-NMI counter (pseudo-NMI) fallback -- one image covers both.
--- arch/arm64/Kconfig | 1 + drivers/firmware/Kconfig | 3 + drivers/firmware/sdei_nmi.c | 247 +++++++++++++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 3 deletions(-)
arch/arm64/Kconfig +1 -0
@@ -205,6 +205,7 @@ config ARM64 select HAVE_FUNCTION_GRAPH_FREGS select HAVE_FUNCTION_GRAPH_TRACER select HAVE_GCC_PLUGINS + select HAVE_HARDLOCKUP_DETECTOR_ARCH if ARM_SDEI_NMI select HAVE_HARDLOCKUP_DETECTOR_PERF if PERF_EVENTS && \ HW_PERF_EVENTS && HAVE_PERF_EVENTS_NMI select HAVE_HW_BREAKPOINT if PERF_EVENTS
drivers/firmware/Kconfig +3 -0
@@ -39,6 +39,7 @@ config ARM_SDE_INTERFACE config ARM_SDEI_NMI bool "SDEI-based cross-CPU NMI service (arm64)" depends on ARM64 && ARM_SDE_INTERFACE + select HARDLOCKUP_DETECTOR_COUNTS_HRTIMER if HARDLOCKUP_DETECTOR help Provides SDEI-based cross-CPU NMI delivery for hooks that need to reach interrupt-masked CPUs on silicon that lacks FEAT_NMI:
@@ -46,6 +47,8 @@ config ARM_SDEI_NMI - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, hung-task auxiliary dumps) + - the hardlockup watchdog backend, when HARDLOCKUP_DETECTOR is + also enabled The driver registers a handler for the SDEI software-signalled event (event 0) and reaches a target CPU by signalling it with
drivers/firmware/sdei_nmi.c +244 -3
285 lines not on this page · raw message
Signature · 1 line
2.54.0
Re: [PATCH 3/4] arm64: wire SDEI NMI into the hardlockup watchdog
Hi,
Quoted 11 lines · Kiryl Shutsemau
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Select
HAVE_HARDLOCKUP_DETECTOR_ARCHso the framework takes its backend from this driver. A per-CPU hrtimer checks its buddy's heartbeat and signals event 0 at a stalled CPU, which runswatchdog_hardlockup_check()NMI-like.The source is chosen at boot: SDEI if firmware provides it, otherwise a perf-NMI counter (pseudo-NMI) fallback -- one image covers both.
Quoted 6 lines
Signed-off-by Kiryl Shutsemau (Meta) <[email protected]>--- arch/arm64/Kconfig | 1 + drivers/firmware/Kconfig | 3 + drivers/firmware/sdei_nmi.c | 247 +++++++++++++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 3 deletions(-)
I'm a little confused about this patch. We already have a buddy hardlockup detector using the hrtimer, and it's even been improved recently to trigger in a smaller time bound. It looks as if you're duplicating bits of the perf and buddy detector here?
I don't think you need this patch at all. The existing buddy detector
+ patches #1 and #2 in your series should be sufficient.
Did I misunderstand?
-Doug
Re: [PATCH 3/4] arm64: wire SDEI NMI into the hardlockup watchdog
Quoted 21 lines · Doug Anderson
Hi,
Quoted 11 lines · Kiryl Shutsemau
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Select
HAVE_HARDLOCKUP_DETECTOR_ARCHso the framework takes its backend from this driver. A per-CPU hrtimer checks its buddy's heartbeat and signals event 0 at a stalled CPU, which runswatchdog_hardlockup_check()NMI-like.The source is chosen at boot: SDEI if firmware provides it, otherwise a perf-NMI counter (pseudo-NMI) fallback -- one image covers both.
Quoted 6 lines
Signed-off-by Kiryl Shutsemau (Meta) <[email protected]>--- arch/arm64/Kconfig | 1 + drivers/firmware/Kconfig | 3 + drivers/firmware/sdei_nmi.c | 247 +++++++++++++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 3 deletions(-)
Quoted 7 lines
I'm a little confused about this patch. We already have a buddy hardlockup detector using the hrtimer, and it's even been improved recently to trigger in a smaller time bound. It looks as if you're duplicating bits of the perf and buddy detector here?
I don't think you need this patch at all. The existing buddy detector
+ patches #1 and #2 in your series should be sufficient.
You're mostly right.
Buddy + #2 covers the console case (the remote branch triggers the culprit's backtrace, which #2 makes deliverable), and #4 gets the wedged CPU's registers into the vmcore.
The one thing this patch adds that a config can't is boot-time source
selection: PERF-compiled kernels have no detector on a pseudo_nmi=0 boot, and PREFER_BUDDY costs the pseudo-NMI machines perf self-detection. But that's arguably out of scope for the patchset.
I'll drop this patch in v2 and run PREFER_BUDDY here. If a runtime perf->buddy fallback ever materializes, the gap closes entirely.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Re: [PATCH 3/4] arm64: wire SDEI NMI into the hardlockup watchdog
Hi,
Quoted 37 lines · Kiryl Shutsemau
Quoted 21 lines · Doug Anderson
Hi,
Quoted 11 lines · Kiryl Shutsemau
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Select
HAVE_HARDLOCKUP_DETECTOR_ARCHso the framework takes its backend from this driver. A per-CPU hrtimer checks its buddy's heartbeat and signals event 0 at a stalled CPU, which runswatchdog_hardlockup_check()NMI-like.The source is chosen at boot: SDEI if firmware provides it, otherwise a perf-NMI counter (pseudo-NMI) fallback -- one image covers both.
Quoted 6 lines
Signed-off-by Kiryl Shutsemau (Meta) <[email protected]>--- arch/arm64/Kconfig | 1 + drivers/firmware/Kconfig | 3 + drivers/firmware/sdei_nmi.c | 247 +++++++++++++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 3 deletions(-)Quoted 7 lines
I'm a little confused about this patch. We already have a buddy hardlockup detector using the hrtimer, and it's even been improved recently to trigger in a smaller time bound. It looks as if you're duplicating bits of the perf and buddy detector here?
I don't think you need this patch at all. The existing buddy detector
+ patches #1 and #2 in your series should be sufficient.You're mostly right.
Buddy + #2 covers the console case (the remote branch triggers the culprit's backtrace, which #2 makes deliverable), and #4 gets the wedged CPU's registers into the vmcore.
Quoted 7 lines
The one thing this patch adds that a config can't is boot-time source
selection: PERF-compiled kernels have no detector on a pseudo_nmi=0 boot, andPREFER_BUDDYcosts the pseudo-NMI machines perf self-detection. But that's arguably out of scope for the patchset.I'll drop this patch in v2 and run
PREFER_BUDDYhere. If a runtimeperf->buddyfallback ever materializes, the gap closes entirely.
Sure. If you're interested in trying to make pref vs. buddy coexist, that should be done in a platform-agnostic way. Feel free to post patches for that. I know we discussed this previously. Ah, here they are:
https://lore.kernel.org/r/[email protected]
I think those got bikeshedded to death and nobody cared enough to keep pushing.
FWIW, my belief is that the buddy detector is superior in every way except that it can't detect when all CPUs lock up simultaneously. ...though I wonder if a nicer way to solve the "all CPUs locked up" is to just NMI-enable the "bark" interrupt of a hardware watchdog timer. That ought to be quite easy...
-Doug
PATCH 4/4 arm64: route crash_smp_send_stop() last resort through SDEI
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
Add SDEI as the final rung after the normal stop IPI (and the pseudo-NMI IPI, if enabled): signal event 0 at the CPUs still online, whose handler runs crash_save_cpu() on the wedged context and parks them. It only ever touches CPUs the normal path couldn't reach.
SDEI is last because a CPU parked in the handler never completes the event, so it is less recoverable -- a cost paid only when nothing else worked.
--- arch/arm64/include/asm/nmi.h | 6 ++ arch/arm64/kernel/smp.c | 24 ++++++ drivers/firmware/Kconfig | 1 + drivers/firmware/sdei_nmi.c | 137 ++++++++++++++++++++++++++++++++++- 4 files changed, 167 insertions(+), 1 deletion(-)
arch/arm64/include/asm/nmi.h +6 -0
@@ -13,12 +13,18 @@*/ #ifdef CONFIG_ARM_SDEI_NMI bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu); +bool sdei_nmi_crash_smp_send_stop (void );#else static inline bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {return false; } + +static inline bool sdei_nmi_crash_smp_send_stop (void ) +{ +return false; +}#endif #endif /* __ASM_NMI_H */
arch/arm64/kernel/smp.c +24 -0
@@ -1288,8 +1288,32 @@ void crash_smp_send_stop(void)return ; crash_stop =1 ; +/* +* Stop the normal way first: IPI_CPU_STOP escalating to a pseudo-NMI +* IPI. Every CPU that responds saves its state via crash_save_cpu() +* and parks in cpu_park_loop() with its online bit cleared -- the +* standard kdump stop, identical to a kernel without SDEI. Crucially +* those CPUs stay in a clean, potentially-reusable state. +*/ smp_send_stop (); +/* +* Whatever is still online didn't respond -- typically a CPU wedged +* with interrupts masked. The plain IPI can't reach it, and a fleet +* that declines the pseudo-NMI hot-path cost has no NMI IPI to +* escalate to. Hit only the survivors with the SDEI cross-CPU NMI +* (no-op if SDEI isn't active, or if everything already stopped): +* firmware delivers out of EL3 regardless of PSTATE.DAIF, and the +* handler captures crash_save_cpu() state from the wedged context +* before parking the CPU. +* +* SDEI is deliberately last: an SDEI-stopped CPU never completes its +* event (it parks inside the handler, so EL3 retains its dispatch +* slot until reset), which is strictly less recoverable than a normal +* stop. We pay that only for CPUs that left no other way to reach them. +*/ +sdei_nmi_crash_smp_send_stop (); +sdei_handler_abort (); }
drivers/firmware/Kconfig +1 -0
@@ -49,6 +49,7 @@ config ARM_SDEI_NMI hung-task auxiliary dumps) - the hardlockup watchdog backend, when HARDLOCKUP_DETECTOR is also enabled + - crash_smp_send_stop() (panic / kdump path) The driver registers a handler for the SDEI software-signalled event (event 0) and reaches a target CPU by signalling it with
drivers/firmware/sdei_nmi.c +136 -1
174 lines not on this page · raw message
Signature · 1 line
2.54.0
Re: [PATCH 4/4] arm64: route crash_smp_send_stop() last resort through SDEI
Hi,
Quoted 20 lines · Kiryl Shutsemau
arch/arm64/kernel/smp.c +13
-0@@ -1288,8 +1288,32 @@ void crash_smp_send_stop(void)return ; crash_stop =1 ; +/* +* Stop the normal way first: IPI_CPU_STOP escalating to a pseudo-NMI +* IPI. Every CPU that responds saves its state via crash_save_cpu() +* and parks in cpu_park_loop() with its online bit cleared -- the +* standard kdump stop, identical to a kernel without SDEI. Crucially +* those CPUs stay in a clean, potentially-reusable state. +*/ smp_send_stop (); +/* +* Whatever is still online didn't respond -- typically a CPU wedged +* with interrupts masked. The plain IPI can't reach it, and a fleet +* that declines the pseudo-NMI hot-path cost has no NMI IPI to +* escalate to. Hit only the survivors with the SDEI cross-CPU NMI +* (no-op if SDEI isn't active, or if everything already stopped):
arch/arm64/kernel/smp.c void crash_smp_send_stop(void) · line 1315
Quoted 10 lines
arch/arm64/kernel/smp.c +10
-0+* firmware delivers out of EL3 regardless of PSTATE.DAIF, and the +* handler captures crash_save_cpu() state from the wedged context +* before parking the CPU. +* +* SDEI is deliberately last: an SDEI-stopped CPU never completes its +* event (it parks inside the handler, so EL3 retains its dispatch +* slot until reset), which is strictly less recoverable than a normal +* stop. We pay that only for CPUs that left no other way to reach them. +*/ +sdei_nmi_crash_smp_send_stop ();
It feels weird to me that you're adding SDEI for "crash stop" but not for regular "stop". It feels like you should modify smp_send_stop() to fall back to SDEI if sending the NMI failed, instead of adding this separate path.
Quoted 33 lines
drivers/firmware/sdei_nmi.c +31
-0static int sdei_nmi_handler (u32 event,struct pt_regs *regs,void *arg) { +int cpu =smp_processor_id (); + +if (READ_ONCE (*this_cpu_ptr (&sdei_nmi_crash_stop_requested))) { +WRITE_ONCE (*this_cpu_ptr (&sdei_nmi_crash_stop_requested),0 ); + +/* +* Capture the wedged context for kdump while pt_regs still +* points at the interrupted PC. This is the main motivation +* for using SDEI here: the plain IPI stop path can't reach an +* interrupt-masked CPU (and the fleet declines pseudo-NMI to +* keep the IRQ-mask hot path cheap), so crash_save_cpu() for +* that CPU would otherwise record nothing useful. +*/ +crash_save_cpu (regs, cpu); +set_cpu_online (cpu, false); + +/* publish the crash state/offline before the requester sees the ack */ +smp_wmb (); +WRITE_ONCE (*this_cpu_ptr (&sdei_nmi_crash_stop_acked),1 ); + +/* +* Park forever from within the SDEI handler. We deliberately +* do NOT issue SDEI_EVENT_COMPLETE: the framework's return +* path restores firmware's saved interrupted context, which +* would land the CPU back wherever it was running (often +* do_idle, which then notices cpu_is_offline=true and BUGs +* at cpuhp_report_idle_dead). Returning the modified pt_regs +* doesn't help -- arch/arm64/kernel/sdei.c::do_sdei_event +* only honours a PC override via its IRQ-state heuristic +* and otherwise hands EL3 its own saved-context slot back. +*
drivers/firmware/sdei_nmi.c static bool sdei_nmi_available; · line 141
Quoted 10 lines
drivers/firmware/sdei_nmi.c +10
-0+* Trade-off: EL3 firmware retains ~one saved-context slot +* per parked CPU until the next hardware reset (~hundreds of +* bytes per CPU). The CPU itself is parked in cpu_park_loop +* exactly as if IPI_CPU_STOP had stopped it; recoverability +* is unchanged versus the existing path (neither is +* recoverable without hardware reset, since PSCI sees the +* CPU as ALREADY_ON in both cases). +*/ +cpu_park_loop (); +/* unreachable */
Any chance we could avoid duplicating stuff from ipi_cpu_crash_stop()?
drivers/firmware/sdei_nmi.c bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu) · line 227
Quoted 5 lines
drivers/firmware/sdei_nmi.c +5
-0+bool sdei_nmi_crash_smp_send_stop (void ) +{ +unsigned int this_cpu, cpu, remaining; +unsigned long timeout; +cpumask_t mask;
The above will probably get you a yell. Putting "cpumask_t" on the stack is a no-no since it can be quite large under certain CONFIG options. This is why it's nearly always defined as "static".
-Doug
Re: [PATCH 4/4] arm64: route crash_smp_send_stop() last resort through SDEI
Quoted 6 lines · Doug Anderson
Quoted 1 line
+
sdei_nmi_crash_smp_send_stop();It feels weird to me that you're adding SDEI for "crash stop" but not for regular "stop". It feels like you should modify
smp_send_stop()to fall back to SDEI if sending the NMI failed, instead of adding this separate path.
Fair. A wedged CPU ignores the reboot-path stop just the same, and the escalation logic already lives in smp.c, so I'll restructure in v2.
One thing to sort out there: this patch parks the stopped CPU inside its SDEI handler without completing the event, which is fine for the crash case (nothing expects the CPU back before reset), but a generic stop path probably wants SDEI_EVENT_COMPLETE_AND_RESUME into a parking stub instead, so that e.g. a regular kexec can bring all CPUs back up in the new kernel. I'll look into that as part of the rework.
Quoted 4 lines
drivers/firmware/sdei_nmi.c static bool sdei_nmi_available; · line 141
Quoted 2 lines
drivers/firmware/sdei_nmi.c +2
-0+cpu_park_loop (); +/* unreachable */ Any chance we could avoid duplicating stuff from
ipi_cpu_crash_stop()?
Yes -- falls out of the above. I will look into this.
Maybe pull the save/offline/park body into a shared helper that both the IPI handler and the SDEI handler call.
Quoted 9 lines
drivers/firmware/sdei_nmi.c bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu) · line 227
Quoted 5 lines
drivers/firmware/sdei_nmi.c +5
-0+bool sdei_nmi_crash_smp_send_stop (void ) +{ +unsigned int this_cpu, cpu, remaining; +unsigned long timeout; +cpumask_t mask;The above will probably get you a yell. Putting "
cpumask_t" on the stack is a no-no since it can be quite large under certain CONFIG options. This is why it's nearly always defined as "static".
Doh! Will make it static in v2 -- safe here since the path is serialized by the crash_stop guard.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Re: [PATCH 4/4] arm64: route crash_smp_send_stop() last resort through SDEI
Quoted 11 lines · Kiryl Shutsemau
Quoted 6 lines · Doug Anderson
Quoted 1 line
+
sdei_nmi_crash_smp_send_stop();It feels weird to me that you're adding SDEI for "crash stop" but not for regular "stop". It feels like you should modify
smp_send_stop()to fall back to SDEI if sending the NMI failed, instead of adding this separate path.Fair. A wedged CPU ignores the reboot-path stop just the same, and the escalation logic already lives in
smp.c, so I'll restructure in v2.
Quoted 6 lines
One thing to sort out there: this patch parks the stopped CPU inside its SDEI handler without completing the event, which is fine for the crash case (nothing expects the CPU back before reset), but a generic stop path probably wants
SDEI_EVENT_COMPLETE_AND_RESUMEinto a parking stub instead, so that e.g. a regular kexec can bring all CPUs back up in the new kernel. I'll look into that as part of the rework.
Regular kexec takes different path and offlines CPU normally. So the next kernel can start them. But crash kernel cannot re-use wedged CPU.
C&R alone doesn't buy us anything. We need to get the CPU to CPU_OFF.
I am trying to do this, but so far no luck. Crash kernel fails to start at all if try to do C&R and then CPU_OFF. C&R alone works, but CPU is still unreachable by the next kernel, as expected.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
v2 cover arm64: cross-CPU NMI via SDEI
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
A class of debug/observability features needs to interrupt a CPU that has its interrupts locally masked: the all-CPU backtrace behind sysrq-l / RCU-stall / hung-task / hard-lockup dumps, and crash_smp_send_stop() capturing a stuck CPU's state into the vmcore. On arm64 these need a mechanism that reaches a CPU spinning with DAIF masked, which a normal IPI cannot.
arm64 has two such mechanisms today:
- GICv3 pseudo-NMI (interrupt priority masking). Its cost is on the interrupt mask/unmask hot path: local_irq_enable() becomes an ICC_PMR_EL1 write plus a synchronising barrier, and exception entry/exit save and restore the PMR, paid on every CPU whether or not an NMI is ever delivered. In our measurements, enabling pseudo-NMI costs up to ~5% on real workloads, and ~66% on a syscall-in-a-loop microbenchmark. A fleet-wide ~5% regression is not acceptable, so these systems run with pseudo-NMI disabled.
- FEAT_NMI (Armv8.8) -- the architectural fix, but absent from deployed silicon and from most of the fleet for years to come.
For deployments that do not run pseudo-NMI, the backtrace and crash paths are degraded: a plain IPI can't reach the masked CPU, so the backtrace of the CPU you care about comes back empty and the kdump is missing the culprit's registers. The hard-lockup detector on these systems is the software buddy detector (HARDLOCKUP_DETECTOR_BUDDY): it detects a stall from a neighbour CPU, but it cannot itself interrupt the wedged CPU, so its report has no stack for the culprit and (with hardlockup_panic) the panic runs on the bystander.
This series adds a third delivery backend that costs nothing on the hot
path: SDEI. Firmware delivers an SDEI event into a CPU regardless of its DAIF state, so interrupt masking stays the cheap PSTATE.DAIF operation and the firmware round-trip is paid only at the rare moment a CPU must be interrupted.
It does not add a hard-lockup detector. Detection stays with the buddy detector (CONFIG_HARDLOCKUP_DETECTOR_PREFER_BUDDY); this series gives the backtrace and crash-stop paths -- including the buddy detector's backtrace of the stalled CPU -- a way to actually reach a masked CPU.
Mechanism
=========
It uses the standard SDEI software-signalled event (event 0) and the SDEI_EVENT_SIGNAL call (DEN0054) -- a spec-defined cross-PE signal, not a vendor extension. The driver registers a handler for event 0 and pokes a target CPU with sdei_event_signal(0, target_mpidr); firmware makes event 0 pending on that PE and dispatches the handler NMI-like.
No firmware change is required beyond SDEI being enabled, which firmware-first RAS (APEI/GHES) deployments already have; the only SDEI-core addition is a thin sdei_event_signal() wrapper over the standard call.
Prior SDEI watchdog work
========================
Out-of-tree SDEI hard-lockup watchdogs exist (e.g. in the openEuler and Anolis kernels). They bind the secure physical timer as an SDEI event, so firmware delivers a periodic self-CPU tick that drives a detector. That requires a new SDEI interrupt-binding API, pushes the watchdog period into firmware, and adds secure-timer EOI handling on the kexec path. This series instead uses only the standard software-signalled event 0, keeps all timing in the kernel (the buddy detector), and the same delivery primitive serves the backtrace and crash-stop users, not just lockup reporting.
Not included / follow-ups
=========================
- No SDEI hard-lockup-detector backend. v1 had one; it is dropped here. The buddy detector plus this series' backtrace already cover the no-pseudo-NMI case, and a dedicated SDEI backend duplicated the perf-NMI detector it had to compile-exclude. Run PREFER_BUDDY.
- A CPU stopped by the SDEI rung is parked, not powered off via PSCI CPU_OFF. Reaching and dumping the wedged CPU -- the point of the series -- works, and this matches ipi_cpu_crash_stop()'s own park fallback. The consequence is that an SMP crash-capture kernel cannot re-online such a CPU (it stays "already on"); the capture kernel boots and runs on the remaining CPUs. Powering the stopped CPU off so a capture kernel can reclaim it requires completing the SDEI event and then CPU_OFF, which hit a firmware-specific issue still under investigation; it is left as a follow-up and does not affect the dump's contents.
Testing
=======
Developed on QEMU 'virt' (Trusted Firmware-A with SDEI enabled) and validated on NVIDIA Grace (Neoverse V2) hardware, under irqchip.gicv3_pseudo_nmi=0 with HARDLOCKUP_DETECTOR_PREFER_BUDDY=y:
- sysrq-l backtrace of an interrupt-masked CPU returns its real stack,
pstate showing DAIF set -- proof SDEI delivered into the masked CPU;
- buddy detector catches a hard lockup (LKDTM) and the wedged CPU's
stack is fetched via the SDEI backtrace;- reboot/halt and the panic/kdump crash stop reach a wedged CPU via the SDEI rung ("SMP: retry stop with SDEI NMI for CPUs N"), and the kdump captures the wedged CPU's registers in the vmcore.
Changes since v1
================
- Dropped the SDEI hard-lockup-detector patch (v1 3/4); use the buddy detector instead (Doug Anderson).
- Reworked the crash-stop patch (v1 4/4) into a third rung of smp_send_stop()'s escalation, shared with the IPI stop path and covering reboot/halt as well as crash; no on-stack cpumask (Doug Anderson).
- 2/3: split the merged comment in arch_trigger_cpumask_backtrace() (Doug Anderson).
- Renamed the driver to drivers/firmware/arm_sdei_nmi.c, to sit beside the SDEI core it builds on (drivers/firmware/arm_sdei.c), and widened that entry's MAINTAINERS glob (arm_sdei.c -> arm_sdei*) to cover it.
- Picked up Reviewed-by from Doug Anderson on 1/3 and 2/3 (the changes above are mechanical / comment-only on those two).
v1: https://lore.kernel.org/all/[email protected]
Also available at:
git://git.kernel.org/pub/scm/linux/kernel/git/kas/linux.git sdei-nmi/v2
Kiryl Shutsemau (Meta) (3):
firmware: arm_sdei: add SDEI_EVENT_SIGNAL support drivers/firmware: add SDEI cross-CPU NMI service for arm64
arm64: escalate smp_send_stop() to an SDEI NMI as a last resort
MAINTAINERS | 2 +- arch/arm64/include/asm/nmi.h | 38 ++++++ arch/arm64/kernel/smp.c | 64 ++++++++++ drivers/firmware/Kconfig | 21 +++ drivers/firmware/Makefile | 1 + drivers/firmware/arm_sdei.c | 12 ++ drivers/firmware/arm_sdei_nmi.c | 220 ++++++++++++++++++++++++++++++++ include/linux/arm_sdei.h | 6 + include/uapi/linux/arm_sdei.h | 1 + 9 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 arch/arm64/include/asm/nmi.h create mode 100644 drivers/firmware/arm_sdei_nmi.c
base-commit: e7ae89a0c97ce2b68b0983cd01eda67cf373517d --
2.54.0
PATCH v2 1/3 firmware: arm_sdei: add SDEI_EVENT_SIGNAL support
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Add sdei_event_signal(), a thin wrapper over the SDEI_EVENT_SIGNAL call (DEN0054) that makes the software-signalled event (event 0) pending on a target PE -- delivered NMI-like even when that PE has interrupts masked. It takes no locks, so it is safe to call from NMI / crash context.
--- drivers/firmware/arm_sdei.c | 12 ++++++++++++ include/linux/arm_sdei.h | 6 ++++++ include/uapi/linux/arm_sdei.h | 1 + 3 files changed, 19 insertions(+)
drivers/firmware/arm_sdei.c +12 -0
@@ -339,6 +339,18 @@ static void _ipi_unmask_cpu(void *ignored)sdei_unmask_local_cpu (); } +/* +* Signal the software-signalled event (event 0) to @mpidr. Does nothing +* but the SMC -- no locks, no event lookup -- so it is safe from NMI / +* crash context (e.g. the cross-CPU NMI service). +*/ +int sdei_event_signal (u32 event_num,u64 mpidr) +{ +return invoke_sdei_fn (SDEI_1_0_FN_SDEI_EVENT_SIGNAL , event_num, + mpidr,0 ,0 ,0 ,NULL ); +} +NOKPROBE_SYMBOL (sdei_event_signal); +static void _ipi_private_reset (void *ignored) {int err;
include/linux/arm_sdei.h +6 -0
@@ -37,6 +37,12 @@ int sdei_event_unregister(u32 event_num);int sdei_event_enable (u32 event_num);int sdei_event_disable (u32 event_num); +/* +* Signal the software-signalled event (event 0) to another PE, NMI-like. +* @mpidr is the target's MPIDR affinity. +*/ +int sdei_event_signal (u32 event_num,u64 mpidr); +/* GHES register/unregister helpers */ int sdei_register_ghes (struct ghes *ghes,sdei_event_callback *normal_cb,sdei_event_callback *critical_cb);
include/uapi/linux/arm_sdei.h +1 -0
@@ -22,6 +22,7 @@#define SDEI_1_0_FN_SDEI_PE_UNMASK SDEI_1_0_FN(0x0C)#define SDEI_1_0_FN_SDEI_INTERRUPT_BIND SDEI_1_0_FN(0x0D)#define SDEI_1_0_FN_SDEI_INTERRUPT_RELEASE SDEI_1_0_FN(0x0E) +#define SDEI_1_0_FN_SDEI_EVENT_SIGNAL SDEI_1_0_FN(0x0F)#define SDEI_1_0_FN_SDEI_PRIVATE_RESET SDEI_1_0_FN(0x11)#define SDEI_1_0_FN_SDEI_SHARED_RESET SDEI_1_0_FN(0x12)
Signature · 1 line
2.54.0
PATCH v2 2/3 drivers/firmware: add SDEI cross-CPU NMI service for arm64
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
Deliver an NMI-like event to an interrupt-masked arm64 CPU via the standard SDEI software-signalled event (event 0), without the pseudo-NMI hot-path cost: register a handler for event 0 and poke a target with sdei_event_signal(0, mpidr).
First user is arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, hung-task/soft-lockup dumps), which otherwise rides an IPI that can't reach a masked CPU. Falls back to the IPI path when SDEI is absent; no watchdog backend yet, so the stock detector is untouched.
--- MAINTAINERS | 2 +- arch/arm64/include/asm/nmi.h | 24 +++++ arch/arm64/kernel/smp.c | 11 +++ drivers/firmware/Kconfig | 19 ++++ drivers/firmware/Makefile | 1 + drivers/firmware/arm_sdei_nmi.c | 149 ++++++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 arch/arm64/include/asm/nmi.h create mode 100644 drivers/firmware/arm_sdei_nmi.c
MAINTAINERS +1 -1
@@ -24794,7 +24794,7 @@ M: James Morse <[email protected]> L: [email protected] (moderated for non-subscribers) S: Maintained F: Documentation/devicetree/bindings/arm/firmware/sdei.txt -F: drivers/firmware/arm_sdei.c +F: drivers/firmware/arm_sdei* F: include/linux/arm_sdei.h F: include/uapi/linux/arm_sdei.h
arch/arm64/include/asm/nmi.h new file +24 -0
@@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __ASM_NMI_H +#define __ASM_NMI_H + +#include <linux/cpumask.h> + +/* +* Cross-CPU NMI provider hooks, consulted by the arm64 arch code before +* its regular-IRQ / pseudo-NMI IPI paths. The SDEI provider in +* drivers/firmware/arm_sdei_nmi.c implements them when active; a future +* FEAT_NMI provider could slot in here too. The stubs let callers stay +* unconditional when ARM_SDEI_NMI is off. +*/ +#ifdef CONFIG_ARM_SDEI_NMI +bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu); +#else +static inline bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask, +int exclude_cpu) +{ +return false; +} +#endif + +#endif /* __ASM_NMI_H */
arch/arm64/kernel/smp.c +11 -0
@@ -45,6 +45,7 @@#include <asm/daifflags.h> #include <asm/kvm_mmu.h> #include <asm/mmu_context.h> +#include <asm/nmi.h> #include <asm/numa.h> #include <asm/processor.h> #include <asm/smp_plat.h>
@@ -927,6 +928,16 @@ static void arm64_backtrace_ipi(cpumask_t *mask)void arch_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) { +/* +* Prefer the SDEI cross-CPU NMI provider when active: firmware +* dispatches the event out of EL3 and reaches CPUs that have +* interrupts locally masked, without the per-IRQ-mask cost that +* pseudo-NMI pays for the same reach. The plain IPI path below +* can't reach such a CPU unless pseudo-NMI is enabled. +*/ +if (sdei_nmi_trigger_cpumask_backtrace (mask, exclude_cpu)) +return ; +/* * NOTE: though nmi_trigger_cpumask_backtrace() has "nmi_" in the name, * nothing about it truly needs to be implemented using an NMI, it's
drivers/firmware/Kconfig +19 -0
@@ -36,6 +36,25 @@ config ARM_SDE_INTERFACE standard for registering callbacks from the platform firmware into the OS. This is typically used to implement RAS notifications. +config ARM_SDEI_NMI + bool "SDEI-based cross-CPU NMI service (arm64)" + depends on ARM64 && ARM_SDE_INTERFACE + help + Provides SDEI-based cross-CPU NMI delivery for hooks that need + to reach interrupt-masked CPUs on silicon that lacks FEAT_NMI: + + - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, + hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, + hung-task auxiliary dumps) + + The driver registers a handler for the SDEI software-signalled + event (event 0) and reaches a target CPU by signalling it with + SDEI_EVENT_SIGNAL. Firmware delivers the event out of EL3 + regardless of the target's PSTATE.DAIF -- forced delivery into a + CPU wedged with interrupts locally masked. + + If unsure, say N. + config EDD tristate "BIOS Enhanced Disk Drive calls determine boot disk" depends on X86
drivers/firmware/Makefile +1 -0
@@ -4,6 +4,7 @@ # obj-$(CONFIG_ARM_SCPI_PROTOCOL) += arm_scpi.o obj-$(CONFIG_ARM_SDE_INTERFACE) += arm_sdei.o +obj-$(CONFIG_ARM_SDEI_NMI) += arm_sdei_nmi.o obj-$(CONFIG_DMI) += dmi_scan.o obj-$(CONFIG_DMI_SYSFS) += dmi-sysfs.o obj-$(CONFIG_EDD) += edd.o
drivers/firmware/arm_sdei_nmi.c new file +149 -0
149 lines not on this page · raw message
Signature · 1 line
2.54.0
PATCH v2 3/3 arm64: escalate smp_send_stop() to an SDEI NMI as a last resort
In reply to Kiryl ShutsemauFrom: "Kiryl Shutsemau (Meta)" <[email protected]>
A CPU wedged with interrupts masked ignores the stop IPI, and without pseudo-NMI there is no NMI IPI to escalate to: a reboot proceeds with the CPU still running, and a kdump misses its registers.
Add a third rung to smp_send_stop()'s escalation: signal SDEI event 0 at whatever is still online after the IPI (and pseudo-NMI IPI, if enabled) rungs. The handler routes like the IPI handlers do -- crash_stop distinguishes a kdump crash stop (crash_save_cpu() on the wedged context) from a plain stop -- and the CPU acks by marking itself offline, which the caller already polls.
arm64_nmi_cpu_stop() lives in smp.c rather than the SDEI provider because it needs the crash_stop discriminator and shares its shape with ipi_cpu_crash_stop(); it is exported only so the provider's event-0 handler, which owns the trigger, can route into it.
Two differences against an IPI-stopped CPU: the SDEI event is never completed, since completing it would resume the wedged context, so EL3 retains the event's dispatch slot until reset; and the CPU parks instead of trying PSCI CPU_OFF, which must not be called from inside an unfinished SDEI event.
--- arch/arm64/include/asm/nmi.h | 14 +++++++ arch/arm64/kernel/smp.c | 53 ++++++++++++++++++++++++ drivers/firmware/Kconfig | 2 + drivers/firmware/arm_sdei_nmi.c | 71 +++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+)
arch/arm64/include/asm/nmi.h +14 -0
@@ -4,21 +4,35 @@#include <linux/cpumask.h> +struct pt_regs ; +/* * Cross-CPU NMI provider hooks, consulted by the arm64 arch code before * its regular-IRQ / pseudo-NMI IPI paths. The SDEI provider in * drivers/firmware/arm_sdei_nmi.c implements them when active; a future * FEAT_NMI provider could slot in here too. The stubs let callers stay * unconditional when ARM_SDEI_NMI is off. +* +* arm64_nmi_cpu_stop() is the reverse direction: the arch entry point +* (arch/arm64/kernel/smp.c) that the provider's NMI handler routes a +* stop request into. */ #ifdef CONFIG_ARM_SDEI_NMI bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu); +bool sdei_nmi_stop_cpus (const cpumask_t *mask); + +void __noreturn arm64_nmi_cpu_stop (struct pt_regs *regs);#else static inline bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {return false; } + +static inline bool sdei_nmi_stop_cpus (const cpumask_t *mask) +{ +return false; +}#endif #endif /* __ASM_NMI_H */
arch/arm64/kernel/smp.c +53 -0
71 lines not on this page · raw message
drivers/firmware/Kconfig +2 -0
@@ -46,6 +46,8 @@ config ARM_SDEI_NMI - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, hung-task auxiliary dumps) + - smp_send_stop() escalation (reboot/halt and the + panic / kdump crash stop) The driver registers a handler for the SDEI software-signalled event (event 0) and reaches a target CPU by signalling it with
drivers/firmware/arm_sdei_nmi.c +71 -0
91 lines not on this page · raw message
Signature · 1 line
2.54.0
Re: [PATCH v2 3/3] arm64: escalate smp_send_stop() to an SDEI NMI as a last resort
Hi,
Quoted 23 lines · Kiryl Shutsemau
arch/arm64/kernel/smp.c +18
-0@@ -910,6 +911,35 @@ static void __noreturn ipi_cpu_crash_stop(unsigned int cpu, struct pt_regs *regs#endif } +#ifdef CONFIG_ARM_SDEI_NMI +/* +* Stop entry for the SDEI cross-CPU NMI service: its event-0 handler +* lands here when this CPU was asked to stop. The bookkeeping mirrors +* the IPI_CPU_STOP{,_NMI} handling; the park happens inside the SDEI +* event, which is never completed -- completing it would have firmware +* resume the interrupted (typically wedged) context. No PSCI CPU_OFF +* either: powering off a PE that EL3 still considers mid-event invites +* firmware trouble. +*/ +void __noreturn arm64_nmi_cpu_stop (struct pt_regs *regs) +{ +unsigned int cpu =smp_processor_id (); + +local_daif_mask (); + +if (IS_ENABLED (CONFIG_KEXEC_CORE ) && crash_stop) +crash_save_cpu (regs, cpu);
arch/arm64/kernel/smp.c static void __noreturn ipi_cpu_crash_stop(unsigned int cpu, struct pt_regs *regs · line 941
Quoted 10 lines
arch/arm64/kernel/smp.c +10
-0+ +/* the ack the stop requester polls for */ +set_cpu_online (cpu, false); + +sdei_mask_local_cpu (); + + cpu_park_loop(); +} +NOKPROBE_SYMBOL (arm64_nmi_cpu_stop); +#endif
Can we combine everything into one function so we don't have to keep all the different stop functionality in sync? Like this (untested):
void __noreturn arm64_nmi_cpu_stop(struct pt_regs *regs, bool die_on_crash) {
unsigned int cpu = smp_processor_id();
bool crash = IS_ENABLED(CONFIG_KEXEC_CORE) && crash_stop;
/*
* Use local_daif_mask() instead of local_irq_disable() to make sure
* that pseudo-NMIs are disabled. The "stop" code starts with
* an IRQ and falls back to NMI (which might be pseudo). If the IRQ
* finally goes through right as we're timing out then the NMI could
* interrupt us. It's better to prevent the NMI and let the IRQ
* finish since the pt_regs will be better.
*/
local_daif_mask();
if (crash)
crash_save_cpu(regs, cpu);
set_cpu_online(cpu, false);
sdei_mask_local_cpu();
if (crash && die_on_crash)
__cpu_try_die(cpu); /* just in case */
cpu_park_loop();
}
Then in do_handle_IPI(), it's just:
case IPI_CPU_STOP:
case IPI_CPU_STOP_NMI:
arm64_nmi_cpu_stop(get_irq_regs(), true);
break;...and from the SDEI code you pass "false" for "die_on_crash".
Perhaps when doing that you'd stop unconditionally getting the cpu in do_handle_IPI() and just call it for IPI_KGDB_ROUNDUP since it would now be the only consumer of that local variable.
FWIW, I'm not totally sure I followed the logic for why "die_on_crash" needs to be "false" for the SDEI case, but I also am not terribly familiar with KEXEC and crash-dumping kernels, since I've never attempted to get that feature to work.
Quoted 10 lines
arch/arm64/kernel/smp.c +6
-0@@ -1263,6 +1293,29 @@ void smp_send_stop(void)udelay (1 ); } +/* +* If CPUs are *still* online, try the SDEI cross-CPU NMI. Firmware +* delivers it regardless of the target's DAIF state, so it reaches +* a CPU spinning with interrupts masked, which neither rung above +* could (without pseudo-NMI there is no NMI rung at all). Allow +* 100ms: a firmware round-trip per CPU, with headroom.
arch/arm64/kernel/smp.c void smp_send_stop(void) · line 1311
Quoted 10 lines
arch/arm64/kernel/smp.c +10
-0+*/ +if (num_other_online_cpus ()) { +/* re-snapshot after the rungs above took CPUs offline */ +smp_rmb (); +cpumask_copy (&mask, cpu_online_mask); +cpumask_clear_cpu (smp_processor_id (), &mask); + +if (sdei_nmi_stop_cpus (&mask)) { +pr_info ("SMP: retry stop with SDEI NMI for CPUs %*pbl\n" , +cpumask_pr_args (&mask));
Perhaps it's being a bit pedantic, but it's a little weird that you're printing a message that sounds like "I'm going to retry with SDEI" after you've already done it. It feels like it would be nominally cleaner (and more parellel with the pseudo-NMI case) if you could separately test if SDEI is available. Then sdei_nmi_stop_cpus() would just return void?
Quoted 6 lines
drivers/firmware/arm_sdei_nmi.c +2
-0@@ -59,8 +64,45 @@ static bool sdei_nmi_available;#define SDEI_NMI_EVENT 0 +/* +* Stop-request dispatch lives on the same SDEI event 0 as everything
drivers/firmware/arm_sdei_nmi.c static bool sdei_nmi_available; · line 78
Quoted 10 lines
drivers/firmware/arm_sdei_nmi.c +10
-0+* else. The requesting CPU sets each target's bit in sdei_nmi_stop_mask +* before signalling event 0; the target's handler test-and-clears its +* bit and hands the CPU to arm64_nmi_cpu_stop(), which saves crash +* state when the stop is a kdump crash-stop, marks the CPU offline +* (which is what the requester polls for) and parks it. +* +* This mirrors the cpumask the framework's nmi_cpu_backtrace() consults +* just below, and a shared mask rather than a separate SDEI event avoids +* extra registrations from firmware. +*/
Do you have any reasoning for why you don't pick a separate EVENT ID for "backtrace" vs. "stop". If you absolutely have to share an ID because they're a limited resource then I guess it's fine, but it would make the code easier to understand / reason about if they were separate IDs.
If you had a separate EVENT ID, then it seems like you could completely eliminate the (potentially large) sdei_nmi_stop_mask variable, right? Any time a "STOP" event fires you can unconditionally consider it to be a stop w/ no globals needed, right?
Quoted 13 lines
drivers/firmware/arm_sdei_nmi.c +9
-0@@ -115,6 +157,35 @@ bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu)return true; } +/* +* Last rung of the stop escalation in smp_send_stop() (see +* arch/arm64/kernel/smp.c). The caller runs the regular stop IPI (and +* the pseudo-NMI stop IPI, where available) first; @mask holds whatever +* stayed online through those -- typically CPUs wedged with interrupts +* masked, unreachable by an IPI. Set each target's stop-request flag and +* signal event 0 at it; a target acks by marking itself offline, which +* the caller polls for. +*
drivers/firmware/arm_sdei_nmi.c bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu) · line 178
Quoted 10 lines
drivers/firmware/arm_sdei_nmi.c +10
-0+* Returns false when SDEI isn't active, so the caller can skip the wait. +*/ +bool sdei_nmi_stop_cpus (const cpumask_t *mask) +{ +unsigned int cpu; + +if (!sdei_nmi_available) +return false; + +cpumask_or (&sdei_nmi_stop_mask, &sdei_nmi_stop_mask, mask);
As per above, hopefully we can get rid of sdei_nmi_stop_mask. ...but if we keep it, I'm curious why "or" and not "copy"?
Re: [PATCH v2 3/3] arm64: escalate smp_send_stop() to an SDEI NMI as a last resort
Quoted 31 lines · Doug Anderson
Hi,
Quoted 28 lines · Kiryl Shutsemau
(quoted diff) +23
-0@@ -910,6 +911,35 @@ static void __noreturn ipi_cpu_crash_stop(unsigned int cpu, struct pt_regs *regs #endif } +#ifdef CONFIG_ARM_SDEI_NMI +/* + * Stop entry for the SDEI cross-CPU NMI service: its event-0 handler + * lands here when this CPU was asked to stop. The bookkeeping mirrors + * the IPI_CPU_STOP{,_NMI} handling; the park happens inside the SDEI + * event, which is never completed -- completing it would have firmware + * resume the interrupted (typically wedged) context. No PSCI CPU_OFF + * either: powering off a PE that EL3 still considers mid-event invites + * firmware trouble. + */ +void __noreturn arm64_nmi_cpu_stop(struct pt_regs *regs) +{ + unsigned int cpu = smp_processor_id(); + + local_daif_mask(); + + if (IS_ENABLED(CONFIG_KEXEC_CORE) && crash_stop) + crash_save_cpu(regs, cpu); + + /* the ack the stop requester polls for */ + set_cpu_online(cpu, false); + + sdei_mask_local_cpu();
Quoted 9 lines
Review static void __noreturn ipi_cpu_crash_stop(unsigned int cpu, struct pt_regs *regs · line 941
Quoted 5 lines
(quoted diff) +5
-0+ + cpu_park_loop(); +} +NOKPROBE_SYMBOL(arm64_nmi_cpu_stop); +#endifCan we combine everything into one function so we don't have to keep all the different stop functionality in sync? Like this (untested):
Okay. Look good to me. See the patch below.
void __noreturn arm64_nmi_cpu_stop(struct pt_regs *regs, bool die_on_crash) The stop IPI handlers call it with die_on_crash=true, the SDEI handler and panic_smp_self_stop() with false. Pretty much your sketch, with the crash = IS_ENABLED(CONFIG_KEXEC_CORE) && crash_stop discriminator inside.
Quoted 2 lines
FWIW, I'm not totally sure I followed the logic for why "
die_on_crash" needs to be "false" for the SDEI case,
It's not about kexec mechanics, it's about the SDEI dispatch state.
The SDEI stop handler parks inside an SDEI event that it deliberately never completes — completing it makes firmware resume the wedged context, which is the opposite of what we want. PSCI CPU_OFF from inside that not-yet-completed event silently wedges EL3 on at least one production firmware (still root-causing on the firmware side), so the SDEI path saves the crashed context and parks instead of powering off.
The only consequence is that an SMP capture kernel can't re-online that CPU. The dump itself is complete. I've left "power the SDEI-stopped CPU off too" as a follow-up and called it out in the cover letter. The IPI crash path is unaffected and still does CPU_OFF, exactly as before.
Quoted 3 lines
Perhaps when doing that you'd stop unconditionally getting the cpu in
do_handle_IPI()and just call it forIPI_KGDB_ROUNDUPsince it would now be the only consumer of that local variable.
I kept the local cpu — after the change it's still used by the IPI_KGDB_ROUNDUP case and the default: pr_crit(), so it didn't become single-use.
Quoted 19 lines
Quoted 19 lines
(quoted diff) +15
-0@@ -1263,6 +1293,29 @@ void smp_send_stop(void) udelay(1); } + /* + * If CPUs are *still* online, try the SDEI cross-CPU NMI. Firmware + * delivers it regardless of the target's DAIF state, so it reaches + * a CPU spinning with interrupts masked, which neither rung above + * could (without pseudo-NMI there is no NMI rung at all). Allow + * 100ms: a firmware round-trip per CPU, with headroom. + */ + if (num_other_online_cpus()) { + /* re-snapshot after the rungs above took CPUs offline */ + smp_rmb(); + cpumask_copy(&mask, cpu_online_mask); + cpumask_clear_cpu(smp_processor_id(), &mask); + + if (sdei_nmi_stop_cpus(&mask)) { + pr_info("SMP: retry stop with SDEI NMI for CPUs %*pbl\n",
Quoted 8 lines
Review void smp_send_stop(void) · line 1311
Quoted 1 line
(quoted diff) +1
-0+ cpumask_pr_args(&mask));Perhaps it's being a bit pedantic, but it's a little weird that you're printing a message that sounds like "I'm going to retry with SDEI" after you've already done it. It feels like it would be nominally cleaner (and more parellel with the pseudo-NMI case) if you could separately test if SDEI is available. Then
sdei_nmi_stop_cpus()would just return void?
Fixed. There's now a sdei_nmi_active() predicate; the rung tests it, prints, then calls sdei_nmi_stop_cpus(), which is now void. It mirrors the pseudo-NMI rung's check-then-act shape.
Quoted 25 lines
Quoted 8 lines
(quoted diff) +2
-0@@ -59,8 +64,45 @@ static bool sdei_nmi_available; #define SDEI_NMI_EVENT 0 +/* + * Stop-request dispatch lives on the same SDEI event 0 as everythingReview static bool sdei_nmi_available; · line 78
Quoted 10 lines
(quoted diff) +10
-0+ * else. The requesting CPU sets each target's bit in sdei_nmi_stop_mask + * before signalling event 0; the target's handler test-and-clears its + * bit and hands the CPU to arm64_nmi_cpu_stop(), which saves crash + * state when the stop is a kdump crash-stop, marks the CPU offline + * (which is what the requester polls for) and parks it. + * + * This mirrors the cpumask the framework's nmi_cpu_backtrace() consults + * just below, and a shared mask rather than a separate SDEI event avoids + * extra registrations from firmware. + */Do you have any reasoning for why you don't pick a separate EVENT ID for "backtrace" vs. "stop". If you absolutely have to share an ID because they're a limited resource then I guess it's fine, but it would make the code easier to understand / reason about if they were separate IDs.
Quoted 4 lines
If you had a separate EVENT ID, then it seems like you could completely eliminate the (potentially large)
sdei_nmi_stop_maskvariable, right? Any time a "STOP" event fires you can unconditionally consider it to be a stop w/ no globals needed, right?
Separate event IDs aren't available: SDEI_EVENT_SIGNAL only ever signals event 0 — it's the one architecturally software-signalled event. Every other event number is an interrupt-bound event that firmware has to define and bind, which is the firmware dependency this series is specifically trying not to add. So backtrace and stop are stuck sharing event 0.
But you're right that the mask should go — just not via a second event. A stop is terminal and system-wide (sdei_nmi_stop_cpus() is only reached from smp_send_stop(), which never returns), so once a stop is requested every later event-0 fire is a stop too. I replaced the cpumask with a single write-once flag the handler reads; a backtrace that races in after a stop has begun just stops that CPU, which is fine. So the (potentially large) variable is gone.
Quoted 18 lines
Quoted 18 lines
(quoted diff) +14
-0@@ -115,6 +157,35 @@ bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu) return true; } +/* + * Last rung of the stop escalation in smp_send_stop() (see + * arch/arm64/kernel/smp.c). The caller runs the regular stop IPI (and + * the pseudo-NMI stop IPI, where available) first; @mask holds whatever + * stayed online through those -- typically CPUs wedged with interrupts + * masked, unreachable by an IPI. Set each target's stop-request flag and + * signal event 0 at it; a target acks by marking itself offline, which + * the caller polls for. + * + * Returns false when SDEI isn't active, so the caller can skip the wait. + */ +bool sdei_nmi_stop_cpus(const cpumask_t *mask) +{ + unsigned int cpu;
Quoted 8 lines
Review bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu) · line 178
Quoted 5 lines
(quoted diff) +5
-0+ + if (!sdei_nmi_available) + return false; + + cpumask_or(&sdei_nmi_stop_mask, &sdei_nmi_stop_mask, mask);As per above, hopefully we can get rid of
sdei_nmi_stop_mask. ...but if we keep it, I'm curious why "or" and not "copy"?
It doesn't matter anymore. Mask is gone.
Thanks for the feedback! Any other comments?
--------------------------------8<-----------------------------------------------
From c25c32428c5f4fd896815acec5633240326e810c Mon Sep 17 00:00:00 2001
From: "Kiryl Shutsemau (Meta)" <[email protected]>
Date: Tue, 2 Jun 2026 15:28:10 +0100
Subject: [PATCHv2.1 3/3] arm64: escalate smp_send_stop() to an SDEI NMI as a last
resort
A CPU wedged with interrupts masked ignores the stop IPI, and without pseudo-NMI there is no NMI IPI to escalate to: a reboot proceeds with the CPU still running, and a kdump misses its registers.
Add a third rung to smp_send_stop(): once the IPI (and pseudo-NMI IPI, if enabled) rungs have run, signal SDEI event 0 at whatever stayed online. Firmware delivers it regardless of the target's DAIF, so it reaches a CPU a plain IPI cannot; the target acks by going offline, which the caller already polls for.
Fold the stop bookkeeping into one arm64_nmi_cpu_stop(regs, die_on_crash), shared by the stop IPI handlers, panic_smp_self_stop() and the SDEI handler, replacing the near-duplicate local_cpu_stop() and ipi_cpu_crash_stop(). @die_on_crash is the only difference: the IPI handlers pass true and PSCI CPU_OFF the CPU on a crash stop so a capture kernel can reclaim it; the SDEI handler and self-stop pass false and park. The SDEI park is required, not conservative -- its handler runs inside an SDEI event that is never completed (completing it resumes the wedged context), and a CPU_OFF from that unfinished-event context wedges EL3 on some firmware (left as a follow-up). The dump is unaffected; only re-onlining the CPU in an SMP capture kernel is lost.
--- arch/arm64/include/asm/nmi.h | 24 +++++++ arch/arm64/kernel/smp.c | 109 +++++++++++++++++++++----------- drivers/firmware/Kconfig | 2 + drivers/firmware/arm_sdei_nmi.c | 75 ++++++++++++++++++++++ 4 files changed, 172 insertions(+), 38 deletions(-)
arch/arm64/include/asm/nmi.h +24 -0
@@ -4,21 +4,45 @@#include <linux/cpumask.h> +struct pt_regs ; +/* * Cross-CPU NMI provider hooks, consulted by the arm64 arch code before * its regular-IRQ / pseudo-NMI IPI paths. The SDEI provider in * drivers/firmware/arm_sdei_nmi.c implements them when active; a future * FEAT_NMI provider could slot in here too. The stubs let callers stay * unconditional when ARM_SDEI_NMI is off. +* +* sdei_nmi_active() lets a caller test for the service before committing +* to (and waiting on) the SDEI stop rung; sdei_nmi_stop_cpus() then signals +* the targets, which ack by going offline. */ #ifdef CONFIG_ARM_SDEI_NMI bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu); +bool sdei_nmi_active (void ); +void sdei_nmi_stop_cpus (const cpumask_t *mask);#else static inline bool sdei_nmi_trigger_cpumask_backtrace (const cpumask_t *mask,int exclude_cpu) {return false; } + +static inline bool sdei_nmi_active (void ) +{ +return false; +} + +static inline void sdei_nmi_stop_cpus (const cpumask_t *mask) { }#endif +/* +* The common "stop this CPU" entry every arm64 stop path funnels through: +* the regular/pseudo-NMI stop IPI handlers, panic_smp_self_stop(), and the +* SDEI cross-CPU NMI handler. @die_on_crash powers the CPU off on the kdump +* crash path (IPI handlers) instead of parking it (SDEI / self-stop). +* Defined in arch/arm64/kernel/smp.c. +*/ +void __noreturn arm64_nmi_cpu_stop (struct pt_regs *regs,bool die_on_crash); +#endif /* __ASM_NMI_H */
arch/arm64/kernel/smp.c +71 -38
145 lines not on this page · raw message
drivers/firmware/Kconfig +2 -0
@@ -46,6 +46,8 @@ config ARM_SDEI_NMI - arch_trigger_cpumask_backtrace() (sysrq-l, RCU stalls, hardlockup_all_cpu_backtrace, soft-lockup secondary dumps, hung-task auxiliary dumps) + - smp_send_stop() escalation (reboot/halt and the + panic / kdump crash stop) The driver registers a handler for the SDEI software-signalled event (event 0) and reaches a target CPU by signalling it with
drivers/firmware/arm_sdei_nmi.c +75 -0
95 lines not on this page · raw message
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Re: [PATCH v2 3/3] arm64: escalate smp_send_stop() to an SDEI NMI as a last resort
Hi,
Quoted 13 lines · Kiryl Shutsemau
Quoted 3 lines
FWIW, I'm not totally sure I followed the logic for why "
die_on_crash" needs to be "false" for the SDEI case,It's not about kexec mechanics, it's about the SDEI dispatch state.
The SDEI stop handler parks inside an SDEI event that it deliberately never completes — completing it makes firmware resume the wedged context, which is the opposite of what we want. PSCI
CPU_OFFfrom inside that not-yet-completed event silently wedges EL3 on at least one production firmware (still root-causing on the firmware side), so the SDEI path saves the crashed context and parks instead of powering off.
Quoted 4 lines
The only consequence is that an SMP capture kernel can't re-online that CPU. The dump itself is complete. I've left "power the SDEI-stopped CPU off too" as a follow-up and called it out in the cover letter. The IPI crash path is unaffected and still does
CPU_OFF, exactly as before.
Ah, OK. This makes sense. I read the cover letter, but I guess this part of it didn't stick in my mind. I wouldn't mind an explanation of what's going on being included as a comment in the code. Then someone down the line won't be left wondering.
Quoted 11 lines
Quoted 10 lines
Do you have any reasoning for why you don't pick a separate EVENT ID for "backtrace" vs. "stop". If you absolutely have to share an ID because they're a limited resource then I guess it's fine, but it would make the code easier to understand / reason about if they were separate IDs.
If you had a separate EVENT ID, then it seems like you could completely eliminate the (potentially large)
sdei_nmi_stop_maskvariable, right? Any time a "STOP" event fires you can unconditionally consider it to be a stop w/ no globals needed, right?
Quoted 6 lines
Separate event IDs aren't available:
SDEI_EVENT_SIGNALonly ever signals event 0 — it's the one architecturally software-signalled event. Every other event number is an interrupt-bound event that firmware has to define and bind, which is the firmware dependency this series is specifically trying not to add. So backtrace and stop are stuck sharing event 0.
Oh well.
Quoted 7 lines
But you're right that the mask should go — just not via a second event. A stop is terminal and system-wide (
sdei_nmi_stop_cpus()is only reached fromsmp_send_stop(), which never returns), so once a stop is requested every later event-0 fire is a stop too. I replaced the cpumask with a single write-once flag the handler reads; a backtrace that races in after a stop has begun just stops that CPU, which is fine. So the (potentially large) variable is gone.
Yeah, this sounds much better, thanks!