From 22077a5f49e79071cddcb4d1ee5cfe49a7c6bc0e Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Fri, 4 Sep 2026 12:18:17 +0200 Subject: [PATCH] platform: posix: run fuzz teardown in thread context The IPC fuzzer aborts partway through a run with: programming error: nsif_cpu0_irq_raised_from_sw called from a HW model thread The between-testcase topology teardown (posix_ipc_teardown(), added in commit d53a76259 "platform: posix: tear down IPC topology between fuzz testcases") was invoked from posix_fuzz_case_begin(), which executes on the libFuzzer driver thread. On native_sim that thread is a "HW model" context: the simulated CPU is halted (posix_is_cpu_running() == false) whenever execution is outside nsi_exec_for(). The teardown frees pipelines and cancels scheduler tasks, taking Zephyr spinlocks (and the LL scheduler's k_mutex). Releasing a spinlock reaches arch_irq_unlock() -> hw_irq_ctrl_change_lock(); if a HW interrupt is pending there it is vectored synchronously via nsif_cpu0_irq_raised_from_sw(), which aborts because the CPU is not running. gdb confirms the pending interrupt is the system tick (irq_status == 0x1, IRQ 0 = TIMER_TICK_IRQ), delivered from pipeline_posn_unlock() -> k_spin_unlock() inside pipeline_free(). Running the teardown on the driver thread was always unsafe, but only became reproducible after a Zephyr update that converted the native_sim system timer from a periodic tick to a one-shot, fully tickless model (drivers/timer/native_sim_timer.c "use the generic timer core", plus the native_simulator hwtimer_set_tick_one_shot() addition). hwtimer_enable() previously armed a periodic tick on a fixed grid and no tick happened to be pending at the between-testcase boundary; the one-shot core now arms the tick at the exact next timeout deadline, which lands at/after an nsi_exec_for() quantum boundary. nsi_exec_for() stops on its time budget, so it returns after the tick fires but before the CPU services it, leaving TIMER_TICK_IRQ pending exactly when the teardown runs. Fix this by running the teardown where SOF frees pipelines during normal operation: the EDF workqueue thread. posix_fuzz_case_begin() now only sets a flag; ipc_platform_do_cmd() consumes it and runs posix_ipc_teardown() before this testcase's first command. There the CPU is running (a pending tick is delivered legitimately) and blocking primitives such as k_mutex are valid. Clearing the pending interrupt on the driver thread was rejected as an alternative: dropping the tick leaves the one-shot timer with no armed deadline (next_timer_time == NSI_NEVER), so the simulator exits via nsi_exit() and libFuzzer reports "fuzz target exited". Validated with the IPC4 seed corpus (342142 runs) and the IPC3 seed corpus (111135 runs); both complete cleanly with no crash artifacts. Signed-off-by: Tomasz Leman --- src/platform/posix/ipc.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/platform/posix/ipc.c b/src/platform/posix/ipc.c index 64586755d18c..64fb21358962 100644 --- a/src/platform/posix/ipc.c +++ b/src/platform/posix/ipc.c @@ -39,6 +39,11 @@ extern size_t posix_fuzz_sz; static uint8_t fuzz_in[65536]; static size_t fuzz_in_sz; +/* Set on the driver thread by posix_fuzz_case_begin(), consumed on the EDF + * workqueue thread by ipc_platform_do_cmd(). See posix_fuzz_case_begin(). + */ +static bool posix_fuzz_teardown_pending; + /* * posix_ipc_teardown - drop all IPC-tracked objects left over from the * previous fuzz testcase so the next one starts from a clean topology. @@ -194,7 +199,15 @@ static void posix_ipc_teardown(void) */ void posix_fuzz_case_begin(void) { - posix_ipc_teardown(); + /* + * Defer the teardown to the EDF workqueue thread (ipc_platform_do_cmd()). + * This runs on the libFuzzer driver thread, where native_sim treats the + * CPU as halted; posix_ipc_teardown() releases spinlocks/mutexes, and + * releasing a spinlock while the system-tick IRQ is pending makes + * native_sim deliver it synchronously and abort ("called from a HW model + * thread"). + */ + posix_fuzz_teardown_pending = true; fuzz_in_sz = 0; } @@ -362,6 +375,17 @@ enum task_state ipc_platform_do_cmd(struct ipc *ipc) { struct ipc_cmd_hdr *hdr; +#ifdef CONFIG_ARCH_POSIX_LIBFUZZER + /* + * Reclaim the previous testcase's topology in thread context, before + * this testcase's first command. See posix_fuzz_case_begin(). + */ + if (posix_fuzz_teardown_pending) { + posix_fuzz_teardown_pending = false; + posix_ipc_teardown(); + } +#endif + #ifdef CONFIG_IPC_MAJOR_4 memset(posix_hostbox, 0, SOF_IPC_MSG_MAX_SIZE); memcpy(posix_hostbox,