#1696·JUCE

[Bug]: TimerThread can post to a MessageManager being destroyed by shutdownJuce_GUI — SIGSEGV when a host unloads a JUCE-built plug-in

Author: jdfCreated Aug 6, 2026Updated Aug 6, 2026

Detailed steps on how to reproduce the bug

When shutdownJuce_GUI() runs while Timer::TimerThread is still alive, the timer thread can call MessageBase::post() against a MessageManager that is concurrently being destroyed, and crash with SIGSEGV inside MessageQueue::post(). The window is opened by shutdownJuce_GUI() itself:

  1. DeletedAtShutdown::deleteAll() destroys ShutdownDetector, whose destructor calls TimerThread::applicationShuttingDown()stopThreadAsync(). This only signals the thread (signalThreadShouldExit() + callbackArrived.signal()); it does not join it. The join lives in ~TimerThread, which only runs when the last SharedResourcePointer<TimerThread> (held per Timer object) is released — i.e. only if every Timer was destroyed before shutdown. ~TimerThread's own jassert acknowledges timers can outlive the event system.
  2. MessageManager::deleteInstance() then runs immediately. ~MessageManager calls doPlatformSpecificShutdown() (destroying the platform MessageQueue) before nulling instance ("do this last…"), so for the whole duration of the destructor, MessageBase::post()'s null-check on MessageManager::instance still passes.
  3. Meanwhile TimerThread::run() can already be past its callbackArrived.wait(...) checks, committed to a messageToSend->post(). There are two sites, and we have captured crashes at both:
    • juce_Timer.cpp:128 (first post): the thread passed callbackArrived.wait(0) just before the shutdown signal landed.
    • juce_Timer.cpp:135 (the 300 ms "message may have been lost, post again" retry): this one is easy to hit in a plug-in during host shutdown, because the wrapper's shutdownJuce_GUI() runs on the host's message thread while the host is no longer servicing the plug-in's queue — the posted CallTimersMessage never runs, callTimers() never signals callbackArrived, so the timer thread reliably sits in the 300 ms timeout and re-posts into the teardown window.

post() reads MessageManager::instance, null-checks it, then calls postMessageToSystemQueue() — a time-of-check/time-of-use race against deleteInstance() on another thread, with the window stretched to the full ~MessageManager duration by the nulling-last ordering.

Reproduction context: a JUCE-based host (statically linked JUCE 8.0.15, hosting via AudioPluginFormatManager) loads a JUCE-built VST3, opens its editor, and quits. Destroying the last AudioPluginInstance unloads the module, which runs shutdownJuce_GUI() in the plug-in's copy of JUCE; any timer still registered at that moment arms the race. Our automated gate (load plug-in → open editor → run 8 s → quit) hit it twice in one evening of runs, at both post sites. The plug-in is a trivial test compressor; nothing about it is exotic. Any host/plug-in pair where both are JUCE-built should be able to reproduce it by cycling quit-with-plugin-loaded, e.g. AudioPluginHost with the demo plug-in.

A possible fix: applicationShuttingDown() could join rather than signal (run()'s waits are all bounded at ≤ 300 ms, and callTimers() runs on the message thread — the caller — so a join from there cannot deadlock and completes within one loop iteration). Alternatively, post() and deleteInstance() could synchronize over the queue's lifetime.

What is the expected behaviour?

Unloading a plug-in module (or any shutdownJuce_GUI()) must not crash: the timer thread should be joined, or the message queue's destruction synchronized with MessageBase::post(), before the MessageManager dies.

Operating systems

macOS

What versions of the operating systems?

macOS 26.5.2 (25F84)

Architectures

Arm64/aarch64

Stacktrace

Crash 1 — the 300 ms retry post (juce_Timer.cpp:135)
Thread "JUCE v8.0.15: Timer", EXC_BAD_ACCESS / SIGSEGV, KERN_INVALID_ADDRESS at 0x0

libsystem_pthread.dylib      pthread_mutex_lock
<plugin>                     juce::CriticalSection::enter() const (juce_SharedCode_posix.h:51)
<plugin>                     juce::GenericScopedLock<juce::CriticalSection>::GenericScopedLock (juce_ScopedLock.h:79)
<plugin>                     juce::ReferenceCountedArray<juce::MessageManager::MessageBase, juce::CriticalSection>::add (juce_ReferenceCountedArray.h:368)
<plugin>                     juce::MessageQueue::post (juce_MessageQueue_mac.h:67)
<plugin>                     juce::MessageManager::postMessageToSystemQueue (juce_MessageManager_mac.mm:436)
<plugin>                     juce::MessageManager::MessageBase::post() (juce_MessageManager.cpp:85)
<plugin>                     juce::Timer::TimerThread::run() (juce_Timer.cpp:135)
<plugin>                     juce::Thread::threadEntryPoint() (juce_Thread.cpp:112)

Crash 2 — the first post (juce_Timer.cpp:128)
Thread "JUCE v8.0.15: Timer", EXC_BAD_ACCESS / SIGSEGV, KERN_INVALID_ADDRESS at 0x0

libsystem_pthread.dylib      pthread_mutex_lock
<plugin>                     juce::CriticalSection::enter() const (juce_SharedCode_posix.h:51)
<plugin>                     juce::GenericScopedLock<juce::CriticalSection>::GenericScopedLock (juce_ScopedLock.h:79)
<plugin>                     juce::ReferenceCountedArray<juce::MessageManager::MessageBase, juce::CriticalSection>::add (juce_ReferenceCountedArray.h:368)
<plugin>                     juce::MessageQueue::post (juce_MessageQueue_mac.h:67)
<plugin>                     juce::MessageManager::postMessageToSystemQueue (juce_MessageManager_mac.mm:436)
<plugin>                     juce::MessageManager::MessageBase::post() (juce_MessageManager.cpp:85)
<plugin>                     juce::Timer::TimerThread::run() (juce_Timer.cpp:128)
<plugin>                     juce::Thread::threadEntryPoint() (juce_Thread.cpp:112)

Plug-in formats (if applicable)

VST3

Plug-in host applications (DAWs) (if applicable)

A custom JUCE-based host (statically linked JUCE 8.0.15). The mechanism applies to any host that unloads the module of a JUCE-built plug-in, and to standalone shutdownJuce_GUI() with live timers.

Testing on the develop branch

The bug is present on the develop branch

(The crashes were observed on 8.0.15; the implicated code in juce_Timer.cppShutdownDetector, stopThreadAsync(), both post sites — and in juce_MessageManager.cppshutdownJuce_GUI() ordering, MessageBase::post(), the nulling-last ~MessageManager — is unchanged on develop as of today, verified against the current sources.)

Code of Conduct

  • I agree to follow the Code of Conduct