Skip to content

Commit d53a762

Browse files
tmlemankv2019i
authored andcommitted
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). * Drains each type in batches: it snapshots up to POSIX_TEARDOWN_BATCH ids, frees them with the typed helper, and repeats until no entry of that type remains, so the number of objects a testcase creates is not bounded by the snapshot size (walking and freeing comp_list in one pass is unsafe because the SOF free helpers unlink each entry). A batch that frees nothing ends the loop so it cannot spin forever. * 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 walks the whole residual list with list_for_item_safe() and removes each entry with list_item_del() + rfree(), so comp_list is guaranteed empty on return regardless of how many objects remained or of any future COMP_TYPE_* the typed passes do not cover. Signed-off-by: Tomasz Leman <tomasz.m.leman@intel.com>
1 parent f5176b9 commit d53a762

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

src/platform/posix/ipc.c

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
#include <sof/lib/mailbox.h>
88
#include <sof/ipc/common.h>
99
#include <sof/ipc/schedule.h>
10+
#include <sof/ipc/topology.h>
1011
#include <sof/schedule/edf_schedule.h>
1112
#include <sof/audio/component_ext.h>
1213
#include <stdbool.h>
1314
#include <stddef.h>
15+
#include <errno.h>
1416

1517
// 6c8f0d53-ff77-4ca1-b825-c0c4e1b0d322
1618
SOF_DEFINE_REG_UUID(ipc_task_posix);
@@ -37,6 +39,153 @@ extern size_t posix_fuzz_sz;
3739
static uint8_t fuzz_in[65536];
3840
static size_t fuzz_in_sz;
3941

