Skip to main content

lion_core/step/
host_call.rs

1// Copyright (C) 2026 HaiyangLi
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Lion Step Host Call (EROS-style 6 operations)
4//!
5//! Corresponds to: Lion/Step/HostCall/Core.lean
6//!
7//! Host function calls that cross the trust boundary between
8//! untrusted plugin code and the trusted kernel.
9//!
10//! EROS consolidation (13 -> 6):
11//! - CapCall: Synchronous capability invocation (was cap_invoke)
12//! - CapSend: Async message send (was ipc_send; delegation is user-space)
13//! - KernelAlloc: Resource allocation (was mem_alloc, resource_create)
14//! - MemFree: Memory deallocation (keep separate for safety)
15//! - CapRevoke: Capability revocation
16//! - Declassify: Security level change
17
18use crate::state::{Message, State};
19use crate::types::{Action, Capability, MemAddr, PluginId, Right, Rights, SecurityLevel, Size};
20
21use super::{HostCallPrecondition, StepError};
22
23/// All host functions that cross the trust boundary (EROS-style 6 operations)
24///
25/// Corresponds to Lean: `inductive HostFunctionId`
26///
27/// EROS consolidation (13 -> 6):
28/// - CapInvoke -> CapCall (synchronous capability invocation)
29/// - CapDelegate, CapAccept -> User-space protocol over CapSend
30/// - IpcSend -> CapSend (async message send)
31/// - IpcReceive -> Removed (mailbox read via CapCall)
32/// - MemAlloc, ResourceCreate, ResourceAccess -> KernelAlloc
33/// - MemFree -> MemFree (keep separate for safety)
34/// - CapRevoke -> CapRevoke (kernel operation)
35/// - WorkflowStart, WorkflowStep -> Removed (user-space library)
36/// - Declassify -> Declassify (IFC operation)
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum HostFunction {
39    /// Synchronous capability invocation (replaces cap_invoke)
40    ///
41    /// Corresponds to Lean: `| cap_call`
42    CapCall,
43
44    /// Async message send (replaces ipc_send; delegation is user-space protocol)
45    ///
46    /// Corresponds to Lean: `| cap_send`
47    CapSend,
48
49    /// Kernel resource allocation (replaces mem_alloc, resource_create)
50    ///
51    /// Corresponds to Lean: `| kernel_alloc`
52    KernelAlloc,
53
54    /// Memory deallocation (keep separate for safety)
55    ///
56    /// Corresponds to Lean: `| mem_free`
57    MemFree,
58
59    /// Capability revocation
60    ///
61    /// Corresponds to Lean: `| cap_revoke`
62    CapRevoke,
63
64    /// Controlled information declassification (IFC operation)
65    ///
66    /// Corresponds to Lean: `| declassify`
67    Declassify,
68
69    /// Create new thread (requires ThreadControl capability)
70    ///
71    /// Corresponds to Lean: `| thread_create`
72    ThreadCreate,
73
74    /// Configure thread bindings (requires ThreadControl capability)
75    ///
76    /// Corresponds to Lean: `| thread_configure`
77    ThreadConfigure,
78
79    /// Resume suspended thread (requires ThreadControl capability)
80    ///
81    /// Corresponds to Lean: `| thread_resume`
82    ThreadResume,
83
84    /// Suspend running thread (requires ThreadControl capability)
85    ///
86    /// Corresponds to Lean: `| thread_suspend`
87    ThreadSuspend,
88
89    /// Terminate thread (requires ThreadControl capability)
90    ///
91    /// Corresponds to Lean: `| thread_destroy`
92    ThreadDestroy,
93}
94
95/// Resource type for KernelAlloc operation
96///
97/// Corresponds to Lean: `inductive ResourceType`
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum ResourceType {
100    /// Allocate memory region
101    Memory {
102        /// Size of the memory region to allocate
103        size: u64,
104    },
105    /// Create capability (simplified payload)
106    Capability {
107        /// Capability payload identifier
108        payload: u64,
109    },
110    /// Create actor (simplified config)
111    Actor {
112        /// Actor configuration identifier
113        config: u64,
114    },
115}
116
117// ============== LEGACY COMPATIBILITY ALIASES ==============
118
119impl HostFunction {
120    /// Legacy alias: CapInvoke -> CapCall
121    #[deprecated(note = "Use HostFunction::CapCall instead (EROS consolidation)")]
122    pub const CAP_INVOKE: HostFunction = HostFunction::CapCall;
123
124    /// Legacy alias: IpcSend -> CapSend
125    #[deprecated(note = "Use HostFunction::CapSend instead (EROS consolidation)")]
126    pub const IPC_SEND: HostFunction = HostFunction::CapSend;
127
128    /// Legacy alias: MemAlloc -> KernelAlloc
129    #[deprecated(note = "Use HostFunction::KernelAlloc instead (EROS consolidation)")]
130    pub const MEM_ALLOC: HostFunction = HostFunction::KernelAlloc;
131}
132
133impl HostFunction {
134    /// Check if this host function is effectful
135    ///
136    /// All host functions cross the trust boundary, so all are effectful.
137    ///
138    /// Corresponds to Lean: `def is_effectful_host_call`
139    #[inline]
140    pub fn is_effectful(&self) -> bool {
141        true
142    }
143
144    /// Check if this is a declassify operation
145    ///
146    /// Corresponds to Lean: `def is_declassify`
147    #[inline]
148    pub fn is_declassify(&self) -> bool {
149        matches!(self, HostFunction::Declassify)
150    }
151
152    /// Check if this is a capability operation (may modify kernel.revocation)
153    ///
154    /// Corresponds to Lean: `def is_cap_operation`
155    #[inline]
156    pub fn is_cap_operation(&self) -> bool {
157        matches!(self, HostFunction::CapCall | HostFunction::CapRevoke)
158    }
159
160    /// Check if this is a memory operation (modifies ghost state)
161    ///
162    /// Corresponds to Lean: `def is_mem_operation`
163    #[inline]
164    pub fn is_mem_operation(&self) -> bool {
165        matches!(self, HostFunction::KernelAlloc | HostFunction::MemFree)
166    }
167
168    /// Execute the host function
169    ///
170    /// Corresponds to Lean: `def KernelExecHost`
171    ///
172    /// # Errors
173    ///
174    /// Returns `StepError::PluginNotFound` if the caller plugin does not exist.
175    /// Returns `StepError::HostCallPreconditionFailed` if function-specific preconditions are not met.
176    /// Returns `StepError::ActorNotFound` if a destination actor does not exist (CapSend).
177    /// Returns `StepError::QuotaExceeded` if the allocation would exceed the plugin's memory quota (KernelAlloc).
178    /// Returns `StepError::AddressAlreadyFreed` if the address is already freed (MemFree).
179    /// Returns `StepError::CapabilityTargetsAddress` if a valid capability targets the address (MemFree).
180    /// Returns `StepError::CapabilityNotFound` if the capability does not exist (CapRevoke).
181    pub fn execute(
182        &self,
183        state: &State,
184        hc: &HostCall,
185        result: &HostResult,
186    ) -> Result<State, StepError> {
187        match self {
188            HostFunction::CapCall => execute_cap_call(state, hc, result),
189            HostFunction::CapSend => execute_cap_send(state, hc, result),
190            HostFunction::KernelAlloc => execute_kernel_alloc(state, hc),
191            HostFunction::MemFree => execute_mem_free(state, hc),
192            HostFunction::CapRevoke => execute_cap_revoke(state, hc),
193            HostFunction::Declassify => execute_declassify(state, hc),
194            // Thread operations — Phase 3 stubs (require State extension)
195            HostFunction::ThreadCreate
196            | HostFunction::ThreadConfigure
197            | HostFunction::ThreadResume
198            | HostFunction::ThreadSuspend
199            | HostFunction::ThreadDestroy => Err(StepError::HostCallPreconditionFailed(
200                HostCallPrecondition::NotImplemented {
201                    operation: "thread operations",
202                },
203            )),
204        }
205    }
206
207    /// Execute the host function (mutating version).
208    ///
209    /// Same validation as `execute` but modifies `&mut State` in place.
210    pub fn execute_mut(
211        &self,
212        state: &mut State,
213        hc: &HostCall,
214        result: &HostResult,
215    ) -> Result<(), StepError> {
216        match self {
217            HostFunction::CapCall => execute_cap_call_mut(state, hc, result),
218            HostFunction::CapSend => execute_cap_send_mut(state, hc, result),
219            HostFunction::KernelAlloc => execute_kernel_alloc_mut(state, hc),
220            HostFunction::MemFree => execute_mem_free_mut(state, hc),
221            HostFunction::CapRevoke => execute_cap_revoke_mut(state, hc),
222            HostFunction::Declassify => execute_declassify_mut(state, hc),
223            HostFunction::ThreadCreate
224            | HostFunction::ThreadConfigure
225            | HostFunction::ThreadResume
226            | HostFunction::ThreadSuspend
227            | HostFunction::ThreadDestroy => Err(StepError::HostCallPreconditionFailed(
228                HostCallPrecondition::NotImplemented {
229                    operation: "thread operations",
230                },
231            )),
232        }
233    }
234
235    /// Check if this is a thread operation (requires ThreadControl capability)
236    ///
237    /// Corresponds to Lean: thread_create/configure/resume/suspend/destroy
238    #[inline]
239    pub fn is_thread_operation(&self) -> bool {
240        matches!(
241            self,
242            HostFunction::ThreadCreate
243                | HostFunction::ThreadConfigure
244                | HostFunction::ThreadResume
245                | HostFunction::ThreadSuspend
246                | HostFunction::ThreadDestroy
247        )
248    }
249}
250
251/// Host call request from plugin
252///
253/// Corresponds to Lean: `structure HostCall`
254#[derive(Debug, Clone)]
255pub struct HostCall {
256    /// The plugin making the call
257    ///
258    /// Corresponds to Lean: `caller : PluginId`
259    pub(crate) caller: PluginId,
260
261    /// The function to invoke
262    ///
263    /// Corresponds to Lean: `function : HostFunctionId`
264    pub(crate) function: HostFunction,
265
266    /// Arguments to the function (simplified from WasmValue)
267    ///
268    /// Corresponds to Lean: `args : List Nat`
269    pub(crate) args: Vec<u64>,
270
271    /// Memory regions to read
272    ///
273    /// Corresponds to Lean: `reads : List (MemAddr x Size)`
274    pub(crate) reads: Vec<(MemAddr, Size)>,
275
276    /// Memory regions to write
277    ///
278    /// Corresponds to Lean: `writes : List (MemAddr x Size)`
279    pub(crate) writes: Vec<(MemAddr, Size)>,
280}
281
282impl HostCall {
283    /// Create a new host call
284    pub fn new(
285        caller: PluginId,
286        function: HostFunction,
287        args: Vec<u64>,
288        reads: Vec<(MemAddr, Size)>,
289        writes: Vec<(MemAddr, Size)>,
290    ) -> Self {
291        HostCall {
292            caller,
293            function,
294            args,
295            reads,
296            writes,
297        }
298    }
299
300    /// Get the caller plugin ID
301    #[inline]
302    pub fn caller(&self) -> PluginId {
303        self.caller
304    }
305
306    /// Get the host function to invoke
307    #[inline]
308    pub fn function(&self) -> HostFunction {
309        self.function
310    }
311
312    /// Get the arguments
313    #[inline]
314    pub fn args(&self) -> &[u64] {
315        &self.args
316    }
317
318    /// Get the read regions
319    #[inline]
320    pub fn reads(&self) -> &[(MemAddr, Size)] {
321        &self.reads
322    }
323
324    /// Get the write regions
325    #[inline]
326    pub fn writes(&self) -> &[(MemAddr, Size)] {
327        &self.writes
328    }
329
330    /// Derive the required authorization action from the host call semantics.
331    ///
332    /// This binds authorization to the exact operation being performed,
333    /// preventing the vulnerability where a valid `Authorized` token for one
334    /// action could be reused to execute a different `HostCall`.
335    ///
336    /// # Errors
337    ///
338    /// Returns `StepError::HostCallPreconditionFailed` if required arguments are missing
339    /// or the operation is not yet implemented (thread operations).
340    pub fn required_action(&self) -> Result<Action, StepError> {
341        // Helper to convert PolicyError to StepError
342        let map_policy_err =
343            |_| StepError::HostCallPreconditionFailed(HostCallPrecondition::NoCapabilityId);
344
345        match self.function {
346            HostFunction::CapCall => {
347                let target: u64 =
348                    self.args
349                        .first()
350                        .copied()
351                        .ok_or(StepError::HostCallPreconditionFailed(
352                            HostCallPrecondition::NoCapabilityId,
353                        ))?;
354
355                Action::new(
356                    self.caller,
357                    target.into(),
358                    Rights::singleton(Right::Read),
359                    "cap_call".to_string(),
360                )
361                .map_err(map_policy_err)
362            }
363            HostFunction::KernelAlloc => Action::new(
364                self.caller,
365                0,
366                Rights::singleton(Right::Create),
367                "kernel_alloc".to_string(),
368            )
369            .map_err(map_policy_err),
370            HostFunction::MemFree => {
371                let addr: u64 =
372                    self.args
373                        .first()
374                        .copied()
375                        .ok_or(StepError::HostCallPreconditionFailed(
376                            HostCallPrecondition::NoAddress,
377                        ))?;
378
379                Action::new(
380                    self.caller,
381                    addr.into(),
382                    Rights::singleton(Right::Delete),
383                    "mem_free".to_string(),
384                )
385                .map_err(map_policy_err)
386            }
387            HostFunction::CapRevoke => {
388                let cap_id: u64 =
389                    self.args
390                        .first()
391                        .copied()
392                        .ok_or(StepError::HostCallPreconditionFailed(
393                            HostCallPrecondition::NoCapabilityId,
394                        ))?;
395
396                Action::new(
397                    self.caller,
398                    cap_id.into(),
399                    Rights::singleton(Right::Revoke),
400                    "cap_revoke".to_string(),
401                )
402                .map_err(map_policy_err)
403            }
404            HostFunction::Declassify => Action::new(
405                self.caller,
406                0,
407                Rights::singleton(Right::Declassify),
408                "declassify".to_string(),
409            )
410            .map_err(map_policy_err),
411            HostFunction::CapSend => Action::new(
412                self.caller,
413                0,
414                Rights::singleton(Right::Send),
415                "cap_send".to_string(),
416            )
417            .map_err(map_policy_err),
418            _ => Err(StepError::HostCallPreconditionFailed(
419                HostCallPrecondition::NotImplemented {
420                    operation: "thread operations",
421                },
422            )),
423        }
424    }
425
426    /// Check preconditions for the host call
427    ///
428    /// Corresponds to Lean: `def host_call_pre`
429    ///
430    /// # Errors
431    ///
432    /// Returns `StepError::PluginNotFound` if the caller plugin does not exist.
433    /// Returns `StepError::HostCallPreconditionFailed` if a read or write region is out of bounds.
434    pub fn check_preconditions(&self, state: &State) -> Result<(), StepError> {
435        // Get the caller plugin
436        let plugin = state
437            .get_plugin(self.caller)
438            .ok_or(StepError::PluginNotFound(self.caller))?;
439
440        // Check read regions are in bounds
441        if let Some(err) = self.find_oob_read(plugin) {
442            return Err(err);
443        }
444
445        // Check write regions are in bounds
446        if let Some(err) = self.find_oob_write(plugin) {
447            return Err(err);
448        }
449
450        Ok(())
451    }
452
453    /// Helper: find first out-of-bounds read region
454    fn find_oob_read(&self, plugin: &crate::state::PluginState) -> Option<StepError> {
455        self.reads.iter().find_map(|&(addr, size)| {
456            if plugin.memory().addr_in_bounds(addr, size) {
457                None
458            } else {
459                Some(StepError::HostCallPreconditionFailed(
460                    HostCallPrecondition::ReadOutOfBounds { addr, size },
461                ))
462            }
463        })
464    }
465
466    /// Helper: find first out-of-bounds write region
467    fn find_oob_write(&self, plugin: &crate::state::PluginState) -> Option<StepError> {
468        self.writes.iter().find_map(|&(addr, size)| {
469            if plugin.memory().addr_in_bounds(addr, size) {
470                None
471            } else {
472                Some(StepError::HostCallPreconditionFailed(
473                    HostCallPrecondition::WriteOutOfBounds { addr, size },
474                ))
475            }
476        })
477    }
478}
479
480/// Result of host call execution
481///
482/// Corresponds to Lean: `structure HostResult`
483#[derive(Debug, Clone, Default)]
484#[must_use = "host call results must be consumed"]
485pub struct HostResult {
486    /// Capabilities created/delegated
487    ///
488    /// Corresponds to Lean: `newCaps : List Capability`
489    pub(crate) new_caps: Vec<Capability>,
490
491    /// Messages queued
492    ///
493    /// Corresponds to Lean: `newMsgs : List Message`
494    pub(crate) new_msgs: Vec<Message>,
495}
496
497impl HostResult {
498    /// Create an empty result
499    pub fn empty() -> Self {
500        HostResult {
501            new_caps: Vec::new(),
502            new_msgs: Vec::new(),
503        }
504    }
505
506    /// Create a result with new capabilities
507    pub fn with_caps(caps: Vec<Capability>) -> Self {
508        HostResult {
509            new_caps: caps,
510            new_msgs: Vec::new(),
511        }
512    }
513
514    /// Create a result with new messages
515    pub fn with_msgs(msgs: Vec<Message>) -> Self {
516        HostResult {
517            new_caps: Vec::new(),
518            new_msgs: msgs,
519        }
520    }
521
522    /// Get new capabilities
523    #[inline]
524    pub fn new_caps(&self) -> &[Capability] {
525        &self.new_caps
526    }
527
528    /// Get new messages
529    #[inline]
530    pub fn new_msgs(&self) -> &[Message] {
531        &self.new_msgs
532    }
533}
534
535// ============== HOST FUNCTION EXECUTION ==============
536
537/// Execute cap_call (was cap_invoke)
538///
539/// Synchronous capability invocation: validate and use capability.
540/// State unchanged (call just validates and uses the cap).
541///
542/// PERF NOTE: Returns `state.clone()` because the API takes `&State` (callers
543/// need to retain the original for error paths and multi-step sequences).
544/// The clone is architecturally necessary for the immutable-state-transition
545/// model that mirrors the Lean spec (`Step s s'`). A by-value `State` API
546/// would eliminate this clone but would require the caller to re-create state
547/// on error paths, trading clone cost for ergonomic complexity.
548fn execute_cap_call(
549    state: &State,
550    hc: &HostCall,
551    _result: &HostResult,
552) -> Result<State, StepError> {
553    // Check caller holds a valid capability
554    let plugin = state
555        .get_plugin(hc.caller)
556        .ok_or(StepError::PluginNotFound(hc.caller))?;
557
558    // At least one held cap must be valid
559    let has_valid_cap = plugin
560        .held_caps_ref()
561        .iter()
562        .any(|&id| state.kernel().revocation().is_valid(id));
563
564    if !has_valid_cap {
565        return Err(StepError::HostCallPreconditionFailed(
566            HostCallPrecondition::NoValidCapability,
567        ));
568    }
569
570    // State unchanged for call — clone required by &State API contract
571    Ok(state.clone())
572}
573
574/// Execute cap_send (was ipc_send)
575///
576/// Async message send. Capability delegation is now user-space protocol:
577/// 1. Holder creates delegation message with cap payload
578/// 2. Sends via cap_send to delegatee
579/// 3. Delegatee receives and calls kernel_alloc to register
580///
581/// PERF NOTE: See `execute_cap_call` for why `state.clone()` is required here.
582/// State is unchanged — the kernel delivers messages asynchronously.
583fn execute_cap_send(state: &State, hc: &HostCall, result: &HostResult) -> Result<State, StepError> {
584    // Check message in result
585    if result.new_msgs.is_empty() {
586        return Err(StepError::HostCallPreconditionFailed(
587            HostCallPrecondition::NoMessage,
588        ));
589    }
590
591    // Validate message source matches caller
592    for msg in &result.new_msgs {
593        if msg.src() != hc.caller {
594            return Err(StepError::HostCallPreconditionFailed(
595                HostCallPrecondition::SourceMismatch,
596            ));
597        }
598
599        // Check destination actor exists and has capacity
600        let dst_actor = state
601            .get_actor(msg.dst())
602            .ok_or(StepError::ActorNotFound(msg.dst()))?;
603
604        if dst_actor.mailbox_len() >= dst_actor.capacity() {
605            return Err(StepError::HostCallPreconditionFailed(
606                HostCallPrecondition::MailboxFull,
607            ));
608        }
609    }
610
611    // State unchanged — clone required by &State API contract
612    Ok(state.clone())
613}
614
615/// Execute kernel_alloc (was mem_alloc, resource_create)
616///
617/// Unified kernel resource allocation.
618/// Enforces memory quota to prevent DoS attacks.
619fn execute_kernel_alloc(state: &State, hc: &HostCall) -> Result<State, StepError> {
620    // Get size from args (first arg is size for memory allocation)
621    let size = hc.args.first().copied().unwrap_or(0);
622
623    // QUOTA CHECK: Verify memory quota not exceeded
624    let plugin = state
625        .get_plugin(hc.caller)
626        .ok_or(StepError::PluginNotFound(hc.caller))?;
627
628    if !plugin.can_alloc_memory(size) {
629        return Err(StepError::QuotaExceeded(
630            0, // memory
631            size,
632            plugin.memory_used(),
633            plugin.memory_quota(),
634        ));
635    }
636
637    // Apply allocation and update quota tracking
638    let mut new_state = state.apply_alloc(hc.caller, size);
639
640    // Update memory used counter in plugin
641    if let Some(p) = new_state.get_plugin_mut(hc.caller) {
642        p.memory_used = p.memory_used.saturating_add(size);
643    }
644
645    Ok(new_state)
646}
647
648/// Execute mem_free
649///
650/// Free kernel-managed memory.
651fn execute_mem_free(state: &State, hc: &HostCall) -> Result<State, StepError> {
652    // Get addr from args
653    let addr: MemAddr = hc
654        .args
655        .first()
656        .copied()
657        .ok_or(StepError::HostCallPreconditionFailed(
658            HostCallPrecondition::NoAddress,
659        ))?;
660
661    // Check not already freed
662    if state.ghost().is_freed(addr) {
663        return Err(StepError::AddressAlreadyFreed(addr));
664    }
665
666    // Check no valid capability targets addr (ResourceId = u128, addr = u64)
667    if let Some(&(cap_id, _)) = state
668        .kernel()
669        .revocation()
670        .iter()
671        .find(|(_, cap)| cap.is_valid() && cap.target() == u128::from(addr))
672    {
673        return Err(StepError::CapabilityTargetsAddress(cap_id, addr));
674    }
675
676    state.apply_free(addr).map_err(|e| {
677        StepError::HostCallPreconditionFailed(HostCallPrecondition::FreeFailed { source: e })
678    })
679}
680
681/// Execute cap_revoke
682///
683/// Revoke capability transitively.
684fn execute_cap_revoke(state: &State, hc: &HostCall) -> Result<State, StepError> {
685    // Get the cap_id from args (stored as u64, widened to CapId = u128)
686    let cap_id: crate::types::CapId =
687        hc.args
688            .first()
689            .copied()
690            .map(u128::from)
691            .ok_or(StepError::HostCallPreconditionFailed(
692                HostCallPrecondition::NoCapabilityId,
693            ))?;
694
695    // Check cap exists
696    if state.kernel().revocation().get(cap_id).is_none() {
697        return Err(StepError::CapabilityNotFound(cap_id));
698    }
699
700    // Apply transitive revocation
701    Ok(state.apply_cap_revoke(cap_id))
702}
703
704/// Execute declassify
705///
706/// Controlled information declassification.
707fn execute_declassify(state: &State, hc: &HostCall) -> Result<State, StepError> {
708    // Get new level from args (encoded as u64)
709    let new_level_val = hc.args.first().copied().unwrap_or(0);
710    let new_level = match new_level_val {
711        0 => SecurityLevel::Public,
712        1 => SecurityLevel::Internal,
713        2 => SecurityLevel::Confidential,
714        3 => SecurityLevel::Secret,
715        other => {
716            return Err(StepError::HostCallPreconditionFailed(
717                HostCallPrecondition::InvalidSecurityLevel { value: other },
718            ));
719        }
720    };
721
722    // Check new level <= current level (can only declassify down)
723    let plugin = state
724        .get_plugin(hc.caller)
725        .ok_or(StepError::PluginNotFound(hc.caller))?;
726
727    if new_level > plugin.level() {
728        return Err(StepError::HostCallPreconditionFailed(
729            HostCallPrecondition::CannotClassifyUp,
730        ));
731    }
732
733    // Update plugin level
734    let mut new_state = state.clone();
735    if let Some(plugin) = new_state.get_plugin_mut(hc.caller) {
736        plugin.level = new_level;
737    }
738
739    Ok(new_state)
740}
741
742// ============== MUTATING HOST FUNCTION VARIANTS ==============
743
744/// Execute cap_call (mutating) -- state-unchanged operation
745fn execute_cap_call_mut(
746    state: &mut State,
747    hc: &HostCall,
748    _result: &HostResult,
749) -> Result<(), StepError> {
750    let plugin = state
751        .get_plugin(hc.caller)
752        .ok_or(StepError::PluginNotFound(hc.caller))?;
753
754    let has_valid_cap = plugin
755        .held_caps_ref()
756        .iter()
757        .any(|&id| state.kernel().revocation().is_valid(id));
758
759    if !has_valid_cap {
760        return Err(StepError::HostCallPreconditionFailed(
761            HostCallPrecondition::NoValidCapability,
762        ));
763    }
764
765    // State unchanged for call
766    Ok(())
767}
768
769/// Execute cap_send (mutating) -- state-unchanged operation
770fn execute_cap_send_mut(
771    state: &mut State,
772    hc: &HostCall,
773    result: &HostResult,
774) -> Result<(), StepError> {
775    if result.new_msgs.is_empty() {
776        return Err(StepError::HostCallPreconditionFailed(
777            HostCallPrecondition::NoMessage,
778        ));
779    }
780
781    for msg in &result.new_msgs {
782        if msg.src() != hc.caller {
783            return Err(StepError::HostCallPreconditionFailed(
784                HostCallPrecondition::SourceMismatch,
785            ));
786        }
787
788        let dst_actor = state
789            .get_actor(msg.dst())
790            .ok_or(StepError::ActorNotFound(msg.dst()))?;
791
792        if dst_actor.mailbox_len() >= dst_actor.capacity() {
793            return Err(StepError::HostCallPreconditionFailed(
794                HostCallPrecondition::MailboxFull,
795            ));
796        }
797    }
798
799    // State unchanged
800    Ok(())
801}
802
803/// Execute kernel_alloc (mutating)
804fn execute_kernel_alloc_mut(state: &mut State, hc: &HostCall) -> Result<(), StepError> {
805    let size = hc.args.first().copied().unwrap_or(0);
806
807    let plugin = state
808        .get_plugin(hc.caller)
809        .ok_or(StepError::PluginNotFound(hc.caller))?;
810
811    if !plugin.can_alloc_memory(size) {
812        return Err(StepError::QuotaExceeded(
813            0, // memory
814            size,
815            plugin.memory_used(),
816            plugin.memory_quota(),
817        ));
818    }
819
820    // Apply allocation in place
821    let _addr = state.apply_alloc_mut(hc.caller, size);
822
823    // Update memory used counter in plugin
824    if let Some(p) = state.get_plugin_mut(hc.caller) {
825        p.memory_used = p.memory_used.saturating_add(size);
826    }
827
828    Ok(())
829}
830
831/// Execute mem_free (mutating)
832fn execute_mem_free_mut(state: &mut State, hc: &HostCall) -> Result<(), StepError> {
833    let addr: MemAddr = hc
834        .args
835        .first()
836        .copied()
837        .ok_or(StepError::HostCallPreconditionFailed(
838            HostCallPrecondition::NoAddress,
839        ))?;
840
841    if state.ghost().is_freed(addr) {
842        return Err(StepError::AddressAlreadyFreed(addr));
843    }
844
845    // ResourceId = u128, addr = u64
846    if let Some(&(cap_id, _)) = state
847        .kernel()
848        .revocation()
849        .iter()
850        .find(|(_, cap)| cap.is_valid() && cap.target() == u128::from(addr))
851    {
852        return Err(StepError::CapabilityTargetsAddress(cap_id, addr));
853    }
854
855    state.apply_free_mut(addr).map_err(|e| {
856        StepError::HostCallPreconditionFailed(HostCallPrecondition::FreeFailed { source: e })
857    })
858}
859
860/// Execute cap_revoke (mutating)
861fn execute_cap_revoke_mut(state: &mut State, hc: &HostCall) -> Result<(), StepError> {
862    // Stored as u64, widened to CapId = u128
863    let cap_id: crate::types::CapId =
864        hc.args
865            .first()
866            .copied()
867            .map(u128::from)
868            .ok_or(StepError::HostCallPreconditionFailed(
869                HostCallPrecondition::NoCapabilityId,
870            ))?;
871
872    if state.kernel().revocation().get(cap_id).is_none() {
873        return Err(StepError::CapabilityNotFound(cap_id));
874    }
875
876    state
877        .apply_cap_revoke_mut(cap_id)
878        .map_err(|_| StepError::CapabilityNotFound(cap_id))?;
879
880    Ok(())
881}
882
883/// Execute declassify (mutating)
884fn execute_declassify_mut(state: &mut State, hc: &HostCall) -> Result<(), StepError> {
885    let new_level_val = hc.args.first().copied().unwrap_or(0);
886    let new_level = match new_level_val {
887        0 => SecurityLevel::Public,
888        1 => SecurityLevel::Internal,
889        2 => SecurityLevel::Confidential,
890        3 => SecurityLevel::Secret,
891        other => {
892            return Err(StepError::HostCallPreconditionFailed(
893                HostCallPrecondition::InvalidSecurityLevel { value: other },
894            ));
895        }
896    };
897
898    let plugin = state
899        .get_plugin(hc.caller)
900        .ok_or(StepError::PluginNotFound(hc.caller))?;
901
902    if new_level > plugin.level() {
903        return Err(StepError::HostCallPreconditionFailed(
904            HostCallPrecondition::CannotClassifyUp,
905        ));
906    }
907
908    if let Some(plugin) = state.get_plugin_mut(hc.caller) {
909        plugin.level = new_level;
910    }
911
912    Ok(())
913}
914
915#[cfg(test)]
916mod tests {
917    use super::*;
918    use crate::state::PluginState;
919
920    #[test]
921    fn test_host_function_is_effectful() {
922        // All host functions are effectful
923        assert!(HostFunction::CapCall.is_effectful());
924        assert!(HostFunction::KernelAlloc.is_effectful());
925        assert!(HostFunction::Declassify.is_effectful());
926    }
927
928    #[test]
929    fn test_host_function_is_declassify() {
930        assert!(!HostFunction::CapCall.is_declassify());
931        assert!(!HostFunction::KernelAlloc.is_declassify());
932        assert!(HostFunction::Declassify.is_declassify());
933    }
934
935    #[test]
936    fn test_host_function_is_cap_operation() {
937        assert!(HostFunction::CapCall.is_cap_operation());
938        assert!(HostFunction::CapRevoke.is_cap_operation());
939        assert!(!HostFunction::CapSend.is_cap_operation());
940        assert!(!HostFunction::KernelAlloc.is_cap_operation());
941    }
942
943    #[test]
944    fn test_host_function_is_mem_operation() {
945        assert!(HostFunction::KernelAlloc.is_mem_operation());
946        assert!(HostFunction::MemFree.is_mem_operation());
947        assert!(!HostFunction::CapCall.is_mem_operation());
948    }
949
950    #[test]
951    fn test_host_call_check_preconditions_no_plugin() {
952        let state = State::empty();
953        let hc = HostCall::new(1, HostFunction::CapCall, vec![], vec![], vec![]);
954
955        let result = hc.check_preconditions(&state);
956        assert!(matches!(result, Err(StepError::PluginNotFound(1))));
957    }
958
959    #[test]
960    fn test_host_call_check_preconditions_out_of_bounds() {
961        let mut state = State::empty();
962        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Public, 100));
963
964        // Read region beyond bounds
965        let hc = HostCall::new(1, HostFunction::CapCall, vec![], vec![(200, 50)], vec![]);
966
967        let result = hc.check_preconditions(&state);
968        assert!(matches!(
969            result,
970            Err(StepError::HostCallPreconditionFailed(_))
971        ));
972    }
973
974    #[test]
975    fn test_execute_kernel_alloc() {
976        let mut state = State::empty();
977        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Public, 1024));
978
979        let hc = HostCall::new(1, HostFunction::KernelAlloc, vec![256], vec![], vec![]);
980        let result = HostResult::empty();
981
982        let new_state = HostFunction::KernelAlloc
983            .execute(&state, &hc, &result)
984            .expect("kernel_alloc should succeed");
985
986        // Ghost state should show allocation
987        assert!(new_state.ghost().resource_count() > state.ghost().resource_count());
988    }
989
990    #[test]
991    fn test_execute_declassify() {
992        let mut state = State::empty();
993        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Secret, 0));
994
995        // Declassify to Public (level 0)
996        let hc = HostCall::new(1, HostFunction::Declassify, vec![0], vec![], vec![]);
997        let result = HostResult::empty();
998
999        let new_state = HostFunction::Declassify
1000            .execute(&state, &hc, &result)
1001            .expect("declassify should succeed");
1002
1003        assert_eq!(new_state.plugin_level(1), Some(SecurityLevel::Public));
1004    }
1005
1006    #[test]
1007    fn test_execute_declassify_up_fails() {
1008        let mut state = State::empty();
1009        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Public, 0));
1010
1011        // Try to classify up to Secret (level 3) - should fail
1012        let hc = HostCall::new(1, HostFunction::Declassify, vec![3], vec![], vec![]);
1013        let result = HostResult::empty();
1014
1015        let err = HostFunction::Declassify
1016            .execute(&state, &hc, &result)
1017            .expect_err("classify up should fail");
1018
1019        assert!(matches!(err, StepError::HostCallPreconditionFailed(_)));
1020    }
1021
1022    #[test]
1023    fn test_execute_declassify_invalid_level_rejected() {
1024        // SEC-002: values >= 4 must be rejected, not silently mapped to Secret
1025        let mut state = State::empty();
1026        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Secret, 0));
1027
1028        for invalid_level in [4, 5, 100, u64::MAX] {
1029            let hc = HostCall::new(
1030                1,
1031                HostFunction::Declassify,
1032                vec![invalid_level],
1033                vec![],
1034                vec![],
1035            );
1036            let result = HostResult::empty();
1037
1038            let err = HostFunction::Declassify
1039                .execute(&state, &hc, &result)
1040                .expect_err(&format!("level {invalid_level} should be rejected"));
1041
1042            assert!(
1043                matches!(
1044                    err,
1045                    StepError::HostCallPreconditionFailed(
1046                        HostCallPrecondition::InvalidSecurityLevel { .. }
1047                    )
1048                ),
1049                "level {invalid_level}: expected InvalidSecurityLevel error, got {err:?}"
1050            );
1051        }
1052    }
1053
1054    #[test]
1055    fn test_execute_declassify_all_valid_levels() {
1056        // All 4 valid levels (0-3) should work for a Secret plugin declassifying down
1057        let mut state = State::empty();
1058        let _ = state.insert_plugin(1, PluginState::empty(SecurityLevel::Secret, 0));
1059
1060        let expected = [
1061            (0, SecurityLevel::Public),
1062            (1, SecurityLevel::Internal),
1063            (2, SecurityLevel::Confidential),
1064            (3, SecurityLevel::Secret),
1065        ];
1066
1067        for (level_val, expected_level) in expected {
1068            let hc = HostCall::new(1, HostFunction::Declassify, vec![level_val], vec![], vec![]);
1069            let result = HostResult::empty();
1070
1071            let new_state = HostFunction::Declassify
1072                .execute(&state, &hc, &result)
1073                .unwrap_or_else(|e| panic!("level {level_val} should succeed: {e:?}"));
1074
1075            assert_eq!(new_state.plugin_level(1), Some(expected_level));
1076        }
1077    }
1078}