RAM2118 commited on
Commit
ef289db
·
verified ·
1 Parent(s): 48e03fd

Fix crash: Out-of-process plugin validation — test plugin in subprocess before loading in-process

Browse files
Files changed (1) hide show
  1. 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
- juce::String errorMessage;
27
- juce::OwnedArray<juce::PluginDescription> typesFound;
28
-
29
- // Wrap in try/catch — some plugins throw or show dialogs that cause exceptions
30
- try
31
  {
32
- for (int i = 0; i < formatManager.getNumFormats(); ++i)
33
- {
34
- auto* format = formatManager.getFormat(i);
35
- format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
36
- if (typesFound.size() > 0)
37
- break;
38
- }
39
  }
40
- catch (...)
 
 
 
 
 
 
 
41
  {
42
- DBG("PluginBridge: Exception during plugin scan for: " + pluginFile.getFullPathName());
43
- return;
 
 
 
 
 
 
 
 
 
 
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
- // Use async creation — safer for AU plugins and plugins that need message thread
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()