@@ -18,7 +18,7 @@ use tokio::sync::oneshot::Sender as OneShotSender;
1818
1919use libloading:: Library ;
2020use std:: ops:: { Deref , DerefMut } ;
21- use std:: path:: Path ;
21+ use std:: path:: { Path , PathBuf } ;
2222use tracing:: { info, warn} ;
2323
2424/// A type alias for the result of plugin manager operations.
@@ -62,11 +62,14 @@ impl DerefMut for LoadedSlipstreamPlugin {
6262pub 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
6770impl 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) ]
377444const 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