// 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 * * 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 #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("Test module to stall a CPU on a CSD lock");