Skip to main content

rich_ext/plugin_loading/
native.rs

1//! Native plugins through the C ABI in [`rich_plugin_api::abi`].
2//!
3//! Opening a library runs its initialisers, and every call runs its code in
4//! this process: nothing here sandboxes it. What this module guarantees is
5//! that only `repr(C)` data crosses the boundary, that a plugin built for
6//! another ABI major is refused before its descriptor is read, and that the
7//! library stays loaded as long as anything registered from it exists.
8
9// Loading a library and calling through its function pointers is unsafe by
10// nature; the checks are in `rich_plugin_api::abi::read_descriptor`.
11#![allow(unsafe_code)]
12
13use std::path::Path;
14use std::sync::Arc;
15
16use rich_plugin_api::abi::{
17    AbiOutput, AbiStr, DYLIB_ENTRY_SYMBOL, PluginDescriptor, PluginFunctions, STATUS_OK,
18    read_descriptor,
19};
20
21use super::{AbiBackend, LoadError, MAX_OUTPUT_BYTES, RuntimeKind, RuntimePlugin};
22
23/// A loaded library and its functions. The vtable's pointers are only valid
24/// while `_library` is loaded, which is why they live together.
25struct NativeBackend {
26    vtable: PluginFunctions,
27    _library: libloading::Library,
28}
29
30impl AbiBackend for NativeBackend {
31    fn call(&self, capability: usize, input: &str, width: u32) -> Result<String, String> {
32        let mut output = AbiOutput::empty();
33        // SAFETY: the vtable came from a descriptor whose ABI version matched,
34        // the library is loaded, and `input` outlives the call.
35        let status =
36            unsafe { (self.vtable.call)(capability, AbiStr::new(input), width, &mut output) };
37        let text = if output.ptr.is_null() {
38            Err("the plugin returned no output".to_string())
39        } else if output.len > MAX_OUTPUT_BYTES {
40            Err(format!(
41                "the plugin returned {} bytes; at most {MAX_OUTPUT_BYTES} are accepted",
42                output.len
43            ))
44        } else {
45            // SAFETY: the plugin hands over `len` initialised bytes at `ptr`.
46            let bytes = unsafe { std::slice::from_raw_parts(output.ptr, output.len) };
47            String::from_utf8(bytes.to_vec()).map_err(|_| "the plugin returned non-UTF-8".into())
48        };
49        if !output.ptr.is_null() {
50            // SAFETY: the output came from this plugin's call; freed once.
51            unsafe { (self.vtable.free)(output) };
52        }
53        match (status, text) {
54            (STATUS_OK, text) => text,
55            (_, Ok(message)) => Err(message),
56            (_, Err(error)) => Err(error),
57        }
58    }
59}
60
61/// Load a native plugin (`.so`, `.dylib`, `.dll`).
62///
63/// **This runs the library's code.** Only load a path the user chose.
64pub fn load_native(path: &Path) -> Result<RuntimePlugin, LoadError> {
65    // Resolve the path first: a bare name would make the loader search the
66    // system library paths instead of opening this file.
67    let path = std::fs::canonicalize(path).map_err(|error| LoadError::Io {
68        path: path.to_path_buf(),
69        message: error.to_string(),
70    })?;
71    // SAFETY: loading runs the library's initialisers. The caller chose this
72    // file; that is the trust decision (see docs/PLUGINS.md).
73    // The file exists (it canonicalized), so a failure here means it is not
74    // a library this system can load: say why, with the loader's own reason.
75    let library =
76        unsafe { libloading::Library::new(&path) }.map_err(|error| LoadError::NotAPlugin {
77            path: path.clone(),
78            message: error_chain(&error),
79        })?;
80    let descriptor = {
81        // SAFETY: the symbol's type is the ABI's entry point type.
82        let entry: libloading::Symbol<unsafe extern "C" fn() -> *const PluginDescriptor> = unsafe {
83            library.get(DYLIB_ENTRY_SYMBOL.as_bytes())
84        }
85        .map_err(|_| LoadError::NotAPlugin {
86            path: path.clone(),
87            message: format!("it does not export {DYLIB_ENTRY_SYMBOL}"),
88        })?;
89        // SAFETY: the entry point takes nothing and returns a descriptor.
90        unsafe { entry() }
91    };
92    // SAFETY: the descriptor points into the library, which is still loaded.
93    let (abi, vtable) = unsafe { read_descriptor(descriptor) }.map_err(|error| LoadError::Abi {
94        path: path.clone(),
95        error,
96    })?;
97    let backend = NativeBackend {
98        vtable,
99        _library: library,
100    };
101    RuntimePlugin::new(abi, RuntimeKind::Native, path, Arc::new(backend))
102}
103
104/// `error` and every `source()` under it: libloading keeps the system
105/// loader's reason (a missing dependency, the wrong architecture) there.
106fn error_chain(error: &dyn std::error::Error) -> String {
107    let mut message = error.to_string();
108    let mut source = error.source();
109    while let Some(cause) = source {
110        let text = cause.to_string();
111        if !message.contains(&text) {
112            message.push_str(": ");
113            message.push_str(&text);
114        }
115        source = cause.source();
116    }
117    message
118}