42+
/*
43+
* posix_ipc_teardown - drop all IPC-tracked objects left over from the
44+
* previous fuzz testcase so the next one starts from a clean topology.
45+
*
46+
* Walking and freeing comp_list in the same pass is unsafe because the
47+
* SOF free helpers unlink the entry from the list, so we snapshot the
48+
* IDs of one type into a local array first and then call the typed
49+
* free function for each one. Passes run in dependency order
50+
* (COMPONENT -> BUFFER -> PIPELINE) to keep the SOF topology layer
51+
* from dereferencing already-freed parents.
52+
*
53+
* Each typed pass drains in batches: it snapshots up to
54+
* POSIX_TEARDOWN_BATCH ids, frees them, and repeats until no entry of
55+
* that type is left, so the number of objects a testcase may create is
56+
* not bounded by the snapshot size.
57+
*
58+
* A final force-drain pass then walks anything still on the list with
59+
* list_for_item_safe() and removes it with list_item_del() + rfree()
60+
* directly. That keeps the harness future-proof against new COMP_TYPE_*
61+
* values and against entries a typed free could not release: whatever
62+
* the reason, comp_list is guaranteed empty on return. The inner union
63+
* object (cd/cb/pipeline) is leaked in that edge case, which is
64+
* acceptable for a fuzzing harness.
65+
*/
66+
#define POSIX_TEARDOWN_BATCH 256
67+
68+
static void posix_ipc_teardown(void)
69+
{
70+
static const uint16_t free_order[] = {
71+
COMP_TYPE_COMPONENT,
72+
#if CONFIG_IPC_MAJOR_3
73+
/*
74+
* IPC4 never stores COMP_TYPE_BUFFER in comp_list
75+
* (see ipc4/helper.c) and ipc_buffer_free() is not
76+
* compiled for IPC4, so skip the buffer pass there.
77+
*/
78+
COMP_TYPE_BUFFER,
79+
#endif
80+
COMP_TYPE_PIPELINE,
81+
};
82+
uint32_t ids[POSIX_TEARDOWN_BATCH];
83+
struct ipc_comp_dev *icd;
84+
struct list_item *pos, *tmp;
85+
int n;
86+
87+
if (!global_ipc)
88+
return;
89+
90+
/*
91+
* Pre-pass: prepare components and pipelines for the ordered free
92+
* passes below.
93+
*
94+
* ipc_comp_free() refuses to free a component unless its state is
95+
* COMP_STATE_READY. A fuzz testcase that ran INIT_INSTANCE followed
96+
* by SET_PIPELINE_STATE:RUNNING will leave components in PREPARE,
97+
* PAUSED, or ACTIVE state. Force every component back to READY so
98+
* the free pass can proceed without silently skipping entries.
99+
*
100+
* For pipelines with an active scheduler task, cancel the task before
101+
* ipc_pipeline_free() calls schedule_task_free() on it. Without this,
102+
* schedule_task_free() blocks waiting for the task to complete, which
103+
* on native_sim can stall for up to 100 LL periods.
104+
*/
105+
list_for_item(pos, &global_ipc->comp_list) {
106+
icd = container_of(pos, struct ipc_comp_dev, list);
107+
if (icd->type == COMP_TYPE_COMPONENT && icd->cd) {
108+
icd->cd->state = COMP_STATE_READY;
109+
/* Ensure buffer lists are valid so ipc_comp_free()
110+
* does not bail at the uninitialized-list check.
111+
* A component whose init failed partway may have
112+
* NULL list pointers.
113+
*/
114+
if (!icd->cd->bsource_list.next)
115+
list_init(&icd->cd->bsource_list);
116+
if (!icd->cd->bsink_list.next)
117+
list_init(&icd->cd->bsink_list);
118+
}
119+
if (icd->type == COMP_TYPE_PIPELINE &&
120+
icd->pipeline && icd->pipeline->pipe_task)
121+
schedule_task_cancel(icd->pipeline->pipe_task);
122+
}
123+
124+
for (int pass = 0; pass < (int)ARRAY_SIZE(free_order); pass++) {
125+
uint16_t type = free_order[pass];
126+
127+
/*
128+
* Drain this type in batches. The ipc_*_free() helpers
129+
* unlink each entry internally, so snapshot up to
130+
* POSIX_TEARDOWN_BATCH ids, free them, and repeat until no
131+
* entry of this type is left. A batch that frees nothing
132+
* (e.g. an entry ipc_comp_free() refuses to release) ends the
133+
* loop so it cannot spin forever; the force-drain pass below
134+
* removes any such straggler.
135+
*/
136+
do {
137+
int freed = 0;
138+
139+
n = 0;
140+
list_for_item(pos, &global_ipc->comp_list) {
141+
icd = container_of(pos, struct ipc_comp_dev,
142+
list);
143+
if (icd->type == type &&
144+
n < POSIX_TEARDOWN_BATCH)
145+
ids[n++] = icd->id;
146+
}
147+
148+
for (int i = 0; i < n; i++) {
149+
int ret = -EINVAL;
150+
151+
switch (type) {
152+
case COMP_TYPE_COMPONENT:
153+
ret = ipc_comp_free(global_ipc, ids[i]);
154+
break;
155+
#if CONFIG_IPC_MAJOR_3
156+
case COMP_TYPE_BUFFER:
157+
ret = ipc_buffer_free(global_ipc, ids[i]);
158+
break;
159+
#endif
160+
case COMP_TYPE_PIPELINE:
161+
ret = ipc_pipeline_free(global_ipc,
162+
ids[i]);
163+
break;
164+
}
165+
if (!ret)
166+
freed++;
167+
}
168+
169+
if (!freed)
170+
break;
171+
} while (n == POSIX_TEARDOWN_BATCH);
172+
}
173+
174+
/*
175+
* Force-drain anything still on the list: unknown/future
176+
* COMP_TYPE_* values or entries a typed free could not release.
177+
* Walk with the _safe variant because each entry is unlinked as it
178+
* is visited. The inner union pointer leaks, but comp_list is empty
179+
* on return regardless of how many objects remained, so the next
180+
* testcase never observes a stale entry.
181+
*/
182+
list_for_item_safe(pos, tmp, &global_ipc->comp_list) {
183+
icd = container_of(pos, struct ipc_comp_dev, list);
184+
list_item_del(&icd->list);
185+
rfree(icd);
186+
}
187+
}
188+
40189
/*
41190
* Testcase-isolation helpers used by the libFuzzer entry point in
42191
* fuzz.c. They keep ownership of the cross-call state in one module
@@ -45,6 +194,7 @@ static size_t fuzz_in_sz;
45194
*/
46195
void posix_fuzz_case_begin(void)
47196
{
197+
posix_ipc_teardown();
48198
fuzz_in_sz = 0;
49199
}
50200

0 commit comments

Comments
 (0)