From: "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.

Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 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(+)

diff --git a/arch/arm64/include/asm/nmi.h b/arch/arm64/include/asm/nmi.h
index 9366be419d18..2a9e6065f7af 100644
--- a/arch/arm64/include/asm/nmi.h
+++ b/arch/arm64/include/asm/nmi.h
@@ -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 */
diff --git a/arch/arm64/kernel/smp.c b/arch/arm64/kernel/smp.c
index a670434a8cae..1af7fdae48db 100644
--- a/arch/arm64/kernel/smp.c
+++ b/arch/arm64/kernel/smp.c
@@ -33,6 +33,7 @@
 #include <linux/kernel_stat.h>
 #include <linux/kexec.h>
 #include <linux/kgdb.h>
+#include <linux/kprobes.h>
 #include <linux/kvm_host.h>
 #include <linux/nmi.h>
 
@@ -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();
+
+       cpu_park_loop();
+}
+NOKPROBE_SYMBOL(arm64_nmi_cpu_stop);
+#endif
+
 static void arm64_send_ipi(const cpumask_t *mask, unsigned int nr)
 {
        unsigned int cpu;
@@ -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",
+                               cpumask_pr_args(&mask));
+
+                       timeout = USEC_PER_MSEC * 100;
+                       while (num_other_online_cpus() && timeout--)
+                               udelay(1);
+               }
+       }
+
        if (num_other_online_cpus()) {
                smp_rmb();
                cpumask_copy(&mask, cpu_online_mask);
diff --git a/drivers/firmware/Kconfig b/drivers/firmware/Kconfig
index 6501087ff90d..ab0ee36d46e7 100644
--- a/drivers/firmware/Kconfig
+++ b/drivers/firmware/Kconfig
@@ -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
diff --git a/drivers/firmware/arm_sdei_nmi.c b/drivers/firmware/arm_sdei_nmi.c
index a82776e7b55a..b34ea42cfe5c 100644
--- a/drivers/firmware/arm_sdei_nmi.c
+++ b/drivers/firmware/arm_sdei_nmi.c
@@ -29,6 +29,11 @@
  *     hardlockup_all_cpu_backtrace, soft-lockup/hung-task secondary
  *     dumps all reach interrupt-masked CPUs.
  *
+ *   - sdei_nmi_stop_cpus() — the last rung of smp_send_stop()'s
+ *     escalation (reboot/halt and the panic/kdump crash stop alike),
+ *     reaching CPUs that ignored the stop IPIs; on the kdump path the
+ *     wedged context is captured into the vmcore before the CPU parks.
+ *
  * 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
@@ -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
+ * 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.
+ */
+static cpumask_t sdei_nmi_stop_mask;
+
 static int sdei_nmi_handler(u32 event, struct pt_regs *regs, void *arg)
 {
+       int cpu = smp_processor_id();
+
+       if (cpumask_test_and_clear_cpu(cpu, &sdei_nmi_stop_mask)) {
+               /*
+                * Never returns, and deliberately never completes the SDEI
+                * event: SDEI_EVENT_COMPLETE has firmware restore the
+                * interrupted context, which would land the CPU back in
+                * the wedged loop (or in do_idle, which BUGs at
+                * cpuhp_report_idle_dead once it sees itself offline).
+                * Returning a 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.
+                *
+                * Trade-off: EL3 retains ~one saved-context slot per parked
+                * CPU until the next hardware reset (~hundreds of bytes per
+                * CPU). Recoverability is unchanged versus an IPI-stopped
+                * CPU: neither comes back without a reset.
+                */
+               arm64_nmi_cpu_stop(regs);
+               /* unreachable */
+       }
+
        /*
         * nmi_cpu_backtrace() no-ops unless this CPU's bit is set in the
         * global backtrace mask (driven by nmi_trigger_cpumask_backtrace()),
@@ -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;
+
+       if (!sdei_nmi_available)
+               return false;
+
+       cpumask_or(&sdei_nmi_stop_mask, &sdei_nmi_stop_mask, mask);
+
+       /* Publish the mask before the SMCs read it on the target side. */
+       smp_wmb();
+
+       for_each_cpu(cpu, mask)
+               sdei_nmi_fire(cpu);
+
+       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
-- 
2.54.0


Reply via email to