[PATCH v4 3/3] panic: allow force_cpu redirect from an NMI
From: Bradley Morgan
Date: Tue Jul 14 2026 - 13:35:41 EST
nmi_panic() calls panic_try_start() before panic(), so it claims
panic_cpu first. When the panic then reaches panic_try_force_cpu(),
the panic_in_progress() check sees panic_cpu set and returns false,
so the redirect to the requested CPU never happens. The crash kernel
runs on the CPU that took the NMI instead.
The buggy call order, on a CPU X that is not the target (target is C):
nmi_panic()
panic_try_start() wins, panic_cpu = X
panic("%s", msg)
vpanic()
panic_try_force_cpu()
panic_in_progress() true, panic_cpu is X
return false redirect bypassed
panic_try_start() already won
__crash_kexec() on X, not C
The fix is to try the redirect before claiming panic_cpu. nmi_panic()
now calls panic_try_force_cpu_fmt() first, and only calls
panic_try_start() when no redirect happens. The requested CPU then
claims panic_cpu itself when it runs panic(), so panic_cpu does not need
to be handed off.
nmi_panic() holds an already formatted string, not a va_list. Add a
variadic wrapper, panic_try_force_cpu_fmt(), so it can reach the
existing formatting guarded by the cmpxchg in panic_try_force_cpu() without
a signature change. The wrapper builds the va_list and the real
function still copies and formats under the redirect cmpxchg, so no
shared buffer is written before ownership.
The redirect sends the IPI via smp_call_function_single_async().
This is safe from NMI context: the doc on the _async variant
states it can be called with interrupts disabled, and kgdb_roundup_cpus()
already calls it from NMI/debug context (kernel/debug/debug_core.c).
The nmi_panic() body is reshaped to a goto self_stop, since panic()
is noreturn and the stop path is shared.
Reported-by: Sashiko <sashiko-bot@xxxxxxxxxx>
Closes: https://sashiko.dev/#/patchset/20260708164312.19044-1-include@xxxxxxxxx
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Bradley Morgan <include@xxxxxxxxx>
---
kernel/panic.c | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/kernel/panic.c b/kernel/panic.c
index c58c72d9f5a0..c81a5c9646e3 100644
--- a/kernel/panic.c
+++ b/kernel/panic.c
@@ -450,12 +450,32 @@ static bool panic_try_force_cpu(const char *fmt, va_list args)
/* IPI/NMI sent, this CPU should stop */
return true;
}
+
+/* For callers without a va_list, such as nmi_panic(). */
+static __printf(1, 2)
+bool panic_try_force_cpu_fmt(const char *fmt, ...)
+{
+ va_list args;
+ bool ret;
+
+ va_start(args, fmt);
+ ret = panic_try_force_cpu(fmt, args);
+ va_end(args);
+
+ return ret;
+}
#else
__printf(1, 0)
static inline bool panic_try_force_cpu(const char *fmt, va_list args)
{
return false;
}
+
+static __printf(1, 2)
+bool panic_try_force_cpu_fmt(const char *fmt, ...)
+{
+ return false;
+}
#endif /* CONFIG_SMP && CONFIG_CRASH_DUMP */
bool panic_try_start(void)
@@ -519,11 +539,20 @@ EXPORT_SYMBOL(panic_on_other_cpu);
*/
void nmi_panic(struct pt_regs *regs, const char *msg)
{
+ /* Try to redirect to the requested CPU when one is set. */
+ if (panic_try_force_cpu_fmt("%s", msg))
+ goto self_stop;
+
+ /* Try to acquire the right to proceed with the noreturn panic(). */
if (panic_try_start())
panic("%s", msg);
- if (panic_on_other_cpu())
- nmi_panic_self_stop(regs);
+ /*
+ * panic_try_start() only fails when a panic is already in progress
+ * on another CPU, in which case this CPU must stop.
+ */
+self_stop:
+ nmi_panic_self_stop(regs);
}
EXPORT_SYMBOL(nmi_panic);
--
2.53.0