Skip to content

Commit 2811e1a

Browse files
committed
attempt to resolve error with loading duplicate plugin
1 parent b3c0902 commit 2811e1a

File tree

1 file changed

+84
-9
lines changed

1 file changed

+84
-9
lines changed

plugins/slipstream_plugin_manager/src/slipstream_manager.rs

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use tokio::sync::oneshot::Sender as OneShotSender;
1818

1919
use libloading::Library;
2020
use std::ops::{Deref, DerefMut};
21-
use std::path::Path;
21+
use std::path::{Path, PathBuf};
2222
use tracing::{info, warn};
2323

2424
/// A type alias for the result of plugin manager operations.
@@ -62,11 +62,14 @@ impl DerefMut for LoadedSlipstreamPlugin {
6262
pub struct SlipstreamPluginManager {
6363
pub plugins: Vec<LoadedSlipstreamPlugin>,
6464
libs: Vec<Library>,
65+
/// Resolved, absolute paths to the loaded `.so` files, parallel to `plugins` / `libs`.
66+
/// Used to detect duplicate loads before calling `dlopen`, preventing unsafe double-loading.
67+
libpaths: Vec<PathBuf>,
6568
}
6669

6770
impl SlipstreamPluginManager {
6871
pub fn new() -> Self {
69-
SlipstreamPluginManager { plugins: Vec::default(), libs: Vec::default() }
72+
SlipstreamPluginManager { plugins: Vec::default(), libs: Vec::default(), libpaths: Vec::default() }
7073
}
7174

7275
/// Unload all plugins and loaded plugin libraries, making sure to fire
@@ -80,6 +83,8 @@ impl SlipstreamPluginManager {
8083
for lib in self.libs.drain(..) {
8184
drop(lib);
8285
}
86+
87+
self.libpaths.clear();
8388
}
8489

8590
/// Check which plugins are interested in regular mapping data.
@@ -167,10 +172,24 @@ impl SlipstreamPluginManager {
167172
&mut self,
168173
slipstream_plugin_config_file: impl AsRef<Path>,
169174
) -> JsonRpcResult<String> {
175+
// Resolve the library path from the config before calling dlopen.
176+
// This lets us detect duplicates without loading the library a second time, which is
177+
// unsafe: a second dlopen on an already-loaded .so can trigger re-execution of Rust
178+
// .init_array startup code, corrupting global state in the running plugin instance.
179+
let resolved_libpath =
180+
resolve_libpath_from_config(slipstream_plugin_config_file.as_ref())?;
181+
182+
// Check for duplicate library path first (catches same .so before dlopen).
183+
if let Some(idx) = self.libpaths.iter().position(|p| p == &resolved_libpath) {
184+
return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(
185+
self.plugins[idx].name().to_string(),
186+
));
187+
}
188+
170189
let (new_lib, mut new_plugin, new_config_file) =
171190
load_plugin_from_config(slipstream_plugin_config_file.as_ref())?;
172191

173-
// Ensure no plugin with this name is already loaded.
192+
// Also guard against a different .so that happens to expose the same plugin name.
174193
if self.plugins.iter().any(|plugin| plugin.name().eq(new_plugin.name())) {
175194
return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(
176195
new_plugin.name().to_string(),
@@ -184,6 +203,7 @@ impl SlipstreamPluginManager {
184203
let name = new_plugin.name().to_string();
185204
self.plugins.push(new_plugin);
186205
self.libs.push(new_lib);
206+
self.libpaths.push(resolved_libpath);
187207

188208
Ok(name)
189209
}
@@ -204,6 +224,10 @@ impl SlipstreamPluginManager {
204224
return Err(SlipstreamPluginManagerError::PluginNotLoaded(name.to_string()));
205225
};
206226

227+
// Resolve the new library path before unloading, so we can track it after reload.
228+
let new_resolved_libpath = resolve_libpath_from_config(config_file.as_ref())
229+
.map_err(|e| SlipstreamPluginManagerError::PluginLoadError(e.to_string()))?;
230+
207231
// Unload the current plugin first.
208232
self._drop_plugin(idx);
209233

@@ -212,7 +236,7 @@ impl SlipstreamPluginManager {
212236
load_plugin_from_config(config_file.as_ref())
213237
.map_err(|e| SlipstreamPluginManagerError::PluginLoadError(e.to_string()))?;
214238

215-
// Ensure no plugin with this name is already loaded.
239+
// Ensure no other plugin with this name is already loaded.
216240
if self.plugins.iter().any(|plugin| plugin.name().eq(new_plugin.name())) {
217241
return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(
218242
new_plugin.name().to_string(),
@@ -226,13 +250,15 @@ impl SlipstreamPluginManager {
226250

227251
self.plugins.push(new_plugin);
228252
self.libs.push(new_lib);
253+
self.libpaths.push(new_resolved_libpath);
229254

230255
Ok(())
231256
}
232257

233258
fn _drop_plugin(&mut self, idx: usize) {
234259
let current_lib = self.libs.remove(idx);
235260
let mut current_plugin = self.plugins.remove(idx);
261+
self.libpaths.remove(idx);
236262
let name = current_plugin.name().to_string();
237263
current_plugin.on_unload();
238264
// The plugin must be dropped before the library to avoid a crash.
@@ -294,6 +320,47 @@ pub enum SlipstreamPluginManagerError {
294320
PluginStartError(String),
295321
}
296322

323+
/// Parses a plugin config file and returns the resolved, absolute path to the `.so`.
324+
///
325+
/// Does NOT open or load the library — safe to call for duplicate detection before `dlopen`.
326+
pub(crate) fn resolve_libpath_from_config(
327+
slipstream_plugin_config_file: &Path,
328+
) -> Result<PathBuf, SlipstreamPluginManagerError> {
329+
use std::{fs::File, io::Read};
330+
331+
let mut file = File::open(slipstream_plugin_config_file).map_err(|e| {
332+
SlipstreamPluginManagerError::CannotOpenConfigFile(format!(
333+
"Failed to open the plugin config file {slipstream_plugin_config_file:?}, error: {e:?}"
334+
))
335+
})?;
336+
337+
let mut contents = String::new();
338+
file.read_to_string(&mut contents).map_err(|e| {
339+
SlipstreamPluginManagerError::CannotReadConfigFile(format!(
340+
"Failed to read the plugin config file {slipstream_plugin_config_file:?}, error: {e:?}"
341+
))
342+
})?;
343+
344+
let result: serde_json::Value = json5::from_str(&contents).map_err(|e| {
345+
SlipstreamPluginManagerError::InvalidConfigFileFormat(format!(
346+
"The config file {slipstream_plugin_config_file:?} is not in a valid Json5 format, error: {e:?}"
347+
))
348+
})?;
349+
350+
let libpath_str = result["libpath"].as_str().ok_or(SlipstreamPluginManagerError::LibPathNotSet)?;
351+
let mut libpath = PathBuf::from(libpath_str);
352+
if libpath.is_relative() {
353+
let config_dir = slipstream_plugin_config_file.parent().ok_or_else(|| {
354+
SlipstreamPluginManagerError::CannotOpenConfigFile(format!(
355+
"Failed to resolve parent of {slipstream_plugin_config_file:?}",
356+
))
357+
})?;
358+
libpath = config_dir.join(libpath);
359+
}
360+
361+
Ok(libpath)
362+
}
363+
297364
/// # Safety
298365
///
299366
/// This function loads the dynamically linked library specified in the path. The library
@@ -376,6 +443,15 @@ const TESTPLUGIN_CONFIG: &str = "TESTPLUGIN_CONFIG";
376443
#[cfg(test)]
377444
const TESTPLUGIN2_CONFIG: &str = "TESTPLUGIN2_CONFIG";
378445

446+
// In tests resolve_libpath_from_config returns the config path itself as a stand-in for
447+
// the .so path. This is sufficient for duplicate detection without real file I/O.
448+
#[cfg(test)]
449+
pub(crate) fn resolve_libpath_from_config(
450+
slipstream_plugin_config_file: &Path,
451+
) -> Result<PathBuf, SlipstreamPluginManagerError> {
452+
Ok(slipstream_plugin_config_file.to_path_buf())
453+
}
454+
379455
// This is mocked for tests to avoid having to do IO with a dynamically linked library
380456
// across different architectures at test time.
381457
#[cfg(test)]
@@ -454,11 +530,10 @@ mod tests {
454530
format!("The plugin '{DUMMY_NAME}' is not loaded")
455531
);
456532

457-
// Mock having loaded plugin (TestPlugin).
458-
let (mut plugin, lib, config) = dummy_plugin_and_library(TestPlugin, DUMMY_CONFIG);
459-
plugin.on_load(config, false).unwrap();
460-
plugin_manager_lock.plugins.push(plugin);
461-
plugin_manager_lock.libs.push(lib);
533+
// Load TestPlugin via the normal path so libpaths is kept in sync.
534+
// (TESTPLUGIN_CONFIG is accepted by the test mock of load_plugin_from_config.)
535+
let load_result = plugin_manager_lock.load_plugin(TESTPLUGIN_CONFIG);
536+
assert!(load_result.is_ok());
462537
assert_eq!(plugin_manager_lock.plugins[0].name(), DUMMY_NAME);
463538

464539
// Try wrong name (same error).

0 commit comments

Comments
 (0)