From 8e0e4420cc63844a8dcc3cb06a26e91e4e227a1d Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Wed, 5 Aug 2026 15:12:42 +0200 Subject: [PATCH] platform: posix: tear down IPC topology between fuzz testcases Without an explicit teardown a fuzz testcase that successfully creates components, buffers or pipelines leaves them registered in global_ipc->comp_list. The next LLVMFuzzerTestOneInput() then sees a non-empty topology it never asked for, which both hides bugs (crashes that depend on freshly-empty state are missed) and fabricates them (crashes that only occur because of carry-over from a previous case are unreproducible when the artifact is replayed on its own). Add a posix-only teardown helper, called from posix_fuzz_case_begin() once at the start of every testcase before the input is staged. The helper: * Runs a pre-pass that forces every COMP_TYPE_COMPONENT to COMP_STATE_READY and initialises any NULL bsource_list/bsink_list pointers, because ipc_comp_free() returns -EINVAL (and silently leaks the entry) for a component that is not READY or whose buffer lists were never list_init()'d - the latter happens when a component was registered but its init failed partway through. The pre-pass also cancels any active pipeline pipe_task so ipc_pipeline_free() does not stall waiting for it (up to 100 LL periods on native_sim). * Snapshots IDs into a local array per pass rather than walking and mutating comp_list simultaneously (the SOF free helpers unlink each entry). * Frees in dependency order COMPONENT -> BUFFER -> PIPELINE so the topology layer never dereferences an already-freed parent. The BUFFER pass is compiled out for IPC4 because ipc4/helper.c never stores COMP_TYPE_BUFFER and ipc_buffer_free() does not exist there. * Ends with a force-drain pass that removes anything still on the list with list_item_del() + rfree(), keeping comp_list guaranteed empty on return and the harness robust against future COMP_TYPE_* additions. Signed-off-by: Tomasz Leman Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/platform/posix/ipc.c | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/platform/posix/ipc.c b/src/platform/posix/ipc.c index 8ef28ec654e4..6a7a5d66b4f6 100644 --- a/src/platform/posix/ipc.c +++ b/src/platform/posix/ipc.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,131 @@ extern size_t posix_fuzz_sz; static uint8_t fuzz_in[65536]; static size_t fuzz_in_sz; +/* + * posix_ipc_teardown - drop all IPC-tracked objects left over from the + * previous fuzz testcase so the next one starts from a clean topology. + * + * Walking and freeing comp_list in the same pass is unsafe because the + * SOF free helpers unlink the entry from the list, so we snapshot the + * IDs of one type into a local array first and then call the typed + * free function for each one. Passes run in dependency order + * (COMPONENT -> BUFFER -> PIPELINE) to keep the SOF topology layer + * from dereferencing already-freed parents. + * + * A final force-drain pass walks anything still on the list and removes + * it with list_item_del() + rfree() directly. That keeps the harness + * future-proof against new COMP_TYPE_* values: even if upstream adds a + * type we forget to register here, comp_list will still be empty on + * the next entry. The inner union object (cd/cb/pipeline) is leaked in + * that edge case, which is acceptable for a fuzzing harness. + */ +#define POSIX_TEARDOWN_MAX_DEVS 256 + +static void posix_ipc_teardown(void) +{ + static const uint16_t free_order[] = { + COMP_TYPE_COMPONENT, +#if CONFIG_IPC_MAJOR_3 + /* + * IPC4 never stores COMP_TYPE_BUFFER in comp_list + * (see ipc4/helper.c) and ipc_buffer_free() is not + * compiled for IPC4, so skip the buffer pass there. + */ + COMP_TYPE_BUFFER, +#endif + COMP_TYPE_PIPELINE, + }; + uint32_t ids[POSIX_TEARDOWN_MAX_DEVS]; + struct ipc_comp_dev *icd; + struct list_item *pos; + int n; + + if (!global_ipc) + return; + + /* + * Pre-pass: prepare components and pipelines for the ordered free + * passes below. + * + * ipc_comp_free() refuses to free a component unless its state is + * COMP_STATE_READY. A fuzz testcase that ran INIT_INSTANCE followed + * by SET_PIPELINE_STATE:RUNNING will leave components in PREPARE, + * PAUSED, or ACTIVE state. Force every component back to READY so + * the free pass can proceed without silently skipping entries. + * + * For pipelines with an active scheduler task, cancel the task before + * ipc_pipeline_free() calls schedule_task_free() on it. Without this, + * schedule_task_free() blocks waiting for the task to complete, which + * on native_sim can stall for up to 100 LL periods. + */ + list_for_item(pos, &global_ipc->comp_list) { + icd = container_of(pos, struct ipc_comp_dev, list); + if (icd->type == COMP_TYPE_COMPONENT && icd->cd) { + icd->cd->state = COMP_STATE_READY; + /* Ensure buffer lists are valid so ipc_comp_free() + * does not bail at the uninitialized-list check. + * A component whose init failed partway may have + * NULL list pointers. + */ + if (!icd->cd->bsource_list.next) + list_init(&icd->cd->bsource_list); + if (!icd->cd->bsink_list.next) + list_init(&icd->cd->bsink_list); + } + if (icd->type == COMP_TYPE_PIPELINE && + icd->pipeline && icd->pipeline->pipe_task) + schedule_task_cancel(icd->pipeline->pipe_task); + } + + for (int pass = 0; pass < (int)ARRAY_SIZE(free_order); pass++) { + uint16_t type = free_order[pass]; + + n = 0; + list_for_item(pos, &global_ipc->comp_list) { + icd = container_of(pos, struct ipc_comp_dev, list); + if (icd->type == type && n < POSIX_TEARDOWN_MAX_DEVS) + ids[n++] = icd->id; + } + + for (int i = 0; i < n; i++) { + switch (type) { + case COMP_TYPE_COMPONENT: + ipc_comp_free(global_ipc, ids[i]); + break; +#if CONFIG_IPC_MAJOR_3 + case COMP_TYPE_BUFFER: + ipc_buffer_free(global_ipc, ids[i]); + break; +#endif + case COMP_TYPE_PIPELINE: + ipc_pipeline_free(global_ipc, ids[i]); + break; + } + } + } + + /* + * Force-drain anything remaining (unknown/future COMP_TYPE_*). + * Snapshot the residual list then remove each entry directly. + * The inner union pointer leaks, but comp_list will be empty + * and the next testcase will not observe stale entries. + */ + n = 0; + list_for_item(pos, &global_ipc->comp_list) { + if (n < POSIX_TEARDOWN_MAX_DEVS) { + icd = container_of(pos, struct ipc_comp_dev, list); + ids[n++] = icd->id; + } + } + for (int i = 0; i < n; i++) { + icd = ipc_get_comp_dev(global_ipc, COMP_TYPE_ANY, ids[i]); + if (icd) { + list_item_del(&icd->list); + rfree(icd); + } + } +} + /* * Testcase-isolation helpers used by the libFuzzer entry point in * fuzz.c. They keep ownership of the cross-call state in one module @@ -45,6 +171,7 @@ static size_t fuzz_in_sz; */ void posix_fuzz_case_begin(void) { + posix_ipc_teardown(); fuzz_in_sz = 0; }