0.16 review: four crash-class fixes (PD-1, PD-3, PD-7, SA-1) - #545
Merged
Merged
Conversation
applicationWillTerminate took a shared_ptr copy of the hosted plugin that stayed alive until the method returned - past mainFinish(), which resets the global and the host's references and then calls entry->deinit(). The local copy was therefore the last owner, so ~Plugin ran destroy() on an already-deinited entry and crashed on exit for any plugin whose deinit tears down global state. Windows already released its reference before mainFinish (see the comment in windows_standalone.cpp's WM_DESTROY handler); the macOS path was missed when that fix landed. Scope the reference to the GUI teardown that needs it so the host's own reset inside mainFinish is the last owner. Not compiled: macOS-only source, developed on Windows. Verified by reading mainFinish (entry.cpp) and ~Plugin (clap_proxy.cpp) rather than by build.
Three crash-class bugs in the new preset-discovery subsystem, fixed
together because they are one story: the crawl thread was unsafe to run,
unsafe to stop, and unsafe to be listened to.
PD-1, encoding. fs::path and the CLAP ABI disagree about what a string
is: the ABI is UTF-8, fs::path is whatever the OS says. On Windows
fs::path{std::string} decodes through the ANSI code page, so a plugin
declaring "C:\Users\Jurgen\...\Presets" got a folder that does not exist
and the location was silently skipped; and path::string() *throws*
std::system_error for any name the code page cannot express, which on a
bare thread is std::terminate for the host - one odd filename in a user
folder was enough. Conversions now go through a pathFromUtf8() helper and
u8string(), the way fsutil.cpp already does everywhere. Deliberately not
fs::u8path(), which is deprecated in C++20 and this builds with -Werror;
u8string() stays std::string on every configuration because
shared_prologue.cmake turns char8_t off whenever the standard is >= 20.
The thread body also catches everything now, and still reports completion
when it does, so a waiter does not sit out its timeout over a failed crawl.
PD-3, lifetime. PresetIndex::resetCache() had no callers at all, and the
ModuleTerminator deleted the hosted library - deinit() and unmap - while
the crawl thread was inside provider->get_metadata() in that very module.
Reaper's in-process rescan is where that shows: either the plugin's
globals go out under a running crawl, or the crawl is joined later from
the static IndexCache destructor, which on Windows is under
DLL_PROCESS_DETACH, where the loader lock keeps the thread from exiting
and the scan simply hangs. The terminator now calls resetCache() first.
Clearing the map was not enough on its own, because a wrapper still
holding a shared_ptr keeps its thread alive, so resetCache() takes the
indices out under the cache lock and abandons and joins each one outside
it. ExitDll/bundleExit/ModuleExit are called by the host rather than from
DllMain, so that join is not under the loader lock.
PD-7, listeners. finish() copied the listener list, dropped the lock and
then called the copies - and a copy cannot be recalled, so a wrapper whose
destructor removed its listener could still be called, on freed memory.
Logic and auval dispose an AU about 100 ms after instantiating it, which
lands squarely inside a folder crawl. finish() now looks each listener up
at its turn and publishes which one it is running and on which thread;
removeCompletionListener() waits for exactly that one, and only when the
caller is not the crawl thread, so a listener removing itself does not
wait on itself. Listeners are still called outside the lock, because they
may re-enter the index.
Verified: VST3 target builds clean under C++17, clap-wrapper-shared-detail
under C++20 (the char8_t-sensitive path), clang-format clean. There are no
tests covering PresetIndex, so this is compile-and-reasoning, not runtime
proof.
Pre-existing and left alone: a listener registered in the exact window
where a crawl completes can still be called twice. Both current consumers
only set an idempotent atomic flag.
This was referenced Sep 13, 2026
baconpaul
approved these changes
Sep 14, 2026
Keep the non-obvious why, drop the narration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First of four batches from the 0.16 release review of
main...next. These are the crash-class findings — the ones that take the host down or hang a scan. They are independent of each other and of the rest of the review.What is here
PD-1 — Windows path encoding could kill the host.
fs::pathand the CLAP ABI disagree about what a string is.path::string()throwsstd::system_errorfor any filename the ANSI code page cannot express, and the crawl runs on a barestd::thread, so oneパッド.presetin a user folder wasstd::terminatefor the host. In the other directionfs::path{std::string}decoded a UTF-8 location as ACP, so a plugin declaring a path with a non-ASCII segment had that location silently skipped. Conversions now go throughpathFromUtf8()andu8string(), asfsutil.cppalready did everywhere, and the thread body catches everything.PD-3 — the crawl thread outlived the hosted CLAP.
PresetIndex::resetCache()had no callers, and theModuleTerminatordeleted the library —deinit()and unmap — while the crawl was insideprovider->get_metadata()in that module. Reaper's in-process rescan either tears the plugin's globals down under a running crawl, or joins it later from a static destructor underDLL_PROCESS_DETACH, where the loader lock stops the thread exiting and the scan hangs. Clearing the cache map alone was not enough, since a live wrapper'sshared_ptrkeeps its thread alive.PD-7 — a listener could run on a destroyed wrapper.
finish()copied the listener list, dropped the lock, then called the copies — and a copy cannot be recalled, so a destructor that removed its listener could still be called on freed memory. Logic andauvaldispose an AU about 100 ms after instantiating it, squarely inside a folder crawl.SA-1 — macOS quit crashed.
applicationWillTerminate:held ashared_ptrcopy of the plugin pastmainFinish(), so it was the last owner and~Pluginrandestroy()on an already-deinited entry. Windows already released beforemainFinish(there is a comment about it in theWM_DESTROYhandler); the macOS path was missed when that landed.Verification, and its limits
clap-wrapper-shared-detailbuilds clean under C++20, which is the configuration that matters for theu8string()changes.clang-formatclean.mainFinishinentry.cppand~Plugininclap_proxy.cpp— but it needs a real macOS build before it is trusted.PresetIndex, so the preset fixes are compile-and-reasoning, not runtime proof.Deliberately left alone
A listener registered in the exact window where a crawl completes can still be called twice. That predates this change and both current consumers only set an idempotent atomic flag.
Note on
fs::u8pathNot used, although it is the textbook C++17 idiom: it is deprecated in C++20 and this project builds with
-Werror.u8string()is safe here becauseshared_prologue.cmaketurnschar8_toff whenever the standard is >= 20, so it returnsstd::stringon every configuration this project builds.