Skip to content

Commit 37474af

Browse files
authored
Merge pull request #447 from bl4vk-0bsidi4n/fix/harden-plugin-system-220
fix: harden plugin loading mechanics with type-safe paths and panic boundaries #220
2 parents 8055eeb + f3b5bcd commit 37474af

1 file changed

Lines changed: 54 additions & 8 deletions

File tree

src/plugins/loader.rs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4244
impl 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

Comments
 (0)