mockforge_plugin_loader/
sandbox.rs

1//! WebAssembly sandbox for secure plugin execution
2//!
3//! This module provides the sandboxed execution environment for plugins,
4//! including resource limits, security boundaries, and isolation.
5
6use super::*;
7use mockforge_plugin_core::{
8    PluginCapabilities, PluginContext, PluginHealth, PluginId, PluginMetrics, PluginResult,
9    PluginState,
10};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use wasmtime::{Engine, Linker, Module, Store};
15use wasmtime_wasi::p2::{WasiCtx, WasiCtxBuilder};
16
17/// Plugin sandbox for secure execution
18pub struct PluginSandbox {
19    /// WebAssembly engine (optional)
20    engine: Option<Arc<Engine>>,
21    /// Sandbox configuration
22    _config: PluginLoaderConfig,
23    /// Active sandboxes
24    active_sandboxes: RwLock<HashMap<PluginId, SandboxInstance>>,
25}
26
27impl PluginSandbox {
28    /// Create a new plugin sandbox
29    pub fn new(config: PluginLoaderConfig) -> Self {
30        // Create WebAssembly engine for plugin execution
31        let engine = Some(Arc::new(Engine::default()));
32
33        Self {
34            engine,
35            _config: config,
36            active_sandboxes: RwLock::new(HashMap::new()),
37        }
38    }
39
40    /// Create a plugin instance in the sandbox
41    pub async fn create_plugin_instance(
42        &self,
43        context: &PluginLoadContext,
44    ) -> LoaderResult<PluginInstance> {
45        let plugin_id = &context.plugin_id;
46
47        // Check if sandbox already exists
48        {
49            let sandboxes = self.active_sandboxes.read().await;
50            if sandboxes.contains_key(plugin_id) {
51                return Err(PluginLoaderError::already_loaded(plugin_id.clone()));
52            }
53        }
54
55        // Create sandbox instance
56        let sandbox = if let Some(ref engine) = self.engine {
57            SandboxInstance::new(engine, context).await?
58        } else {
59            // Create a stub sandbox instance when WebAssembly is disabled
60            SandboxInstance::stub_new(context).await?
61        };
62
63        // Store sandbox instance
64        let mut sandboxes = self.active_sandboxes.write().await;
65        sandboxes.insert(plugin_id.clone(), sandbox);
66
67        // Create core plugin instance
68        let mut core_instance =
69            mockforge_plugin_core::PluginInstance::new(plugin_id.clone(), context.manifest.clone());
70        core_instance.set_state(PluginState::Ready);
71
72        Ok(core_instance)
73    }
74
75    /// Execute a plugin function in the sandbox
76    pub async fn execute_plugin_function(
77        &self,
78        plugin_id: &PluginId,
79        function_name: &str,
80        context: &PluginContext,
81        input: &[u8],
82    ) -> LoaderResult<PluginResult<serde_json::Value>> {
83        let mut sandboxes = self.active_sandboxes.write().await;
84        let sandbox = sandboxes
85            .get_mut(plugin_id)
86            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
87
88        sandbox.execute_function(function_name, context, input).await
89    }
90
91    /// Get plugin health from sandbox
92    pub async fn get_plugin_health(&self, plugin_id: &PluginId) -> LoaderResult<PluginHealth> {
93        let sandboxes = self.active_sandboxes.read().await;
94        let sandbox = sandboxes
95            .get(plugin_id)
96            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
97
98        Ok(sandbox.get_health().await)
99    }
100
101    /// Destroy a plugin sandbox
102    pub async fn destroy_sandbox(&self, plugin_id: &PluginId) -> LoaderResult<()> {
103        let mut sandboxes = self.active_sandboxes.write().await;
104        if let Some(mut sandbox) = sandboxes.remove(plugin_id) {
105            sandbox.destroy().await?;
106        }
107        Ok(())
108    }
109
110    /// List active sandboxes
111    pub async fn list_active_sandboxes(&self) -> Vec<PluginId> {
112        let sandboxes = self.active_sandboxes.read().await;
113        sandboxes.keys().cloned().collect()
114    }
115
116    /// Get sandbox resource usage
117    pub async fn get_sandbox_resources(
118        &self,
119        plugin_id: &PluginId,
120    ) -> LoaderResult<SandboxResources> {
121        let sandboxes = self.active_sandboxes.read().await;
122        let sandbox = sandboxes
123            .get(plugin_id)
124            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
125
126        Ok(sandbox.get_resources().await)
127    }
128
129    /// Check sandbox health
130    pub async fn check_sandbox_health(&self, plugin_id: &PluginId) -> LoaderResult<SandboxHealth> {
131        let sandboxes = self.active_sandboxes.read().await;
132        let sandbox = sandboxes
133            .get(plugin_id)
134            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
135
136        Ok(sandbox.check_health().await)
137    }
138}
139
140/// Individual sandbox instance
141pub struct SandboxInstance {
142    /// Plugin ID
143    _plugin_id: PluginId,
144    /// WebAssembly module
145    _module: Module,
146    /// WebAssembly store
147    store: Store<WasiCtx>,
148    /// Linker for the instance
149    linker: Linker<WasiCtx>,
150    /// Sandbox resources
151    resources: SandboxResources,
152    /// Health monitor
153    health: SandboxHealth,
154    /// Execution limits
155    limits: ExecutionLimits,
156}
157
158impl SandboxInstance {
159    /// Create a new sandbox instance
160    async fn new(engine: &Engine, context: &PluginLoadContext) -> LoaderResult<Self> {
161        let plugin_id = &context.plugin_id;
162
163        // Load WASM module
164        let module = Module::from_file(engine, &context.plugin_path)
165            .map_err(|e| PluginLoaderError::wasm(format!("Failed to load WASM module: {}", e)))?;
166
167        // Create WASI context
168        let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
169
170        // Create WebAssembly store
171        let mut store = Store::new(engine, wasi_ctx);
172
173        // Create linker
174        let linker = Linker::new(engine);
175
176        // Add WASI functions using the updated API
177        // For now, skip WASI integration until proper wasmtime-wasi version is resolved
178        // This is a non-critical feature for the main MockForge functionality
179
180        // Instantiate the module
181        linker
182            .instantiate(&mut store, &module)
183            .map_err(|e| PluginLoaderError::wasm(format!("Failed to instantiate module: {}", e)))?;
184
185        // Set up execution limits
186        let plugin_capabilities = PluginCapabilities::default();
187        let limits = ExecutionLimits::from_capabilities(&plugin_capabilities);
188
189        Ok(Self {
190            _plugin_id: plugin_id.clone(),
191            _module: module,
192            store,
193            linker,
194            resources: SandboxResources::default(),
195            health: SandboxHealth::healthy(),
196            limits,
197        })
198    }
199
200    /// Create a stub sandbox instance (when WebAssembly is disabled)
201    async fn stub_new(context: &PluginLoadContext) -> LoaderResult<Self> {
202        let plugin_id = &context.plugin_id;
203
204        // Create dummy values for when WebAssembly is disabled
205        let module = Module::new(&Engine::default(), [])
206            .map_err(|e| PluginLoaderError::wasm(format!("Failed to create stub module: {}", e)))?;
207
208        let wasi_ctx = WasiCtxBuilder::new().inherit_stderr().inherit_stdout().build();
209
210        let store = Store::new(&Engine::default(), wasi_ctx);
211        let linker = Linker::new(&Engine::default());
212
213        let plugin_capabilities = PluginCapabilities::default();
214        let limits = ExecutionLimits::from_capabilities(&plugin_capabilities);
215
216        Ok(Self {
217            _plugin_id: plugin_id.clone(),
218            _module: module,
219            store,
220            linker,
221            resources: SandboxResources::default(),
222            health: SandboxHealth::healthy(),
223            limits,
224        })
225    }
226
227    /// Execute a function in the sandbox
228    async fn execute_function(
229        &mut self,
230        function_name: &str,
231        context: &PluginContext,
232        input: &[u8],
233    ) -> LoaderResult<PluginResult<serde_json::Value>> {
234        // Update resource tracking
235        self.resources.execution_count += 1;
236        self.resources.last_execution = chrono::Utc::now();
237
238        // Check execution limits
239        if self.resources.execution_count > self.limits.max_executions {
240            return Err(PluginLoaderError::resource_limit(format!(
241                "Maximum executions exceeded: {} allowed, {} used",
242                self.limits.max_executions, self.resources.execution_count
243            )));
244        }
245
246        // Check time limits
247        let time_since_last = chrono::Utc::now().signed_duration_since(self.resources.created_at);
248        let time_since_last_std =
249            std::time::Duration::from_secs(time_since_last.num_seconds() as u64);
250        if time_since_last_std > self.limits.max_lifetime {
251            return Err(PluginLoaderError::resource_limit(format!(
252                "Maximum lifetime exceeded: {}s allowed, {}s used",
253                self.limits.max_lifetime.as_secs(),
254                time_since_last_std.as_secs()
255            )));
256        }
257
258        // Prepare function call
259        let start_time = std::time::Instant::now();
260
261        // Get function from linker
262        let _func = self.linker.get(&mut self.store, "", function_name).ok_or_else(|| {
263            PluginLoaderError::execution(format!("Function '{}' not found", function_name))
264        })?;
265
266        // Execute function (simplified - real implementation would handle WASM calling conventions)
267        let result = self.call_wasm_function(function_name, context, input).await;
268
269        // Update resource tracking
270        let execution_time = start_time.elapsed();
271        self.resources.total_execution_time += execution_time;
272        self.resources.last_execution_time = execution_time;
273
274        if execution_time > self.resources.max_execution_time {
275            self.resources.max_execution_time = execution_time;
276        }
277
278        match result {
279            Ok(data) => {
280                self.resources.success_count += 1;
281                Ok(PluginResult::success(data, execution_time.as_millis() as u64))
282            }
283            Err(e) => {
284                self.resources.error_count += 1;
285                Ok(PluginResult::failure(e.to_string(), execution_time.as_millis() as u64))
286            }
287        }
288    }
289
290    /// Call WebAssembly function
291    async fn call_wasm_function(
292        &mut self,
293        function_name: &str,
294        context: &PluginContext,
295        input: &[u8],
296    ) -> Result<serde_json::Value, String> {
297        // Serialize context and input for WASM
298        let context_json = serde_json::to_string(context)
299            .map_err(|e| format!("Failed to serialize context: {}", e))?;
300        let combined_input = format!("{}\n{}", context_json, String::from_utf8_lossy(input));
301
302        // Get the exported function from the linker
303        let func_extern = self
304            .linker
305            .get(&mut self.store, "", function_name)
306            .ok_or_else(|| format!("Function '{}' not found in WASM module", function_name))?;
307        let func = func_extern
308            .into_func()
309            .ok_or_else(|| format!("Export '{}' is not a function", function_name))?;
310
311        // Allocate memory in WASM for the input string
312        let input_bytes = combined_input.as_bytes();
313        let input_len = input_bytes.len() as i32;
314
315        // Get alloc function
316        let alloc_extern = self.linker.get(&mut self.store, "", "alloc").ok_or_else(|| {
317            "WASM module must export an 'alloc' function for memory allocation".to_string()
318        })?;
319        let alloc_func = alloc_extern
320            .into_func()
321            .ok_or_else(|| "Export 'alloc' is not a function".to_string())?;
322
323        let mut alloc_result = [wasmtime::Val::I32(0)];
324        alloc_func
325            .call(&mut self.store, &[wasmtime::Val::I32(input_len)], &mut alloc_result)
326            .map_err(|e| format!("Failed to allocate memory for input: {}", e))?;
327
328        let input_ptr = match alloc_result[0] {
329            wasmtime::Val::I32(ptr) => ptr,
330            _ => return Err("alloc function did not return a valid pointer".to_string()),
331        };
332
333        // Write the input string to WASM memory
334        let memory_extern = self
335            .linker
336            .get(&mut self.store, "", "memory")
337            .ok_or_else(|| "WASM module must export a 'memory'".to_string())?;
338        let memory = memory_extern
339            .into_memory()
340            .ok_or_else(|| "Export 'memory' is not a memory".to_string())?;
341
342        memory
343            .write(&mut self.store, input_ptr as usize, input_bytes)
344            .map_err(|e| format!("Failed to write input to WASM memory: {}", e))?;
345
346        // Call the plugin function with the input pointer and length
347        let mut func_result = [wasmtime::Val::I32(0), wasmtime::Val::I32(0)];
348        func.call(
349            &mut self.store,
350            &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
351            &mut func_result,
352        )
353        .map_err(|e| format!("Failed to call WASM function '{}': {}", function_name, e))?;
354
355        // Extract the return values (assuming the function returns (ptr, len))
356        let output_ptr = match func_result[0] {
357            wasmtime::Val::I32(ptr) => ptr,
358            _ => {
359                return Err(format!(
360                    "Function '{}' did not return a valid output pointer",
361                    function_name
362                ))
363            }
364        };
365
366        let output_len = match func_result[1] {
367            wasmtime::Val::I32(len) => len,
368            _ => {
369                return Err(format!(
370                    "Function '{}' did not return a valid output length",
371                    function_name
372                ))
373            }
374        };
375
376        // Read the output from WASM memory
377        let mut output_bytes = vec![0u8; output_len as usize];
378        memory
379            .read(&mut self.store, output_ptr as usize, &mut output_bytes)
380            .map_err(|e| format!("Failed to read output from WASM memory: {}", e))?;
381
382        // Deallocate the memory if there's a dealloc function
383        if let Some(dealloc_extern) = self.linker.get(&mut self.store, "", "dealloc") {
384            if let Some(dealloc_func) = dealloc_extern.into_func() {
385                let _ = dealloc_func.call(
386                    &mut self.store,
387                    &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
388                    &mut [],
389                );
390                let _ = dealloc_func.call(
391                    &mut self.store,
392                    &[
393                        wasmtime::Val::I32(output_ptr),
394                        wasmtime::Val::I32(output_len),
395                    ],
396                    &mut [],
397                );
398            }
399        }
400
401        // Parse the output as JSON
402        let output_str = String::from_utf8(output_bytes)
403            .map_err(|e| format!("Failed to convert output to string: {}", e))?;
404
405        serde_json::from_str(&output_str)
406            .map_err(|e| format!("Failed to parse WASM output as JSON: {}", e))
407    }
408
409    /// Get sandbox health
410    async fn get_health(&self) -> PluginHealth {
411        if self.health.is_healthy {
412            PluginHealth::healthy(
413                "Sandbox is healthy".to_string(),
414                PluginMetrics {
415                    total_executions: self.resources.execution_count,
416                    successful_executions: self.resources.success_count,
417                    failed_executions: self.resources.error_count,
418                    avg_execution_time_ms: self.resources.avg_execution_time_ms(),
419                    max_execution_time_ms: self.resources.max_execution_time.as_millis() as u64,
420                    memory_usage_bytes: self.resources.memory_usage,
421                    peak_memory_usage_bytes: self.resources.peak_memory_usage,
422                },
423            )
424        } else {
425            PluginHealth::unhealthy(
426                PluginState::Error,
427                self.health.last_error.clone(),
428                PluginMetrics::default(),
429            )
430        }
431    }
432
433    /// Get sandbox resources
434    async fn get_resources(&self) -> SandboxResources {
435        self.resources.clone()
436    }
437
438    /// Check sandbox health
439    async fn check_health(&self) -> SandboxHealth {
440        self.health.clone()
441    }
442
443    /// Destroy the sandbox
444    async fn destroy(&mut self) -> LoaderResult<()> {
445        // Cleanup resources
446        self.health.is_healthy = false;
447        self.health.last_error = "Sandbox destroyed".to_string();
448        Ok(())
449    }
450}
451
452/// Sandbox resource tracking
453#[derive(Debug, Clone, Default)]
454pub struct SandboxResources {
455    /// Total execution count
456    pub execution_count: u64,
457    /// Successful execution count
458    pub success_count: u64,
459    /// Error execution count
460    pub error_count: u64,
461    /// Total execution time
462    pub total_execution_time: std::time::Duration,
463    /// Last execution time
464    pub last_execution_time: std::time::Duration,
465    /// Maximum execution time
466    pub max_execution_time: std::time::Duration,
467    /// Current memory usage
468    pub memory_usage: usize,
469    /// Peak memory usage
470    pub peak_memory_usage: usize,
471    /// Creation time
472    pub created_at: chrono::DateTime<chrono::Utc>,
473    /// Last execution time
474    pub last_execution: chrono::DateTime<chrono::Utc>,
475}
476
477impl SandboxResources {
478    /// Get average execution time in milliseconds
479    pub fn avg_execution_time_ms(&self) -> f64 {
480        if self.execution_count == 0 {
481            0.0
482        } else {
483            self.total_execution_time.as_millis() as f64 / self.execution_count as f64
484        }
485    }
486
487    /// Get success rate as percentage
488    pub fn success_rate(&self) -> f64 {
489        if self.execution_count == 0 {
490            0.0
491        } else {
492            (self.success_count as f64 / self.execution_count as f64) * 100.0
493        }
494    }
495
496    /// Check if resource limits are exceeded
497    pub fn check_limits(&self, limits: &ExecutionLimits) -> bool {
498        self.execution_count <= limits.max_executions
499            && self.memory_usage <= limits.max_memory_bytes
500            && self.total_execution_time <= limits.max_total_time
501    }
502}
503
504/// Sandbox health status
505#[derive(Debug, Clone)]
506pub struct SandboxHealth {
507    /// Whether sandbox is healthy
508    pub is_healthy: bool,
509    /// Last health check time
510    pub last_check: chrono::DateTime<chrono::Utc>,
511    /// Last error message
512    pub last_error: String,
513    /// Health check results
514    pub checks: Vec<HealthCheck>,
515}
516
517impl SandboxHealth {
518    /// Create healthy status
519    pub fn healthy() -> Self {
520        Self {
521            is_healthy: true,
522            last_check: chrono::Utc::now(),
523            last_error: String::new(),
524            checks: Vec::new(),
525        }
526    }
527
528    /// Create unhealthy status
529    pub fn unhealthy<S: Into<String>>(error: S) -> Self {
530        Self {
531            is_healthy: false,
532            last_check: chrono::Utc::now(),
533            last_error: error.into(),
534            checks: Vec::new(),
535        }
536    }
537
538    /// Add health check result
539    pub fn add_check(&mut self, check: HealthCheck) {
540        let failed = !check.passed;
541        let error_message = if failed {
542            Some(check.message.clone())
543        } else {
544            None
545        };
546
547        self.checks.push(check);
548        self.last_check = chrono::Utc::now();
549
550        // Update overall health
551        if failed {
552            self.is_healthy = false;
553            if let Some(msg) = error_message {
554                self.last_error = msg;
555            }
556        }
557    }
558
559    /// Run health checks
560    pub async fn run_checks(&mut self, resources: &SandboxResources, limits: &ExecutionLimits) {
561        self.checks.clear();
562
563        // Memory usage check
564        let memory_check = if resources.memory_usage <= limits.max_memory_bytes {
565            HealthCheck::pass("Memory usage within limits")
566        } else {
567            HealthCheck::fail(format!(
568                "Memory usage {} exceeds limit {}",
569                resources.memory_usage, limits.max_memory_bytes
570            ))
571        };
572        self.add_check(memory_check);
573
574        // Execution count check
575        let execution_check = if resources.execution_count <= limits.max_executions {
576            HealthCheck::pass("Execution count within limits")
577        } else {
578            HealthCheck::fail(format!(
579                "Execution count {} exceeds limit {}",
580                resources.execution_count, limits.max_executions
581            ))
582        };
583        self.add_check(execution_check);
584
585        // Success rate check
586        let success_rate = resources.success_rate();
587        let success_check = if success_rate >= 90.0 {
588            HealthCheck::pass(format!("Success rate: {:.1}%", success_rate))
589        } else {
590            HealthCheck::fail(format!("Low success rate: {:.1}%", success_rate))
591        };
592        self.add_check(success_check);
593    }
594}
595
596/// Health check result
597#[derive(Debug, Clone)]
598pub struct HealthCheck {
599    /// Check name
600    pub name: String,
601    /// Whether check passed
602    pub passed: bool,
603    /// Check message
604    pub message: String,
605    /// Check timestamp
606    pub timestamp: chrono::DateTime<chrono::Utc>,
607}
608
609impl HealthCheck {
610    /// Create passing check
611    pub fn pass<S: Into<String>>(message: S) -> Self {
612        Self {
613            name: "health_check".to_string(),
614            passed: true,
615            message: message.into(),
616            timestamp: chrono::Utc::now(),
617        }
618    }
619
620    /// Create failing check
621    pub fn fail<S: Into<String>>(message: S) -> Self {
622        Self {
623            name: "health_check".to_string(),
624            passed: false,
625            message: message.into(),
626            timestamp: chrono::Utc::now(),
627        }
628    }
629}
630
631/// Execution limits for sandbox
632#[derive(Debug, Clone)]
633pub struct ExecutionLimits {
634    /// Maximum number of executions
635    pub max_executions: u64,
636    /// Maximum total execution time
637    pub max_total_time: std::time::Duration,
638    /// Maximum lifetime
639    pub max_lifetime: std::time::Duration,
640    /// Maximum memory usage
641    pub max_memory_bytes: usize,
642    /// Maximum CPU time per execution
643    pub max_cpu_time_per_execution: std::time::Duration,
644}
645
646impl Default for ExecutionLimits {
647    fn default() -> Self {
648        Self {
649            max_executions: 1000,
650            max_total_time: std::time::Duration::from_secs(300), // 5 minutes
651            max_lifetime: std::time::Duration::from_secs(3600),  // 1 hour
652            max_memory_bytes: 10 * 1024 * 1024,                  // 10MB
653            max_cpu_time_per_execution: std::time::Duration::from_secs(5),
654        }
655    }
656}
657
658impl ExecutionLimits {
659    /// Create limits from plugin capabilities
660    pub fn from_capabilities(capabilities: &PluginCapabilities) -> Self {
661        Self {
662            max_executions: 10000, // Override with capability-based limits
663            max_total_time: std::time::Duration::from_secs(600), // 10 minutes
664            max_lifetime: std::time::Duration::from_secs(86400), // 24 hours
665            max_memory_bytes: capabilities.resources.max_memory_bytes,
666            max_cpu_time_per_execution: std::time::Duration::from_millis(
667                (capabilities.resources.max_cpu_percent * 1000.0) as u64,
668            ),
669        }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    #[tokio::test]
678    async fn test_sandbox_resources() {
679        let resources = SandboxResources {
680            execution_count: 10,
681            success_count: 8,
682            error_count: 2,
683            total_execution_time: std::time::Duration::from_millis(1000),
684            ..Default::default()
685        };
686
687        assert_eq!(resources.avg_execution_time_ms(), 100.0);
688        assert_eq!(resources.success_rate(), 80.0);
689    }
690
691    #[tokio::test]
692    async fn test_execution_limits() {
693        let limits = ExecutionLimits::default();
694        assert_eq!(limits.max_executions, 1000);
695        assert_eq!(limits.max_memory_bytes, 10 * 1024 * 1024);
696    }
697
698    #[tokio::test]
699    async fn test_health_checks() {
700        let mut health = SandboxHealth::healthy();
701        assert!(health.is_healthy);
702
703        health.add_check(HealthCheck::fail("Test failure"));
704        assert!(!health.is_healthy);
705        assert_eq!(health.last_error, "Test failure");
706    }
707
708    #[tokio::test]
709    async fn test_plugin_sandbox_creation() {
710        let config = PluginLoaderConfig::default();
711        let sandbox = PluginSandbox::new(config);
712
713        let active = sandbox.list_active_sandboxes().await;
714        assert!(active.is_empty());
715    }
716}