[refactor](trx-rs): remove shared-library plugin system

Drop the plugin loading infrastructure (libloading-based dynamic .so/.dylib/.dll
loading) from both trx-server and trx-client. The feature was unused and posed an
unnecessary security risk by executing arbitrary native code from disk.

Removed:
- src/trx-app/src/plugins.rs (plugin discovery, validation, FFI registration)
- examples/trx-plugin-example/ (cdylib example plugin)
- libloading dependency from trx-app
- load_backend_plugins / load_frontend_plugins calls from server and client
- Plugin documentation from README.md and CLAUDE.md

https://claude.ai/code/session_01DTEUpz3XPUeWmz74NeaFgb
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-26 11:41:38 +00:00
committed by Stan Grams
parent 9692e31c8c
commit c8f33b8939
11 changed files with 2 additions and 304 deletions
-1
View File
@@ -14,5 +14,4 @@ toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
dirs = "6"
libloading = "0.8"
thiserror = "2"
-2
View File
@@ -4,10 +4,8 @@
pub mod config;
pub mod logging;
pub mod plugins;
pub mod util;
pub use config::{ConfigError, ConfigFile};
pub use logging::init_logging;
pub use plugins::{load_backend_plugins, load_frontend_plugins};
pub use util::normalize_name;
-189
View File
@@ -1,189 +0,0 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: BSD-2-Clause
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
use libloading::{Library, Symbol};
use tracing::{info, warn};
/// Environment variable to disable plugin loading entirely.
const PLUGIN_DISABLE_ENV: &str = "TRX_PLUGINS_DISABLED";
const PLUGIN_ENV: &str = "TRX_PLUGIN_DIRS";
const BACKEND_ENTRYPOINT: &str = "trx_register_backend";
const FRONTEND_ENTRYPOINT: &str = "trx_register_frontend";
#[cfg(windows)]
const PATH_SEPARATOR: char = ';';
#[cfg(not(windows))]
const PATH_SEPARATOR: char = ':';
#[cfg(windows)]
const PLUGIN_EXTENSIONS: &[&str] = &["dll"];
#[cfg(target_os = "macos")]
const PLUGIN_EXTENSIONS: &[&str] = &["dylib"];
#[cfg(all(unix, not(target_os = "macos")))]
const PLUGIN_EXTENSIONS: &[&str] = &["so"];
pub fn load_backend_plugins(context: NonNull<std::ffi::c_void>) -> Vec<Library> {
load_plugins_for_entrypoint(BACKEND_ENTRYPOINT, context)
}
pub fn load_frontend_plugins(context: NonNull<std::ffi::c_void>) -> Vec<Library> {
load_plugins_for_entrypoint(FRONTEND_ENTRYPOINT, context)
}
fn load_plugins_for_entrypoint(
entrypoint: &str,
context: NonNull<std::ffi::c_void>,
) -> Vec<Library> {
// Allow disabling plugin loading entirely via environment variable.
if std::env::var(PLUGIN_DISABLE_ENV).is_ok() {
info!("Plugin loading disabled via {}", PLUGIN_DISABLE_ENV);
return Vec::new();
}
let mut libraries = Vec::new();
let search_paths = plugin_search_paths();
if search_paths.is_empty() {
return libraries;
}
info!("Plugin search paths: {:?}", search_paths);
for path in search_paths {
if let Err(err) = load_plugins_from_dir(&path, entrypoint, context, &mut libraries) {
warn!("Plugin scan failed for {:?}: {}", path, err);
}
}
libraries
}
fn load_plugins_from_dir(
path: &Path,
entrypoint: &str,
context: NonNull<std::ffi::c_void>,
libraries: &mut Vec<Library>,
) -> std::io::Result<()> {
if !path.exists() {
return Ok(());
}
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if !is_plugin_file(&path) {
continue;
}
// Validate file permissions before loading.
if !validate_plugin_file(&path) {
continue;
}
unsafe {
match Library::new(&path) {
Ok(lib) => {
if let Err(err) = register_library(&lib, &path, entrypoint, context) {
warn!("Plugin {:?} failed to register: {}", path, err);
continue;
}
info!("Loaded plugin {:?}", path);
libraries.push(lib);
}
Err(err) => {
warn!("Failed to load plugin {:?}: {}", path, err);
}
}
}
}
Ok(())
}
unsafe fn register_library(
lib: &Library,
path: &Path,
entrypoint: &str,
context: NonNull<std::ffi::c_void>,
) -> Result<(), String> {
let entry: Symbol<unsafe extern "C" fn(*mut std::ffi::c_void)> = lib
.get(entrypoint.as_bytes())
.map_err(|e| format!("missing entrypoint {}: {}", entrypoint, e))?;
entry(context.as_ptr());
info!("Registered plugin {:?}", path);
Ok(())
}
fn plugin_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(env_paths) = std::env::var(PLUGIN_ENV) {
for raw in env_paths.split(PATH_SEPARATOR) {
if raw.trim().is_empty() {
continue;
}
paths.push(PathBuf::from(raw));
}
}
paths.push(PathBuf::from("plugins"));
if let Some(config_dir) = dirs::config_dir() {
paths.push(config_dir.join("trx-rs").join("plugins"));
}
paths
}
/// Validate plugin file before loading: check ownership and permissions.
///
/// On Unix, reject files that are world-writable (mode & 0o002) to prevent
/// loading tampered libraries. On non-Unix platforms, always accept.
fn validate_plugin_file(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
match std::fs::metadata(path) {
Ok(meta) => {
let mode = meta.mode();
if mode & 0o002 != 0 {
warn!(
"Skipping world-writable plugin {:?} (mode {:o})",
path, mode
);
return false;
}
true
}
Err(e) => {
warn!("Cannot stat plugin {:?}: {}", path, e);
false
}
}
}
#[cfg(not(unix))]
{
let _ = path;
true
}
}
fn is_plugin_file(path: &Path) -> bool {
path.extension()
.and_then(OsStr::to_str)
.map(|ext| {
PLUGIN_EXTENSIONS
.iter()
.any(|e| ext.eq_ignore_ascii_case(e))
})
.unwrap_or(false)
}