mockforge_plugin_core/
runtime.rs

1//! WebAssembly runtime for plugin execution
2//!
3//! This module provides the WebAssembly runtime environment for secure
4//! execution of MockForge plugins. It handles plugin loading, sandboxing,
5//! and communication between the host and plugin code.
6
7use crate::{
8    PluginCapabilities, PluginContext, PluginError, PluginHealth, PluginId, PluginManifest,
9    PluginMetrics, PluginResult, PluginState, Result,
10};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::{Arc, OnceLock};
14use tokio::sync::RwLock;
15use tracing;
16use wasmtime::{
17    Config, Engine, Linker, Module, PoolingAllocationConfig, ResourceLimiter, Store, StoreLimits,
18    StoreLimitsBuilder,
19};
20use wasmtime_wasi::p2::WasiCtxBuilder;
21use wasmtime_wasi::preview1::{self, WasiP1Ctx};
22use wasmtime_wasi::{DirPerms, FilePerms};
23
24/// WebAssembly runtime for plugin execution
25pub struct PluginRuntime {
26    /// WebAssembly engine (lazy-initialized only when plugins are loaded)
27    engine: OnceLock<Engine>,
28    /// Active plugin instances
29    plugins: RwLock<HashMap<PluginId, Arc<RwLock<PluginInstance>>>>,
30    /// Runtime configuration
31    config: RuntimeConfig,
32}
33
34impl PluginRuntime {
35    /// Create a new plugin runtime
36    ///
37    /// Note: The WebAssembly engine is lazy-initialized on first plugin load
38    /// to avoid unnecessary memory allocation when no plugins are used.
39    pub fn new(config: RuntimeConfig) -> Result<Self> {
40        Ok(Self {
41            engine: OnceLock::new(),
42            plugins: RwLock::new(HashMap::new()),
43            config,
44        })
45    }
46
47    /// Get or initialize the WebAssembly engine
48    fn get_engine(&self) -> &Engine {
49        self.engine.get_or_init(|| {
50            // Lazy initialization: only create engine when first plugin is loaded
51            // Configure engine with security and resource limits
52            let mut config = Config::new();
53
54            // Enable fuel consumption tracking for CPU time limits
55            config.consume_fuel(true);
56
57            // Enable epoch-based interruption for wall clock timeouts
58            config.epoch_interruption(true);
59
60            // Enable memory limiting
61            config.max_wasm_stack(2 * 1024 * 1024); // 2MB stack limit
62
63            // Disable features that could be security risks
64            config.wasm_threads(false);
65            config.wasm_bulk_memory(true); // Allow for efficiency
66            config.wasm_simd(false); // Disable SIMD for now
67            config.wasm_multi_memory(false);
68
69            // Enable pooling allocator for better performance and memory isolation
70            config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(
71                PoolingAllocationConfig::default(),
72            ));
73
74            Engine::new(&config).expect("Failed to create WASM engine with security config")
75        })
76    }
77
78    /// Load a plugin from WebAssembly module
79    pub async fn load_plugin(
80        &self,
81        plugin_id: PluginId,
82        manifest: PluginManifest,
83        wasm_path: &Path,
84    ) -> Result<()> {
85        // Security: Validate plugin path is within allowed directories
86        self.validate_plugin_path(wasm_path)?;
87
88        // Security: Check file size limits
89        self.validate_file_size(wasm_path)?;
90
91        // Validate plugin capabilities against runtime limits
92        let plugin_capabilities = PluginCapabilities::from_strings(&manifest.capabilities);
93        self.validate_capabilities(&plugin_capabilities)?;
94
95        // Security: Validate manifest integrity
96        self.validate_manifest_security(&manifest)?;
97
98        // Load WASM module with additional validation
99        // This will lazy-initialize the engine if it hasn't been created yet
100        let engine = self.get_engine();
101        let module = Module::from_file(engine, wasm_path)
102            .map_err(|e| PluginError::wasm(format!("Failed to load WASM module: {}", e)))?;
103
104        // Security: Validate module against declared capabilities
105        ModuleValidator::validate_module(&module, &plugin_capabilities)?;
106
107        // Security: Check for dangerous imports/exports
108        self.validate_module_security(&module)?;
109
110        // Create plugin instance
111        let instance =
112            PluginInstance::new(plugin_id.clone(), manifest, module, self.config.clone()).await?;
113
114        // Store plugin instance
115        let mut plugins = self.plugins.write().await;
116        #[allow(clippy::arc_with_non_send_sync)]
117        plugins.insert(plugin_id, Arc::new(RwLock::new(instance)));
118
119        Ok(())
120    }
121
122    /// Unload a plugin
123    pub async fn unload_plugin(&self, plugin_id: &PluginId) -> Result<()> {
124        let mut plugins = self.plugins.write().await;
125        if let Some(instance) = plugins.remove(plugin_id) {
126            let mut instance = instance.write().await;
127            instance.unload().await?;
128        }
129        Ok(())
130    }
131
132    /// Execute a plugin function
133    pub async fn execute_plugin_function<T>(
134        &self,
135        plugin_id: &PluginId,
136        function_name: &str,
137        context: &PluginContext,
138        input: &[u8],
139    ) -> Result<PluginResult<T>>
140    where
141        T: serde::de::DeserializeOwned,
142    {
143        let plugins = self.plugins.read().await;
144        let instance = plugins
145            .get(plugin_id)
146            .ok_or_else(|| PluginError::execution("Plugin not found"))?;
147
148        let mut instance = instance.write().await;
149        instance.execute_function(function_name, context, input).await
150    }
151
152    /// Get plugin health status
153    pub async fn get_plugin_health(&self, plugin_id: &PluginId) -> Result<PluginHealth> {
154        let plugins = self.plugins.read().await;
155        let instance = plugins
156            .get(plugin_id)
157            .ok_or_else(|| PluginError::execution("Plugin not found"))?;
158
159        let instance = instance.read().await;
160        Ok(instance.get_health().await)
161    }
162
163    /// List loaded plugins
164    pub async fn list_plugins(&self) -> Vec<PluginId> {
165        let plugins = self.plugins.read().await;
166        plugins.keys().cloned().collect()
167    }
168
169    /// Get plugin metrics
170    pub async fn get_plugin_metrics(&self, plugin_id: &PluginId) -> Result<PluginMetrics> {
171        let plugins = self.plugins.read().await;
172        let instance = plugins
173            .get(plugin_id)
174            .ok_or_else(|| PluginError::execution("Plugin not found"))?;
175
176        let instance = instance.read().await;
177        Ok(instance.metrics.clone())
178    }
179
180    /// Validate plugin capabilities against runtime limits
181    fn validate_capabilities(&self, capabilities: &PluginCapabilities) -> Result<()> {
182        // Check memory limits
183        if capabilities.resources.max_memory_bytes > self.config.max_memory_per_plugin {
184            return Err(PluginError::security(format!(
185                "Plugin memory limit {} exceeds runtime limit {}",
186                capabilities.resources.max_memory_bytes, self.config.max_memory_per_plugin
187            )));
188        }
189
190        // Check CPU limits
191        if capabilities.resources.max_cpu_percent > self.config.max_cpu_per_plugin {
192            return Err(PluginError::security(format!(
193                "Plugin CPU limit {:.2}% exceeds runtime limit {:.2}%",
194                capabilities.resources.max_cpu_percent, self.config.max_cpu_per_plugin
195            )));
196        }
197
198        // Check execution time limits
199        if capabilities.resources.max_execution_time_ms > self.config.max_execution_time_ms {
200            return Err(PluginError::security(format!(
201                "Plugin execution time limit {}ms exceeds runtime limit {}ms",
202                capabilities.resources.max_execution_time_ms, self.config.max_execution_time_ms
203            )));
204        }
205
206        // Check network permissions
207        if capabilities.network.allow_http && !self.config.allow_network_access {
208            return Err(PluginError::security(
209                "Plugin requires network access but runtime disallows it",
210            ));
211        }
212
213        Ok(())
214    }
215
216    /// Security: Validate plugin path is within allowed directories
217    fn validate_plugin_path(&self, wasm_path: &Path) -> Result<()> {
218        let canonicalized = wasm_path
219            .canonicalize()
220            .map_err(|e| PluginError::security(format!("Invalid plugin path: {}", e)))?;
221
222        // Check if path is within allowed plugin directories
223        if self.config.allowed_fs_paths.is_empty() {
224            return Err(PluginError::security("No allowed plugin paths configured"));
225        }
226
227        for allowed_path in &self.config.allowed_fs_paths {
228            if canonicalized.starts_with(allowed_path) {
229                return Ok(());
230            }
231        }
232
233        Err(PluginError::security(format!(
234            "Plugin path {} is not within allowed directories",
235            canonicalized.display()
236        )))
237    }
238
239    /// Security: Check file size limits
240    fn validate_file_size(&self, wasm_path: &Path) -> Result<()> {
241        let metadata = std::fs::metadata(wasm_path).map_err(|e| {
242            PluginError::security(format!("Cannot read plugin file metadata: {}", e))
243        })?;
244
245        const MAX_PLUGIN_SIZE: u64 = 50 * 1024 * 1024; // 50MB limit
246        if metadata.len() > MAX_PLUGIN_SIZE {
247            return Err(PluginError::security(format!(
248                "Plugin file size {} exceeds maximum allowed size {}",
249                metadata.len(),
250                MAX_PLUGIN_SIZE
251            )));
252        }
253
254        Ok(())
255    }
256
257    /// Security: Validate manifest integrity and security properties
258    fn validate_manifest_security(&self, manifest: &PluginManifest) -> Result<()> {
259        // Validate plugin name contains only safe characters
260        if !manifest.info.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
261            return Err(PluginError::security("Plugin name contains unsafe characters"));
262        }
263
264        // Check for dangerous capabilities
265        let dangerous_caps = ["raw_syscalls", "kernel_access", "direct_memory"];
266        for cap in &manifest.capabilities {
267            if dangerous_caps.contains(&cap.as_str()) {
268                return Err(PluginError::security(format!(
269                    "Dangerous capability not allowed: {}",
270                    cap
271                )));
272            }
273        }
274
275        // Validate author field exists and is reasonable
276        if manifest.info.author.name.is_empty() || manifest.info.author.name.len() > 100 {
277            return Err(PluginError::security("Invalid author field in manifest"));
278        }
279
280        // Validate plugin ID format
281        if manifest.info.id.0.is_empty() || manifest.info.id.0.len() > 100 {
282            return Err(PluginError::security("Invalid plugin ID format"));
283        }
284
285        // Validate description length
286        if manifest.info.description.len() > 1000 {
287            return Err(PluginError::security("Plugin description too long"));
288        }
289
290        Ok(())
291    }
292
293    /// Security: Check for dangerous imports/exports in WASM module
294    fn validate_module_security(&self, module: &Module) -> Result<()> {
295        // Check imports for dangerous functions
296        for import in module.imports() {
297            match import.module() {
298                "env" => {
299                    // Allow basic environment functions
300                    match import.name() {
301                        "memory" | "table" => continue,
302                        name if name.starts_with("__") => {
303                            return Err(PluginError::security(format!(
304                                "Dangerous import function: {}",
305                                name
306                            )));
307                        }
308                        _ => continue,
309                    }
310                }
311                "wasi_snapshot_preview1" => {
312                    // Allow standard WASI functions
313                    continue;
314                }
315                module_name => {
316                    return Err(PluginError::security(format!(
317                        "Dangerous import module: {}",
318                        module_name
319                    )));
320                }
321            }
322        }
323
324        // Check exports for required functions
325        let mut has_init = false;
326        let mut has_process = false;
327
328        for export in module.exports() {
329            match export.name() {
330                "init" => has_init = true,
331                "process" => has_process = true,
332                name if name.starts_with("_") => {
333                    return Err(PluginError::security(format!(
334                        "Private export function not allowed: {}",
335                        name
336                    )));
337                }
338                _ => continue,
339            }
340        }
341
342        if !has_init || !has_process {
343            return Err(PluginError::security("Plugin must export 'init' and 'process' functions"));
344        }
345
346        Ok(())
347    }
348}
349
350/// Runtime configuration
351#[derive(Debug, Clone)]
352pub struct RuntimeConfig {
353    /// Maximum memory per plugin (bytes)
354    pub max_memory_per_plugin: usize,
355    /// Maximum CPU usage per plugin (0.0-1.0)
356    pub max_cpu_per_plugin: f64,
357    /// Maximum execution time per plugin (milliseconds)
358    pub max_execution_time_ms: u64,
359    /// Allow network access
360    pub allow_network_access: bool,
361    /// Allowed filesystem paths for plugins (empty for no access)
362    pub allowed_fs_paths: Vec<String>,
363    /// Maximum concurrent plugin executions
364    pub max_concurrent_executions: usize,
365    /// Plugin cache directory
366    pub cache_dir: Option<String>,
367    /// Enable debug logging
368    pub debug_logging: bool,
369}
370
371impl Default for RuntimeConfig {
372    fn default() -> Self {
373        Self {
374            max_memory_per_plugin: 10 * 1024 * 1024, // 10MB
375            max_cpu_per_plugin: 0.5,                 // 50% of one core
376            max_execution_time_ms: 5000,             // 5 seconds
377            allow_network_access: false,
378            allowed_fs_paths: vec![],
379            max_concurrent_executions: 10,
380            cache_dir: None,
381            debug_logging: false,
382        }
383    }
384}
385
386/// WASI context with resource limits
387pub struct WasiCtxWithLimits {
388    /// WASI P1 context
389    wasi: WasiP1Ctx,
390    /// Store limits
391    limits: StoreLimits,
392}
393
394impl WasiCtxWithLimits {
395    fn new(wasi: WasiP1Ctx, limits: StoreLimits) -> Self {
396        Self { wasi, limits }
397    }
398}
399
400/// Implement ResourceLimiter to enforce memory limits
401impl ResourceLimiter for WasiCtxWithLimits {
402    fn memory_growing(
403        &mut self,
404        current: usize,
405        desired: usize,
406        _maximum: Option<usize>,
407    ) -> anyhow::Result<bool> {
408        // Check if the desired memory growth exceeds our limits
409        self.limits.memory_growing(current, desired, _maximum)
410    }
411
412    fn table_growing(
413        &mut self,
414        current: usize,
415        desired: usize,
416        _maximum: Option<usize>,
417    ) -> anyhow::Result<bool> {
418        // Check if the desired table growth exceeds our limits
419        self.limits.table_growing(current, desired, _maximum)
420    }
421}
422
423/// Plugin instance wrapper
424pub struct PluginInstance {
425    /// Plugin ID
426    #[allow(dead_code)]
427    plugin_id: PluginId,
428    /// Plugin manifest
429    #[allow(dead_code)]
430    manifest: PluginManifest,
431    /// WebAssembly instance with WASI support
432    instance: wasmtime::Instance,
433    /// WebAssembly store with WASI context and limits
434    store: Store<WasiCtxWithLimits>,
435    /// Plugin state
436    state: PluginState,
437    /// Plugin metrics
438    metrics: PluginMetrics,
439    /// Runtime configuration
440    #[allow(dead_code)]
441    config: RuntimeConfig,
442    /// Creation time
443    #[allow(dead_code)]
444    created_at: chrono::DateTime<chrono::Utc>,
445    /// Execution limits
446    limits: ExecutionLimits,
447}
448
449impl PluginInstance {
450    /// Create a new plugin instance
451    async fn new(
452        plugin_id: PluginId,
453        manifest: PluginManifest,
454        module: Module,
455        config: RuntimeConfig,
456    ) -> Result<Self> {
457        // Create execution limits from config
458        let limits = ExecutionLimits {
459            memory_limit: config.max_memory_per_plugin,
460            cpu_time_limit: config.max_execution_time_ms * 1_000_000, // Convert ms to ns
461            wall_time_limit: config.max_execution_time_ms * 2 * 1_000_000, // 2x for wall time
462            fuel_limit: (config.max_execution_time_ms * 1_000),       // ~1K fuel per ms
463        };
464
465        // Build store limits for memory enforcement
466        let store_limits = StoreLimitsBuilder::new()
467            .memory_size(limits.memory_limit)
468            .table_elements(1000) // Limit table size
469            .instances(1) // Single instance per store
470            .tables(10) // Limit number of tables
471            .memories(1) // Single memory per instance
472            .build();
473
474        // Create WASI context with appropriate permissions
475        let mut wasi_ctx_builder = WasiCtxBuilder::new();
476
477        // Configure WASI based on runtime config
478        let wasi_ctx_builder = wasi_ctx_builder.inherit_stdio();
479
480        // Preopen allowed filesystem paths
481        for path in &config.allowed_fs_paths {
482            wasi_ctx_builder.preopened_dir(
483                Path::new(path),
484                path.as_str(),
485                DirPerms::all(),
486                FilePerms::all(),
487            )?;
488        }
489
490        let wasi_ctx = wasi_ctx_builder.build_p1();
491
492        // Wrap WASI context with resource limits
493        let ctx_with_limits = WasiCtxWithLimits::new(wasi_ctx, store_limits);
494
495        // Create WebAssembly store with WASI context and limits
496        let mut store = Store::new(module.engine(), ctx_with_limits);
497
498        // Set store limiter to enforce memory limits
499        store.limiter(|ctx| &mut ctx.limits);
500
501        // Configure fuel for CPU time limiting
502        store
503            .set_fuel(limits.fuel_limit)
504            .map_err(|e| PluginError::wasm(format!("Failed to set fuel limit: {}", e)))?;
505
506        // Set epoch deadline for wall clock timeout
507        // Epoch is incremented by a background thread in production
508        store.set_epoch_deadline(1);
509
510        // Link WASI functions to the store
511        let mut linker = Linker::<WasiCtxWithLimits>::new(module.engine());
512        preview1::add_to_linker_sync(&mut linker, |t| &mut t.wasi)
513            .map_err(|e| PluginError::wasm(format!("Failed to add WASI to linker: {}", e)))?;
514
515        // Instantiate the module with WASI support
516        let instance = linker.instantiate(&mut store, &module).map_err(|e| {
517            PluginError::wasm(format!("Failed to instantiate WASM module with WASI: {}", e))
518        })?;
519
520        // Note: Component model support can be added here when available
521        // The component model will provide better interface definitions and composition
522
523        Ok(Self {
524            plugin_id,
525            manifest,
526            instance,
527            store,
528            state: PluginState::Loaded,
529            metrics: PluginMetrics::default(),
530            config,
531            created_at: chrono::Utc::now(),
532            limits,
533        })
534    }
535
536    /// Execute a plugin function
537    async fn execute_function<T>(
538        &mut self,
539        function_name: &str,
540        context: &PluginContext,
541        _input: &[u8],
542    ) -> Result<PluginResult<T>>
543    where
544        T: serde::de::DeserializeOwned,
545    {
546        let start_time = std::time::Instant::now();
547
548        // Update state
549        self.state = PluginState::Executing;
550        self.metrics.total_executions += 1;
551
552        // Reset fuel before execution to prevent fuel starvation
553        self.store
554            .set_fuel(self.limits.fuel_limit)
555            .map_err(|e| PluginError::execution(format!("Failed to reset fuel: {}", e)))?;
556
557        // Reset epoch deadline for this execution
558        self.store.set_epoch_deadline(1);
559
560        // Prepare input parameters
561        let context_json = serde_json::to_string(context)
562            .map_err(|e| PluginError::execution(format!("Failed to serialize context: {}", e)))?;
563
564        // Execute function (this is a simplified implementation)
565        // In practice, you'd need to handle the WASM calling convention
566        let result = self.call_plugin_function(function_name, &context_json).await;
567
568        // Check remaining fuel to track CPU usage
569        let fuel_consumed = match self.store.get_fuel() {
570            Ok(remaining) => self.limits.fuel_limit.saturating_sub(remaining),
571            Err(_) => 0, // Fuel tracking disabled or error
572        };
573
574        // Update metrics
575        let execution_time = start_time.elapsed();
576        self.metrics.avg_execution_time_ms = (self.metrics.avg_execution_time_ms
577            * (self.metrics.total_executions - 1) as f64
578            + execution_time.as_millis() as f64)
579            / self.metrics.total_executions as f64;
580
581        if execution_time.as_millis() as u64 > self.metrics.max_execution_time_ms {
582            self.metrics.max_execution_time_ms = execution_time.as_millis() as u64;
583        }
584
585        // Update state
586        self.state = PluginState::Ready;
587
588        match result {
589            Ok(output) => {
590                self.metrics.successful_executions += 1;
591                match serde_json::from_slice::<T>(&output) {
592                    Ok(data) => {
593                        tracing::debug!(
594                            "Plugin execution completed: {} fuel consumed, {}ms elapsed",
595                            fuel_consumed,
596                            execution_time.as_millis()
597                        );
598                        Ok(PluginResult::success(data, execution_time.as_millis() as u64))
599                    }
600                    Err(e) => {
601                        self.metrics.failed_executions += 1;
602                        Err(PluginError::execution(format!("Failed to deserialize result: {}", e)))
603                    }
604                }
605            }
606            Err(e) => {
607                self.metrics.failed_executions += 1;
608                tracing::error!(
609                    "Plugin execution failed: {} fuel consumed, {}ms elapsed, error: {}",
610                    fuel_consumed,
611                    execution_time.as_millis(),
612                    e
613                );
614                Err(e)
615            }
616        }
617    }
618
619    /// Call a plugin function (simplified implementation)
620    async fn call_plugin_function(&mut self, function_name: &str, input: &str) -> Result<Vec<u8>> {
621        // Get the exported function from the WASM instance
622        let func = self.instance.get_func(&mut self.store, function_name).ok_or_else(|| {
623            PluginError::execution(format!("Function '{}' not found in WASM module", function_name))
624        })?;
625
626        // Allocate memory in the WASM store for the input string
627        let input_bytes = input.as_bytes();
628        let input_len = input_bytes.len() as i32;
629
630        // Allocate space for the input string in WASM memory
631        let alloc_func = self.instance.get_func(&mut self.store, "alloc").ok_or_else(|| {
632            PluginError::execution(
633                "WASM module must export an 'alloc' function for memory allocation",
634            )
635        })?;
636
637        let mut alloc_result = [wasmtime::Val::I32(0)];
638        alloc_func
639            .call(&mut self.store, &[wasmtime::Val::I32(input_len)], &mut alloc_result)
640            .map_err(|e| {
641                PluginError::execution(format!("Failed to allocate memory for input: {}", e))
642            })?;
643
644        let input_ptr = match alloc_result[0] {
645            wasmtime::Val::I32(ptr) => ptr,
646            _ => {
647                return Err(PluginError::execution("alloc function did not return a valid pointer"))
648            }
649        };
650
651        // Write the input string to WASM memory
652        let memory = self
653            .instance
654            .get_memory(&mut self.store, "memory")
655            .ok_or_else(|| PluginError::execution("WASM module must export a 'memory'"))?;
656
657        memory.write(&mut self.store, input_ptr as usize, input_bytes).map_err(|e| {
658            PluginError::execution(format!("Failed to write input to WASM memory: {}", e))
659        })?;
660
661        // Call the plugin function with the input pointer and length
662        let mut func_result = [wasmtime::Val::I32(0), wasmtime::Val::I32(0)];
663        func.call(
664            &mut self.store,
665            &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
666            &mut func_result,
667        )
668        .map_err(|e| {
669            PluginError::execution(format!(
670                "Failed to call WASM function '{}': {}",
671                function_name, e
672            ))
673        })?;
674
675        // Extract the return values (assuming the function returns (ptr, len))
676        let output_ptr = match func_result[0] {
677            wasmtime::Val::I32(ptr) => ptr,
678            _ => {
679                return Err(PluginError::execution(format!(
680                    "Function '{}' did not return a valid output pointer",
681                    function_name
682                )))
683            }
684        };
685
686        let output_len = match func_result[1] {
687            wasmtime::Val::I32(len) => len,
688            _ => {
689                return Err(PluginError::execution(format!(
690                    "Function '{}' did not return a valid output length",
691                    function_name
692                )))
693            }
694        };
695
696        // Read the output from WASM memory
697        let mut output_bytes = vec![0u8; output_len as usize];
698        memory
699            .read(&mut self.store, output_ptr as usize, &mut output_bytes)
700            .map_err(|e| {
701                PluginError::execution(format!("Failed to read output from WASM memory: {}", e))
702            })?;
703
704        // Deallocate the memory if there's a dealloc function
705        if let Some(dealloc_func) = self.instance.get_func(&mut self.store, "dealloc") {
706            let _ = dealloc_func.call(
707                &mut self.store,
708                &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
709                &mut [],
710            );
711            let _ = dealloc_func.call(
712                &mut self.store,
713                &[
714                    wasmtime::Val::I32(output_ptr),
715                    wasmtime::Val::I32(output_len),
716                ],
717                &mut [],
718            );
719        }
720
721        Ok(output_bytes)
722    }
723
724    /// Get plugin health
725    async fn get_health(&self) -> PluginHealth {
726        PluginHealth::healthy("Plugin is running".to_string(), self.metrics.clone())
727    }
728
729    /// Unload plugin
730    async fn unload(&mut self) -> Result<()> {
731        self.state = PluginState::Unloading;
732        // Cleanup resources here
733        self.state = PluginState::Unloaded;
734        Ok(())
735    }
736}
737
738/// Plugin execution limits
739pub struct ExecutionLimits {
740    /// Memory limit (bytes)
741    pub memory_limit: usize,
742    /// CPU time limit (nanoseconds)
743    pub cpu_time_limit: u64,
744    /// Wall clock time limit (nanoseconds)
745    pub wall_time_limit: u64,
746    /// Fuel limit (WASM execution fuel)
747    pub fuel_limit: u64,
748}
749
750impl Default for ExecutionLimits {
751    fn default() -> Self {
752        Self {
753            memory_limit: 10 * 1024 * 1024,  // 10MB
754            cpu_time_limit: 5_000_000_000,   // 5 seconds
755            wall_time_limit: 10_000_000_000, // 10 seconds
756            fuel_limit: 1_000_000,           // 1M fuel units
757        }
758    }
759}
760
761/// Plugin security context
762pub struct SecurityContext {
763    /// Allowed syscalls
764    pub allowed_syscalls: Vec<String>,
765    /// Blocked syscalls
766    pub blocked_syscalls: Vec<String>,
767    /// Network access policy
768    pub network_policy: NetworkPolicy,
769    /// File system access policy
770    pub filesystem_policy: FilesystemPolicy,
771}
772
773impl Default for SecurityContext {
774    fn default() -> Self {
775        Self {
776            allowed_syscalls: vec![
777                "fd_write".to_string(),
778                "fd_read".to_string(),
779                "random_get".to_string(),
780                "clock_time_get".to_string(),
781            ],
782            blocked_syscalls: vec![
783                "path_open".to_string(),
784                "sock_open".to_string(),
785                "proc_exec".to_string(),
786            ],
787            network_policy: NetworkPolicy::DenyAll,
788            filesystem_policy: FilesystemPolicy::DenyAll,
789        }
790    }
791}
792
793/// Network access policy
794#[derive(Debug, Clone)]
795pub enum NetworkPolicy {
796    /// Allow all network access
797    AllowAll,
798    /// Deny all network access
799    DenyAll,
800    /// Allow access to specific hosts
801    AllowHosts(Vec<String>),
802}
803
804/// File system access policy
805#[derive(Debug, Clone)]
806pub enum FilesystemPolicy {
807    /// Allow all file system access
808    AllowAll,
809    /// Deny all file system access
810    DenyAll,
811    /// Allow access to specific paths
812    AllowPaths(Vec<String>),
813}
814
815/// WASM module validator
816pub struct ModuleValidator;
817
818impl ModuleValidator {
819    /// Validate a WASM module for security against declared capabilities
820    pub fn validate_module(module: &Module, capabilities: &PluginCapabilities) -> Result<()> {
821        // Check for dangerous imports based on capabilities
822        Self::validate_imports(module, capabilities)?;
823
824        Ok(())
825    }
826
827    /// Validate WASM imports against plugin capabilities
828    fn validate_imports(module: &Module, capabilities: &PluginCapabilities) -> Result<()> {
829        for import in module.imports() {
830            let module_name = import.module();
831            let field_name = import.name();
832
833            match module_name {
834                "wasi_snapshot_preview1" | "wasi:io/streams" | "wasi:filesystem/types" => {
835                    Self::validate_wasi_import(field_name, capabilities)?;
836                }
837                "mockforge:plugin/host" => {
838                    // Host functions are generally allowed
839                    Self::validate_host_import(field_name)?;
840                }
841                _ => {
842                    return Err(PluginError::security(format!(
843                        "Disallowed import module: {}",
844                        module_name
845                    )));
846                }
847            }
848        }
849
850        Ok(())
851    }
852
853    /// Validate WASI imports against capabilities
854    fn validate_wasi_import(field_name: &str, capabilities: &PluginCapabilities) -> Result<()> {
855        // Check filesystem operations
856        let filesystem_functions = [
857            "fd_read",
858            "fd_write",
859            "fd_close",
860            "fd_fdstat_get",
861            "path_open",
862            "path_readlink",
863            "path_filestat_get",
864        ];
865
866        if filesystem_functions.contains(&field_name)
867            && capabilities.filesystem.read_paths.is_empty()
868            && capabilities.filesystem.write_paths.is_empty()
869        {
870            return Err(PluginError::security(format!(
871                "Plugin imports filesystem function '{}' but has no filesystem capabilities",
872                field_name
873            )));
874        }
875
876        // Allow other safe WASI functions
877        let allowed_functions = [
878            "fd_read",
879            "fd_write",
880            "fd_close",
881            "fd_fdstat_get",
882            "path_open",
883            "path_readlink",
884            "path_filestat_get",
885            "clock_time_get",
886            "proc_exit",
887            "random_get",
888        ];
889
890        if !allowed_functions.contains(&field_name) {
891            return Err(PluginError::security(format!("Disallowed WASI function: {}", field_name)));
892        }
893
894        Ok(())
895    }
896
897    /// Validate host function imports
898    fn validate_host_import(field_name: &str) -> Result<()> {
899        let allowed_functions = [
900            "log_message",
901            "get_config_value",
902            "store_data",
903            "retrieve_data",
904        ];
905
906        if !allowed_functions.contains(&field_name) {
907            return Err(PluginError::security(format!("Disallowed host function: {}", field_name)));
908        }
909
910        Ok(())
911    }
912
913    /// Extract plugin interface from WASM module
914    pub fn extract_plugin_interface(module: &Module) -> Result<PluginInterface> {
915        let mut functions = Vec::new();
916
917        // Iterate over module exports to find functions
918        for export in module.exports() {
919            if let wasmtime::ExternType::Func(func_type) = export.ty() {
920                // Convert WASM parameter types to our ValueType
921                let parameters: Vec<ValueType> = func_type
922                    .params()
923                    .filter_map(|param| match param {
924                        wasmtime::ValType::I32 => Some(ValueType::I32),
925                        wasmtime::ValType::I64 => Some(ValueType::I64),
926                        wasmtime::ValType::F32 => Some(ValueType::F32),
927                        wasmtime::ValType::F64 => Some(ValueType::F64),
928                        _ => {
929                            // For now, skip unsupported types (like V128, Ref, etc.)
930                            // In a full implementation, you might want to handle these
931                            None
932                        }
933                    })
934                    .collect();
935
936                // Convert WASM return type (assuming single return for simplicity)
937                let return_type = func_type.results().next().and_then(|result| match result {
938                    wasmtime::ValType::I32 => Some(ValueType::I32),
939                    wasmtime::ValType::I64 => Some(ValueType::I64),
940                    wasmtime::ValType::F32 => Some(ValueType::F32),
941                    wasmtime::ValType::F64 => Some(ValueType::F64),
942                    _ => {
943                        // Skip unsupported return types
944                        None
945                    }
946                });
947
948                functions.push(PluginFunction {
949                    name: export.name().to_string(),
950                    signature: FunctionSignature {
951                        parameters,
952                        return_type,
953                    },
954                    documentation: None, // Could be extracted from custom sections in the future
955                });
956            }
957        }
958
959        Ok(PluginInterface { functions })
960    }
961}
962
963/// Plugin interface description
964#[derive(Debug, Clone)]
965pub struct PluginInterface {
966    /// Available functions
967    pub functions: Vec<PluginFunction>,
968}
969
970/// Plugin function description
971#[derive(Debug, Clone)]
972pub struct PluginFunction {
973    /// Function name
974    pub name: String,
975    /// Function signature
976    pub signature: FunctionSignature,
977    /// Documentation
978    pub documentation: Option<String>,
979}
980
981/// Function signature
982#[derive(Debug, Clone)]
983pub struct FunctionSignature {
984    /// Parameter types
985    pub parameters: Vec<ValueType>,
986    /// Return type
987    pub return_type: Option<ValueType>,
988}
989
990/// WASM value type
991#[derive(Debug, Clone)]
992pub enum ValueType {
993    /// 32-bit integer
994    I32,
995    /// 64-bit integer
996    I64,
997    /// 32-bit float
998    F32,
999    /// 64-bit float
1000    F64,
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005
1006    #[test]
1007    fn test_module_compiles() {
1008        // Basic compilation test
1009    }
1010}