use super::context::{WasiConfigurer, WasiSpec};
use super::error::WasiError;
use crate::loader::LoaderError;
use sen_plugin_api::{Capabilities, ExecuteResult, PluginManifest, API_VERSION};
use std::path::PathBuf;
use wasmtime::*;
use wasmtime_wasi::preview1::WasiP1Ctx;
use wasmtime_wasi::WasiCtxBuilder;
#[derive(Debug, Clone)]
pub struct WasiLoaderConfig {
pub working_directory: PathBuf,
pub follow_symlinks: bool,
pub require_existence: bool,
pub fuel_limit: u64,
pub max_stack_size: usize,
}
impl Default for WasiLoaderConfig {
fn default() -> Self {
Self {
working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
follow_symlinks: true,
require_existence: true,
fuel_limit: 10_000_000,
max_stack_size: 1024 * 1024, }
}
}
pub struct WasiState {
pub wasi: WasiP1Ctx,
}
impl WasiState {
pub fn from_spec(spec: WasiSpec) -> Result<Self, WasiError> {
let wasi = spec.build_p1_ctx()?;
Ok(Self { wasi })
}
pub fn empty() -> Self {
let wasi = WasiCtxBuilder::new().build_p1();
Self { wasi }
}
}
pub struct WasiPluginLoader {
engine: Engine,
config: WasiLoaderConfig,
}
pub struct WasiLoadedPlugin {
pub manifest: PluginManifest,
pub instance: WasiPluginInstance,
}
pub struct WasiPluginInstance {
engine: Engine,
module: Module,
config: WasiLoaderConfig,
capabilities: Capabilities,
}
#[inline]
fn unpack_ptr_len(packed: i64) -> (i32, i32) {
let ptr = (packed >> 32) as i32;
let len = (packed & 0xFFFFFFFF) as i32;
(ptr, len)
}
impl WasiPluginLoader {
pub fn new(config: WasiLoaderConfig) -> Result<Self, LoaderError> {
let mut engine_config = Config::new();
engine_config.consume_fuel(true);
engine_config.max_wasm_stack(config.max_stack_size);
engine_config.wasm_memory64(false);
let engine = Engine::new(&engine_config).map_err(LoaderError::EngineCreation)?;
Ok(Self { engine, config })
}
pub fn with_working_directory(working_directory: PathBuf) -> Result<Self, LoaderError> {
Self::new(WasiLoaderConfig {
working_directory,
..Default::default()
})
}
pub fn load(&self, wasm_bytes: &[u8]) -> Result<WasiLoadedPlugin, LoaderError> {
let module =
Module::new(&self.engine, wasm_bytes).map_err(LoaderError::ModuleCompilation)?;
let mut store = Store::new(&self.engine, WasiState::empty());
store
.set_fuel(self.config.fuel_limit)
.map_err(|e| LoaderError::StoreConfig(format!("Failed to set fuel: {}", e)))?;
let mut linker: Linker<WasiState> = Linker::new(&self.engine);
wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |state| &mut state.wasi).map_err(
|e| LoaderError::Instantiation(anyhow::anyhow!("Failed to add WASI to linker: {}", e)),
)?;
let instance = linker
.instantiate(&mut store, &module)
.map_err(LoaderError::Instantiation)?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| LoaderError::FunctionNotFound("memory".to_string()))?;
let dealloc_fn = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "plugin_dealloc")
.map_err(|_| LoaderError::FunctionNotFound("plugin_dealloc".to_string()))?;
let manifest_fn = instance
.get_typed_func::<(), i64>(&mut store, "plugin_manifest")
.map_err(|_| LoaderError::FunctionNotFound("plugin_manifest".to_string()))?;
let packed = manifest_fn.call(&mut store, ()).map_err(|e| {
if e.downcast_ref::<Trap>()
.is_some_and(|t| *t == Trap::OutOfFuel)
{
LoaderError::FuelExhausted
} else {
LoaderError::FunctionCall {
function: "plugin_manifest",
source: e,
}
}
})?;
let (ptr, len) = unpack_ptr_len(packed);
if ptr < 0 || len < 0 {
return Err(LoaderError::MemoryAccess(format!(
"Invalid manifest pointer/length: ptr={}, len={}",
ptr, len
)));
}
let manifest_bytes = read_memory(&store, &memory, ptr as usize, len as usize)?;
let manifest: PluginManifest =
rmp_serde::from_slice(&manifest_bytes).map_err(LoaderError::Deserialization)?;
if manifest.api_version != API_VERSION {
return Err(LoaderError::ApiVersionMismatch {
expected: API_VERSION,
actual: manifest.api_version,
});
}
if let Err(e) = dealloc_fn.call(&mut store, (ptr, len)) {
tracing::warn!(error = %e, "Failed to deallocate manifest memory");
}
let capabilities = manifest.capabilities.clone();
Ok(WasiLoadedPlugin {
manifest,
instance: WasiPluginInstance {
engine: self.engine.clone(),
module,
config: self.config.clone(),
capabilities,
},
})
}
}
impl WasiPluginInstance {
pub fn execute(&self, args: &[String]) -> Result<ExecuteResult, LoaderError> {
let spec = WasiConfigurer::new()
.with_capabilities(&self.capabilities)
.with_working_directory(self.config.working_directory.clone())
.with_args(args.to_vec())
.follow_symlinks(self.config.follow_symlinks)
.require_existence(self.config.require_existence)
.build()
.map_err(|e| LoaderError::StoreConfig(format!("WASI configuration failed: {}", e)))?;
tracing::debug!(
capabilities = %spec.permission_summary(),
"Executing plugin with WASI capabilities"
);
let wasi_state = WasiState::from_spec(spec).map_err(|e| {
LoaderError::StoreConfig(format!("WASI context creation failed: {}", e))
})?;
let mut store = Store::new(&self.engine, wasi_state);
store
.set_fuel(self.config.fuel_limit)
.map_err(|e| LoaderError::StoreConfig(format!("Failed to set fuel: {}", e)))?;
let mut linker: Linker<WasiState> = Linker::new(&self.engine);
wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |state| &mut state.wasi).map_err(
|e| LoaderError::Instantiation(anyhow::anyhow!("Failed to add WASI to linker: {}", e)),
)?;
let instance = linker
.instantiate(&mut store, &self.module)
.map_err(LoaderError::Instantiation)?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| LoaderError::FunctionNotFound("memory".to_string()))?;
let alloc_fn = instance
.get_typed_func::<i32, i32>(&mut store, "plugin_alloc")
.map_err(|_| LoaderError::FunctionNotFound("plugin_alloc".to_string()))?;
let dealloc_fn = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "plugin_dealloc")
.map_err(|_| LoaderError::FunctionNotFound("plugin_dealloc".to_string()))?;
let execute_fn = instance
.get_typed_func::<(i32, i32), i64>(&mut store, "plugin_execute")
.map_err(|_| LoaderError::FunctionNotFound("plugin_execute".to_string()))?;
let args_bytes = rmp_serde::to_vec(args)
.map_err(|e| LoaderError::MemoryAccess(format!("Failed to serialize args: {}", e)))?;
let args_len: i32 = args_bytes.len().try_into().map_err(|_| {
LoaderError::MemoryAccess(format!(
"Arguments too large: {} bytes exceeds i32::MAX",
args_bytes.len()
))
})?;
let args_ptr =
alloc_fn
.call(&mut store, args_len)
.map_err(|e| LoaderError::FunctionCall {
function: "plugin_alloc",
source: e,
})?;
memory
.write(&mut store, args_ptr as usize, &args_bytes)
.map_err(|e| LoaderError::MemoryAccess(format!("Failed to write args: {}", e)))?;
let packed = execute_fn
.call(&mut store, (args_ptr, args_len))
.map_err(|e| {
if e.downcast_ref::<Trap>()
.is_some_and(|t| *t == Trap::OutOfFuel)
{
LoaderError::FuelExhausted
} else {
LoaderError::FunctionCall {
function: "plugin_execute",
source: e,
}
}
})?;
let (result_ptr, result_len) = unpack_ptr_len(packed);
if result_ptr < 0 || result_len < 0 {
return Err(LoaderError::MemoryAccess(format!(
"Invalid result pointer/length: ptr={}, len={}",
result_ptr, result_len
)));
}
let result_bytes = read_memory(&store, &memory, result_ptr as usize, result_len as usize)?;
let result: ExecuteResult =
rmp_serde::from_slice(&result_bytes).map_err(LoaderError::Deserialization)?;
if let Err(e) = dealloc_fn.call(&mut store, (args_ptr, args_len)) {
tracing::warn!(error = %e, "Failed to deallocate args memory");
}
if let Err(e) = dealloc_fn.call(&mut store, (result_ptr, result_len)) {
tracing::warn!(error = %e, "Failed to deallocate result memory");
}
Ok(result)
}
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
}
fn read_memory(
store: &Store<WasiState>,
memory: &Memory,
ptr: usize,
len: usize,
) -> Result<Vec<u8>, LoaderError> {
let data = memory.data(store);
let end = ptr.checked_add(len).ok_or_else(|| {
LoaderError::MemoryAccess(format!("Integer overflow: ptr={}, len={}", ptr, len))
})?;
if end > data.len() {
return Err(LoaderError::MemoryAccess(format!(
"Out of bounds: ptr={}, len={}, memory_size={}",
ptr,
len,
data.len()
)));
}
Ok(data[ptr..end].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wasi_loader_creation() {
let loader = WasiPluginLoader::new(WasiLoaderConfig::default());
assert!(loader.is_ok());
}
#[test]
fn test_wasi_state_empty() {
let _state = WasiState::empty();
}
}