Skip to main content

kojacoord_plugin_system/
loader.rs

1use crate::api::{Plugin, PluginContext, PluginMetadata};
2use crate::integrity::PluginVerifier;
3use anyhow::{Context, Result};
4use libloading::{Library, Symbol};
5use std::path::Path;
6
7pub struct PluginLoader {
8    libraries: Vec<(String, Library)>,
9    verifier: PluginVerifier,
10}
11
12impl PluginLoader {
13    pub fn new() -> Self {
14        Self {
15            libraries: Vec::new(),
16            verifier: PluginVerifier::new(),
17        }
18    }
19
20    /// Create a loader that verifies plugin binaries against `verifier`.
21    pub fn with_verifier(verifier: PluginVerifier) -> Self {
22        Self {
23            libraries: Vec::new(),
24            verifier,
25        }
26    }
27
28    /// Access the integrity verifier to configure trusted hashes at runtime.
29    pub fn verifier_mut(&mut self) -> &mut PluginVerifier {
30        &mut self.verifier
31    }
32
33    pub fn load_plugin<P: AsRef<Path>>(
34        &mut self,
35        path: P,
36        context: &PluginContext,
37    ) -> Result<(Box<dyn Plugin>, PluginMetadata)> {
38        let path = path.as_ref();
39
40        // Verify the binary's integrity BEFORE mapping it into the process.
41        // A native plugin runs with full privileges, so an untrusted binary is
42        // arbitrary code execution.
43        self.verifier
44            .verify(path)
45            .context("plugin integrity verification failed")?;
46
47        // SAFETY: Loading a native library and calling its FFI entry points is
48        // inherently unsafe — we trust that a verified plugin exports
49        // `get_metadata`/`create_plugin` with the documented C ABI and that
50        // `create_plugin` returns a heap-allocated `*mut dyn Plugin` whose
51        // ownership is transferred to us (reconstructed via `Box::from_raw`).
52        // The library handle is retained in `self.libraries` so the code backing
53        // the returned plugin stays mapped for its lifetime.
54        unsafe {
55            let library = Library::new(path).context("Failed to load plugin library")?;
56
57            let get_metadata: Symbol<unsafe extern "C" fn() -> PluginMetadata> = library
58                .get(b"get_metadata")
59                .context("Missing get_metadata symbol")?;
60
61            let metadata = get_metadata();
62
63            if !self.check_version_compatibility(&metadata.min_proxy_version) {
64                return Err(anyhow::anyhow!(
65                    "Plugin requires proxy version {}, current is {}",
66                    metadata.min_proxy_version,
67                    env!("CARGO_PKG_VERSION")
68                ));
69            }
70
71            let create_plugin: Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> = library
72                .get(b"create_plugin")
73                .context("Missing create_plugin symbol")?;
74
75            let plugin_ptr = create_plugin();
76            if plugin_ptr.is_null() {
77                return Err(anyhow::anyhow!("create_plugin returned a null pointer"));
78            }
79            // Own the plugin as a Box so callers can take `&mut` (needed for
80            // on_load / on_enable / register_packet_hooks / handle_event).
81            let mut plugin: Box<dyn Plugin> = Box::from_raw(plugin_ptr);
82
83            plugin.on_load(context)?;
84
85            self.libraries.push((metadata.name.clone(), library));
86
87            Ok((plugin, metadata))
88        }
89    }
90
91    pub fn unload_all(&mut self) {
92        self.libraries.clear();
93    }
94
95    fn check_version_compatibility(&self, required: &str) -> bool {
96        let current = env!("CARGO_PKG_VERSION");
97        current >= required
98    }
99}
100
101impl Default for PluginLoader {
102    fn default() -> Self {
103        Self::new()
104    }
105}