blob: 30c6c3332c3fc06c4b034de2a80792be075d957a [file] [edit]
// 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");