use cordis::{ErrorCode, PluginHandle, VERSION};
use cordis_include::resolver::unknown_plugin;
use libloading::{Library, Symbol};
use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
pub use crate::export_plugin;
pub use cordis::utils::BoxFuture;
pub use cordis::{Config, Context, Inject, Plugin, PluginOutput, Result};
const ABI_SYMBOL: &[u8] = b"cordis_plugin_abi\0";
const FINGERPRINT_SYMBOL: &[u8] = b"cordis_plugin_fingerprint\0";
const CREATE_SYMBOL: &[u8] = b"cordis_plugin_create\0";
pub const DYNAMIC_ABI: u32 = 1;
type CreateFn = unsafe extern "C" fn() -> *mut c_void;
pub fn fingerprint() -> &'static str {
static FINGERPRINT: OnceLock<String> = OnceLock::new();
FINGERPRINT.get_or_init(|| {
format!(
"cordis-dynamic/{DYNAMIC_ABI} cordis-rs/{VERSION} \
rustc/{}-{} target/{} panic/{}",
env!("CORDIS_RUSTC_RELEASE"),
env!("CORDIS_RUSTC_COMMIT_HASH"),
env!("CORDIS_BUILD_TARGET"),
env!("CORDIS_BUILD_PANIC"),
)
})
}
pub fn plugin_fingerprint_cstr() -> *const c_char {
static FINGERPRINT_C: OnceLock<CString> = OnceLock::new();
FINGERPRINT_C
.get_or_init(|| {
CString::new(fingerprint()).expect("fingerprint without interior NUL")
})
.as_ptr()
}
pub fn create_boxed_plugin(make: impl FnOnce() -> Box<dyn Plugin>) -> *mut c_void {
catch_unwind(AssertUnwindSafe(|| {
let plugin: Box<dyn Plugin> = make();
let guarded: Box<dyn Plugin> = Box::new(PanicGuard { inner: plugin });
let boxed: Box<Box<dyn Plugin>> = Box::new(guarded);
Box::into_raw(boxed) as *mut c_void
}))
.unwrap_or(std::ptr::null_mut())
}
const PANICKED_NAME: &str = "(plugin panicked in name())";
fn empty_inject() -> &'static Inject {
static EMPTY: OnceLock<Inject> = OnceLock::new();
EMPTY.get_or_init(|| Inject::new(Vec::<String>::new()))
}
pub struct PanicGuard {
inner: Box<dyn Plugin>,
}
impl Plugin for PanicGuard {
fn name(&self) -> &str {
catch_unwind(AssertUnwindSafe(|| self.inner.name())).unwrap_or(PANICKED_NAME)
}
fn inject(&self) -> &Inject {
catch_unwind(AssertUnwindSafe(|| self.inner.inject())).unwrap_or_else(|_| empty_inject())
}
fn validate_config(&self, config: Config) -> Result<Config> {
catch_unwind(AssertUnwindSafe(|| self.inner.validate_config(config)))
.unwrap_or_else(|panic| Err(guard_panic("validate_config()", panic)))
}
fn apply(&self, ctx: Context, config: Config) -> BoxFuture<Result<PluginOutput>> {
let inner = catch_unwind(AssertUnwindSafe(|| self.inner.apply(ctx, config)))
.unwrap_or_else(|panic| {
let error = guard_panic("apply()", panic);
Box::pin(async move { Err(error) })
});
Box::pin(GuardedFuture { inner })
}
}
struct GuardedFuture {
inner: BoxFuture<Result<PluginOutput>>,
}
impl std::future::Future for GuardedFuture {
type Output = Result<PluginOutput>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match catch_unwind(AssertUnwindSafe(|| self.inner.as_mut().poll(cx))) {
Ok(poll) => poll,
Err(panic) => std::task::Poll::Ready(Err(guard_panic("apply()", panic))),
}
}
}
fn guard_panic(operation: &str, panic: Box<dyn std::any::Any + Send>) -> cordis::CordisError {
let message = if let Some(text) = panic.downcast_ref::<&str>() {
(*text).to_owned()
} else if let Some(text) = panic.downcast_ref::<String>() {
text.clone()
} else {
"non-string panic payload".to_owned()
};
cordis::CordisError::with_message(
ErrorCode::Plugin,
format!("dynamic plugin panicked in {operation}: {message}"),
)
}
#[macro_export]
macro_rules! export_plugin {
($make:expr) => {
#[unsafe(no_mangle)]
pub extern "C" fn cordis_plugin_abi() -> u32 {
$crate::dynamic::DYNAMIC_ABI
}
#[unsafe(no_mangle)]
pub extern "C" fn cordis_plugin_fingerprint() -> *const ::std::ffi::c_char {
$crate::dynamic::plugin_fingerprint_cstr()
}
#[unsafe(no_mangle)]
pub extern "C" fn cordis_plugin_create() -> *mut ::std::ffi::c_void {
$crate::dynamic::create_boxed_plugin(|| Box::new($make))
}
};
}
#[derive(Clone)]
struct LoadedPlugin {
create: CreateFn,
path: PathBuf,
}
pub struct DynamicPluginResolver {
dirs: Arc<Vec<PathBuf>>,
loaded: Arc<Mutex<HashMap<String, LoadedPlugin>>>,
}
impl DynamicPluginResolver {
pub fn new<I, D>(dirs: I) -> Self
where
I: IntoIterator<Item = D>,
D: Into<PathBuf>,
{
Self {
dirs: Arc::new(dirs.into_iter().map(Into::into).collect()),
loaded: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn dirs(&self) -> &[PathBuf] {
&self.dirs
}
pub fn plugin_path(&self, name: &str) -> Option<PathBuf> {
self.dirs
.iter()
.flat_map(|dir| {
library_file_names(name)
.into_iter()
.map(move |file| dir.join(file))
})
.find(|path| path.is_file())
}
fn loaded_plugin(&self, name: &str) -> Result<LoadedPlugin> {
let mut loaded = crate::lock(&self.loaded);
if let Some(plugin) = loaded.get(name) {
return Ok(plugin.clone());
}
let Some(path) = self.plugin_path(name) else {
return Err(unknown_plugin(name));
};
let plugin = load_library(name, &path)?;
loaded.insert(name.to_owned(), plugin.clone());
Ok(plugin)
}
}
impl Clone for DynamicPluginResolver {
fn clone(&self) -> Self {
Self {
dirs: Arc::clone(&self.dirs),
loaded: Arc::clone(&self.loaded),
}
}
}
impl cordis_include::PluginResolver for DynamicPluginResolver {
fn resolve(&self, name: &str) -> Result<PluginHandle> {
validate_name(name)?;
let loaded = self.loaded_plugin(name)?;
let plugin = create_instance(name, &loaded)?;
Ok(PluginHandle::new(plugin))
}
}
fn validate_name(name: &str) -> Result<()> {
let invalid = name.is_empty()
|| name == "."
|| name == ".."
|| name.chars().any(|c| matches!(c, '/' | '\\' | '\0' | ':'));
if invalid {
return Err(cordis::CordisError::with_message(
ErrorCode::Plugin,
format!("invalid dynamic plugin name `{name}`"),
));
}
Ok(())
}
fn library_file_names(name: &str) -> Vec<String> {
if cfg!(target_os = "macos") {
vec![format!("lib{name}.dylib"), format!("{name}.dylib")]
} else if cfg!(target_os = "windows") {
vec![format!("{name}.dll"), format!("lib{name}.dll")]
} else {
vec![format!("lib{name}.so"), format!("{name}.so")]
}
}
#[allow(unsafe_code)]
fn load_library(name: &str, path: &Path) -> Result<LoadedPlugin> {
let context = format!("dynamic plugin `{name}` ({})", path.display());
let library = unsafe { Library::new(path) }
.map_err(|error| plugin_error(&context, format!("cannot be loaded: {error}")))?;
let abi: Symbol<unsafe extern "C" fn() -> u32> =
unsafe { library.get(ABI_SYMBOL) }.map_err(|error| {
plugin_error(
&context,
format!("does not export `cordis_plugin_abi` (not a cordis plugin?): {error}"),
)
})?;
let fingerprint_sym: Symbol<unsafe extern "C" fn() -> *const c_char> =
unsafe { library.get(FINGERPRINT_SYMBOL) }.map_err(|error| {
plugin_error(
&context,
format!("does not export `cordis_plugin_fingerprint`: {error}"),
)
})?;
let create: Symbol<CreateFn> = unsafe { library.get(CREATE_SYMBOL) }.map_err(|error| {
plugin_error(
&context,
format!("does not export `cordis_plugin_create`: {error}"),
)
})?;
let reported_abi = unsafe { abi() };
if reported_abi != DYNAMIC_ABI {
return Err(plugin_error(
&context,
format!("reports ABI version {reported_abi}, this loader speaks {DYNAMIC_ABI}"),
));
}
let reported_fingerprint = unsafe { fingerprint_sym() };
let reported_fingerprint = if reported_fingerprint.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(reported_fingerprint) }
.to_string_lossy()
.into_owned()
};
if reported_fingerprint != fingerprint() {
return Err(plugin_error(
&context,
format!(
"was built for a different toolchain or cordis version; \
library fingerprint `{reported_fingerprint}` does not match \
this process' `{}`",
fingerprint()
),
));
}
let create = *create;
std::mem::forget(library);
Ok(LoadedPlugin {
create,
path: path.to_path_buf(),
})
}
#[allow(unsafe_code)]
fn create_instance(name: &str, loaded: &LoadedPlugin) -> Result<DynamicPlugin> {
let context = format!("dynamic plugin `{name}` ({})", loaded.path.display());
let pointer = unsafe { (loaded.create)() };
if pointer.is_null() {
return Err(plugin_error(
&context,
"create returned null (constructor panicked?)",
));
}
let boxed: Box<Box<dyn Plugin>> = unsafe { Box::from_raw(pointer as *mut Box<dyn Plugin>) };
Ok(DynamicPlugin { inner: *boxed })
}
struct DynamicPlugin {
inner: Box<dyn Plugin>,
}
impl Plugin for DynamicPlugin {
fn name(&self) -> &str {
self.inner.name()
}
fn inject(&self) -> &Inject {
self.inner.inject()
}
fn validate_config(&self, config: Config) -> Result<Config> {
self.inner.validate_config(config)
}
fn apply(&self, ctx: Context, config: Config) -> BoxFuture<Result<PluginOutput>> {
self.inner.apply(ctx, config)
}
}
fn plugin_error(context: &str, message: impl Into<String>) -> cordis::CordisError {
cordis::CordisError::with_message(ErrorCode::Plugin, format!("{context}: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
use cordis_include::PluginResolver as _;
#[test]
fn fingerprint_pins_every_abi_ingredient() {
let fingerprint = fingerprint();
assert!(fingerprint.contains("cordis-dynamic/"), "{fingerprint}");
assert!(
fingerprint.contains(&format!("cordis-rs/{VERSION}")),
"{fingerprint}"
);
assert!(fingerprint.contains("rustc/"), "{fingerprint}");
assert!(fingerprint.contains("target/"), "{fingerprint}");
assert!(fingerprint.contains("panic/"), "{fingerprint}");
}
#[test]
fn names_that_could_escape_the_directories_are_rejected() {
for name in ["", ".", "..", "../escape", "a/b", "a\\b", "c:drive"] {
assert!(validate_name(name).is_err(), "{name:?}");
}
for name in ["greeter", "my-plugin", "plugin_v2"] {
assert!(validate_name(name).is_ok(), "{name:?}");
}
}
#[test]
fn library_file_names_follow_the_platform_scheme() {
let names = library_file_names("greeter");
if cfg!(target_os = "macos") {
assert_eq!(names, ["libgreeter.dylib", "greeter.dylib"]);
} else if cfg!(target_os = "windows") {
assert_eq!(names, ["greeter.dll", "libgreeter.dll"]);
} else {
assert_eq!(names, ["libgreeter.so", "greeter.so"]);
}
}
#[test]
fn missing_libraries_report_unknown_plugin() {
let resolver = DynamicPluginResolver::new([std::env::temp_dir()]);
let error = resolver
.resolve("definitely_not_a_real_plugin")
.unwrap_err();
assert!(
error.to_string().contains("no plugin registered"),
"{error}"
);
}
#[test]
fn traversal_names_are_rejected_before_touching_the_filesystem() {
let resolver = DynamicPluginResolver::new(["/"]);
let error = resolver.resolve("../etc/passwd").unwrap_err();
assert!(
error.to_string().contains("invalid dynamic plugin name"),
"{error}"
);
}
}