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; } 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