Skip to content

AudioEngine::audioEndpointDiagnostics() reads audio-thread state cross-thread (TX capture health snapshot) #4289

Description

@ten9876

Non-blocking follow-up from the review of #4233 / #4251 (both merged). Flagged by @aethersdr-agent and confirmed independently.

Problem

AudioEngine::audioEndpointDiagnostics() (src/core/AudioEngine.cpp) is const and invoked from AutomationServer (get audio) and DeviceDiagnostics on the main/bridge thread, but AudioEngine lives on m_audioThread (MainWindow.cpp m_audio->moveToThread(m_audioThread)). The method reads state that is owned and mutated on the audio thread:

  • m_txCaptureHealth.snapshot() — the TxCaptureHealthTracker's non-atomic members (m_tciSuppressedCallbacks, m_lastMicReadMs, m_currentlySaturated, …), written in onTxAudioReady, recordMicRead, and the queued stateChanged lambda;
  • txCaptureBufferedBytes()m_micDevice->bytesAvailable(), which races the Linux drain's m_micDevice->readAll() ([audio] Fix Linux PC Audio capture stall after TCI TX #4251) mutating the same QIODevice;
  • txCaptureBufferCapacityBytes()m_audioSource->bufferSize(); and (macOS) m_micBuffer->size().

So the bridge thread reads while the audio thread writes → a data race on non-atomic members. Torn reads are diagnostic-only (benign counter values, and m_micDevice is a QPointer so it degrades to null rather than dangling), which is why it wasn't blocking.

Why it's worth fixing

It's consistent with a pre-existing pattern in the same method (it already reads m_audioSource->state() / error(), m_micDevice->isOpen() cross-thread), but the tell is the asymmetry the author deliberately created in #4233/#4251: recordLocalTxAttempt is marshaled onto the owning thread "so diagnostics cannot race readyRead/stateChanged" — yet the read side of the very same data is not marshaled. bytesAvailable() racing readAll() on one QIODevice is also a genuinely unsafe concurrent Qt-object call, not just a torn scalar.

Options

  • Marshal the whole snapshot onto the audio thread (QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection)) and return the assembled values — cleanest, matches the write-side discipline.
  • Or expose the diagnostic-relevant fields as std::atomic and copy the buffer byte counts under a small mutex.
  • Ideally fix the whole method, not just the TX-capture-health additions, so the pre-existing state()/isOpen() reads are covered too.

Low priority / diagnostic-only. Ref: #4233, #4251.

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting-responseWaiting for reporter to provide additional informationbugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions