Skip to main content

nub_program/
blob.rs

1//! The [`ProgramBlob`] type and its region geometry.
2
3use alloc::collections::BTreeMap;
4use alloc::vec::Vec;
5
6use crate::abi::{DATA_BASE, PAGE_SIZE};
7
8/// Which of the four fixed data regions a [`Region`] describes.
9///
10/// The order of the variants is the address order: regions are laid
11/// out from [`DATA_BASE`] upward as stack, ro, rw, heap.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum RegionKind {
14    /// Guest stack. Always present. Grows downward from
15    /// [`Regions::stack_top`].
16    Stack,
17    /// Read-only data (`.rodata`). Backed by [`ProgramBlob::ro_data`].
18    Ro,
19    /// Read-write data (`.data` + `.bss`). Backed by
20    /// [`ProgramBlob::rw_data`].
21    Rw,
22    /// Heap. Zero-initialized; the guest's allocator owns it.
23    Heap,
24}
25
26impl RegionKind {
27    /// Whether the runtime must map this region read-only.
28    pub const fn is_read_only(self) -> bool {
29        matches!(self, RegionKind::Ro)
30    }
31}
32
33/// One data region's placement in the guest address space.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Region {
36    pub kind: RegionKind,
37    /// Page number (address / [`PAGE_SIZE`]), not an offset from
38    /// [`DATA_BASE`].
39    pub base_page: u32,
40    pub page_count: u32,
41}
42
43impl Region {
44    /// Guest address of the first byte.
45    pub const fn start(&self) -> u64 {
46        self.base_page as u64 * PAGE_SIZE as u64
47    }
48
49    /// Region length in bytes (always a whole number of pages).
50    pub const fn size(&self) -> u64 {
51        self.page_count as u64 * PAGE_SIZE as u64
52    }
53}
54
55/// Data-region geometry: the page count of each of the four fixed
56/// regions. Placement is derived, not stored — regions stack linearly
57/// from [`DATA_BASE`] in the order stack, ro, rw, heap, so the page
58/// counts alone determine every base address.
59///
60/// A region with zero pages is omitted from [`Regions::iter`] entirely
61/// and occupies no address space.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub struct Regions {
64    pub stack_pages: u32,
65    pub ro_pages: u32,
66    pub rw_pages: u32,
67    pub heap_pages: u32,
68}
69
70impl Regions {
71    /// Iterate every non-empty region in address (and [`RegionKind`])
72    /// order: stack, ro?, rw?, heap?.
73    ///
74    /// Consumers that pack a content-addressed arena in insertion
75    /// order depend on this order being stable; do not reorder it.
76    pub fn iter(&self) -> impl Iterator<Item = Region> + '_ {
77        let mut next_page = DATA_BASE / PAGE_SIZE;
78        [
79            (RegionKind::Stack, self.stack_pages),
80            (RegionKind::Ro, self.ro_pages),
81            (RegionKind::Rw, self.rw_pages),
82            (RegionKind::Heap, self.heap_pages),
83        ]
84        .into_iter()
85        .filter_map(move |(kind, page_count)| {
86            if page_count == 0 {
87                return None;
88            }
89            let base_page = next_page;
90            next_page += page_count;
91            Some(Region {
92                kind,
93                base_page,
94                page_count,
95            })
96        })
97    }
98
99    /// Look up one region by kind, or `None` if it is empty.
100    pub fn get(&self, kind: RegionKind) -> Option<Region> {
101        self.iter().find(|r| r.kind == kind)
102    }
103
104    /// Top-of-stack address (initial SP). RISC-V SP grows downward, so
105    /// the first push lands at `stack_top - 8`.
106    pub const fn stack_top(&self) -> u64 {
107        (DATA_BASE / PAGE_SIZE) as u64 * PAGE_SIZE as u64
108            + self.stack_pages as u64 * PAGE_SIZE as u64
109    }
110
111    /// Total pages across all regions.
112    pub const fn total_pages(&self) -> u32 {
113        self.stack_pages + self.ro_pages + self.rw_pages + self.heap_pages
114    }
115
116    /// Total data length in bytes, i.e. the size of the flat memory
117    /// image a runtime must materialize at [`DATA_BASE`].
118    pub const fn data_extent(&self) -> u64 {
119        self.total_pages() as u64 * PAGE_SIZE as u64
120    }
121
122    /// One past the last data byte, in guest addresses.
123    pub const fn data_end(&self) -> u64 {
124        DATA_BASE as u64 + self.data_extent()
125    }
126}
127
128/// One exported entry point.
129#[derive(Debug, Clone, PartialEq, Eq, Default)]
130pub struct Endpoint {
131    /// Byte offset into [`ProgramBlob::code`]. A runtime seeds
132    /// `PC = abi::CODE_BASE + entry_pc`.
133    pub entry_pc: u64,
134    /// Number of register args the caller supplies.
135    pub arg_registers: u8,
136    /// Opaque pass-through of the third descriptor metadata byte. nub
137    /// does not interpret it; a personality may (JAVM reads it as the
138    /// arg-cnode size).
139    pub arg_meta: u8,
140    /// Register-file overrides applied before entry, keyed by PVM
141    /// register index. The linker always seeds
142    /// [`abi::SP_REG`](crate::abi::SP_REG) with
143    /// [`Regions::stack_top`].
144    pub initial_regs: BTreeMap<u8, u64>,
145}
146
147/// A self-contained PVM2 program: raw code plus the region geometry and
148/// initial contents a runtime needs to build its address space.
149///
150/// This is the personality-free artifact the linker emits. A
151/// capability-based personality may *wrap* it — JAVM's cap `Image` is
152/// one such wrapping, adding cnode slots, content hashing and an SSZ
153/// encoding — but nothing here knows about capabilities.
154///
155/// # Invariants
156///
157/// [`ProgramBlob::new`] establishes and [`ProgramBlob::validate`]
158/// re-checks:
159///
160/// - `ro_data.len() == regions.ro_pages * PAGE_SIZE`
161/// - `rw_data.len() == regions.rw_pages * PAGE_SIZE`
162/// - `code.len() <= MAX_CODE_SIZE`
163/// - `regions.data_end() <= ADDRESS_SPACE_END`
164/// - at least one endpoint
165///
166/// Stack and heap have no backing bytes: they are zero-initialized.
167#[derive(Debug, Clone, PartialEq, Eq, Default)]
168pub struct ProgramBlob {
169    /// PVM2 bytecode, mapped read-only at
170    /// [`abi::CODE_BASE`](crate::abi::CODE_BASE).
171    pub code: Vec<u8>,
172    /// Page counts for the four data regions.
173    pub regions: Regions,
174    /// Read-only region contents, exactly `ro_pages * PAGE_SIZE` bytes.
175    pub ro_data: Vec<u8>,
176    /// Read-write region contents, exactly `rw_pages * PAGE_SIZE` bytes.
177    pub rw_data: Vec<u8>,
178    /// Exported entry points, keyed by endpoint index.
179    pub endpoints: BTreeMap<u8, Endpoint>,
180}
181
182/// Why a [`ProgramBlob`] is not well-formed.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum InvalidProgram {
185    /// `code.len()` exceeds `MAX_CODE_SIZE` — code would overlap
186    /// `DATA_BASE`.
187    CodeTooLarge { len: usize },
188    /// The data regions run past the 4 GiB guest address space.
189    DataOutOfRange { end: u64 },
190    /// A region's backing buffer length disagrees with its page count.
191    RegionLengthMismatch {
192        kind: RegionKind,
193        expected: usize,
194        actual: usize,
195    },
196    /// A program with no entry point can never be invoked.
197    NoEndpoints,
198}
199
200impl core::fmt::Display for InvalidProgram {
201    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
202        match self {
203            InvalidProgram::CodeTooLarge { len } => write!(
204                f,
205                "code length {len:#x} exceeds MAX_CODE_SIZE {:#x}",
206                crate::abi::MAX_CODE_SIZE
207            ),
208            InvalidProgram::DataOutOfRange { end } => {
209                write!(f, "data end {end:#x} exceeds the 4 GiB guest range")
210            }
211            InvalidProgram::RegionLengthMismatch {
212                kind,
213                expected,
214                actual,
215            } => write!(
216                f,
217                "{kind:?} region backing buffer is {actual} bytes, expected {expected}"
218            ),
219            InvalidProgram::NoEndpoints => f.write_str("program declares no endpoints"),
220        }
221    }
222}
223
224impl core::error::Error for InvalidProgram {}
225
226impl ProgramBlob {
227    /// Build a blob, zero-extending `ro_data`/`rw_data` to their page
228    /// counts so the length invariants hold, then validate.
229    ///
230    /// The linker hands over `.rodata`/`.data`+`.bss` buffers whose
231    /// lengths are whatever the ELF sections were; normalizing to whole
232    /// pages here is what makes encode/decode round-trip exactly.
233    pub fn new(
234        code: Vec<u8>,
235        regions: Regions,
236        mut ro_data: Vec<u8>,
237        mut rw_data: Vec<u8>,
238        endpoints: BTreeMap<u8, Endpoint>,
239    ) -> Result<Self, InvalidProgram> {
240        ro_data.resize(regions.ro_pages as usize * PAGE_SIZE as usize, 0);
241        rw_data.resize(regions.rw_pages as usize * PAGE_SIZE as usize, 0);
242        let blob = ProgramBlob {
243            code,
244            regions,
245            ro_data,
246            rw_data,
247            endpoints,
248        };
249        blob.validate()?;
250        Ok(blob)
251    }
252
253    /// Re-check the documented invariants.
254    pub fn validate(&self) -> Result<(), InvalidProgram> {
255        if self.code.len() > crate::abi::MAX_CODE_SIZE as usize {
256            return Err(InvalidProgram::CodeTooLarge {
257                len: self.code.len(),
258            });
259        }
260        if self.regions.data_end() > crate::abi::ADDRESS_SPACE_END {
261            return Err(InvalidProgram::DataOutOfRange {
262                end: self.regions.data_end(),
263            });
264        }
265        for (kind, data, pages) in [
266            (RegionKind::Ro, &self.ro_data, self.regions.ro_pages),
267            (RegionKind::Rw, &self.rw_data, self.regions.rw_pages),
268        ] {
269            let expected = pages as usize * PAGE_SIZE as usize;
270            if data.len() != expected {
271                return Err(InvalidProgram::RegionLengthMismatch {
272                    kind,
273                    expected,
274                    actual: data.len(),
275                });
276            }
277        }
278        if self.endpoints.is_empty() {
279            return Err(InvalidProgram::NoEndpoints);
280        }
281        Ok(())
282    }
283
284    /// The backing bytes for `kind`, or `None` for the zero-initialized
285    /// stack and heap regions.
286    pub fn region_data(&self, kind: RegionKind) -> Option<&[u8]> {
287        match kind {
288            RegionKind::Ro => Some(&self.ro_data),
289            RegionKind::Rw => Some(&self.rw_data),
290            RegionKind::Stack | RegionKind::Heap => None,
291        }
292    }
293
294    /// Materialize the flat data image a runtime maps at
295    /// [`DATA_BASE`]: `regions.data_extent()` bytes, with each region's
296    /// backing bytes at its offset and everything else zero.
297    pub fn memory_image(&self) -> Vec<u8> {
298        let mut image = alloc::vec![0u8; self.regions.data_extent() as usize];
299        for region in self.regions.iter() {
300            let Some(data) = self.region_data(region.kind) else {
301                continue;
302            };
303            let off = (region.start() - DATA_BASE as u64) as usize;
304            image[off..off + data.len()].copy_from_slice(data);
305        }
306        image
307    }
308}