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}