From: Breno Leitao <leitao@debian.org>
To: paulmck@kernel.org, Andrew Morton <akpm@linux-foundation.org>,
d@ilvokhin.com
Cc: linux-kernel@vger.kernel.org,
Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@kernel.org>,
Sebastian Andrzej Siewior <bigeasy@linutronix.de>,
linux-kernel@vger.kernel.org, kernel-team@meta.com,
Thomas Gleixner <tglx@kernel.org>,
Breno Leitao <leitao@debian.org>
Subject: [PATCH v2 3/3] lib/test_csd_lock: Add a module to stall a CPU on a CSD lock
Date: Mon, 10 Aug 2026 04:29:26 -0700 [thread overview]
Message-ID: <20260810-csd-stall-duration-v2-3-795083bf04a4@debian.org> (raw)
In-Reply-To: <20260810-csd-stall-duration-v2-0-795083bf04a4@debian.org>
Add test_csd_lock, a module that keeps one CPU from answering an IPI for
as long as its stall_ms parameter says, so that the CSD-lock debug code
has a stall to report. With in_handler=1 the CPU stalls inside a CSD
handler instead, which is the case where the IPI is not re-sent.
The module needs CONFIG_CSD_LOCK_WAIT_DEBUG and csdlock_debug=1. Loading
it runs one stall and then fails the load with -EAGAIN, the way
test_lockup does, so that nothing is left loaded afterwards.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
lib/Kconfig.debug | 12 ++++
lib/Makefile | 1 +
lib/test_csd_lock.c | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 186 insertions(+)
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 1244dcac2294a..d693caaeae873 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -1372,6 +1372,18 @@ config WQ_CPU_INTENSIVE_REPORT
triggering likely indicates that the work item should be switched
to use an unbound workqueue.
+config TEST_CSD_LOCK
+ tristate "Test module to stall a CPU on a CSD lock"
+ depends on m
+ depends on CSD_LOCK_WAIT_DEBUG
+ help
+ This builds the "test_csd_lock" module, which keeps one CPU from
+ answering an IPI for as long as its stall_ms parameter says, so
+ that the CSD-lock debug code has a stall to report. It needs
+ csdlock_debug=1 to be of any use.
+
+ If unsure, say N.
+
config TEST_LOCKUP
tristate "Test module to generate lockups"
depends on m
diff --git a/lib/Makefile b/lib/Makefile
index 7f75cc6edf94a..92f0ab7b740b4 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -99,6 +99,7 @@ obj-$(CONFIG_TEST_DEBUG_VIRTUAL) += test_debug_virtual.o
obj-$(CONFIG_TEST_MEMCAT_P) += test_memcat_p.o
obj-$(CONFIG_TEST_OBJAGG) += test_objagg.o
obj-$(CONFIG_TEST_MEMINIT) += test_meminit.o
+obj-$(CONFIG_TEST_CSD_LOCK) += test_csd_lock.o
obj-$(CONFIG_TEST_LOCKUP) += test_lockup.o
obj-$(CONFIG_TEST_HMM) += test_hmm.o
obj-$(CONFIG_TEST_FREE_PAGES) += test_free_pages.o
diff --git a/lib/test_csd_lock.c b/lib/test_csd_lock.c
new file mode 100644
index 0000000000000..30c6c3332c3fc
--- /dev/null
+++ b/lib/test_csd_lock.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Keep one CPU from answering an IPI, so that the CSD-lock debug code in
+ * kernel/smp.c has a stall to report.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates
+ * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+ *
+ * The target either spins with interrupts disabled, which leaves it idle as
+ * far as the debug code can tell and gets the IPI re-sent, or spins inside a
+ * CSD handler, which does not. The recovery message differs between the two.
+ *
+ * Loading the module runs one stall, then fails the load with -EAGAIN so
+ * that nothing is left loaded afterwards:
+ *
+ * echo 500 > /sys/module/smp/parameters/csd_lock_timeout
+ * modprobe test_csd_lock stall_ms=1000 in_handler=0
+ *
+ * csd_lock_timeout has to be below stall_ms for the stall to be reported at
+ * all, and the report has to come out before the CPU answers, so leave it
+ * some room.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/completion.h>
+#include <linux/cpu.h>
+#include <linux/cpumask.h>
+#include <linux/ktime.h>
+#include <linux/module.h>
+#include <linux/smp.h>
+#include <linux/workqueue.h>
+
+#define STALL_MS_MAX 10000
+
+static unsigned int stall_ms = 1000;
+module_param(stall_ms, uint, 0444);
+MODULE_PARM_DESC(stall_ms, "Time the target CPU ignores the IPI, in milliseconds.");
+
+static int stall_cpu = -1;
+module_param(stall_cpu, int, 0444);
+MODULE_PARM_DESC(stall_cpu, "CPU to stall, or -1 for the first online one.");
+
+static bool in_handler;
+module_param(in_handler, bool, 0444);
+MODULE_PARM_DESC(in_handler, "Stall inside a CSD handler instead of with interrupts disabled.");
+
+static int target_cpu;
+static bool target_stalling;
+static bool hog_launched;
+static struct work_struct irqoff_work;
+static struct work_struct sender_work;
+static call_single_data_t hog_csd;
+static DECLARE_COMPLETION(hog_done);
+
+static void csd_test_nop(void *unused)
+{
+}
+
+static void csd_test_spin(void)
+{
+ u64 end = ktime_get_mono_fast_ns() + (u64)stall_ms * NSEC_PER_MSEC;
+
+ while (ktime_get_mono_fast_ns() < end)
+ cpu_relax();
+}
+
+/* Nothing is running for the target while interrupts are off, so it gets a new IPI. */
+static void csd_test_irqoff_fn(struct work_struct *work)
+{
+ local_irq_disable();
+ /* Pairs with the load in csd_test_sender_fn(), which waits for this. */
+ smp_store_release(&target_stalling, true);
+ csd_test_spin();
+ local_irq_enable();
+}
+
+/* Here cur_csd stays set on the target, which suppresses the re-send. */
+static void csd_test_hog_fn(void *unused)
+{
+ /* Pairs with the load in csd_test_sender_fn(), which waits for this. */
+ smp_store_release(&target_stalling, true);
+ csd_test_spin();
+ complete(&hog_done);
+}
+
+/*
+ * Start the stall from here rather than from module init, so that however
+ * long this work item waits to be scheduled comes off before the target
+ * stops answering, not out of the middle of the stall.
+ */
+static void csd_test_sender_fn(struct work_struct *work)
+{
+ u64 deadline, ts;
+ int err;
+
+ if (in_handler) {
+ hog_csd.func = csd_test_hog_fn;
+ err = smp_call_function_single_async(target_cpu, &hog_csd);
+ if (err) {
+ pr_err("cannot queue the CSD handler on CPU%d: %d\n", target_cpu, err);
+ return;
+ }
+ } else {
+ queue_work_on(target_cpu, system_highpri_wq, &irqoff_work);
+ }
+ WRITE_ONCE(hog_launched, true);
+
+ deadline = ktime_get_mono_fast_ns() + (u64)STALL_MS_MAX * NSEC_PER_MSEC;
+ /* Pairs with the store in the stall functions: send once it is stuck. */
+ while (!smp_load_acquire(&target_stalling)) {
+ if (ktime_get_mono_fast_ns() > deadline) {
+ pr_err("CPU%d never stopped answering\n", target_cpu);
+ return;
+ }
+ cpu_relax();
+ }
+
+ ts = ktime_get_mono_fast_ns();
+ smp_call_function_single(target_cpu, csd_test_nop, NULL, 1);
+ pr_info("CPU%d answered after %llu ns\n", target_cpu,
+ ktime_get_mono_fast_ns() - ts);
+}
+
+static int __init test_csd_lock_init(void)
+{
+ int sender_cpu;
+ int ret = 0;
+
+ if (!stall_ms || stall_ms > STALL_MS_MAX) {
+ pr_err("stall_ms must be between 1 and %d\n", STALL_MS_MAX);
+ return -EINVAL;
+ }
+
+ INIT_WORK(&irqoff_work, csd_test_irqoff_fn);
+ INIT_WORK(&sender_work, csd_test_sender_fn);
+
+ cpus_read_lock();
+
+ target_cpu = stall_cpu < 0 ? cpumask_first(cpu_online_mask) : stall_cpu;
+ sender_cpu = nr_cpu_ids;
+ if (target_cpu < nr_cpu_ids && cpu_online(target_cpu))
+ sender_cpu = cpumask_any_but(cpu_online_mask, target_cpu);
+ if (sender_cpu >= nr_cpu_ids) {
+ pr_err("need CPU%d and one other CPU online\n", target_cpu);
+ ret = -EINVAL;
+ goto unlock;
+ }
+
+ pr_info("stalling CPU%d for %u ms %s, IPI from CPU%d\n", target_cpu, stall_ms,
+ in_handler ? "inside a CSD handler" : "with interrupts disabled", sender_cpu);
+
+ queue_work_on(sender_cpu, system_highpri_wq, &sender_work);
+ flush_work(&sender_work);
+ flush_work(&irqoff_work);
+
+ /* The CSD has to be idle again before this module goes away. */
+ if (in_handler && READ_ONCE(hog_launched) &&
+ !wait_for_completion_timeout(&hog_done, msecs_to_jiffies(2 * STALL_MS_MAX)))
+ pr_err("CSD handler on CPU%d never finished\n", target_cpu);
+
+ /* The stall is over and there is nothing left to hold, so go away. */
+ ret = -EAGAIN;
+unlock:
+ cpus_read_unlock();
+
+ return ret;
+}
+module_init(test_csd_lock_init);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Breno Leitao <leitao@debian.org>");
+MODULE_DESCRIPTION("Test module to stall a CPU on a CSD lock");
--
2.53.0-Meta
prev parent reply other threads:[~2026-08-10 11:31 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-10 11:29 [PATCH v2 0/3] locking/csd-lock: Report how long a CSD stall lasted Breno Leitao
2026-08-10 11:29 ` [PATCH v2 1/3] locking/csd-lock: Pack csd_lock_wait_toolong() state into a struct Breno Leitao
2026-08-10 14:32 ` Dmitry Ilvokhin
2026-08-10 11:29 ` [PATCH v2 2/3] locking/csd-lock: Report how long a stuck CSD lock took to recover Breno Leitao
2026-08-10 11:29 ` Breno Leitao [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260810-csd-stall-duration-v2-3-795083bf04a4@debian.org \
--to=leitao@debian.org \
--cc=akpm@linux-foundation.org \
--cc=bigeasy@linutronix.de \
--cc=d@ilvokhin.com \
--cc=kernel-team@meta.com \
--cc=linux-kernel@vger.kernel.org \
--cc=mingo@kernel.org \
--cc=paulmck@kernel.org \
--cc=peterz@infradead.org \
--cc=tglx@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox