aethersdr / aethersdr/AetherSDR

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

Open
#4,289 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

awaiting-response bug
Dominant language
C++
Stars
221
Forks
117
Avg merge
2d 7h
Merged PRs (30d)
299

Description

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() (#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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/core/AudioEngine.cpp at AudioEngine::audioEndpointDiagnostics(), then read MainWindow.cpp and the AutomationServer and DeviceDiagnostics callers to understand thread ownership. Trace the related TX capture state and buffer access, including the write paths mentioned in the issue. Done means the diagnostic snapshot no longer reads audio-thread-owned state or Qt objects concurrently, including the pre-existing reads.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
audio-video-rtc, desktop
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.