Finding
The DSP task writes processed samples to the ring buffer in a spin loop at engine.rs:496-504. RingBuffer::push_frame returns false whenever used + n >= self.capacity (ring_buffer.rs:79). When a decoded frame's sample count n is greater than or equal to the ring buffer's total capacity, the predicate 0 + n >= capacity is true no matter how far the consumer drains, so push_frame can never succeed. The loop yields forever via tokio::task::yield_now(). Its only exit is STATE_STOPPED, which nothing in this path sets automatically.
Evidence
crates/akouo-core/src/engine.rs:500 — the spin-loop write:
if ring.push_frame(&samples) {
crates/akouo-core/src/ring_buffer.rs:79 — the capacity check that can never pass for an oversized frame:
if used + n >= self.capacity { return false; }
Why this matters
A decoder that emits a single frame at least as large as the ring buffer capacity — from a large Symphonia decode buffer, a malformed container, or a deliberately contrived media file — permanently livelocks the DSP task. The output callback drains the buffer to empty and plays silence while the DSP thread spins at 100% CPU; no error event is emitted and no watchdog fires. Under the counter-surveillance threat model a single hostile audio file is enough to wedge the audio engine and burn CPU and battery indefinitely with no user-visible cause — a silent, file-triggered denial of service.
Desired correction
Guard the write loop. If samples.len() >= ring.capacity, split the frame into capacity-sized chunks before writing; if the loop still cannot make progress, emit an error event and exit rather than spinning forever.
Done when: a frame whose sample count exceeds ring_buffer_capacity causes playback to stop with an error event (or to be chunked and played through) rather than hanging the DSP task in an unbounded spin.
Finding
The DSP task writes processed samples to the ring buffer in a spin loop at
engine.rs:496-504.RingBuffer::push_framereturnsfalsewheneverused + n >= self.capacity(ring_buffer.rs:79). When a decoded frame's sample countnis greater than or equal to the ring buffer's total capacity, the predicate0 + n >= capacityistrueno matter how far the consumer drains, sopush_framecan never succeed. The loop yields forever viatokio::task::yield_now(). Its only exit isSTATE_STOPPED, which nothing in this path sets automatically.Evidence
crates/akouo-core/src/engine.rs:500— the spin-loop write:crates/akouo-core/src/ring_buffer.rs:79— the capacity check that can never pass for an oversized frame:Why this matters
A decoder that emits a single frame at least as large as the ring buffer capacity — from a large Symphonia decode buffer, a malformed container, or a deliberately contrived media file — permanently livelocks the DSP task. The output callback drains the buffer to empty and plays silence while the DSP thread spins at 100% CPU; no error event is emitted and no watchdog fires. Under the counter-surveillance threat model a single hostile audio file is enough to wedge the audio engine and burn CPU and battery indefinitely with no user-visible cause — a silent, file-triggered denial of service.
Desired correction
Guard the write loop. If
samples.len() >= ring.capacity, split the frame into capacity-sized chunks before writing; if the loop still cannot make progress, emit an error event and exit rather than spinning forever.Done when: a frame whose sample count exceeds
ring_buffer_capacitycauses playback to stop with an error event (or to be chunked and played through) rather than hanging the DSP task in an unbounded spin.