From 5e339c19a0ccc0d218c1aaef465179b189fe1372 Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Thu, 28 May 2026 13:12:03 +0200 Subject: [PATCH 1/2] platform: posix: ipc: expose fuzz-case staging-state hooks The libFuzzer entry point in fuzz.c stages each testcase by writing posix_fuzz_buf/sz and raising the fuzz IRQ; fuzz_isr() then drains those bytes into the static fuzz_in[] queue and feeds them into the IPC layer one message at a time. Two pieces of state therefore survive across LLVMFuzzerTestOneInput() calls: * `posix_fuzz_sz` - the raw input length still to consume, * `fuzz_in[] / _sz` - the per-call staging queue. The fuzzer harness has no way to inspect either of them today, which makes it impossible to tell whether a previous testcase fully drained before the next one begins. That is the root cause of the "not reproducible" crashes documented in FUZZER_ISOLATION_RESEARCH.md. Introduce three small helpers, kept in the module that owns the state, with no callers yet: posix_fuzz_case_begin() - drop the staging queue at the start of a new testcase, posix_fuzz_case_pending() - true while either buffer still has bytes to deliver, posix_fuzz_case_abort() - wipe both buffers (used when a case exceeds the simulator tick budget). A follow-up commit wires these into LLVMFuzzerTestOneInput(). This commit is a pure code-addition refactor: no callers, no behaviour change, the build still emits the same object code for the existing entry points. Signed-off-by: Tomasz Leman --- src/platform/posix/ipc.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/platform/posix/ipc.c b/src/platform/posix/ipc.c index 35e203c2d468..ec485576bb36 100644 --- a/src/platform/posix/ipc.c +++ b/src/platform/posix/ipc.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include // 6c8f0d53-ff77-4ca1-b825-c0c4e1b0d322 SOF_DEFINE_REG_UUID(ipc_task_posix); @@ -33,6 +35,28 @@ extern size_t posix_fuzz_sz; static uint8_t fuzz_in[65536]; static size_t fuzz_in_sz; +/* + * Testcase-isolation helpers used by the libFuzzer entry point in + * fuzz.c. They keep ownership of the cross-call state in one module + * so a new testcase never observes leftovers from a previous one that + * failed to drain inside the simulator tick budget. + */ +void posix_fuzz_case_begin(void) +{ + fuzz_in_sz = 0; +} + +bool posix_fuzz_case_pending(void) +{ + return posix_fuzz_sz != 0 || fuzz_in_sz != 0; +} + +void posix_fuzz_case_abort(void) +{ + posix_fuzz_sz = 0; + fuzz_in_sz = 0; +} + // The protocol here is super simple: the first byte is a message size // in units of 16 bits (the buffer maximum defaults to 384 bytes, and // I didn't want to waste space early in the buffer lest I confuse the From 6c36c80e2d22ebd6b62dd28025e5651b025cc0d4 Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Thu, 28 May 2026 13:13:18 +0200 Subject: [PATCH 2/2] platform: posix: drain-or-abort loop for fuzz testcase isolation The libFuzzer harness used to stage the testcase bytes, raise the fuzz IRQ, and then unconditionally run the native_sim scheduler for CONFIG_ZEPHYR_POSIX_FUZZ_TICKS ticks before returning. That has two problems for reproducibility: * If the OS finishes draining the IPC much faster than the tick budget (the common case), we still burn the full budget, which slows exec/s without buying any coverage. * If the OS does NOT finish within the budget (deep handlers, long pipeline walks, large payloads), the staged input buffer plus the per-call fuzz_in[] queue carry over into the next testcase. That leaks state across cases and is the root cause of crashes that disappear when replayed individually. Split the budget into POSIX_FUZZ_DRAIN_QUANTA (=8) quanta and after each one ask the IPC layer whether anything is still pending; return as soon as the queue is empty, otherwise run the abort hook to wipe both the raw fuzz buffer and the staged IPC payload before the next call. Together with the hooks added in the previous commit this guarantees that LLVMFuzzerTestOneInput observes a clean staging state on entry regardless of what the previous case did. No protocol or coverage change is intended; the goal is reproducible crashes and slightly higher throughput on short inputs. Signed-off-by: Tomasz Leman --- src/platform/posix/fuzz.c | 51 ++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/platform/posix/fuzz.c b/src/platform/posix/fuzz.c index e5c515c15a45..75a7dcf0ed6e 100644 --- a/src/platform/posix/fuzz.c +++ b/src/platform/posix/fuzz.c @@ -14,23 +14,52 @@ const uint8_t *posix_fuzz_buf; size_t posix_fuzz_sz; +/* + * Implemented in sof/src/platform/posix/ipc.c. These small hooks let + * us reset and observe the staging state owned by the IPC layer so + * testcases are properly isolated from each other. + */ +extern void posix_fuzz_case_begin(void); +extern void posix_fuzz_case_abort(void); +extern bool posix_fuzz_case_pending(void); + +/* Number of simulator quanta the budget is split into for the + * drain-or-abort loop. More quanta = earlier exit on quick testcases. + */ +#define POSIX_FUZZ_DRAIN_QUANTA 8 + /** * Entry point for fuzzing. Works by placing the data * into two known symbols, triggering an app-visible interrupt, and - * then letting the simulator run for a fixed amount of time (intended to be - * "long enough" to handle the event and reach a quiescent state - * again) + * then letting the simulator run for up to a fixed amount of time + * split into small quanta, exiting as soon as the OS has drained the + * staged fuzz input. If the budget is exhausted before drain we drop + * pending state to keep testcases isolated. */ NATIVE_SIMULATOR_IF int LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz) { static bool runner_initialized; + uint64_t total_us; + uint64_t quantum_us; + int i; if (!runner_initialized) { nsi_init(0, NULL); runner_initialized = true; } + total_us = k_ticks_to_us_ceil64(CONFIG_ZEPHYR_POSIX_FUZZ_TICKS); + quantum_us = (total_us + POSIX_FUZZ_DRAIN_QUANTA - 1) / POSIX_FUZZ_DRAIN_QUANTA; + if (quantum_us == 0) + quantum_us = 1; + + /* Fresh testcase: drop any leftovers from a previous case before + * staging the new buffer, so state from the prior input cannot + * influence this one. + */ + posix_fuzz_case_begin(); + /* Provide the fuzz data to the embedded OS as an interrupt, with * "DMA-like" data placed into posix_fuzz_buf/sz. */ @@ -38,9 +67,19 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t sz) posix_fuzz_sz = sz; hw_irq_ctrl_set_irq(CONFIG_ZEPHYR_POSIX_FUZZ_IRQ); - /* Give the OS time to process whatever happened in that - * interrupt and reach an idle state. + /* Bounded drain loop: run simulator in small quanta and exit + * as soon as both the raw input buffer and the staged IPC + * queue are empty. + */ + for (i = 0; i < POSIX_FUZZ_DRAIN_QUANTA; i++) { + nsi_exec_for(quantum_us); + if (!posix_fuzz_case_pending()) + return 0; + } + + /* Budget exhausted without full drain: hard reset so the next + * testcase starts clean. */ - nsi_exec_for(k_ticks_to_us_ceil64(CONFIG_ZEPHYR_POSIX_FUZZ_TICKS)); + posix_fuzz_case_abort(); return 0; }