Skip to main content

javm_cap/
wire.rs

1//! Wire-form caps for the host ↔ guest `put_cap` RPC.
2//!
3//! [`Cap`] and its inner types use the SSZ derive macro for content
4//! hashing and carry rkyv-incompatible fields (`SparseList` in
5//! [`CNodeCap`], `Arc<PageBytes>` in `PageSlot::Loaded`). Adding
6//! rkyv derives there would either require hand-written `Archive` /
7//! `Serialize` / `Deserialize` impls for those types or a
8//! transformation wrapper. We pick a third option: a sibling enum
9//! whose shape mirrors `Cap` but flattens or omits the unsupported
10//! fields, with explicit `From<&Cap>` / `TryInto<Cap>` conversions
11//! at the wire boundary.
12//!
13//! ## V0 limitations
14//!
15//! - **`WireCap::CNode` only carries materialized `Hash` slot
16//!   entries.** `SparseList` cached-subtree-roots and
17//!   `MissingOr::Missing(_)` placeholders are dropped on the wire;
18//!   the receiver reconstructs a fresh [`CNodeCap`] without them.
19//!   `Ref(_)` slot targets are rejected (`WireConvertError::CapHasRef`)
20//!   because the receiver has no way to resolve them in its own
21//!   `CapRef` namespace.
22//! - **`WireCap::Data` only supports `DataContent::Inline`.** The
23//!   `Paged` variant errors out (`WireConvertError::PagedData`) —
24//!   `PageRef = Arc<PageBytes>` doesn't archive cleanly and the V0
25//!   bench guests don't need it.
26//! - **`WireCap::Instance` only carries `Hash` `root_cnode`** (no
27//!   live `Ref` targets, same reasoning as CNode).
28//!
29//! These limits cover the smoke-test path (Image + empty CNode +
30//! Instance with no rw_overlays containing Refs) and are tightened
31//! at the type level: the wire types simply don't have fields for
32//! the unsupported shapes.
33
34use alloc::boxed::Box;
35use alloc::vec::Vec;
36
37use crate::cap::{Cap, CapHashOrRef, NUM_REGS, TypeCap};
38use crate::cnode::CNodeCap;
39use crate::data::{DataCap, DataContent};
40use crate::image_cap::{EndpointDef, ImageCap, ImageSlotEntry, MemoryMapping};
41use crate::instance::{InstanceCap, RwOverlay};
42use crate::slot::SlotIdx;
43
44/// Failures the wire-form conversion can produce. All non-fatal:
45/// they indicate the cap shape isn't supported on the V0 RPC path.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum WireConvertError {
48    /// A `Cap` field held a `CapHashOrRef::Ref(_)` target. The
49    /// receiver has no way to resolve refs in its own `CapRef`
50    /// namespace, so refs are rejected.
51    CapHasRef,
52    /// A `Cap::Data` carried a `DataContent::Paged` body. V0 doesn't
53    /// serialise paged data.
54    PagedData,
55    /// A `Cap::CNode` carried a `MissingOr::Missing(_)` placeholder.
56    /// The wire form only carries materialized slot entries.
57    CNodeMissingSlot,
58}
59
60/// Wire-shaped cap. Derives `rkyv::{Archive, Serialize, Deserialize}`
61/// using only `alloc::Vec`/`Box` and plain `repr(C)` fields. The
62/// shape mirrors [`Cap`] but with the constraints called out in the
63/// module docs.
64#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
65pub enum WireCap {
66    Instance(WireInstanceCap),
67    Image(WireImageCap),
68    Data(WireDataCap),
69    CNode(WireCNodeCap),
70    Type(WireTypeCap),
71}
72
73/// Wire form of [`InstanceCap`]. `root_cnode` collapses
74/// [`CapHashOrRef`] down to a plain hash (V0: refs unsupported).
75#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
76pub struct WireInstanceCap {
77    pub image_hash_chain: [u8; 32],
78    pub image_hash: [u8; 32],
79    pub root_cnode_hash: [u8; 32],
80    pub rw_overlays: Vec<WireRwOverlay>,
81    pub mem_size: u32,
82    pub regs: [u64; NUM_REGS],
83    pub pc: u64,
84    pub gas_remaining: u64,
85}
86
87#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
88pub struct WireRwOverlay {
89    pub start: u32,
90    pub bytes: Vec<u8>,
91}
92
93/// Wire form of [`ImageCap`]. Direct field-for-field mirror — all
94/// inner types are derive-compatible already.
95#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
96pub struct WireImageCap {
97    pub code: Vec<u8>,
98    pub bitmask: Vec<u8>,
99    pub jump_table: Vec<u32>,
100    pub endpoints: Vec<WireEndpointDef>,
101    pub mappings: Vec<WireMemoryMapping>,
102    pub pinned: Vec<WireImageSlotEntry>,
103    pub initial: Vec<WireImageSlotEntry>,
104    pub yield_marker_slot: Option<u32>,
105}
106
107#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
108pub struct WireEndpointDef {
109    pub entry_pc: u64,
110    pub stack_top: u64,
111    pub arg_cnode_slot: u32,
112    pub arg_cnode_size: u8,
113    pub initial_regs: [u64; NUM_REGS],
114}
115
116#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
117pub struct WireMemoryMapping {
118    pub start: u64,
119    pub size: u64,
120    pub source_path: Vec<u32>,
121    pub source_path_len: u8,
122}
123
124#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
125pub struct WireImageSlotEntry {
126    pub slot: u32,
127    pub cap_hash: [u8; 32],
128}
129
130/// Wire form of [`DataCap`]. V0: inline-only.
131#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
132pub struct WireDataCap {
133    pub bytes: Vec<u8>,
134}
135
136/// Wire form of [`CNodeCap`]. Flat list of `(slot, hash)` pairs;
137/// only materialized `Hash` slot entries are carried.
138#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
139pub struct WireCNodeCap {
140    pub size_log: u8,
141    pub slots: Vec<WireCNodeSlot>,
142}
143
144#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
145pub struct WireCNodeSlot {
146    pub slot: u32,
147    pub cap_hash: [u8; 32],
148}
149
150#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
151pub struct WireTypeCap {
152    pub image_hash_chain: [u8; 32],
153}
154
155// --- Conversions ---
156
157impl WireCap {
158    /// Build a wire form from a borrowed [`Cap`]. The cap is read but
159    /// not mutated; the wire form owns its own allocations.
160    pub fn from_cap(cap: &Cap) -> Result<Self, WireConvertError> {
161        Ok(match cap {
162            Cap::Instance(i) => WireCap::Instance(WireInstanceCap::from_instance(i)?),
163            Cap::Image(i) => WireCap::Image(WireImageCap::from_image(i)),
164            Cap::Data(d) => WireCap::Data(WireDataCap::from_data(d)?),
165            Cap::CNode(c) => WireCap::CNode(WireCNodeCap::from_cnode(c)?),
166            Cap::Type(t) => WireCap::Type(WireTypeCap {
167                image_hash_chain: t.image_hash_chain,
168            }),
169        })
170    }
171
172    /// Recover an owned [`Cap`] from this wire form. Allocates fresh
173    /// storage in the caller's allocator.
174    pub fn into_cap(self) -> Result<Cap, WireConvertError> {
175        Ok(match self {
176            WireCap::Instance(i) => Cap::Instance(i.into_instance()),
177            WireCap::Image(i) => Cap::Image(i.into_image()),
178            WireCap::Data(d) => Cap::Data(d.into_data()),
179            WireCap::CNode(c) => Cap::CNode(c.into_cnode()?),
180            WireCap::Type(t) => Cap::Type(TypeCap {
181                image_hash_chain: t.image_hash_chain,
182            }),
183        })
184    }
185}
186
187impl WireInstanceCap {
188    fn from_instance(inst: &InstanceCap) -> Result<Self, WireConvertError> {
189        let root_cnode_hash = match inst.root_cnode {
190            CapHashOrRef::Hash(h) => h,
191            CapHashOrRef::Ref(_) => return Err(WireConvertError::CapHasRef),
192        };
193        let rw_overlays = inst
194            .rw_overlays
195            .iter()
196            .map(|ov| WireRwOverlay {
197                start: ov.start,
198                bytes: ov.bytes.clone(),
199            })
200            .collect();
201        Ok(Self {
202            image_hash_chain: inst.image_hash_chain,
203            image_hash: inst.image_hash,
204            root_cnode_hash,
205            rw_overlays,
206            mem_size: inst.mem_size,
207            regs: inst.regs,
208            pc: inst.pc,
209            gas_remaining: inst.gas_remaining,
210        })
211    }
212
213    fn into_instance(self) -> InstanceCap {
214        let rw_overlays = self
215            .rw_overlays
216            .into_iter()
217            .map(|w| RwOverlay {
218                start: w.start,
219                bytes: w.bytes,
220            })
221            .collect();
222        InstanceCap {
223            image_hash_chain: self.image_hash_chain,
224            image_hash: self.image_hash,
225            root_cnode: CapHashOrRef::Hash(self.root_cnode_hash),
226            rw_overlays,
227            mem_size: self.mem_size,
228            regs: self.regs,
229            pc: self.pc,
230            gas_remaining: self.gas_remaining,
231        }
232    }
233}
234
235impl WireImageCap {
236    fn from_image(img: &ImageCap) -> Self {
237        let endpoints = img
238            .endpoints
239            .iter()
240            .map(|e| WireEndpointDef {
241                entry_pc: e.entry_pc,
242                stack_top: e.stack_top,
243                arg_cnode_slot: e.arg_cnode_slot.get(),
244                arg_cnode_size: e.arg_cnode_size,
245                initial_regs: e.initial_regs,
246            })
247            .collect();
248        let mappings = img
249            .mappings
250            .iter()
251            .map(|m| WireMemoryMapping {
252                start: m.start,
253                size: m.size,
254                source_path: m.source_path.iter().map(|s| s.get()).collect(),
255                source_path_len: m.source_path_len,
256            })
257            .collect();
258        let pinned = img
259            .pinned
260            .iter()
261            .map(|e| WireImageSlotEntry {
262                slot: e.slot.get(),
263                cap_hash: e.cap_hash,
264            })
265            .collect();
266        let initial = img
267            .initial
268            .iter()
269            .map(|e| WireImageSlotEntry {
270                slot: e.slot.get(),
271                cap_hash: e.cap_hash,
272            })
273            .collect();
274        Self {
275            code: img.code.clone(),
276            bitmask: img.bitmask.clone(),
277            jump_table: img.jump_table.clone(),
278            endpoints,
279            mappings,
280            pinned,
281            initial,
282            yield_marker_slot: img.yield_marker_slot.map(|s| s.get()),
283        }
284    }
285
286    fn into_image(self) -> ImageCap {
287        let endpoints = self
288            .endpoints
289            .into_iter()
290            .map(|w| EndpointDef {
291                entry_pc: w.entry_pc,
292                stack_top: w.stack_top,
293                arg_cnode_slot: SlotIdx(w.arg_cnode_slot),
294                arg_cnode_size: w.arg_cnode_size,
295                initial_regs: w.initial_regs,
296            })
297            .collect();
298        let mappings = self
299            .mappings
300            .into_iter()
301            .map(|w| {
302                let mut source_path = [SlotIdx(0); crate::cap::MAX_SOURCE_DEPTH];
303                for (i, v) in w.source_path.iter().enumerate() {
304                    if i >= crate::cap::MAX_SOURCE_DEPTH {
305                        break;
306                    }
307                    source_path[i] = SlotIdx(*v);
308                }
309                MemoryMapping {
310                    start: w.start,
311                    size: w.size,
312                    source_path,
313                    source_path_len: w.source_path_len,
314                }
315            })
316            .collect();
317        let pinned = self
318            .pinned
319            .into_iter()
320            .map(|w| ImageSlotEntry {
321                slot: SlotIdx(w.slot),
322                cap_hash: w.cap_hash,
323            })
324            .collect();
325        let initial = self
326            .initial
327            .into_iter()
328            .map(|w| ImageSlotEntry {
329                slot: SlotIdx(w.slot),
330                cap_hash: w.cap_hash,
331            })
332            .collect();
333        ImageCap {
334            code: self.code,
335            bitmask: self.bitmask,
336            jump_table: self.jump_table,
337            endpoints,
338            mappings,
339            pinned,
340            initial,
341            yield_marker_slot: self.yield_marker_slot.map(SlotIdx),
342        }
343    }
344}
345
346impl WireDataCap {
347    fn from_data(d: &DataCap) -> Result<Self, WireConvertError> {
348        match &d.content {
349            DataContent::Inline(bytes) => Ok(Self {
350                bytes: bytes.clone(),
351            }),
352            DataContent::Paged { .. } => Err(WireConvertError::PagedData),
353        }
354    }
355
356    fn into_data(self) -> DataCap {
357        // Build a page-aligned, zero-padded buffer so the receiver's
358        // `DataCap` retains the page-alignment invariant the kernel
359        // expects when direct-mapping data caps into ring 3.
360        let bytes = self.bytes;
361        let mut buf = crate::data::alloc_page_aligned_zeroed(bytes.len());
362        let copy_len = bytes.len().min(buf.len());
363        buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
364        DataCap {
365            content: DataContent::Inline(buf),
366        }
367    }
368}
369
370impl WireCNodeCap {
371    fn from_cnode(cn: &CNodeCap) -> Result<Self, WireConvertError> {
372        let mut slots = Vec::new();
373        for (idx, entry) in cn.slots.iter() {
374            match entry {
375                ssz::MissingOr::Materialized(CapHashOrRef::Hash(h)) => {
376                    slots.push(WireCNodeSlot {
377                        slot: idx as u32,
378                        cap_hash: *h,
379                    });
380                }
381                ssz::MissingOr::Materialized(CapHashOrRef::Ref(_)) => {
382                    return Err(WireConvertError::CapHasRef);
383                }
384                ssz::MissingOr::Missing(_) => {
385                    return Err(WireConvertError::CNodeMissingSlot);
386                }
387            }
388        }
389        Ok(Self {
390            size_log: cn.size_log,
391            slots,
392        })
393    }
394
395    fn into_cnode(self) -> Result<CNodeCap, WireConvertError> {
396        // Reconstruct via the public CNodeCap API so invariants
397        // (size_log bound + slot-fits check) are upheld.
398        let mut cn =
399            CNodeCap::new(self.size_log).map_err(|_| WireConvertError::CNodeMissingSlot)?;
400        for entry in self.slots {
401            cn.set(
402                SlotIdx(entry.slot),
403                Some(CapHashOrRef::Hash(entry.cap_hash)),
404            )
405            .map_err(|_| WireConvertError::CNodeMissingSlot)?;
406        }
407        Ok(cn)
408    }
409}
410
411// --- Helpers used by the host driver ---
412
413/// Box-ed convenience: produce a `Box<Cap>` from an archived
414/// `WireCap`. Used by the guest's `put_cap` RPC handler to deposit
415/// the decoded cap into its directory.
416pub fn box_from_wire(wire: WireCap) -> Result<Box<Cap>, WireConvertError> {
417    wire.into_cap().map(Box::new)
418}
419
420/// Convenience: pretty-print a `WireConvertError` without depending
421/// on `thiserror` (so this module stays usable in `no_std` contexts
422/// that don't pull in the error infra).
423impl core::fmt::Display for WireConvertError {
424    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
425        match self {
426            WireConvertError::CapHasRef => {
427                f.write_str("cap holds a CapHashOrRef::Ref target; refs are unsupported on the wire")
428            }
429            WireConvertError::PagedData => {
430                f.write_str("DataContent::Paged is not supported on the wire (V0 inline-only)")
431            }
432            WireConvertError::CNodeMissingSlot => {
433                f.write_str("CNodeCap slot is unrepresentable on the wire (missing placeholder or oversized cnode)")
434            }
435        }
436    }
437}
438
439#[cfg(feature = "std")]
440impl std::error::Error for WireConvertError {}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::cap_hash::cap_hash;
446
447    #[test]
448    fn type_cap_roundtrip_preserves_hash() {
449        let cap = Cap::Type(TypeCap {
450            image_hash_chain: [0xAB; 32],
451        });
452        let wire = WireCap::from_cap(&cap).expect("from_cap");
453        let recovered = wire.into_cap().expect("into_cap");
454        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
455    }
456
457    #[test]
458    fn empty_cnode_roundtrip_preserves_hash() {
459        let cap = Cap::CNode(CNodeCap::new(0).expect("cnode"));
460        let wire = WireCap::from_cap(&cap).expect("from_cap");
461        let recovered = wire.into_cap().expect("into_cap");
462        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
463    }
464
465    #[test]
466    fn inline_data_roundtrip_preserves_hash() {
467        let cap = Cap::data_inline(b"hello-rkyv");
468        let wire = WireCap::from_cap(&cap).expect("from_cap");
469        let recovered = wire.into_cap().expect("into_cap");
470        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
471    }
472
473    #[test]
474    fn rkyv_archive_roundtrip_data_cap() {
475        let cap = Cap::data_inline(b"archive me");
476        let wire = WireCap::from_cap(&cap).expect("from_cap");
477        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&wire).expect("rkyv encode");
478        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(bytes.len());
479        aligned.extend_from_slice(&bytes);
480        let decoded: WireCap =
481            rkyv::from_bytes::<WireCap, rkyv::rancor::Error>(&aligned).expect("rkyv decode");
482        let recovered = decoded.into_cap().expect("into_cap");
483        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
484    }
485
486    #[test]
487    fn image_cap_roundtrip_preserves_hash() {
488        // Mirror what the smoke test publishes: a minimal Image with
489        // one endpoint and no slot references.
490        let mut img = crate::image::Image::empty();
491        img.code = alloc::vec![0u8, 10u8, 42];
492        img.packed_bitmask = alloc::vec![0b011u8];
493        let mut endpoints = alloc::collections::BTreeMap::new();
494        endpoints.insert(
495            0u8,
496            crate::image::EndpointDef {
497                entry_pc: 1,
498                arg_registers: 0,
499                arg_cnode_size: 0,
500                initial_regs: alloc::collections::BTreeMap::new(),
501            },
502        );
503        img.endpoints = endpoints;
504        let cap = Cap::image_with_slots(&img, &[], &[]).expect("image_with_slots");
505        let wire = WireCap::from_cap(&cap).expect("from_cap");
506        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&wire).expect("rkyv encode");
507        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(bytes.len());
508        aligned.extend_from_slice(&bytes);
509        let decoded: WireCap =
510            rkyv::from_bytes::<WireCap, rkyv::rancor::Error>(&aligned).expect("rkyv decode");
511        let recovered = decoded.into_cap().expect("into_cap");
512        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
513    }
514
515    #[test]
516    fn instance_cap_roundtrip_preserves_hash() {
517        let cap = Cap::instance_with_overlays(
518            [0u8; 32],
519            [0xAA; 32],
520            [0xBB; 32],
521            &[],
522            4096,
523            [0u64; NUM_REGS],
524            0,
525            0,
526        );
527        let wire = WireCap::from_cap(&cap).expect("from_cap");
528        let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&wire).expect("rkyv encode");
529        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity(bytes.len());
530        aligned.extend_from_slice(&bytes);
531        let decoded: WireCap =
532            rkyv::from_bytes::<WireCap, rkyv::rancor::Error>(&aligned).expect("rkyv decode");
533        let recovered = decoded.into_cap().expect("into_cap");
534        assert_eq!(cap_hash(&cap), cap_hash(&recovered));
535    }
536}