MY LLM WROTE:
Describe the bug
If an audio-device error throws while the client is running, the client crashes — not when the error happens, but up to a minute later, while the error dialog is on screen.
This is not #3754. I found it while investigating that issue and checked the connection specifically. #3754 reports BUG IN CLIENT OF LIBMALLOC: not an allocated block at __cxa_finalize_ranges → exit; this is a use-after-scope at timer dispatch. I ran the same chain in stock builds under macOS MallocErrorAbort/MallocGuardEdges and glibc MALLOC_CHECK_=3 and never got an allocator message, which follows once you notice the dangling object is on the stack, so no allocator check can fire. Same code region and same intermittent character, different crash.
CClientDlg is a stack object in main() (main.cpp:1008). A CGenErr thrown from the asynchronous audio-reinit path unwinds main()'s try (main.cpp:978) and destroys it. The handler at main.cpp:1107 then calls QMessageBox::critical (main.cpp:1113), which runs QDialog::exec() — a nested event loop that keeps dispatching timers, and which stays open until the user clicks Quit.
Meanwhile clientdlg.cpp:869 holds a pending timer:
QTimer::singleShot ( 60000, [this]() { lblUpdateCheck->hide(); } );
That is the 2-argument overload. It captures this but registers no context object, so Qt cannot auto-cancel it when CClientDlg is destroyed. When it comes due inside the nested loop it dereferences the destroyed dialog, and the client crashes.
The timer is only armed after connecting to a server that reports a strictly newer, suffix-free version, which is why this is intermittent rather than constant.
It is the only bare call site of its kind in the tree — serverdlg.cpp:924 and :930 already use the 3-argument context-aware form.
To Reproduce
Two things have to overlap: the update-check label must be showing, and an audio-device exception must escape within the timer's 60 seconds.
The second needs no special setup. On JACK, stopping JACK under a running client runs jack_on_shutdown → CSound::shutdownCallback (sound/jack/sound.cpp:463) → EmitReinitRequestSignal ( RS_ONLY_RESTART_AND_INIT ) → CClient::OnSndCrdReinitRequest (client.cpp:849) → Init() → CSound::Init() → throw CGenErr (sound/jack/sound.cpp:338). On CoreAudio the same slot reaches throw CGenErr at sound/coreaudio-mac/sound.cpp:917 and :925 on a buffer-size mismatch. I observed the JACK route happening on its own, with no instrumentation of the throw: the reinit slot entered, both warnings printed, and the dialog destroyed by the unwind.
To measure it rather than wait for luck, I built with two test-only hooks — one driving the real OnCLVersionAndOSReceived slot with version 99.9.9, one throwing from the reinit slot — and shortened the timer. Nothing about the two defects themselves was modified.
Expected behavior
No crash. A timer whose callback touches a widget should not outlive that widget.
Screenshots
Not applicable.
Operating system
Reproduced on both, built from source:
- Ubuntu Linux, Qt 5.15.13, offscreen QPA
- macOS 12.7.6, x86_64, Qt 5.15.2
Version of Jamulus
3.12.2dev. All line numbers above are against current main (9d1eca4e), where both defects are unchanged. The singleShot line has been as it is since e9865ff2 (2026-05-14).
Additional context
28/28 crashes under ASan (Linux 20/20, macOS 8/8), with identical stacks on both platforms:
<lambda> clientdlg.cpp:869
QSingleShotTimer::timerEvent
QTimerInfoList::activateTimers
QEventLoop::exec
QDialog::exec <- the QMessageBox
ASan classifies it stack-use-after-scope, READ of size 8 — stack rather than heap, which follows from the dialog living in main()'s frame. Stock builds without ASan crash just as reliably: 15/15 SIGSEGV on Linux, 8/8 EXC_BAD_ACCESS on macOS, with QWidget::hide() on top.
This does not fire on the normal quit path, which is presumably why it has gone unnoticed. I measured that rather than assuming it: a 2 ms heartbeat timer outliving the dialog shows Qt stops dispatching timers within ±2 ms of close() being called, with roughly 110 further ticks due and none delivered, 3/3 on each platform. The error path is what makes the bug reachable, because a modal dialog inside an exception handler is a fully pumping event loop running after stack objects have been unwound.
Two independent one-line fixes:
// clientdlg.cpp:869 - Qt auto-cancels when the context object is destroyed
QTimer::singleShot ( 60000, this, [this]() { lblUpdateCheck->hide(); } );
// client.cpp:856 / 891 - releases the mutex even if an exception unwinds
QMutexLocker locker ( &MutexDriverReinit );
The first stops this crash. The second stops the exception escaping in the first place, and fixes a second problem on that path: the throw skips MutexDriverReinit.unlock() at client.cpp:891, so the mutex is left locked permanently. Every trial prints both Qt has caught an exception thrown from an event handler and QMutex: destroying locked mutex. There are three catch ( CGenErr ) sites — main.cpp:1107, clientdlg.cpp:1254, recorder/jamrecorder.cpp:432 — and none of them covers this asynchronous path.
MY LLM WROTE:
Describe the bug
If an audio-device error throws while the client is running, the client crashes — not when the error happens, but up to a minute later, while the error dialog is on screen.
This is not #3754. I found it while investigating that issue and checked the connection specifically. #3754 reports
BUG IN CLIENT OF LIBMALLOC: not an allocated blockat__cxa_finalize_ranges → exit; this is a use-after-scope at timer dispatch. I ran the same chain in stock builds under macOSMallocErrorAbort/MallocGuardEdgesand glibcMALLOC_CHECK_=3and never got an allocator message, which follows once you notice the dangling object is on the stack, so no allocator check can fire. Same code region and same intermittent character, different crash.CClientDlgis a stack object inmain()(main.cpp:1008). ACGenErrthrown from the asynchronous audio-reinit path unwindsmain()'stry(main.cpp:978) and destroys it. The handler atmain.cpp:1107then callsQMessageBox::critical(main.cpp:1113), which runsQDialog::exec()— a nested event loop that keeps dispatching timers, and which stays open until the user clicks Quit.Meanwhile
clientdlg.cpp:869holds a pending timer:That is the 2-argument overload. It captures
thisbut registers no context object, so Qt cannot auto-cancel it whenCClientDlgis destroyed. When it comes due inside the nested loop it dereferences the destroyed dialog, and the client crashes.The timer is only armed after connecting to a server that reports a strictly newer, suffix-free version, which is why this is intermittent rather than constant.
It is the only bare call site of its kind in the tree —
serverdlg.cpp:924and:930already use the 3-argument context-aware form.To Reproduce
Two things have to overlap: the update-check label must be showing, and an audio-device exception must escape within the timer's 60 seconds.
The second needs no special setup. On JACK, stopping JACK under a running client runs
jack_on_shutdown→CSound::shutdownCallback(sound/jack/sound.cpp:463) →EmitReinitRequestSignal ( RS_ONLY_RESTART_AND_INIT )→CClient::OnSndCrdReinitRequest(client.cpp:849) →Init()→CSound::Init()→throw CGenErr(sound/jack/sound.cpp:338). On CoreAudio the same slot reachesthrow CGenErratsound/coreaudio-mac/sound.cpp:917and:925on a buffer-size mismatch. I observed the JACK route happening on its own, with no instrumentation of the throw: the reinit slot entered, both warnings printed, and the dialog destroyed by the unwind.To measure it rather than wait for luck, I built with two test-only hooks — one driving the real
OnCLVersionAndOSReceivedslot with version99.9.9, one throwing from the reinit slot — and shortened the timer. Nothing about the two defects themselves was modified.Expected behavior
No crash. A timer whose callback touches a widget should not outlive that widget.
Screenshots
Not applicable.
Operating system
Reproduced on both, built from source:
Version of Jamulus
3.12.2dev. All line numbers above are against current
main(9d1eca4e), where both defects are unchanged. ThesingleShotline has been as it is sincee9865ff2(2026-05-14).Additional context
28/28 crashes under ASan (Linux 20/20, macOS 8/8), with identical stacks on both platforms:
ASan classifies it
stack-use-after-scope, READ of size 8 — stack rather than heap, which follows from the dialog living inmain()'s frame. Stock builds without ASan crash just as reliably: 15/15SIGSEGVon Linux, 8/8EXC_BAD_ACCESSon macOS, withQWidget::hide()on top.This does not fire on the normal quit path, which is presumably why it has gone unnoticed. I measured that rather than assuming it: a 2 ms heartbeat timer outliving the dialog shows Qt stops dispatching timers within ±2 ms of
close()being called, with roughly 110 further ticks due and none delivered, 3/3 on each platform. The error path is what makes the bug reachable, because a modal dialog inside an exception handler is a fully pumping event loop running after stack objects have been unwound.Two independent one-line fixes:
The first stops this crash. The second stops the exception escaping in the first place, and fixes a second problem on that path: the throw skips
MutexDriverReinit.unlock()atclient.cpp:891, so the mutex is left locked permanently. Every trial prints bothQt has caught an exception thrown from an event handlerandQMutex: destroying locked mutex. There are threecatch ( CGenErr )sites —main.cpp:1107,clientdlg.cpp:1254,recorder/jamrecorder.cpp:432— and none of them covers this asynchronous path.