| // 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"); |