Skip to main content

lion_core/state/
plugin.rs

1// Copyright (C) 2026 HaiyangLi
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Lion State Plugin
4//!
5//! Corresponds to: Lion/State/Plugin.lean
6//!
7//! Plugin state for WASM sandbox instances.
8//!
9//! Design Note (from Lean): heldCaps stores CapId references, not Capability copies.
10//! This follows the Handle/State Separation pattern:
11//! - Plugins hold handles (CapId) - immutable references
12//! - Kernel holds state (Capability with valid flag) - mutable
13//! - Validity is checked at USE time via kernel table lookup
14//! - Revocation just flips valid=false in table, no plugin cleanup needed
15
16use super::LinearMemory;
17use crate::types::{CapId, SecurityLevel, Size};
18
19/// Error type for plugin operations
20#[derive(Debug)]
21pub enum PluginError {
22    /// Capability not held by this plugin
23    CapNotHeld(CapId),
24    /// Memory access out of bounds (addr, len, bounds)
25    MemoryOutOfBounds(u64, u64, u64),
26    /// Memory quota exceeded (requested, used, quota)
27    MemoryQuotaExceeded(u64, u64, u64),
28    /// Capability quota exceeded (current, quota)
29    CapQuotaExceeded(u64, u64),
30    /// IPC queue limit exceeded (current, limit)
31    IpcQueueLimitExceeded(u64, u64),
32}
33
34/// Plugin-local state (WASM globals, tables, etc.)
35///
36/// Corresponds to Lean: `def PluginLocal := Unit`
37///
38/// Concrete (Unit) for derived instances - actual state is abstract.
39/// In Rust, we use an empty struct as the equivalent of Unit.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct PluginLocal;
42
43/// Default quota value: half of u64::MAX to allow arithmetic headroom
44/// Corresponds to Lean: `def defaultQuota : Nat := (2^63 : Nat)`
45pub const DEFAULT_QUOTA: u64 = u64::MAX / 2;
46
47/// State of a single plugin instance
48///
49/// Corresponds to Lean: `@[ext] structure PluginState`
50///
51/// INVARIANTS:
52/// - heldCaps contains only capability IDs, not actual capabilities
53/// - Validity of held capabilities is checked at use time via kernel lookup
54/// - Memory bounds are immutable after creation
55/// - Resource usage must not exceed quotas (DoS prevention)
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct PluginState {
58    /// Security classification level
59    ///
60    /// Corresponds to Lean: `level : SecurityLevel`
61    pub(crate) level: SecurityLevel,
62
63    /// Linear memory sandbox
64    ///
65    /// Corresponds to Lean: `memory : LinearMemory`
66    pub(crate) memory: LinearMemory,
67
68    /// Capability IDs held (handles, not copies)
69    ///
70    /// Corresponds to Lean: `heldCaps : Finset CapId`
71    /// NOTE: Using Vec with sorted uniqueness.
72    pub(crate) held_caps: Vec<CapId>,
73
74    /// Opaque internal state
75    ///
76    /// Corresponds to Lean: `localState : PluginLocal`
77    pub(crate) local_state: PluginLocal,
78
79    // Resource quotas (DoS prevention)
80    // Corresponds to Lean quota fields in PluginState
81    /// Maximum memory allocation in bytes
82    ///
83    /// Corresponds to Lean: `memoryQuota : Nat`
84    pub(crate) memory_quota: u64,
85
86    /// Current memory usage in bytes
87    ///
88    /// Corresponds to Lean: `memoryUsed : Nat`
89    pub(crate) memory_used: u64,
90
91    /// Maximum held capabilities
92    ///
93    /// Corresponds to Lean: `capQuota : Nat`
94    pub(crate) cap_quota: u64,
95
96    /// Maximum pending IPC messages
97    ///
98    /// Corresponds to Lean: `ipcQueueLimit : Nat`
99    pub(crate) ipc_queue_limit: u64,
100}
101
102impl PluginState {
103    /// Create empty plugin with given security level and memory size
104    ///
105    /// Corresponds to Lean: `def PluginState.empty (level : SecurityLevel) (memSize : Nat) : PluginState`
106    pub fn empty(level: SecurityLevel, mem_size: Size) -> Self {
107        PluginState {
108            level,
109            memory: LinearMemory::empty(mem_size),
110            held_caps: Vec::new(),
111            local_state: PluginLocal,
112            memory_quota: DEFAULT_QUOTA,
113            memory_used: 0,
114            cap_quota: DEFAULT_QUOTA,
115            ipc_queue_limit: DEFAULT_QUOTA,
116        }
117    }
118
119    /// Create plugin with custom quotas
120    ///
121    /// Corresponds to Lean: `def PluginState.withQuotas`
122    pub fn with_quotas(
123        level: SecurityLevel,
124        mem_size: Size,
125        memory_quota: u64,
126        cap_quota: u64,
127        ipc_queue_limit: u64,
128    ) -> Self {
129        PluginState {
130            level,
131            memory: LinearMemory::empty(mem_size),
132            held_caps: Vec::new(),
133            local_state: PluginLocal,
134            memory_quota,
135            memory_used: 0,
136            cap_quota,
137            ipc_queue_limit,
138        }
139    }
140
141    /// Check if plugin holds a capability by ID
142    ///
143    /// Corresponds to Lean: `def PluginState.holds_cap (ps : PluginState) (capId : CapId) : Prop`
144    #[inline]
145    pub fn holds_cap(&self, cap_id: CapId) -> bool {
146        self.held_caps.binary_search(&cap_id).is_ok()
147    }
148
149    /// Grant capability ID to plugin (idempotent)
150    ///
151    /// Corresponds to Lean: `def PluginState.grant_cap`
152    ///
153    /// Returns the new PluginState with the capability ID added.
154    pub fn grant_cap(&self, cap_id: CapId) -> Self {
155        let mut new_caps = self.held_caps.clone();
156        Self::insert_cap_sorted(&mut new_caps, cap_id);
157        PluginState {
158            level: self.level,
159            memory: self.memory.clone(),
160            held_caps: new_caps,
161            local_state: self.local_state,
162            memory_quota: self.memory_quota,
163            memory_used: self.memory_used,
164            cap_quota: self.cap_quota,
165            ipc_queue_limit: self.ipc_queue_limit,
166        }
167    }
168
169    /// Helper: insert cap_id into sorted vec, maintaining uniqueness
170    fn insert_cap_sorted(caps: &mut Vec<CapId>, cap_id: CapId) {
171        match caps.binary_search(&cap_id) {
172            Ok(_) => {} // Already present
173            Err(pos) => caps.insert(pos, cap_id),
174        }
175    }
176
177    /// Grant capability ID to plugin (mutating version)
178    pub fn grant_cap_mut(&mut self, cap_id: CapId) {
179        Self::insert_cap_sorted(&mut self.held_caps, cap_id);
180    }
181
182    /// Remove capability ID from plugin
183    ///
184    /// Corresponds to Lean: `def PluginState.revoke_cap`
185    ///
186    /// Returns the new PluginState with the capability ID removed.
187    pub fn revoke_cap(&self, cap_id: CapId) -> Self {
188        let mut new_caps = self.held_caps.clone();
189        Self::remove_cap_sorted(&mut new_caps, cap_id);
190        PluginState {
191            level: self.level,
192            memory: self.memory.clone(),
193            held_caps: new_caps,
194            local_state: self.local_state,
195            memory_quota: self.memory_quota,
196            memory_used: self.memory_used,
197            cap_quota: self.cap_quota,
198            ipc_queue_limit: self.ipc_queue_limit,
199        }
200    }
201
202    /// Helper: remove cap_id from sorted vec
203    fn remove_cap_sorted(caps: &mut Vec<CapId>, cap_id: CapId) {
204        if let Ok(pos) = caps.binary_search(&cap_id) {
205            caps.remove(pos);
206        }
207    }
208
209    /// Remove capability ID from plugin (mutating version)
210    pub fn revoke_cap_mut(&mut self, cap_id: CapId) {
211        Self::remove_cap_sorted(&mut self.held_caps, cap_id);
212    }
213
214    /// Get the security level
215    #[inline]
216    pub fn level(&self) -> SecurityLevel {
217        self.level
218    }
219
220    /// Get a reference to the memory
221    #[inline]
222    pub fn memory(&self) -> &LinearMemory {
223        &self.memory
224    }
225
226    /// Get a mutable reference to the memory
227    #[inline]
228    #[allow(dead_code)] // Lean correspondence
229    pub(crate) fn memory_mut(&mut self) -> &mut LinearMemory {
230        &mut self.memory
231    }
232
233    /// Get the number of held capabilities
234    #[inline]
235    pub fn held_cap_count(&self) -> usize {
236        self.held_caps.len()
237    }
238
239    /// Get the local state
240    #[inline]
241    pub fn local_state(&self) -> PluginLocal {
242        self.local_state
243    }
244
245    /// Get held capability IDs as a Vec
246    ///
247    /// Note: Returns a cloned Vec.
248    /// The Vec is maintained in sorted order.
249    pub fn held_caps(&self) -> Vec<CapId> {
250        self.held_caps.clone()
251    }
252
253    /// Get a reference to held capability IDs
254    pub fn held_caps_ref(&self) -> &Vec<CapId> {
255        &self.held_caps
256    }
257
258    /// Get memory bounds
259    #[inline]
260    pub fn memory_bounds(&self) -> Size {
261        self.memory.bounds()
262    }
263
264    // ============== QUOTA OPERATIONS ==============
265
266    /// Check if memory allocation would exceed quota
267    ///
268    /// Corresponds to Lean: `def PluginState.canAllocMemory`
269    /// Returns false if addition would overflow (DoS prevention)
270    #[inline]
271    pub fn can_alloc_memory(&self, size: u64) -> bool {
272        // Use checked_add to detect overflow - if overflow occurs, quota is exceeded
273        match self.memory_used.checked_add(size) {
274            Some(total) => total <= self.memory_quota,
275            None => false, // Overflow means quota exceeded
276        }
277    }
278
279    /// Check if plugin can hold another capability
280    ///
281    /// Corresponds to Lean: `def PluginState.canHoldCap`
282    #[inline]
283    pub fn can_hold_cap(&self) -> bool {
284        (self.held_caps.len() as u64) < self.cap_quota
285    }
286
287    /// Check if adding to IPC queue would exceed limit
288    ///
289    /// Corresponds to Lean: `def PluginState.canQueueIpc`
290    #[inline]
291    pub fn can_queue_ipc(&self, queue_len: u64) -> bool {
292        queue_len < self.ipc_queue_limit
293    }
294
295    /// Record memory allocation (update used counter)
296    ///
297    /// Corresponds to Lean: `def PluginState.allocMemory`
298    /// Returns false if quota would be exceeded
299    pub fn alloc_memory(&mut self, size: u64) -> bool {
300        if !self.can_alloc_memory(size) {
301            return false;
302        }
303        self.memory_used = self.memory_used.saturating_add(size);
304        true
305    }
306
307    /// Record memory deallocation (update used counter)
308    ///
309    /// Corresponds to Lean: `def PluginState.freeMemory`
310    pub fn free_memory(&mut self, size: u64) {
311        self.memory_used = self.memory_used.saturating_sub(size);
312    }
313
314    /// Get current memory usage
315    #[inline]
316    pub fn memory_used(&self) -> u64 {
317        self.memory_used
318    }
319
320    /// Get memory quota
321    #[inline]
322    pub fn memory_quota(&self) -> u64 {
323        self.memory_quota
324    }
325
326    /// Get capability quota
327    #[inline]
328    pub fn cap_quota(&self) -> u64 {
329        self.cap_quota
330    }
331
332    /// Get IPC queue limit
333    #[inline]
334    pub fn ipc_queue_limit(&self) -> u64 {
335        self.ipc_queue_limit
336    }
337}
338
339impl Default for PluginState {
340    /// Default plugin: Public level (no clearance), empty memory, no caps.
341    ///
342    /// Corresponds to Lean: `noncomputable instance : Inhabited PluginState`
343    ///
344    /// This is the semantically correct default - uninitialized plugins
345    /// have no security privileges.
346    fn default() -> Self {
347        PluginState {
348            level: SecurityLevel::Public,
349            memory: LinearMemory::empty(0),
350            held_caps: Vec::new(),
351            local_state: PluginLocal,
352            memory_quota: DEFAULT_QUOTA,
353            memory_used: 0,
354            cap_quota: DEFAULT_QUOTA,
355            ipc_queue_limit: DEFAULT_QUOTA,
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_plugin_local_default() {
366        let local = PluginLocal;
367        assert_eq!(local, PluginLocal);
368    }
369
370    #[test]
371    fn test_plugin_state_empty() {
372        let ps = PluginState::empty(SecurityLevel::Confidential, 1024);
373        assert_eq!(ps.level(), SecurityLevel::Confidential);
374        assert_eq!(ps.memory_bounds(), 1024);
375        assert_eq!(ps.held_cap_count(), 0);
376    }
377
378    #[test]
379    fn test_plugin_state_default_level() {
380        // Corresponds to Lean theorem: PluginState.default_level
381        let ps = PluginState::default();
382        assert_eq!(ps.level(), SecurityLevel::Public);
383    }
384
385    #[test]
386    fn test_plugin_state_grant_cap() {
387        let mut ps = PluginState::empty(SecurityLevel::Public, 0);
388
389        // Grant cap
390        ps.grant_cap_mut(42);
391        assert!(ps.holds_cap(42));
392        assert_eq!(ps.held_cap_count(), 1);
393
394        // Grant same cap again (idempotent)
395        ps.grant_cap_mut(42);
396        assert!(ps.holds_cap(42));
397        assert_eq!(ps.held_cap_count(), 1);
398    }
399
400    #[test]
401    fn test_plugin_state_grant_cap_mem() {
402        // Corresponds to Lean theorem: PluginState.grant_cap_mem
403        let ps = PluginState::empty(SecurityLevel::Public, 0);
404        let ps = ps.grant_cap(42);
405        assert!(ps.holds_cap(42));
406    }
407
408    #[test]
409    fn test_plugin_state_grant_cap_preserves() {
410        // Corresponds to Lean theorem: PluginState.grant_cap_preserves
411        let ps = PluginState::empty(SecurityLevel::Public, 0);
412        let ps = ps.grant_cap(1);
413        let ps = ps.grant_cap(2);
414
415        // Both should be held
416        assert!(ps.holds_cap(1));
417        assert!(ps.holds_cap(2));
418    }
419
420    #[test]
421    fn test_plugin_state_revoke_cap_not_mem() {
422        // Corresponds to Lean theorem: PluginState.revoke_cap_not_mem
423        let ps = PluginState::empty(SecurityLevel::Public, 0);
424        let ps = ps.grant_cap(42);
425        let ps = ps.revoke_cap(42);
426
427        assert!(!ps.holds_cap(42));
428        assert_eq!(ps.held_cap_count(), 0);
429    }
430
431    #[test]
432    fn test_plugin_state_revoke_cap_preserves() {
433        // Corresponds to Lean theorem: PluginState.revoke_cap_preserves
434        let ps = PluginState::empty(SecurityLevel::Public, 0);
435        let ps = ps.grant_cap(1);
436        let ps = ps.grant_cap(2);
437        let ps = ps.revoke_cap(1);
438
439        // 2 should still be held
440        assert!(!ps.holds_cap(1));
441        assert!(ps.holds_cap(2));
442    }
443
444    #[test]
445    fn test_plugin_state_grant_cap_memory() {
446        // Corresponds to Lean theorem: PluginState.grant_cap_memory
447        let ps = PluginState::empty(SecurityLevel::Public, 1024);
448        let old_memory = ps.memory().clone();
449        let ps = ps.grant_cap(42);
450
451        // Memory should be unchanged
452        assert_eq!(ps.memory(), &old_memory);
453    }
454
455    #[test]
456    fn test_plugin_state_grant_cap_memory_bounds() {
457        // Corresponds to Lean theorem: PluginState.grant_cap_memory_bounds
458        let ps = PluginState::empty(SecurityLevel::Public, 1024);
459        let ps = ps.grant_cap(42);
460
461        // Bounds should be unchanged
462        assert_eq!(ps.memory_bounds(), 1024);
463    }
464
465    #[test]
466    fn test_plugin_state_grant_cap_level() {
467        // Corresponds to Lean theorem: PluginState.grant_cap_level
468        let ps = PluginState::empty(SecurityLevel::Confidential, 0);
469        let ps = ps.grant_cap(42);
470
471        // Level should be unchanged
472        assert_eq!(ps.level(), SecurityLevel::Confidential);
473    }
474
475    #[test]
476    fn test_plugin_state_grant_cap_local_state() {
477        // Corresponds to Lean theorem: PluginState.grant_cap_localState
478        let ps = PluginState::empty(SecurityLevel::Public, 0);
479        let old_local = ps.local_state();
480        let ps = ps.grant_cap(42);
481
482        // Local state should be unchanged
483        assert_eq!(ps.local_state(), old_local);
484    }
485
486    #[test]
487    fn test_plugin_state_held_caps_iterator() {
488        let mut ps = PluginState::empty(SecurityLevel::Public, 0);
489        ps.grant_cap_mut(1);
490        ps.grant_cap_mut(3);
491        ps.grant_cap_mut(2);
492
493        // BTreeSet provides deterministic iteration order
494        let caps = ps.held_caps();
495        assert_eq!(caps, vec![1, 2, 3]);
496    }
497
498    // ============== QUOTA TESTS ==============
499
500    #[test]
501    fn test_default_quota() {
502        let ps = PluginState::default();
503        assert_eq!(ps.memory_quota(), DEFAULT_QUOTA);
504        assert_eq!(ps.memory_used(), 0);
505        assert_eq!(ps.cap_quota(), DEFAULT_QUOTA);
506        assert_eq!(ps.ipc_queue_limit(), DEFAULT_QUOTA);
507    }
508
509    #[test]
510    fn test_with_quotas() {
511        let ps = PluginState::with_quotas(
512            SecurityLevel::Confidential,
513            1024,
514            4096, // memory quota
515            10,   // cap quota
516            100,  // ipc limit
517        );
518        assert_eq!(ps.level(), SecurityLevel::Confidential);
519        assert_eq!(ps.memory_bounds(), 1024);
520        assert_eq!(ps.memory_quota(), 4096);
521        assert_eq!(ps.cap_quota(), 10);
522        assert_eq!(ps.ipc_queue_limit(), 100);
523    }
524
525    #[test]
526    fn test_can_alloc_memory() {
527        let ps = PluginState::with_quotas(
528            SecurityLevel::Public,
529            0,
530            1000, // 1000 byte quota
531            10,
532            100,
533        );
534
535        // Should allow allocation within quota
536        assert!(ps.can_alloc_memory(500));
537        assert!(ps.can_alloc_memory(1000));
538
539        // Should reject allocation exceeding quota
540        assert!(!ps.can_alloc_memory(1001));
541    }
542
543    #[test]
544    fn test_alloc_memory_tracking() {
545        let mut ps = PluginState::with_quotas(SecurityLevel::Public, 0, 1000, 10, 100);
546
547        // First allocation
548        assert!(ps.alloc_memory(400));
549        assert_eq!(ps.memory_used(), 400);
550
551        // Second allocation
552        assert!(ps.alloc_memory(400));
553        assert_eq!(ps.memory_used(), 800);
554
555        // Third allocation would exceed quota
556        assert!(!ps.alloc_memory(300));
557        assert_eq!(ps.memory_used(), 800); // unchanged
558
559        // But smaller allocation still works
560        assert!(ps.alloc_memory(200));
561        assert_eq!(ps.memory_used(), 1000);
562    }
563
564    #[test]
565    fn test_free_memory_tracking() {
566        let mut ps = PluginState::with_quotas(SecurityLevel::Public, 0, 1000, 10, 100);
567
568        ps.alloc_memory(800);
569        assert_eq!(ps.memory_used(), 800);
570
571        ps.free_memory(300);
572        assert_eq!(ps.memory_used(), 500);
573
574        // Can allocate again after freeing
575        assert!(ps.can_alloc_memory(500));
576        assert!(ps.alloc_memory(500));
577        assert_eq!(ps.memory_used(), 1000);
578    }
579
580    #[test]
581    fn test_can_hold_cap() {
582        let mut ps = PluginState::with_quotas(
583            SecurityLevel::Public,
584            0,
585            1000,
586            3, // max 3 caps
587            100,
588        );
589
590        assert!(ps.can_hold_cap());
591        ps.grant_cap_mut(1);
592        assert!(ps.can_hold_cap());
593        ps.grant_cap_mut(2);
594        assert!(ps.can_hold_cap());
595        ps.grant_cap_mut(3);
596        assert!(!ps.can_hold_cap()); // at quota now
597
598        // After revoke, can hold more
599        ps.revoke_cap_mut(1);
600        assert!(ps.can_hold_cap());
601    }
602
603    #[test]
604    fn test_can_queue_ipc() {
605        let ps = PluginState::with_quotas(
606            SecurityLevel::Public,
607            0,
608            1000,
609            10,
610            5, // max 5 IPC messages
611        );
612
613        assert!(ps.can_queue_ipc(0));
614        assert!(ps.can_queue_ipc(4));
615        assert!(!ps.can_queue_ipc(5));
616        assert!(!ps.can_queue_ipc(100));
617    }
618
619    #[test]
620    fn test_quota_overflow_safety() {
621        let mut ps = PluginState::with_quotas(
622            SecurityLevel::Public,
623            0,
624            u64::MAX, // max quota
625            10,
626            100,
627        );
628
629        // Even with max quota, saturating add should not overflow
630        ps.memory_used = u64::MAX - 10;
631        assert!(!ps.can_alloc_memory(100)); // would overflow
632        assert!(ps.can_alloc_memory(5)); // still within bounds
633    }
634}