@@ -37,6 +37,8 @@ pub enum PluginLoadError {
3737 } ,
3838 /// The `starforge-plugin.toml` manifest failed validation.
3939 ManifestIncompatible { path : String , detail : String } ,
40+ /// The plugin panicked or crashed unexpectedly during active registration routines.
41+ RegistrationRuntimePanic { path : String , detail : String } ,
4042}
4143
4244impl PluginLoadError {
@@ -48,6 +50,7 @@ impl PluginLoadError {
4850 Self :: AbiBuildMismatch { .. } => "abi_mismatch" ,
4951 Self :: UnsupportedCoreVersion { .. } => "unsupported_core_version" ,
5052 Self :: ManifestIncompatible { .. } => "manifest_incompatible" ,
53+ Self :: RegistrationRuntimePanic { .. } => "runtime_panic" ,
5154 }
5255 }
5356
@@ -85,6 +88,11 @@ impl PluginLoadError {
8588 Detail: {detail}\n \
8689 Fix: Update 'starforge-plugin.toml' to match the running StarForge version.",
8790 ) ,
91+ Self :: RegistrationRuntimePanic { path, detail } => format ! (
92+ "Plugin crashed during registration for '{path}'.\n \
93+ Detail: {detail}\n \
94+ Fix: Review third-party plugin internal setup safety rules or contact the maintainer.",
95+ ) ,
8896 }
8997 }
9098}
@@ -120,7 +128,7 @@ impl PluginManager {
120128 /// # Safety
121129 /// The caller must ensure the plugin at `path` is a valid StarForge plugin
122130 /// compiled with a compatible Rust toolchain and ABI.
123- pub unsafe fn load_plugin < P : AsRef < OsStr > > ( & mut self , path : P ) -> Result < ( ) > {
131+ pub unsafe fn load_plugin < P : AsRef < Path > > ( & mut self , path : P ) -> Result < ( ) > {
124132 self . load_plugin_diagnosed ( path)
125133 . map_err ( |e| anyhow:: anyhow!( "{}" , e) )
126134 }
@@ -130,15 +138,15 @@ impl PluginManager {
130138 ///
131139 /// # Safety
132140 /// Same contract as [`load_plugin`].
133- pub unsafe fn load_plugin_diagnosed < P : AsRef < OsStr > > (
141+ pub unsafe fn load_plugin_diagnosed < P : AsRef < Path > > (
134142 & mut self ,
135143 path : P ,
136144 ) -> std:: result:: Result < ( ) , PluginLoadError > {
137145 let path_ref = path. as_ref ( ) ;
138146 let path_display = path_ref. to_string_lossy ( ) . to_string ( ) ;
139147
140148 // ── Open the shared library ──────────────────────────────────────────
141- let library = Library :: new ( path_ref) . map_err ( |e| PluginLoadError :: InvalidLibrary {
149+ let library = Library :: new ( path_ref. as_os_str ( ) ) . map_err ( |e| PluginLoadError :: InvalidLibrary {
142150 path : path_display. clone ( ) ,
143151 detail : e. to_string ( ) ,
144152 } ) ?;
@@ -174,7 +182,7 @@ impl PluginManager {
174182 }
175183
176184 // ── Manifest compatibility (if present beside the library) ───────────
177- if let Ok ( Some ( mf) ) = manifest:: load_manifest_for_library ( Path :: new ( path_ref) ) {
185+ if let Ok ( Some ( mf) ) = manifest:: load_manifest_for_library ( path_ref) {
178186 mf. validate ( )
179187 . map_err ( |e| PluginLoadError :: ManifestIncompatible {
180188 path : path_display. clone ( ) ,
@@ -183,7 +191,25 @@ impl PluginManager {
183191 }
184192
185193 let mut registrar = ProxyRegistrar :: new ( ) ;
186- ( decl. register ) ( & mut registrar) ;
194+
195+ // Protect the system execution loop from third-party registration panics
196+ let register_result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
197+ ( decl. register ) ( & mut registrar) ;
198+ } ) ) ;
199+
200+ if let Err ( panic_payload) = register_result {
201+ let detail = if let Some ( s) = panic_payload. downcast_ref :: < & str > ( ) {
202+ s. to_string ( )
203+ } else if let Some ( s) = panic_payload. downcast_ref :: < String > ( ) {
204+ s. clone ( )
205+ } else {
206+ "Unknown closure panic origin" . to_string ( )
207+ } ;
208+ return Err ( PluginLoadError :: RegistrationRuntimePanic {
209+ path : path_display,
210+ detail,
211+ } ) ;
212+ }
187213
188214 let plugin_core_version = decl. core_version . to_string ( ) ;
189215 for plugin in registrar. plugins {
@@ -218,7 +244,7 @@ impl PluginManager {
218244 if let Some ( ( plugin, _) ) = self . plugins . get ( name) {
219245 plugin. execute ( args)
220246 } else {
221- Err ( format ! ( "Plugin '{}' not found" , name) )
247+ return Err ( format ! ( "Plugin '{}' not found" , name) ) ;
222248 }
223249 }
224250}
@@ -294,6 +320,15 @@ mod tests {
294320 assert_eq ! ( e. category( ) , "manifest_incompatible" ) ;
295321 }
296322
323+ #[ test]
324+ fn registration_runtime_panic_category ( ) {
325+ let e = PluginLoadError :: RegistrationRuntimePanic {
326+ path : "/tmp/plugin.so" . into ( ) ,
327+ detail : "poisoned pointer access" . into ( ) ,
328+ } ;
329+ assert_eq ! ( e. category( ) , "runtime_panic" ) ;
330+ }
331+
297332 // ── Diagnostic messages contain actionable guidance ──────────────────────
298333
299334 #[ test]
@@ -357,6 +392,17 @@ mod tests {
357392 assert ! ( msg. contains( "0.1.0" ) ) ;
358393 }
359394
395+ #[ test]
396+ fn registration_runtime_panic_diagnostic_mentions_rules ( ) {
397+ let e = PluginLoadError :: RegistrationRuntimePanic {
398+ path : "/tmp/plugin.so" . into ( ) ,
399+ detail : "forced assertion failure" . into ( ) ,
400+ } ;
401+ let msg = e. diagnostic ( ) ;
402+ assert ! ( msg. contains( "crashed during registration" ) ) ;
403+ assert ! ( msg. contains( "forced assertion failure" ) ) ;
404+ }
405+
360406 #[ test]
361407 fn display_matches_diagnostic ( ) {
362408 let e = PluginLoadError :: InvalidLibrary {
@@ -371,12 +417,12 @@ mod tests {
371417 #[ test]
372418 fn nonexistent_path_returns_invalid_library ( ) {
373419 let mut pm = PluginManager :: new ( ) ;
374- let result = unsafe { pm. load_plugin_diagnosed ( "/nonexistent/path/plugin.so" ) } ;
420+ let result = unsafe { pm. load_plugin_diagnosed ( Path :: new ( "/nonexistent/path/plugin.so" ) ) } ;
375421 match result {
376422 Err ( PluginLoadError :: InvalidLibrary { path, .. } ) => {
377423 assert ! ( path. contains( "plugin.so" ) ) ;
378424 }
379425 other => panic ! ( "Expected InvalidLibrary, got {:?}" , other) ,
380426 }
381427 }
382- }
428+ }
0 commit comments