drivers/firmware: add SDEI cross-CPU NMI service for arm64

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.

Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Reviewed-by: Douglas Anderson <dianders@chromium.org>
Tested-by: Yin Fengwei <fengwei_yin@linux.alibaba.com>
Signed-off-by: Will Deacon <will@kernel.org>
This commit is contained in:
Kiryl Shutsemau (Meta)
2026-06-29 16:07:17 +01:00
committed by Will Deacon
parent 9141eda9ee
commit 0c2c56598c
6 changed files with 215 additions and 1 deletions

View File

@@ -25093,7 +25093,7 @@ M: James Morse <james.morse@arm.com>
L: linux-arm-kernel@lists.infradead.org (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

View File

@@ -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 */

View File

@@ -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>
@@ -932,6 +933,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

View File

@@ -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 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

View File

@@ -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

View File

@@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-2.0
/*
* arm64 SDEI-based cross-CPU NMI service.
*
* Delivering an "NMI-shaped" event to an EL1 context that has locally
* masked interrupts, on silicon without FEAT_NMI, can be done two ways:
*
* - pseudo-NMI: mask "interrupts" via the GIC priority register
* (ICC_PMR_EL1) instead of PSTATE.DAIF, leaving a high-priority band
* deliverable. Functionally this works -- but it reimplements every
* local_irq_disable()/enable() and exception entry/exit as a PMR
* write plus synchronisation, a cost paid on that hot path forever,
* whether or not an NMI is ever delivered.
*
* - SDEI: leave interrupt masking as the cheap PSTATE.DAIF operation
* and have the firmware bounce an EL3-routed Group-0 SGI back to
* NS-EL1 as an event callback. The cost is a firmware round-trip,
* but only at the rare moment delivery is actually needed.
*
* This driver takes the second path: it keeps the IRQ-mask hot path
* free and pays only when it fires, which is what makes cross-CPU NMI
* affordable on hardware where the pseudo-NMI tax isn't, until FEAT_NMI
* makes NMI masking cheap in the architecture itself.
*
* Capabilities provided:
*
* - sdei_nmi_trigger_cpumask_backtrace() override for arm64's
* arch_trigger_cpumask_backtrace(), so sysrq-l, RCU stall dumps,
* hardlockup_all_cpu_backtrace, soft-lockup/hung-task secondary
* dumps all reach interrupt-masked CPUs.
*
* Delivery uses the standard SDEI software-signalled event (event 0) and
* SDEI_EVENT_SIGNAL. We register a handler for event 0, enable it, and
* poke a target CPU with sdei_event_signal(0, mpidr): firmware makes
* event 0 pending on that PE and dispatches the handler NMI-like,
* regardless of the target's DAIF.
* Availability is simply whether event 0 registers and enables -- if SDEI
* and its software-signalled event are present we use it, otherwise the
* driver stays inert.
*/
#define pr_fmt(fmt) "sdei_nmi: " fmt
#include <linux/arm_sdei.h>
#include <linux/cpumask.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/nmi.h>
#include <linux/printk.h>
#include <linux/ptrace.h>
#include <linux/smp.h>
#include <linux/types.h>
#include <asm/nmi.h>
#include <asm/smp_plat.h>
static bool sdei_nmi_available;
#define SDEI_NMI_EVENT 0
static int sdei_nmi_handler(u32 event, struct pt_regs *regs, void *arg)
{
/*
* nmi_cpu_backtrace() no-ops unless this CPU's bit is set in the
* global backtrace mask (driven by nmi_trigger_cpumask_backtrace()),
* so a fire that reaches a CPU not being backtraced is harmless.
*/
nmi_cpu_backtrace(regs);
return SDEI_EV_HANDLED;
}
NOKPROBE_SYMBOL(sdei_nmi_handler);
static void sdei_nmi_fire(unsigned int target_cpu)
{
int err = sdei_event_signal(SDEI_NMI_EVENT, cpu_logical_map(target_cpu));
if (err)
pr_warn("SDEI_EVENT_SIGNAL to CPU %u failed: %d\n",
target_cpu, err);
}
/*
* Raise callback for nmi_trigger_cpumask_backtrace(): signal event 0
* at every CPU still pending in @mask. The framework excludes the local
* CPU from @mask before calling us.
*/
static void sdei_nmi_raise_backtrace(cpumask_t *mask)
{
unsigned int cpu;
/*
* Publish backtrace_mask (set by nmi_trigger_cpumask_backtrace())
* before signalling. As in the stop path, the SMC is not a memory
* store, so dsb(ishst) is needed for the target to observe the mask.
*/
dsb(ishst);
for_each_cpu(cpu, mask)
sdei_nmi_fire(cpu);
}
/*
* Override hook for arch_trigger_cpumask_backtrace() (see
* arch/arm64/kernel/smp.c). Returns true when SDEI handled the request,
* which is the case whenever SDEI is active; on a false return the arch
* falls back to its regular-IRQ (or pseudo-NMI, if enabled) IPI.
*
* On a kernel built without paying the pseudo-NMI hot-path cost (the
* usual case for this driver's target), the IPI can't reach a CPU that
* has interrupts masked -- so the backtrace of the one CPU you care
* about comes back empty. SDEI is dispatched out of EL3 and lands
* regardless of the target's DAIF, without taxing the IRQ-mask path.
*/
bool sdei_nmi_trigger_cpumask_backtrace(const cpumask_t *mask, int exclude_cpu)
{
if (!sdei_nmi_available)
return false;
nmi_trigger_cpumask_backtrace(mask, exclude_cpu,
sdei_nmi_raise_backtrace);
return true;
}
/*
* device_initcall (after arch_initcall(sdei_init), so the SDEI subsystem
* is up): probe the firmware, register the event, and turn on the
* cross-CPU service. If the probe fails the driver stays inert and the
* override hooks decline, leaving the arch's own paths in place.
*/
static int __init sdei_nmi_init(void)
{
int err;
if (!sdei_is_present())
return 0;
err = sdei_event_register(SDEI_NMI_EVENT, sdei_nmi_handler, NULL);
if (err) {
pr_err("sdei_event_register(%u) failed: %d\n",
SDEI_NMI_EVENT, err);
return 0;
}
err = sdei_event_enable(SDEI_NMI_EVENT);
if (err) {
pr_err("sdei_event_enable(%u) failed: %d\n",
SDEI_NMI_EVENT, err);
sdei_event_unregister(SDEI_NMI_EVENT);
return 0;
}
sdei_nmi_available = true;
pr_info("using SDEI cross-CPU NMI (SDEI_EVENT_SIGNAL, event %u)\n",
SDEI_NMI_EVENT);
return 0;
}
device_initcall(sdei_nmi_init);