Skip to main content

ruvix_boot/
manifest.rs

1//! RVF Manifest parsing for boot and component mounting.
2//!
3//! The RVF manifest describes the structure of an RVF package:
4//! - Component graph (DAG of components with queue wiring)
5//! - Memory schema (region declarations)
6//! - Proof policy (per-component tier requirements)
7//! - Rollback hooks (WASM functions for state rollback)
8//! - Witness log policy (retention, compression, export)
9
10use ruvix_types::{ProofTier, RegionPolicy, WitTypeId};
11
12#[cfg(feature = "alloc")]
13use alloc::{string::String, vec::Vec};
14
15#[cfg(feature = "std")]
16use std::{string::String, vec::Vec};
17
18/// Maximum length of a component name.
19#[allow(dead_code)]
20pub const MAX_COMPONENT_NAME_LEN: usize = 256;
21
22/// Maximum rollback hook code size in bytes.
23#[allow(dead_code)]
24pub const MAX_ROLLBACK_HOOK_SIZE: usize = 64 * 1024; // 64 KiB
25
26/// RVF Manifest containing the complete package description.
27///
28/// The manifest is parsed and verified during Stage 1 of boot.
29/// All fields are validated before any kernel objects are created.
30#[derive(Debug, Clone)]
31pub struct RvfManifest {
32    /// Manifest format version.
33    pub version: ManifestVersion,
34
35    /// SHA-256 hash of the package contents.
36    pub content_hash: [u8; 32],
37
38    /// Component graph (DAG structure).
39    pub component_graph: ComponentGraph,
40
41    /// Memory schema (region declarations).
42    pub memory_schema: MemorySchema,
43
44    /// Proof policy (per-component tier requirements).
45    pub proof_policy: ProofPolicy,
46
47    /// Rollback hooks (WASM functions for state rollback).
48    pub rollback_hooks: RollbackHooks,
49
50    /// Witness log policy (retention, compression, export).
51    pub witness_log_policy: WitnessLogPolicy,
52
53    /// Minimum capabilities required by this package.
54    pub required_capabilities: RequiredCapabilities,
55}
56
57/// Manifest format version.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[repr(C)]
60pub struct ManifestVersion {
61    /// Major version (breaking changes).
62    pub major: u16,
63    /// Minor version (backward-compatible additions).
64    pub minor: u16,
65    /// Patch version (bug fixes).
66    pub patch: u16,
67}
68
69impl ManifestVersion {
70    /// Current manifest version.
71    pub const CURRENT: Self = Self {
72        major: 1,
73        minor: 0,
74        patch: 0,
75    };
76
77    /// Creates a new manifest version.
78    #[inline]
79    #[must_use]
80    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
81        Self { major, minor, patch }
82    }
83
84    /// Checks if this version is compatible with the current version.
85    #[inline]
86    #[must_use]
87    pub const fn is_compatible(&self) -> bool {
88        self.major == Self::CURRENT.major
89    }
90}
91
92impl Default for ManifestVersion {
93    fn default() -> Self {
94        Self::CURRENT
95    }
96}
97
98/// Component graph representing the DAG of WASM components.
99///
100/// Components are topologically sorted for initialization order.
101/// Queue wiring defines inter-component communication.
102#[derive(Debug, Clone, Default)]
103pub struct ComponentGraph {
104    /// Component declarations (topologically sorted).
105    pub components: Vec<ComponentDecl>,
106
107    /// Queue wiring (connections between components).
108    pub wirings: Vec<QueueWiring>,
109}
110
111impl ComponentGraph {
112    /// Creates an empty component graph.
113    #[inline]
114    #[must_use]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Returns the number of components.
120    #[inline]
121    #[must_use]
122    pub fn component_count(&self) -> usize {
123        self.components.len()
124    }
125
126    /// Returns the number of queue wirings.
127    #[inline]
128    #[must_use]
129    pub fn wiring_count(&self) -> usize {
130        self.wirings.len()
131    }
132
133    /// Validates the component graph structure.
134    ///
135    /// Returns `true` if:
136    /// - Components form a valid DAG (no cycles)
137    /// - All wiring references valid components
138    /// - Root component exists (index 0) or graph is empty (Phase A allows empty)
139    #[must_use]
140    pub fn validate(&self) -> bool {
141        // Phase A: Allow empty component graphs for test manifests
142        // Phase B would require at least one component
143        if self.components.is_empty() {
144            return true;
145        }
146
147        // Check wiring references
148        let count = self.component_count();
149        for wiring in &self.wirings {
150            if wiring.source_component as usize >= count
151                || wiring.target_component as usize >= count
152            {
153                return false;
154            }
155        }
156
157        true
158    }
159}
160
161/// Component declaration within an RVF package.
162#[derive(Debug, Clone)]
163pub struct ComponentDecl {
164    /// Component index (0 = root component).
165    pub index: u32,
166
167    /// Component name (for debugging).
168    pub name: String,
169
170    /// SHA-256 hash of the WASM component code.
171    pub code_hash: [u8; 32],
172
173    /// Offset in the RVF package to the WASM code.
174    pub code_offset: u64,
175
176    /// Size of the WASM code in bytes.
177    pub code_size: u64,
178
179    /// WIT type ID for the component's interface.
180    pub wit_type: WitTypeId,
181
182    /// Entry point function name.
183    pub entry_point: String,
184
185    /// Dependencies (indices of components this depends on).
186    pub dependencies: Vec<u32>,
187}
188
189impl ComponentDecl {
190    /// Returns the component name as a string slice.
191    #[must_use]
192    pub fn name_str(&self) -> &str {
193        &self.name
194    }
195
196    /// Returns the entry point as a string slice.
197    #[must_use]
198    pub fn entry_point_str(&self) -> &str {
199        &self.entry_point
200    }
201}
202
203/// Queue wiring between components.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205#[repr(C)]
206pub struct QueueWiring {
207    /// Source component index.
208    pub source_component: u32,
209
210    /// Source port name hash.
211    pub source_port_hash: u32,
212
213    /// Target component index.
214    pub target_component: u32,
215
216    /// Target port name hash.
217    pub target_port_hash: u32,
218
219    /// Queue capacity (number of entries).
220    pub queue_capacity: u32,
221
222    /// Maximum message size in bytes.
223    pub max_message_size: u32,
224
225    /// WIT type ID for message schema validation.
226    pub message_type: WitTypeId,
227}
228
229/// Memory schema containing region declarations.
230#[derive(Debug, Clone, Default)]
231pub struct MemorySchema {
232    /// Region declarations.
233    pub regions: Vec<RegionDecl>,
234
235    /// Total memory required in bytes.
236    pub total_memory_required: u64,
237}
238
239impl MemorySchema {
240    /// Creates an empty memory schema.
241    #[inline]
242    #[must_use]
243    pub fn new() -> Self {
244        Self::default()
245    }
246
247    /// Returns the number of region declarations.
248    #[inline]
249    #[must_use]
250    pub fn region_count(&self) -> usize {
251        self.regions.len()
252    }
253
254    /// Validates the memory schema.
255    #[must_use]
256    pub fn validate(&self) -> bool {
257        // Calculate total memory and check for overflow
258        let mut total: u64 = 0;
259        for region in &self.regions {
260            if let Some(size) = region.size_bytes() {
261                total = match total.checked_add(size) {
262                    Some(t) => t,
263                    None => return false,
264                };
265            }
266        }
267
268        total <= self.total_memory_required
269    }
270}
271
272/// Region declaration within the memory schema.
273#[derive(Debug, Clone)]
274pub struct RegionDecl {
275    /// Region index (unique within the manifest).
276    pub index: u32,
277
278    /// Region name (for debugging).
279    pub name: String,
280
281    /// Region access policy.
282    pub policy: RegionPolicy,
283
284    /// Owning component index.
285    pub owner_component: u32,
286
287    /// Components with read access (indices).
288    pub read_access: Vec<u32>,
289
290    /// Components with write access (indices).
291    pub write_access: Vec<u32>,
292}
293
294impl RegionDecl {
295    /// Returns the size of this region in bytes, if determinable.
296    #[must_use]
297    pub fn size_bytes(&self) -> Option<u64> {
298        match &self.policy {
299            RegionPolicy::Immutable => None, // Size determined at creation
300            RegionPolicy::AppendOnly { max_size } => Some(*max_size as u64),
301            RegionPolicy::Slab { slot_size, slot_count } => {
302                Some((*slot_size as u64) * (*slot_count as u64))
303            }
304        }
305    }
306}
307
308/// Proof policy containing per-component tier requirements.
309#[derive(Debug, Clone, Default)]
310pub struct ProofPolicy {
311    /// Per-component proof tier requirements.
312    pub component_tiers: Vec<ComponentProofTier>,
313
314    /// Default tier for components not explicitly listed.
315    pub default_tier: ProofTier,
316
317    /// Whether to allow tier escalation at runtime.
318    pub allow_tier_escalation: bool,
319}
320
321impl ProofPolicy {
322    /// Creates a new proof policy with the given default tier.
323    #[inline]
324    #[must_use]
325    pub fn new(default_tier: ProofTier) -> Self {
326        Self {
327            component_tiers: Vec::new(),
328            default_tier,
329            allow_tier_escalation: false,
330        }
331    }
332
333    /// Gets the proof tier for a component.
334    #[must_use]
335    pub fn tier_for_component(&self, component_index: u32) -> ProofTier {
336        for ct in &self.component_tiers {
337            if ct.component_index == component_index {
338                return ct.required_tier;
339            }
340        }
341        self.default_tier
342    }
343}
344
345/// Per-component proof tier requirement.
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347#[repr(C)]
348pub struct ComponentProofTier {
349    /// Component index.
350    pub component_index: u32,
351
352    /// Required proof tier for this component.
353    pub required_tier: ProofTier,
354
355    /// Operations requiring proof (bitmask).
356    pub proof_required_ops: ProofRequiredOps,
357}
358
359/// Bitmask for operations requiring proof.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
361#[repr(transparent)]
362pub struct ProofRequiredOps(pub u32);
363
364impl ProofRequiredOps {
365    /// No operations require proof.
366    pub const NONE: Self = Self(0);
367
368    /// Vector put operations require proof.
369    pub const VECTOR_PUT: Self = Self(1 << 0);
370
371    /// Graph apply operations require proof.
372    pub const GRAPH_APPLY: Self = Self(1 << 1);
373
374    /// Region writes require proof.
375    pub const REGION_WRITE: Self = Self(1 << 2);
376
377    /// Queue sends require proof.
378    pub const QUEUE_SEND: Self = Self(1 << 3);
379
380    /// All operations require proof.
381    pub const ALL: Self = Self(0b1111);
382
383    /// Checks if an operation requires proof.
384    #[inline]
385    #[must_use]
386    pub const fn requires(&self, op: Self) -> bool {
387        (self.0 & op.0) != 0
388    }
389}
390
391/// Container for rollback hooks.
392#[derive(Debug, Clone, Default)]
393pub struct RollbackHooks {
394    /// Rollback hook declarations.
395    pub hooks: Vec<RollbackHook>,
396}
397
398impl RollbackHooks {
399    /// Creates an empty rollback hooks container.
400    #[inline]
401    #[must_use]
402    pub fn new() -> Self {
403        Self::default()
404    }
405
406    /// Returns the number of rollback hooks.
407    #[inline]
408    #[must_use]
409    pub fn count(&self) -> usize {
410        self.hooks.len()
411    }
412}
413
414/// Rollback hook (WASM function for state rollback).
415#[derive(Debug, Clone)]
416pub struct RollbackHook {
417    /// Hook index.
418    pub index: u32,
419
420    /// Component index this hook applies to.
421    pub component_index: u32,
422
423    /// WASM function name to call for rollback.
424    pub function_name: String,
425
426    /// Regions this hook can access during rollback.
427    pub accessible_regions: Vec<u32>,
428
429    /// Timeout for rollback execution in microseconds.
430    pub timeout_us: u64,
431}
432
433/// Witness log policy configuration.
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435#[repr(C)]
436pub struct WitnessLogPolicy {
437    /// Maximum entries to retain before rotation.
438    pub max_entries: u64,
439
440    /// Maximum size in bytes before rotation.
441    pub max_size_bytes: u64,
442
443    /// Retention period in seconds (0 = forever).
444    pub retention_seconds: u64,
445
446    /// Compression algorithm.
447    pub compression: WitnessCompression,
448
449    /// Export policy.
450    pub export_policy: WitnessExportPolicy,
451
452    /// Whether to hash-chain entries for integrity.
453    pub hash_chain: bool,
454}
455
456impl Default for WitnessLogPolicy {
457    fn default() -> Self {
458        Self {
459            max_entries: 1_000_000,
460            max_size_bytes: 100 * 1024 * 1024, // 100 MiB
461            retention_seconds: 0,              // Forever
462            compression: WitnessCompression::None,
463            export_policy: WitnessExportPolicy::OnRotation,
464            hash_chain: true,
465        }
466    }
467}
468
469/// Witness log compression algorithm.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
471#[repr(u8)]
472pub enum WitnessCompression {
473    /// No compression.
474    #[default]
475    None = 0,
476
477    /// LZ4 compression (fast).
478    Lz4 = 1,
479
480    /// Zstd compression (better ratio).
481    Zstd = 2,
482}
483
484/// Witness log export policy.
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
486#[repr(u8)]
487pub enum WitnessExportPolicy {
488    /// Export on log rotation.
489    #[default]
490    OnRotation = 0,
491
492    /// Export on shutdown.
493    OnShutdown = 1,
494
495    /// Never export automatically.
496    Never = 2,
497
498    /// Export to external attestation service.
499    External = 3,
500}
501
502/// Required capabilities for the RVF package.
503#[derive(Debug, Clone, Default)]
504pub struct RequiredCapabilities {
505    /// Requires physical memory access.
506    pub physical_memory: bool,
507
508    /// Requires interrupt queue access.
509    pub interrupt_queue: bool,
510
511    /// Requires timer access.
512    pub timer: bool,
513
514    /// Requires vector store access.
515    pub vector_store: bool,
516
517    /// Requires graph store access.
518    pub graph_store: bool,
519
520    /// Minimum memory required in bytes.
521    pub min_memory_bytes: u64,
522}
523
524impl RvfManifest {
525    /// Parses an RVF manifest from bytes.
526    ///
527    /// # Errors
528    ///
529    /// Returns `KernelError::InvalidManifest` if the manifest is malformed.
530    pub fn parse(data: &[u8]) -> Result<Self, ruvix_types::KernelError> {
531        // Check minimum size
532        if data.len() < 96 {
533            return Err(ruvix_types::KernelError::InvalidManifest);
534        }
535
536        // Parse magic number "RVF1"
537        if &data[0..4] != b"RVF1" {
538            return Err(ruvix_types::KernelError::InvalidManifest);
539        }
540
541        // Parse version
542        let major = u16::from_le_bytes([data[4], data[5]]);
543        let minor = u16::from_le_bytes([data[6], data[7]]);
544        let patch = u16::from_le_bytes([data[8], data[9]]);
545        let version = ManifestVersion::new(major, minor, patch);
546
547        if !version.is_compatible() {
548            return Err(ruvix_types::KernelError::InvalidManifest);
549        }
550
551        // Parse content hash
552        let mut content_hash = [0u8; 32];
553        content_hash.copy_from_slice(&data[10..42]);
554
555        // Parse section offsets
556        let component_graph_offset = u32::from_le_bytes([data[42], data[43], data[44], data[45]]) as usize;
557        let memory_schema_offset = u32::from_le_bytes([data[46], data[47], data[48], data[49]]) as usize;
558        let proof_policy_offset = u32::from_le_bytes([data[50], data[51], data[52], data[53]]) as usize;
559        let rollback_hooks_offset = u32::from_le_bytes([data[54], data[55], data[56], data[57]]) as usize;
560        let witness_log_offset = u32::from_le_bytes([data[58], data[59], data[60], data[61]]) as usize;
561        let required_caps_offset = u32::from_le_bytes([data[62], data[63], data[64], data[65]]) as usize;
562
563        // Parse each section (simplified for Phase A)
564        let component_graph = Self::parse_component_graph(data, component_graph_offset)?;
565        let memory_schema = Self::parse_memory_schema(data, memory_schema_offset)?;
566        let proof_policy = Self::parse_proof_policy(data, proof_policy_offset)?;
567        let rollback_hooks = Self::parse_rollback_hooks(data, rollback_hooks_offset)?;
568        let witness_log_policy = Self::parse_witness_log_policy(data, witness_log_offset)?;
569        let required_capabilities = Self::parse_required_capabilities(data, required_caps_offset)?;
570
571        Ok(Self {
572            version,
573            content_hash,
574            component_graph,
575            memory_schema,
576            proof_policy,
577            rollback_hooks,
578            witness_log_policy,
579            required_capabilities,
580        })
581    }
582
583    fn parse_component_graph(_data: &[u8], offset: usize) -> Result<ComponentGraph, ruvix_types::KernelError> {
584        if offset == 0 {
585            return Ok(ComponentGraph::new());
586        }
587
588        // Minimal parsing for Phase A
589        Ok(ComponentGraph::new())
590    }
591
592    fn parse_memory_schema(_data: &[u8], offset: usize) -> Result<MemorySchema, ruvix_types::KernelError> {
593        if offset == 0 {
594            return Ok(MemorySchema::new());
595        }
596
597        Ok(MemorySchema::new())
598    }
599
600    fn parse_proof_policy(_data: &[u8], offset: usize) -> Result<ProofPolicy, ruvix_types::KernelError> {
601        if offset == 0 {
602            return Ok(ProofPolicy::new(ProofTier::Standard));
603        }
604
605        Ok(ProofPolicy::new(ProofTier::Standard))
606    }
607
608    fn parse_rollback_hooks(_data: &[u8], offset: usize) -> Result<RollbackHooks, ruvix_types::KernelError> {
609        if offset == 0 {
610            return Ok(RollbackHooks::new());
611        }
612
613        Ok(RollbackHooks::new())
614    }
615
616    fn parse_witness_log_policy(data: &[u8], offset: usize) -> Result<WitnessLogPolicy, ruvix_types::KernelError> {
617        if offset == 0 || offset >= data.len() {
618            return Ok(WitnessLogPolicy::default());
619        }
620
621        // Parse witness log policy fields
622        if offset + 32 > data.len() {
623            return Ok(WitnessLogPolicy::default());
624        }
625
626        let max_entries = u64::from_le_bytes([
627            data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
628            data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
629        ]);
630
631        let max_size_bytes = u64::from_le_bytes([
632            data[offset + 8], data[offset + 9], data[offset + 10], data[offset + 11],
633            data[offset + 12], data[offset + 13], data[offset + 14], data[offset + 15],
634        ]);
635
636        let retention_seconds = u64::from_le_bytes([
637            data[offset + 16], data[offset + 17], data[offset + 18], data[offset + 19],
638            data[offset + 20], data[offset + 21], data[offset + 22], data[offset + 23],
639        ]);
640
641        let compression = match data[offset + 24] {
642            1 => WitnessCompression::Lz4,
643            2 => WitnessCompression::Zstd,
644            _ => WitnessCompression::None,
645        };
646
647        let export_policy = match data[offset + 25] {
648            1 => WitnessExportPolicy::OnShutdown,
649            2 => WitnessExportPolicy::Never,
650            3 => WitnessExportPolicy::External,
651            _ => WitnessExportPolicy::OnRotation,
652        };
653
654        let hash_chain = data[offset + 26] != 0;
655
656        Ok(WitnessLogPolicy {
657            max_entries,
658            max_size_bytes,
659            retention_seconds,
660            compression,
661            export_policy,
662            hash_chain,
663        })
664    }
665
666    fn parse_required_capabilities(_data: &[u8], offset: usize) -> Result<RequiredCapabilities, ruvix_types::KernelError> {
667        if offset == 0 {
668            return Ok(RequiredCapabilities::default());
669        }
670
671        Ok(RequiredCapabilities::default())
672    }
673
674    /// Validates the entire manifest.
675    #[must_use]
676    pub fn validate(&self) -> bool {
677        self.component_graph.validate() && self.memory_schema.validate()
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn test_manifest_version_compatibility() {
687        let v1_0_0 = ManifestVersion::new(1, 0, 0);
688        let v1_1_0 = ManifestVersion::new(1, 1, 0);
689        let v2_0_0 = ManifestVersion::new(2, 0, 0);
690
691        assert!(v1_0_0.is_compatible());
692        assert!(v1_1_0.is_compatible());
693        assert!(!v2_0_0.is_compatible());
694    }
695
696    #[test]
697    fn test_component_graph_empty() {
698        let graph = ComponentGraph::new();
699        assert_eq!(graph.component_count(), 0);
700        // Phase A: Empty graphs are valid for test manifests
701        assert!(graph.validate());
702    }
703
704    #[test]
705    fn test_memory_schema_empty() {
706        let schema = MemorySchema::new();
707        assert_eq!(schema.region_count(), 0);
708        assert!(schema.validate()); // Empty schema is valid
709    }
710
711    #[test]
712    fn test_proof_policy_default_tier() {
713        let policy = ProofPolicy::new(ProofTier::Reflex);
714        assert_eq!(policy.tier_for_component(0), ProofTier::Reflex);
715        assert_eq!(policy.tier_for_component(999), ProofTier::Reflex);
716    }
717
718    #[test]
719    fn test_proof_required_ops() {
720        let ops = ProofRequiredOps::VECTOR_PUT;
721        assert!(ops.requires(ProofRequiredOps::VECTOR_PUT));
722        assert!(!ops.requires(ProofRequiredOps::GRAPH_APPLY));
723
724        let all = ProofRequiredOps::ALL;
725        assert!(all.requires(ProofRequiredOps::VECTOR_PUT));
726        assert!(all.requires(ProofRequiredOps::REGION_WRITE));
727    }
728
729    #[test]
730    fn test_witness_log_policy_default() {
731        let policy = WitnessLogPolicy::default();
732        assert_eq!(policy.max_entries, 1_000_000);
733        assert!(policy.hash_chain);
734        assert_eq!(policy.compression, WitnessCompression::None);
735    }
736
737    #[test]
738    fn test_manifest_parse_invalid_magic() {
739        let data = b"XXXX";
740        assert!(RvfManifest::parse(data).is_err());
741    }
742
743    #[test]
744    fn test_manifest_parse_too_short() {
745        let data = b"RVF1";
746        assert!(RvfManifest::parse(data).is_err());
747    }
748}