Three identical field crash dumps (2026-07-17, CFG fail-fast 0xc0000409
param 0xa = GUARD_ICALL_CHECK_FAILURE on the JUCE message thread, plugin
WndProc frame on the stack) traced to the dispatch timeout desync: every
lifecycle call site could give up after 15 s while its op was still queued,
leaving two owners mutating engine/plugin lifecycle state with no ordering
authority.
- New LifecycleExecutor (guide §12 P0): named lifecycle ops, strictly FIFO
on the message thread, engine-generation stamped at submit and no-oped
when stale. The wait is unbounded with a 15 s watchdog that reports and
keeps waiting; false now always means "verifiably did not run" — the
"false but it may still run later" state is gone.
- initialize/doShutdown bump the generation and run as executor ops;
shutdown is the one bounded caller (60 s, then leak the pump — beats
hanging process exit behind a wedged driver call).
- runDeviceLifecycleOp and closeAllPluginEditorWindows route through the
executor; the editor-teardown 15 s give-up (delayed #56 UAF) is removed.
- pinPluginModuleForever: plugin modules are pinned on load so JUCE's
refcount can never unload them mid-session — a queued window/timer
message into an unloaded module is the CFG fail-fast in the dumps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up (PR #113, finding 1): ProbeDeviceOptions constructs and
destroys short-lived ASIO device objects (DeviceSetup::probeDual /
rateSupportedBy createDevice) on the Node thread - the same
create/destroy-off-the-message-thread pattern as the lifecycle ops, and a
probe ASIOAudioIODevice owns the same reset-timer machinery. Wrap it in
runDeviceLifecycleOp for consistency.
Finding 2 (timeout desync - op reported failed may still complete late)
is documented as a known limitation on the helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A null MessageManager (pre-init or mid-shutdown) previously fell through
to inline execution on the caller's thread while reporting success -
exactly the unserialised device teardown this helper prevents. Return
false instead, per the documented unavailable/timeout contract.
Addresses CodeRabbit review on #113.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tester crash (dump 2026-07-15, Focusrite USB ASIO): the driver's deferred
kAsioResetRequest fires a juce::Timer on the addon's JUCE message thread
(ASIOAudioIODevice::timerCallback -> reloadChannelNames) while the Node
thread concurrently destroys the device inside setAudioDevices/stopAudio —
use-after-free, ~5 minutes after every launch.
New runDeviceLifecycleOp() marshals every binding that can create or
destroy a juce::AudioIODevice (setDevice, device-type switches, start/stop,
stream output open/close, extra-input bind/unbind, add/removeSource) onto
the message thread on Windows, serialising them with those timers. Inline
on macOS (dispatch already inline) and Linux (ALSA main-thread contract
unchanged), and inline when already on the message thread to avoid
self-deadlock. Closures capture by value and return through shared_ptr so
a timed-out dispatch that runs late can't touch the caller's dead stack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 8 CodeRabbit findings verified against the code and fixed:
- ChainOps: macOS LoadVST routes its addProcessor through chainMutationMutex
(macOS is a first-class platform; deadlock-safe — a worker holding the
mutex never waits on the Node/main thread there). All four single-slot
workers (LoadVST/NAM/IR/ReplaceIR) now bump chainGeneration so the
executor's foreign-write detection sees direct loads, not just presets.
- Rebuild barrier (beginChainRebuild/endChainRebuild): LoadPreset and
ClearChain arm it before editor teardown; OpenPluginEditor refuses to
open while a teardown+clear/rebuild is pending (#56 window between
closeAllPluginEditorWindows returning and the worker taking the mutex).
- EditorWindows: all slot/processor resolution in editor lambdas runs under
a try_lock of chainMutationMutex (try_lock, never blocking — workers
holding the mutex block-wait on the message thread). Sandbox promotion
bumps chainGeneration. editorWindows map is now message-thread-only
(duplicate-window check and close-erase moved into the queued lambdas).
Null slot->processor recheck after a faulted promotion capture.
- closeAllPluginEditorWindows returns false on refused post / 15s timeout;
ClearChain skips the clear and LoadPreset resolves {success:false}
instead of freeing processors under a live editor.
- AddonContext: dispatchOnMessageThread reports refused-post/timeout;
doShutdown leaves the message thread running when teardown didn't
complete instead of unloading mid-destruction.
- RendererBus::push rejects NaN/Inf/non-positive rates and a step that
underflows to zero; new testRejectsUnusableRates unit case.
Verified: addon builds clean, all 78 JS tests pass (storm, contracts,
executor, N-API fuzz), all 5 engine_units native tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AddonContext (src/audio/addon/): engine/vstHost lifetime + snapshot rule,
the JUCE message thread with the macOS no-pump fork quarantined into ONE
file, the shutdown latch (exposed as isShuttingDown), doShutdown with a UI
teardown hook (NodeAddon points it at the editor-window nuke, #56), and the
pending-async-load registry. NodeAddon keeps using-declarations so the
binding bodies are unchanged. Also fixes SetBackingSpeed's bare `engine`
dereference — the one binding that dodged the file's own snapshot rule.
NapiHelpers (typed extractors argInt/argSlotId/argFiniteFloat/argBool/
argMidiChannel/argMidiByte) + rewrites of the unguarded bindings — the
deep-read §2 fix, done once: SetParameter/SetBypass/RemoveProcessor/
MoveProcessor/SetMultiBypass/SendMidiToSlot/SetGain plus the St-1 routing
quartet (SetPan/SetPostGain/SetBranch/SetBranchSrc). NaN slot ids no longer
coerce to slot 0; MIDI channel/program are range-checked before JUCE.
New gate: tests/napi-arg-fuzz.test.js — table-driven garbage (NaN/Inf/
negative/string/missing/object) against the real addon; chain state must be
byte-identical after the storm and a valid call must still apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>