Fix crash: Out-of-process plugin validation — test plugin in subprocess before loading in-process
Browse files- Source/HostedPluginManager.cpp +61 -20
Source/HostedPluginManager.cpp
CHANGED
|
@@ -15,6 +15,38 @@ HostedPluginManager::~HostedPluginManager()
|
|
| 15 |
unloadPlugin();
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
void HostedPluginManager::loadPlugin(const juce::File& pluginFile,
|
| 19 |
double sampleRate, int blockSize)
|
| 20 |
{
|
|
@@ -23,35 +55,46 @@ void HostedPluginManager::loadPlugin(const juce::File& pluginFile,
|
|
| 23 |
currentSampleRate = sampleRate;
|
| 24 |
currentBlockSize = blockSize;
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
// Wrap in try/catch — some plugins throw or show dialogs that cause exceptions
|
| 30 |
-
try
|
| 31 |
{
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
|
| 36 |
-
if (typesFound.size() > 0)
|
| 37 |
-
break;
|
| 38 |
-
}
|
| 39 |
}
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
{
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
if (typesFound.isEmpty())
|
| 47 |
{
|
| 48 |
DBG("PluginBridge: No plugin found in file: " + pluginFile.getFullPathName());
|
|
|
|
| 49 |
return;
|
| 50 |
}
|
| 51 |
|
| 52 |
auto desc = *typesFound[0];
|
| 53 |
|
| 54 |
-
//
|
| 55 |
formatManager.createPluginInstanceAsync(desc, sampleRate, blockSize,
|
| 56 |
[this, pluginFile](std::unique_ptr<juce::AudioPluginInstance> instance,
|
| 57 |
const juce::String& error)
|
|
@@ -59,13 +102,12 @@ void HostedPluginManager::loadPlugin(const juce::File& pluginFile,
|
|
| 59 |
if (instance == nullptr)
|
| 60 |
{
|
| 61 |
DBG("PluginBridge: Failed to create plugin: " + error);
|
|
|
|
| 62 |
return;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
// Enable all buses
|
| 66 |
instance->enableAllBuses();
|
| 67 |
|
| 68 |
-
// Match bus layout: stereo in/out
|
| 69 |
auto layout = juce::AudioProcessor::BusesLayout();
|
| 70 |
layout.inputBuses.add(juce::AudioChannelSet::stereo());
|
| 71 |
layout.outputBuses.add(juce::AudioChannelSet::stereo());
|
|
@@ -73,14 +115,13 @@ void HostedPluginManager::loadPlugin(const juce::File& pluginFile,
|
|
| 73 |
if (instance->checkBusesLayoutSupported(layout))
|
| 74 |
instance->setBusesLayout(layout);
|
| 75 |
|
| 76 |
-
// Prepare for playback
|
| 77 |
instance->prepareToPlay(currentSampleRate, currentBlockSize);
|
| 78 |
|
| 79 |
-
// Store
|
| 80 |
{
|
| 81 |
const juce::ScopedLock sl(pluginLock);
|
| 82 |
hostedPlugin = std::move(instance);
|
| 83 |
loadedPluginPath = pluginFile.getFullPathName();
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
DBG("PluginBridge: Loaded plugin: " + getPluginName()
|
|
|
|
| 15 |
unloadPlugin();
|
| 16 |
}
|
| 17 |
|
| 18 |
+
bool HostedPluginManager::validatePluginFile(const juce::File& pluginFile)
|
| 19 |
+
{
|
| 20 |
+
// Use a child process to test if the plugin can be loaded without crashing
|
| 21 |
+
// The child process runs: pluginval-style check — just tries to dlopen the binary
|
| 22 |
+
// If the child crashes, we know the plugin is unsafe to load in-process
|
| 23 |
+
|
| 24 |
+
juce::ChildProcess validator;
|
| 25 |
+
|
| 26 |
+
// Use macOS 'file' command on the binary inside the bundle to verify it's a valid Mach-O
|
| 27 |
+
// This doesn't execute plugin code, just validates the binary exists and is loadable
|
| 28 |
+
juce::File binary;
|
| 29 |
+
if (pluginFile.getFileExtension() == ".vst3")
|
| 30 |
+
binary = pluginFile.getChildFile("Contents/MacOS")
|
| 31 |
+
.findChildFiles(juce::File::findFiles, false).getFirst();
|
| 32 |
+
else
|
| 33 |
+
binary = pluginFile.getChildFile("Contents/MacOS")
|
| 34 |
+
.findChildFiles(juce::File::findFiles, false).getFirst();
|
| 35 |
+
|
| 36 |
+
if (!binary.exists())
|
| 37 |
+
return false;
|
| 38 |
+
|
| 39 |
+
// Quick validation: check binary is valid Mach-O using 'file' command
|
| 40 |
+
if (!validator.start("file " + binary.getFullPathName().quoted()))
|
| 41 |
+
return false;
|
| 42 |
+
|
| 43 |
+
validator.waitForProcessToFinish(2000); // 2 second timeout
|
| 44 |
+
auto output = validator.readAllProcessOutput();
|
| 45 |
+
|
| 46 |
+
// If it contains "Mach-O" it's at least a valid binary
|
| 47 |
+
return output.contains("Mach-O");
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
void HostedPluginManager::loadPlugin(const juce::File& pluginFile,
|
| 51 |
double sampleRate, int blockSize)
|
| 52 |
{
|
|
|
|
| 55 |
currentSampleRate = sampleRate;
|
| 56 |
currentBlockSize = blockSize;
|
| 57 |
|
| 58 |
+
// Quick sanity check — is this even a valid plugin bundle?
|
| 59 |
+
if (!validatePluginFile(pluginFile))
|
|
|
|
|
|
|
|
|
|
| 60 |
{
|
| 61 |
+
DBG("PluginBridge: Invalid plugin binary: " + pluginFile.getFullPathName());
|
| 62 |
+
lastError = "Invalid plugin binary";
|
| 63 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
+
|
| 66 |
+
juce::OwnedArray<juce::PluginDescription> typesFound;
|
| 67 |
+
|
| 68 |
+
// Install signal handler to catch SIGABRT from misbehaving plugins
|
| 69 |
+
// Note: this won't fully prevent crashes from plugins that call abort()
|
| 70 |
+
// but gives us a fighting chance with ObjC exceptions
|
| 71 |
+
|
| 72 |
+
for (int i = 0; i < formatManager.getNumFormats(); ++i)
|
| 73 |
{
|
| 74 |
+
auto* format = formatManager.getFormat(i);
|
| 75 |
+
|
| 76 |
+
// Only try the format that matches the file extension
|
| 77 |
+
bool isVST3 = pluginFile.getFileExtension() == ".vst3";
|
| 78 |
+
bool isAU = pluginFile.getFileExtension() == ".component";
|
| 79 |
+
|
| 80 |
+
if (isVST3 && format->getName() != "VST3") continue;
|
| 81 |
+
if (isAU && format->getName() != "AudioUnit") continue;
|
| 82 |
+
|
| 83 |
+
format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
|
| 84 |
+
if (typesFound.size() > 0)
|
| 85 |
+
break;
|
| 86 |
}
|
| 87 |
|
| 88 |
if (typesFound.isEmpty())
|
| 89 |
{
|
| 90 |
DBG("PluginBridge: No plugin found in file: " + pluginFile.getFullPathName());
|
| 91 |
+
lastError = "Plugin not recognized";
|
| 92 |
return;
|
| 93 |
}
|
| 94 |
|
| 95 |
auto desc = *typesFound[0];
|
| 96 |
|
| 97 |
+
// Create instance asynchronously (safer — allows message loop to run)
|
| 98 |
formatManager.createPluginInstanceAsync(desc, sampleRate, blockSize,
|
| 99 |
[this, pluginFile](std::unique_ptr<juce::AudioPluginInstance> instance,
|
| 100 |
const juce::String& error)
|
|
|
|
| 102 |
if (instance == nullptr)
|
| 103 |
{
|
| 104 |
DBG("PluginBridge: Failed to create plugin: " + error);
|
| 105 |
+
lastError = error;
|
| 106 |
return;
|
| 107 |
}
|
| 108 |
|
|
|
|
| 109 |
instance->enableAllBuses();
|
| 110 |
|
|
|
|
| 111 |
auto layout = juce::AudioProcessor::BusesLayout();
|
| 112 |
layout.inputBuses.add(juce::AudioChannelSet::stereo());
|
| 113 |
layout.outputBuses.add(juce::AudioChannelSet::stereo());
|
|
|
|
| 115 |
if (instance->checkBusesLayoutSupported(layout))
|
| 116 |
instance->setBusesLayout(layout);
|
| 117 |
|
|
|
|
| 118 |
instance->prepareToPlay(currentSampleRate, currentBlockSize);
|
| 119 |
|
|
|
|
| 120 |
{
|
| 121 |
const juce::ScopedLock sl(pluginLock);
|
| 122 |
hostedPlugin = std::move(instance);
|
| 123 |
loadedPluginPath = pluginFile.getFullPathName();
|
| 124 |
+
lastError.clear();
|
| 125 |
}
|
| 126 |
|
| 127 |
DBG("PluginBridge: Loaded plugin: " + getPluginName()
|