Skip to main content

lion_core/step/
mod.rs

1// Copyright (C) 2026 HaiyangLi
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Lion Step Module
4//!
5//! Step relation and all step constructors for the Lion microkernel state machine.
6
7mod authorization;
8mod host_call;
9mod kernel_op;
10mod plugin_internal;
11
12pub use authorization::{AuthorizationError, Authorized};
13pub use host_call::{HostCall, HostFunction, HostResult, ResourceType};
14pub use kernel_op::KernelOp;
15pub use plugin_internal::PluginInternal;
16
17use crate::state::{State, StateError};
18use crate::types::{ActorId, CapId, MemAddr, PluginId, SecurityLevel, Size, WorkflowId};
19
20/// Reasons a plugin precondition check can fail.
21#[derive(Debug)]
22pub enum PluginPrecondition {
23    /// Plugin's actor is blocked on another actor
24    Blocked {
25        /// The plugin that is blocked
26        pid: PluginId,
27        /// The actor it is blocked on (if known)
28        blocked_on: Option<ActorId>,
29    },
30    /// A read memory region is out of bounds
31    ReadOutOfBounds {
32        /// Start address of the region
33        addr: MemAddr,
34        /// Size of the region
35        size: Size,
36        /// Memory bounds of the plugin
37        bounds: Size,
38    },
39    /// A write memory region is out of bounds
40    WriteOutOfBounds {
41        /// Start address of the region
42        addr: MemAddr,
43        /// Size of the region
44        size: Size,
45        /// Memory bounds of the plugin
46        bounds: Size,
47    },
48    /// Attempted to consume from an empty mailbox
49    MailboxEmpty,
50}
51
52impl std::fmt::Display for PluginPrecondition {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            PluginPrecondition::Blocked { pid, blocked_on } => {
56                write!(f, "Plugin {pid} is blocked on {blocked_on:?}")
57            }
58            PluginPrecondition::ReadOutOfBounds { addr, size, bounds } => {
59                write!(
60                    f,
61                    "Read region {addr}+{size} out of bounds (memory size {bounds})"
62                )
63            }
64            PluginPrecondition::WriteOutOfBounds { addr, size, bounds } => {
65                write!(
66                    f,
67                    "Write region {addr}+{size} out of bounds (memory size {bounds})"
68                )
69            }
70            PluginPrecondition::MailboxEmpty => {
71                write!(f, "Cannot consume mailbox: mailbox is empty")
72            }
73        }
74    }
75}
76
77/// Reasons a host call precondition check can fail.
78#[derive(Debug)]
79pub enum HostCallPrecondition {
80    /// A read memory region is out of bounds
81    ReadOutOfBounds {
82        /// Start address of the region
83        addr: MemAddr,
84        /// Size of the region
85        size: Size,
86    },
87    /// A write memory region is out of bounds
88    WriteOutOfBounds {
89        /// Start address of the region
90        addr: MemAddr,
91        /// Size of the region
92        size: Size,
93    },
94    /// No valid capability is held by the caller
95    NoValidCapability,
96    /// No message provided for send operation
97    NoMessage,
98    /// Message source does not match the caller
99    SourceMismatch,
100    /// Destination mailbox is full
101    MailboxFull,
102    /// Invalid security level value
103    InvalidSecurityLevel {
104        /// The invalid level value
105        value: u64,
106    },
107    /// Cannot classify up (only declassification is allowed)
108    CannotClassifyUp,
109    /// No address argument provided for free operation
110    NoAddress,
111    /// No capability ID argument provided for revoke operation
112    NoCapabilityId,
113    /// Underlying free operation failed
114    FreeFailed {
115        /// The state error that caused the failure
116        source: StateError,
117    },
118    /// Operation requires Phase 3 State extension (threads/scheduler)
119    NotImplemented {
120        /// Description of the unimplemented operation
121        operation: &'static str,
122    },
123}
124
125impl std::fmt::Display for HostCallPrecondition {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        match self {
128            HostCallPrecondition::ReadOutOfBounds { addr, size } => {
129                write!(f, "Read region {addr}+{size} out of bounds")
130            }
131            HostCallPrecondition::WriteOutOfBounds { addr, size } => {
132                write!(f, "Write region {addr}+{size} out of bounds")
133            }
134            HostCallPrecondition::NoValidCapability => write!(f, "No valid capability held"),
135            HostCallPrecondition::NoMessage => write!(f, "No message to send"),
136            HostCallPrecondition::SourceMismatch => {
137                write!(f, "Message source doesn't match caller")
138            }
139            HostCallPrecondition::MailboxFull => write!(f, "Destination mailbox full"),
140            HostCallPrecondition::InvalidSecurityLevel { value } => {
141                write!(f, "Invalid security level: {value}")
142            }
143            HostCallPrecondition::CannotClassifyUp => write!(f, "Cannot classify up"),
144            HostCallPrecondition::NoAddress => write!(f, "No address to free"),
145            HostCallPrecondition::NoCapabilityId => write!(f, "No capability ID to revoke"),
146            HostCallPrecondition::FreeFailed { source } => {
147                write!(f, "Free failed: {source:?}")
148            }
149            HostCallPrecondition::NotImplemented { operation } => {
150                write!(
151                    f,
152                    "{operation} requires Phase 3 State extension (threads/scheduler)"
153                )
154            }
155        }
156    }
157}
158
159/// Reasons a kernel operation can fail.
160#[derive(Debug)]
161pub enum KernelOpError {
162    /// Actor has no pending messages to route
163    NoPendingMessages {
164        /// The destination actor
165        dst: ActorId,
166    },
167    /// Actor mailbox is at capacity
168    MailboxAtCapacity {
169        /// The destination actor
170        dst: ActorId,
171    },
172    /// Workflow not found
173    WorkflowNotFound {
174        /// The workflow ID
175        wid: WorkflowId,
176    },
177    /// Workflow is not in the running state
178    WorkflowNotRunning {
179        /// The workflow ID
180        wid: WorkflowId,
181    },
182    /// Operation requires Phase 3 State extension (threads/scheduler)
183    NotImplemented {
184        /// Description of the unimplemented operation
185        operation: &'static str,
186    },
187    /// Counter overflow (time or epoch would exceed u64::MAX)
188    CounterOverflow(String),
189}
190
191impl std::fmt::Display for KernelOpError {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            KernelOpError::NoPendingMessages { dst } => {
195                write!(f, "Actor {dst} has no pending messages")
196            }
197            KernelOpError::MailboxAtCapacity { dst } => {
198                write!(f, "Actor {dst} mailbox at capacity")
199            }
200            KernelOpError::WorkflowNotFound { wid } => write!(f, "Workflow {wid} not found"),
201            KernelOpError::WorkflowNotRunning { wid } => {
202                write!(f, "Workflow {wid} is not running")
203            }
204            KernelOpError::NotImplemented { operation } => {
205                write!(
206                    f,
207                    "{operation} requires Phase 3 State extension (threads/scheduler)"
208                )
209            }
210            KernelOpError::CounterOverflow(msg) => {
211                write!(f, "counter overflow: {msg}")
212            }
213        }
214    }
215}
216
217/// Reasons a state transition can be invalid.
218#[derive(Debug)]
219pub enum InvalidTransitionReason {
220    /// Attempted transition between incompatible states
221    IncompatibleStates {
222        /// Description of what was expected
223        expected: &'static str,
224        /// Description of what was found
225        found: &'static str,
226    },
227}
228
229impl std::fmt::Display for InvalidTransitionReason {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        match self {
232            InvalidTransitionReason::IncompatibleStates { expected, found } => {
233                write!(f, "expected {expected}, found {found}")
234            }
235        }
236    }
237}
238
239/// Error type for step operations.
240#[derive(Debug)]
241pub enum StepError {
242    /// Plugin not found
243    PluginNotFound(PluginId),
244    /// Actor not found
245    ActorNotFound(PluginId),
246    /// Memory address already freed (double-free prevention)
247    AddressAlreadyFreed(MemAddr),
248    /// Capability targets this address (temporal safety violation)
249    CapabilityTargetsAddress(CapId, MemAddr),
250    /// Capability not found
251    CapabilityNotFound(CapId),
252    /// Authorization failed
253    AuthorizationFailed(AuthorizationError),
254    /// Plugin precondition failed
255    PluginPreconditionFailed(PluginPrecondition),
256    /// Host call precondition failed
257    HostCallPreconditionFailed(HostCallPrecondition),
258    /// Kernel operation failed
259    KernelOpFailed(KernelOpError),
260    /// Invalid state transition
261    InvalidTransition(InvalidTransitionReason),
262    /// State operation failed
263    StateError(StateError),
264    /// Resource quota exceeded (DoS prevention)
265    QuotaExceeded(u8, u64, u64, u64),
266}
267
268impl std::fmt::Display for StepError {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        match self {
271            StepError::PluginNotFound(id) => write!(f, "plugin {id} not found"),
272            StepError::ActorNotFound(id) => write!(f, "actor {id} not found"),
273            StepError::AddressAlreadyFreed(addr) => write!(f, "address {addr} already freed"),
274            StepError::CapabilityTargetsAddress(cap, addr) => {
275                write!(f, "capability {cap} targets address {addr}")
276            }
277            StepError::CapabilityNotFound(id) => write!(f, "capability {id} not found"),
278            StepError::AuthorizationFailed(e) => write!(f, "authorization failed: {e}"),
279            StepError::PluginPreconditionFailed(reason) => {
280                write!(f, "plugin precondition failed: {reason}")
281            }
282            StepError::HostCallPreconditionFailed(reason) => {
283                write!(f, "host call precondition failed: {reason}")
284            }
285            StepError::KernelOpFailed(reason) => write!(f, "kernel operation failed: {reason}"),
286            StepError::InvalidTransition(reason) => write!(f, "invalid transition: {reason}"),
287            StepError::StateError(e) => write!(f, "state error: {e}"),
288            StepError::QuotaExceeded(kind, requested, current, quota) => {
289                let kind_name = match kind {
290                    0 => "memory",
291                    1 => "capability",
292                    2 => "ipc_queue",
293                    _ => "unknown",
294                };
295                write!(
296                    f,
297                    "{kind_name} quota exceeded: requested {requested}, current {current}, quota {quota}"
298                )
299            }
300        }
301    }
302}
303
304impl std::error::Error for StepError {}
305
306impl From<AuthorizationError> for StepError {
307    fn from(e: AuthorizationError) -> Self {
308        StepError::AuthorizationFailed(e)
309    }
310}
311
312impl From<StateError> for StepError {
313    fn from(e: StateError) -> Self {
314        StepError::StateError(e)
315    }
316}
317
318/// Step represents a single state transition in the Lion microkernel.
319#[derive(Debug, Clone)]
320#[allow(clippy::large_enum_variant)]
321#[must_use = "steps must be executed to apply state transitions"]
322pub enum Step {
323    /// Plugin-internal computation (untrusted, sandboxed)
324    PluginInternal {
325        /// The plugin performing computation
326        pid: PluginId,
327        /// The internal computation descriptor
328        pi: PluginInternal,
329    },
330
331    /// Host call (trust boundary, mediated)
332    HostCall {
333        /// The host call request
334        hc: HostCall,
335        /// Authorization witness (validated at execution)
336        auth: Authorized,
337        /// Result of host call execution
338        result: HostResult,
339    },
340
341    /// Kernel-internal operation (trusted TCB)
342    KernelInternal {
343        /// The kernel operation to execute
344        op: KernelOp,
345    },
346
347    /// Direct memory free operation (refinement bridge)
348    MemFree {
349        /// The plugin requesting the free
350        caller: PluginId,
351        /// The address to free
352        addr: MemAddr,
353    },
354
355    /// Direct capability revoke operation (refinement bridge)
356    CapRevoke {
357        /// The plugin requesting revocation
358        caller: PluginId,
359        /// The capability ID to revoke
360        cap_id: CapId,
361    },
362}
363
364impl Step {
365    /// Create a host call step with ATOMIC authorization.
366    ///
367    /// The action is derived from the `HostCall` via `hc.required_action()`,
368    /// binding authorization to the exact operation being performed.
369    /// This prevents the vulnerability where a valid `Authorized` token
370    /// for one action could be reused to execute a different `HostCall`.
371    pub fn host_call_atomic(
372        state: &State,
373        hc: HostCall,
374        cap_id: CapId,
375        ctx: crate::types::PolicyContext,
376        result: HostResult,
377    ) -> Result<Self, StepError> {
378        let cap = state
379            .get_cap(cap_id)
380            .cloned()
381            .ok_or(StepError::CapabilityNotFound(cap_id))?;
382
383        let action = hc.required_action()?;
384        if cap.holder() != hc.caller() {
385            return Err(StepError::AuthorizationFailed(
386                AuthorizationError::HolderMismatch {
387                    expected: hc.caller(),
388                    actual: cap.holder(),
389                },
390            ));
391        }
392
393        let auth = Authorized::validate_atomic(state, cap, action, ctx)?;
394        Ok(Step::HostCall { hc, auth, result })
395    }
396
397    /// Check if this step is effectful (crosses trust boundary).
398    pub fn is_effectful(&self) -> bool {
399        matches!(self, Step::HostCall { .. })
400    }
401
402    /// Get the subject (executing plugin) of the step, if any.
403    pub fn subject(&self) -> Option<PluginId> {
404        match self {
405            Step::PluginInternal { pid, .. } => Some(*pid),
406            Step::HostCall { hc, .. } => Some(hc.caller),
407            Step::KernelInternal { .. } => None,
408            Step::MemFree { caller, .. } | Step::CapRevoke { caller, .. } => Some(*caller),
409        }
410    }
411
412    /// Get the security level of the step.
413    pub fn level(&self, state: &State) -> SecurityLevel {
414        match self.subject() {
415            Some(pid) => state.plugin_level(pid).unwrap_or(SecurityLevel::Public),
416            None => SecurityLevel::Secret,
417        }
418    }
419
420    /// Check if this is a declassify operation.
421    pub fn is_declassify(&self) -> bool {
422        matches!(
423            self,
424            Step::HostCall {
425                hc: HostCall {
426                    function: HostFunction::Declassify,
427                    ..
428                },
429                ..
430            }
431        )
432    }
433
434    /// Execute this step, returning the new state.
435    pub fn execute(&self, state: &State) -> Result<State, StepError> {
436        match self {
437            Step::PluginInternal { pid, pi } => execute_plugin_internal(state, *pid, pi),
438            Step::HostCall { hc, auth, result } => execute_host_call(state, hc, auth, result),
439            Step::KernelInternal { op } => execute_kernel_internal(state, op),
440            Step::MemFree { caller: _, addr } => execute_mem_free(state, *addr),
441            Step::CapRevoke { caller: _, cap_id } => execute_cap_revoke(state, *cap_id),
442        }
443    }
444
445    /// Mutating execution -- modifies state in place (production path).
446    ///
447    /// Equivalent to `execute` but avoids the full-state clone by mutating
448    /// in place. The validation logic is identical.
449    pub fn execute_mut(&self, state: &mut State) -> Result<(), StepError> {
450        match self {
451            Step::PluginInternal { pid, pi } => execute_plugin_internal_mut(state, *pid, pi),
452            Step::HostCall { hc, auth, result } => execute_host_call_mut(state, hc, auth, result),
453            Step::KernelInternal { op } => execute_kernel_internal_mut(state, op),
454            Step::MemFree { caller: _, addr } => execute_mem_free_mut(state, *addr),
455            Step::CapRevoke { caller: _, cap_id } => execute_cap_revoke_mut(state, *cap_id),
456        }
457    }
458}
459
460fn execute_plugin_internal(
461    state: &State,
462    pid: PluginId,
463    pi: &PluginInternal,
464) -> Result<State, StepError> {
465    let plugin = state
466        .get_plugin(pid)
467        .ok_or(StepError::PluginNotFound(pid))?;
468
469    pi.check_preconditions(state, pid)?;
470
471    let mut new_state = state.clone();
472
473    if let Some(_new_plugin) = new_state.get_plugin_mut(pid) {
474        for &(addr, _size) in &pi.writes {
475            if !plugin.memory().addr_in_bounds(addr, 1) {
476                return Err(StepError::PluginPreconditionFailed(
477                    PluginPrecondition::WriteOutOfBounds {
478                        addr,
479                        size: 1,
480                        bounds: plugin.memory_bounds(),
481                    },
482                ));
483            }
484        }
485
486        if pi.consume_mailbox {
487            if let Some(actor) = new_state.get_actor_mut(pid) {
488                let _ = actor.consume_mut();
489            }
490        }
491    }
492
493    Ok(new_state)
494}
495
496fn execute_host_call(
497    state: &State,
498    hc: &HostCall,
499    auth: &Authorized,
500    result: &HostResult,
501) -> Result<State, StepError> {
502    // Verify authorization subject matches the host call caller
503    if auth.action().subject() != hc.caller() {
504        return Err(StepError::AuthorizationFailed(
505            AuthorizationError::HolderMismatch {
506                expected: auth.action().subject(),
507                actual: hc.caller(),
508            },
509        ));
510    }
511
512    // Verify the authorized action matches what the host call requires
513    let derived = hc.required_action()?;
514    if &derived != auth.action() {
515        return Err(StepError::InvalidTransition(
516            InvalidTransitionReason::IncompatibleStates {
517                expected: "authorized host call",
518                found: "mismatched host call",
519            },
520        ));
521    }
522
523    auth.validate(state)?;
524    hc.check_preconditions(state)?;
525    hc.function.execute(state, hc, result)
526}
527
528fn execute_kernel_internal(state: &State, op: &KernelOp) -> Result<State, StepError> {
529    op.execute(state)
530}
531
532fn execute_mem_free(state: &State, addr: MemAddr) -> Result<State, StepError> {
533    if state.ghost().is_freed(addr) {
534        return Err(StepError::AddressAlreadyFreed(addr));
535    }
536
537    // ResourceId = u128, addr = u64 (MemAddr)
538    if let Some(&(cap_id, _)) = state
539        .kernel()
540        .revocation()
541        .iter()
542        .find(|(_, cap)| cap.is_valid() && cap.target() == u128::from(addr))
543    {
544        return Err(StepError::CapabilityTargetsAddress(cap_id, addr));
545    }
546
547    state.apply_free(addr).map_err(StepError::StateError)
548}
549
550fn execute_cap_revoke(state: &State, cap_id: CapId) -> Result<State, StepError> {
551    if state.kernel().revocation().get(cap_id).is_none() {
552        return Err(StepError::CapabilityNotFound(cap_id));
553    }
554
555    Ok(state.apply_cap_revoke(cap_id))
556}
557
558// ============== MUTATING VARIANTS ==============
559//
560// These perform the same validation as their pure counterparts
561// but modify `&mut State` in place instead of cloning.
562
563fn execute_plugin_internal_mut(
564    state: &mut State,
565    pid: PluginId,
566    pi: &PluginInternal,
567) -> Result<(), StepError> {
568    let plugin = state
569        .get_plugin(pid)
570        .ok_or(StepError::PluginNotFound(pid))?;
571
572    pi.check_preconditions(state, pid)?;
573
574    // Validate writes before mutating (borrows plugin immutably)
575    for &(addr, _size) in &pi.writes {
576        if !plugin.memory().addr_in_bounds(addr, 1) {
577            return Err(StepError::PluginPreconditionFailed(
578                PluginPrecondition::WriteOutOfBounds {
579                    addr,
580                    size: 1,
581                    bounds: plugin.memory_bounds(),
582                },
583            ));
584        }
585    }
586
587    if pi.consume_mailbox {
588        if let Some(actor) = state.get_actor_mut(pid) {
589            let _ = actor.consume_mut();
590        }
591    }
592
593    Ok(())
594}
595
596fn execute_host_call_mut(
597    state: &mut State,
598    hc: &HostCall,
599    auth: &Authorized,
600    result: &HostResult,
601) -> Result<(), StepError> {
602    // Verify authorization subject matches the host call caller
603    if auth.action().subject() != hc.caller() {
604        return Err(StepError::AuthorizationFailed(
605            AuthorizationError::HolderMismatch {
606                expected: auth.action().subject(),
607                actual: hc.caller(),
608            },
609        ));
610    }
611
612    // Verify the authorized action matches what the host call requires
613    let derived = hc.required_action()?;
614    if &derived != auth.action() {
615        return Err(StepError::InvalidTransition(
616            InvalidTransitionReason::IncompatibleStates {
617                expected: "authorized host call",
618                found: "mismatched host call",
619            },
620        ));
621    }
622
623    auth.validate(state)?;
624    hc.check_preconditions(state)?;
625    hc.function.execute_mut(state, hc, result)
626}
627
628fn execute_kernel_internal_mut(state: &mut State, op: &KernelOp) -> Result<(), StepError> {
629    op.execute_mut(state)
630}
631
632fn execute_mem_free_mut(state: &mut State, addr: MemAddr) -> Result<(), StepError> {
633    if state.ghost().is_freed(addr) {
634        return Err(StepError::AddressAlreadyFreed(addr));
635    }
636
637    // ResourceId = u128, addr = u64 (MemAddr)
638    if let Some(&(cap_id, _)) = state
639        .kernel()
640        .revocation()
641        .iter()
642        .find(|(_, cap)| cap.is_valid() && cap.target() == u128::from(addr))
643    {
644        return Err(StepError::CapabilityTargetsAddress(cap_id, addr));
645    }
646
647    state.apply_free_mut(addr).map_err(StepError::StateError)
648}
649
650fn execute_cap_revoke_mut(state: &mut State, cap_id: CapId) -> Result<(), StepError> {
651    if state.kernel().revocation().get(cap_id).is_none() {
652        return Err(StepError::CapabilityNotFound(cap_id));
653    }
654
655    state
656        .apply_cap_revoke_mut(cap_id)
657        .map_err(|_| StepError::CapabilityNotFound(cap_id))
658}
659
660/// Reachability relation -- reflexive transitive closure of Step.
661pub fn reachable(start: &State, end: &State, steps: &[Step]) -> Result<bool, StepError> {
662    let mut current = start.clone();
663    for step in steps {
664        current = step.execute(&current)?;
665    }
666    Ok(current.time() == end.time())
667}