From 76ce2220f4a0fc76018a9140e4ece28bdc146dff Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Tue, 26 May 2026 17:39:39 +0200 Subject: [PATCH] pipeline: reject double-connect of already-attached buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pipeline_connect() had no guard against being called twice for the same buffer-component pair. Calling list_item_prepend() on a node that is already in a doubly-linked list corrupts the list by creating a self-loop where node->next points back to itself instead of to the list head. The corruption was discovered through IPC3 fuzzing in persistent mode. Without per-testcase topology teardown, components and buffers created by testcase N survive into testcase N+1. When N+1 sends a TPLG_COMP_CONNECT for IDs that N already connected, ipc_comp_connect() finds the surviving objects and calls pipeline_connect() a second time. The same sequence can also be triggered within a single testcase by two CONNECT messages for the same pair. The self-loop causes ipc_comp_free() to hang indefinitely. The function walks bsource_list / bsink_list with comp_dev_for_each_producer_safe() whose termination condition checks node->next == &comp->bsource_list. With the self-loop that check is always false. The walk runs inside irq_local_disable(), so the native_sim timer cannot preempt the thread and nsi_exec_for() never returns, making libFuzzer's max_total_time limit unreachable. A second failure mode arises when ipc_buffer_free() calls pipeline_disconnect() on the corrupted buffer. list_item_del() updates comp->bsource_list.next to node->next, which due to the self-loop is the node itself — leaving the component's list head pointing into the freed buffer memory. When that memory is reused by a later allocation and overwritten, the next walk of bsource_list dereferences an invalid pointer, crashing with a null dereference or a corrupt-pointer access. Add an early-exit guard in pipeline_connect() before buffer_attach(): if the buffer's relevant list node (source_list for COMP_TO_BUFFER, sink_list for BUFFER_TO_COMP) is not a self-loop singleton, the buffer is already attached and the connect is rejected with -EINVAL. list_is_empty() returns true only for a freshly created or correctly disconnected buffer, so no valid use case is rejected. Verified with -s address on the full IPC3 corpus (~66K inputs, 75 s): zero crashes, zero hangs, ~2800 exec/s. The unfixed build stalled indefinitely on the same run. Signed-off-by: Tomasz Leman --- src/audio/pipeline/pipeline-graph.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/audio/pipeline/pipeline-graph.c b/src/audio/pipeline/pipeline-graph.c index 47d5d0127fd0..84b86bbce62e 100644 --- a/src/audio/pipeline/pipeline-graph.c +++ b/src/audio/pipeline/pipeline-graph.c @@ -198,6 +198,7 @@ int pipeline_connect(struct comp_dev *comp, struct comp_buffer *buffer, int dir) { struct list_item *comp_list; + struct list_item *buf_list; PPL_LOCK_DECLARE; if (dir == PPL_CONN_DIR_COMP_TO_BUFFER) @@ -207,6 +208,29 @@ int pipeline_connect(struct comp_dev *comp, struct comp_buffer *buffer, PPL_LOCK(); + /* + * Guard against double-connecting the same buffer. Calling + * list_item_prepend() on a node that is already in a list creates a + * self-loop (node->next == node) that permanently corrupts the list. + * Consequences: + * - ipc_comp_free() enters an unbounded loop inside irq_local_disable, + * stalling the simulation indefinitely. + * - pipeline_disconnect() / list_item_del() fails to unlink the buffer + * from the component, leaving a dangling pointer that causes + * use-after-free when the buffer is later freed. + * This can be triggered by a second IPC CONNECT message for the same + * buffer-component pair (within one testcase, or via state carry-over + * between fuzzer testcases when IPC topology is not torn down). + */ + buf_list = dir == PPL_DIR_DOWNSTREAM ? + &buffer->source_list : &buffer->sink_list; + if (!list_is_empty(buf_list)) { + comp_err(comp, "buffer %d already connected dir %d", + buf_get_id(buffer), dir); + PPL_UNLOCK(); + return -EINVAL; + } + comp_list = comp_buffer_list(comp, dir); buffer_attach(buffer, comp_list, dir); buffer_set_comp(buffer, comp, dir);