Skip to main content

javm_exec/
mem.rs

1//! Flat-buffer memory model + the [`Memory`] trait that abstracts
2//! over different memory backends (software-copy here, hardware-paged
3//! in the bare-metal Hyperlight guest).
4//!
5//! Matches v2 javm's `flat_mem` layout for perf parity: a single
6//! contiguous `Vec<u8>` indexed by 32-bit address. Reads/writes are
7//! bounds-checked against `flat_mem.len()`; on out-of-range the
8//! caller gets `false`/`None` and translates to `ExitReason::PageFault`.
9//!
10//! Per-page permissions are tracked separately in `flat_perms` (one
11//! byte per page) so the JIT signal handler can detect ro-write
12//! faults without involving the interpreter. The interpreter itself
13//! relies on the page-protected mmap mapping (Stage 3 / kernel
14//! integration) for read-only enforcement; this layer just bounds-
15//! checks.
16//!
17//! The fast-path read/write helpers use `read_unaligned` /
18//! `write_unaligned` via raw pointers — single MOV on x86. Same
19//! shape as v2 `javm/src/interpreter/mod.rs:198-309`.
20
21use alloc::vec::Vec;
22
23/// PVM page size: 4 KiB.
24pub const PAGE_SIZE: u32 = 1 << 12;
25
26/// Per-page permission byte (matches v2's `flat_perms` semantics).
27pub mod perm {
28    /// Page is inaccessible (read or write faults).
29    pub const NONE: u8 = 0;
30    /// Page is readable; writes fault.
31    pub const RO: u8 = 1;
32    /// Page is readable + writable.
33    pub const RW: u8 = 2;
34}
35
36/// Mapping permission for [`Memory::map_region`]. RO regions back
37/// `perm::RO` pages; RW regions back `perm::RW` pages.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum Access {
40    ReadOnly,
41    ReadWrite,
42}
43
44/// Outcome of a memory access (slow path; the fast inline helpers
45/// return raw `Option` / `bool`).
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum MemAccess {
48    Ok,
49    /// Page not mapped at the page-aligned address.
50    PageFault(u32),
51    /// Page is read-only and the access is a write.
52    WriteProtected(u32),
53}
54
55/// Setup-time error for [`Memory::map_region`].
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum MapError {
58    /// `start` is not page-aligned.
59    UnalignedStart(u64),
60    /// `size` is not a multiple of [`PAGE_SIZE`].
61    UnalignedSize(u64),
62    /// `start + size` overflows `usize` on this platform, or exceeds
63    /// the addressable range supported by `Mem`.
64    Overflow,
65}
66
67/// Memory backend abstraction.
68///
69/// The interpreter is generic over `M: Memory` so the same source
70/// compiles for two substrates:
71///
72/// - **Software-copy**: [`CopyingMemory`] (this module) — an owning
73///   `Vec<u8>` with per-page permissions. Runs in-process.
74/// - **Hardware-paged**: a future bare-metal impl in
75///   `nub-arch-x86` that maps PVM pages onto real CPU pages
76///   via the in-guest IDT + page tables.
77///
78/// Hot-path methods (`read_u*`/`write_u*`) return `Option<T>` or
79/// `bool` to keep the interpreter loop branch-free. Implementations
80/// should mark these `#[inline]` (or `#[inline(always)]`) — the
81/// interpreter calls them through trait dispatch, and we want
82/// monomorphisation to collapse to direct function calls.
83pub trait Memory {
84    // ---- Hot-path width-typed reads. ----
85    fn read_u8(&self, addr: u32) -> Option<u8>;
86    fn read_u16_le(&self, addr: u32) -> Option<u16>;
87    fn read_u32_le(&self, addr: u32) -> Option<u32>;
88    fn read_u64_le(&self, addr: u32) -> Option<u64>;
89
90    // ---- Hot-path width-typed writes. ----
91    fn write_u8(&mut self, addr: u32, val: u8) -> bool;
92    fn write_u16_le(&mut self, addr: u32, val: u16) -> bool;
93    fn write_u32_le(&mut self, addr: u32, val: u32) -> bool;
94    fn write_u64_le(&mut self, addr: u32, val: u64) -> bool;
95
96    // ---- Setup-time + cold-path. ----
97
98    /// Declare a mapped region. See [`CopyingMemory::map_region`] for
99    /// the canonical semantics.
100    fn map_region(
101        &mut self,
102        start: u64,
103        size: u64,
104        access: Access,
105        init: Option<&[u8]>,
106    ) -> Result<(), MapError>;
107
108    /// Per-page permission byte for the page containing `addr`.
109    /// Returns [`perm::NONE`] if `addr` is out of range.
110    fn perm_of(&self, addr: u32) -> u8;
111
112    /// Read `dst.len()` bytes starting at `addr` into `dst`.
113    fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess>;
114
115    /// Write `data.len()` bytes starting at `addr`. Per-page perm
116    /// checks apply; out-of-range or RO-page writes return `Err`.
117    fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess>;
118}
119
120/// Address-space mapping for one execution context.
121///
122/// Flat-buffer layout matching v2 javm. The buffer's length defines
123/// the upper bound of valid addresses; per-page permissions live in
124/// `perms`. Implements [`Memory`] via inherent-method delegation so
125/// concrete callers don't need to import the trait.
126#[derive(Clone, Debug)]
127pub struct CopyingMemory {
128    /// Contiguous byte buffer covering `0..flat_mem.len()`.
129    pub flat_mem: Vec<u8>,
130    /// One permission byte per `PAGE_SIZE`-page in `flat_mem`.
131    /// `perms.len() == flat_mem.len() / PAGE_SIZE` (rounded up).
132    pub perms: Vec<u8>,
133    /// Heap base address (for sbrk).
134    pub heap_base: u32,
135    /// Current heap top.
136    pub heap_top: u32,
137    /// Maximum heap pages (sbrk refuses beyond this).
138    pub max_heap_pages: u32,
139}
140
141impl Default for CopyingMemory {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147/// Compatibility alias for the pre-trait name. Consumers can keep
148/// writing `Mem`; new code should prefer `CopyingMemory` (when the
149/// concrete impl is wanted) or be generic over `M: Memory`.
150pub type Mem = CopyingMemory;
151
152impl CopyingMemory {
153    /// Empty memory; no pages allocated.
154    pub fn new() -> Self {
155        Self {
156            flat_mem: Vec::new(),
157            perms: Vec::new(),
158            heap_base: 0,
159            heap_top: 0,
160            max_heap_pages: 0,
161        }
162    }
163
164    /// Construct with a pre-sized flat buffer (zero-initialized).
165    /// `n_pages` is the number of `PAGE_SIZE`-pages.
166    pub fn with_pages(n_pages: u32, default_perm: u8) -> Self {
167        let bytes = (n_pages as usize) * (PAGE_SIZE as usize);
168        Self {
169            flat_mem: vec![0u8; bytes],
170            perms: vec![default_perm; n_pages as usize],
171            heap_base: 0,
172            heap_top: 0,
173            max_heap_pages: 0,
174        }
175    }
176
177    /// Returns true iff `addr` is within `flat_mem`.
178    #[inline(always)]
179    pub fn is_in_bounds(&self, addr: u32) -> bool {
180        (addr as usize) < self.flat_mem.len()
181    }
182
183    /// Per-page permission for the page containing `addr`. Returns
184    /// `perm::NONE` if the address is out of range.
185    pub fn perm_of(&self, addr: u32) -> u8 {
186        let page = (addr / PAGE_SIZE) as usize;
187        self.perms.get(page).copied().unwrap_or(perm::NONE)
188    }
189
190    // ---- Fast-path read helpers (inline; single bounds check + raw pointer load). ----
191
192    #[inline(always)]
193    pub fn read_u8(&self, addr: u32) -> Option<u8> {
194        let a = addr as usize;
195        if a < self.flat_mem.len() {
196            // SAFETY: bounds-checked.
197            Some(unsafe { *self.flat_mem.get_unchecked(a) })
198        } else {
199            None
200        }
201    }
202
203    #[inline(always)]
204    pub fn read_u16_le(&self, addr: u32) -> Option<u16> {
205        let a = addr as usize;
206        if a + 2 <= self.flat_mem.len() {
207            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u16>().read_unaligned() })
208        } else {
209            None
210        }
211    }
212
213    #[inline(always)]
214    pub fn read_u32_le(&self, addr: u32) -> Option<u32> {
215        let a = addr as usize;
216        if a + 4 <= self.flat_mem.len() {
217            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u32>().read_unaligned() })
218        } else {
219            None
220        }
221    }
222
223    #[inline(always)]
224    pub fn read_u64_le(&self, addr: u32) -> Option<u64> {
225        let a = addr as usize;
226        if a + 8 <= self.flat_mem.len() {
227            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u64>().read_unaligned() })
228        } else {
229            None
230        }
231    }
232
233    // ---- Fast-path write helpers. ----
234
235    #[inline(always)]
236    pub fn write_u8(&mut self, addr: u32, val: u8) -> bool {
237        let a = addr as usize;
238        if a < self.flat_mem.len() {
239            unsafe {
240                *self.flat_mem.get_unchecked_mut(a) = val;
241            }
242            true
243        } else {
244            false
245        }
246    }
247
248    #[inline(always)]
249    pub fn write_u16_le(&mut self, addr: u32, val: u16) -> bool {
250        let a = addr as usize;
251        if a + 2 <= self.flat_mem.len() {
252            unsafe {
253                self.flat_mem
254                    .as_mut_ptr()
255                    .add(a)
256                    .cast::<u16>()
257                    .write_unaligned(val);
258            }
259            true
260        } else {
261            false
262        }
263    }
264
265    #[inline(always)]
266    pub fn write_u32_le(&mut self, addr: u32, val: u32) -> bool {
267        let a = addr as usize;
268        if a + 4 <= self.flat_mem.len() {
269            unsafe {
270                self.flat_mem
271                    .as_mut_ptr()
272                    .add(a)
273                    .cast::<u32>()
274                    .write_unaligned(val);
275            }
276            true
277        } else {
278            false
279        }
280    }
281
282    #[inline(always)]
283    pub fn write_u64_le(&mut self, addr: u32, val: u64) -> bool {
284        let a = addr as usize;
285        if a + 8 <= self.flat_mem.len() {
286            unsafe {
287                self.flat_mem
288                    .as_mut_ptr()
289                    .add(a)
290                    .cast::<u64>()
291                    .write_unaligned(val);
292            }
293            true
294        } else {
295            false
296        }
297    }
298
299    /// Declare a mapped region at `[start, start + size)` with
300    /// per-page permissions `access` and optional initial bytes.
301    ///
302    /// - `start` and `size` must each be multiples of [`PAGE_SIZE`].
303    /// - Grows `flat_mem` to cover `start + size` if necessary
304    ///   (newly-grown bytes are zero-initialized; their pages
305    ///   default to [`perm::NONE`] before this call sets them).
306    /// - Sets pages in `[start / PAGE_SIZE, (start + size) /
307    ///   PAGE_SIZE)` to the permission byte for `access`.
308    /// - If `init` is `Some(bytes)`, copies `bytes[..bytes.len()
309    ///   .min(size)]` into `flat_mem[start..]`; the rest of the
310    ///   region remains zero-filled (matches the DataCap canonical
311    ///   form: trailing zeros are stripped from `content`, but the
312    ///   logical `size` may be larger).
313    pub fn map_region(
314        &mut self,
315        start: u64,
316        size: u64,
317        access: Access,
318        init: Option<&[u8]>,
319    ) -> Result<(), MapError> {
320        let page = PAGE_SIZE as u64;
321        if !start.is_multiple_of(page) {
322            return Err(MapError::UnalignedStart(start));
323        }
324        if !size.is_multiple_of(page) {
325            return Err(MapError::UnalignedSize(size));
326        }
327        let end = start.checked_add(size).ok_or(MapError::Overflow)?;
328        let end_usize: usize = end.try_into().map_err(|_| MapError::Overflow)?;
329
330        // Grow flat_mem + perms to cover [0, end).
331        if end_usize > self.flat_mem.len() {
332            self.flat_mem.resize(end_usize, 0);
333            let needed_pages = end_usize.div_ceil(PAGE_SIZE as usize);
334            if self.perms.len() < needed_pages {
335                self.perms.resize(needed_pages, perm::NONE);
336            }
337        }
338
339        // Set permissions on the affected pages.
340        let perm_byte = match access {
341            Access::ReadOnly => perm::RO,
342            Access::ReadWrite => perm::RW,
343        };
344        let first_page = (start / page) as usize;
345        let last_page = ((end / page) as usize).saturating_sub(1);
346        if size > 0 {
347            for p in first_page..=last_page {
348                self.perms[p] = perm_byte;
349            }
350        }
351
352        // Copy initial bytes if any. The destination starts as zero
353        // either from initial allocation or the grow above, so any
354        // trailing region beyond `init` is implicitly zero.
355        if let Some(bytes) = init {
356            let n = bytes.len().min(size as usize);
357            let s = start as usize;
358            self.flat_mem[s..s + n].copy_from_slice(&bytes[..n]);
359        }
360
361        Ok(())
362    }
363
364    // ---- Slow-path helpers (for tests / non-hot paths). ----
365
366    /// Read `len` bytes from `addr`. Returns `Err` on out-of-range.
367    pub fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess> {
368        let a = addr as usize;
369        let end = a
370            .checked_add(len)
371            .ok_or(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)))?;
372        if end > self.flat_mem.len() {
373            return Err(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)));
374        }
375        Ok(self.flat_mem[a..end].to_vec())
376    }
377
378    /// Write `data` starting at `addr`. Returns `Err` on out-of-range
379    /// or write-protected page. Writes are NOT rolled back on partial
380    /// failure (test-only API).
381    pub fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess> {
382        let a = addr as usize;
383        let end = a
384            .checked_add(data.len())
385            .ok_or(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)))?;
386        if end > self.flat_mem.len() {
387            return Err(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)));
388        }
389        // Check perms per page touched.
390        let start_page = a / (PAGE_SIZE as usize);
391        let last_page = (end - 1) / (PAGE_SIZE as usize);
392        for p in start_page..=last_page {
393            if self.perms.get(p).copied().unwrap_or(perm::NONE) != perm::RW {
394                return Err(MemAccess::WriteProtected((p as u32) * PAGE_SIZE));
395            }
396        }
397        self.flat_mem[a..end].copy_from_slice(data);
398        Ok(())
399    }
400}
401
402// `Memory` impl delegates to inherent methods via UFCS (no name
403// clash, no recursion). All bodies are `#[inline(always)]` so trait
404// dispatch is zero-cost after monomorphisation.
405impl Memory for CopyingMemory {
406    #[inline(always)]
407    fn read_u8(&self, addr: u32) -> Option<u8> {
408        CopyingMemory::read_u8(self, addr)
409    }
410    #[inline(always)]
411    fn read_u16_le(&self, addr: u32) -> Option<u16> {
412        CopyingMemory::read_u16_le(self, addr)
413    }
414    #[inline(always)]
415    fn read_u32_le(&self, addr: u32) -> Option<u32> {
416        CopyingMemory::read_u32_le(self, addr)
417    }
418    #[inline(always)]
419    fn read_u64_le(&self, addr: u32) -> Option<u64> {
420        CopyingMemory::read_u64_le(self, addr)
421    }
422    #[inline(always)]
423    fn write_u8(&mut self, addr: u32, val: u8) -> bool {
424        CopyingMemory::write_u8(self, addr, val)
425    }
426    #[inline(always)]
427    fn write_u16_le(&mut self, addr: u32, val: u16) -> bool {
428        CopyingMemory::write_u16_le(self, addr, val)
429    }
430    #[inline(always)]
431    fn write_u32_le(&mut self, addr: u32, val: u32) -> bool {
432        CopyingMemory::write_u32_le(self, addr, val)
433    }
434    #[inline(always)]
435    fn write_u64_le(&mut self, addr: u32, val: u64) -> bool {
436        CopyingMemory::write_u64_le(self, addr, val)
437    }
438    #[inline]
439    fn map_region(
440        &mut self,
441        start: u64,
442        size: u64,
443        access: Access,
444        init: Option<&[u8]>,
445    ) -> Result<(), MapError> {
446        CopyingMemory::map_region(self, start, size, access, init)
447    }
448    #[inline]
449    fn perm_of(&self, addr: u32) -> u8 {
450        CopyingMemory::perm_of(self, addr)
451    }
452    #[inline]
453    fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess> {
454        CopyingMemory::read(self, addr, len)
455    }
456    #[inline]
457    fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess> {
458        CopyingMemory::write(self, addr, data)
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn read_u8_in_bounds() {
468        let mut m = Mem::with_pages(1, perm::RW);
469        m.write_u8(0x100, 0xAB);
470        assert_eq!(m.read_u8(0x100), Some(0xAB));
471    }
472
473    #[test]
474    fn read_u8_out_of_bounds_returns_none() {
475        let m = Mem::with_pages(1, perm::RW);
476        assert_eq!(m.read_u8(PAGE_SIZE), None);
477        assert_eq!(m.read_u8(u32::MAX), None);
478    }
479
480    #[test]
481    fn read_write_u32_le() {
482        let mut m = Mem::with_pages(1, perm::RW);
483        m.write_u32_le(0x10, 0xDEAD_BEEF);
484        assert_eq!(m.read_u32_le(0x10), Some(0xDEAD_BEEF));
485    }
486
487    #[test]
488    fn unaligned_access_works() {
489        let mut m = Mem::with_pages(1, perm::RW);
490        m.write_u32_le(0x103, 0x1234_5678);
491        assert_eq!(m.read_u32_le(0x103), Some(0x1234_5678));
492    }
493
494    #[test]
495    fn read_u32_straddling_end_returns_none() {
496        let m = Mem::with_pages(1, perm::RW);
497        // PAGE_SIZE - 2 → would read 4 bytes ending at PAGE_SIZE + 2, OOB.
498        assert_eq!(m.read_u32_le(PAGE_SIZE - 2), None);
499    }
500
501    #[test]
502    fn ro_page_write_via_slow_path_faults() {
503        let mut m = Mem::with_pages(1, perm::RO);
504        let res = m.write(0, &[1]);
505        assert!(matches!(res, Err(MemAccess::WriteProtected(_))));
506    }
507
508    #[test]
509    fn perm_of_page_after_set() {
510        let m = Mem::with_pages(2, perm::RW);
511        assert_eq!(m.perm_of(0), perm::RW);
512        assert_eq!(m.perm_of(PAGE_SIZE), perm::RW);
513        // Out of range
514        assert_eq!(m.perm_of(2 * PAGE_SIZE), perm::NONE);
515    }
516
517    #[test]
518    fn slow_path_read_write_round_trip() {
519        let mut m = Mem::with_pages(1, perm::RW);
520        m.write(0, &[1, 2, 3, 4]).unwrap();
521        assert_eq!(m.read(0, 4).unwrap(), vec![1, 2, 3, 4]);
522    }
523
524    #[test]
525    fn map_region_grows_buffer_and_sets_perms() {
526        let mut m = Mem::new();
527        m.map_region(
528            2 * PAGE_SIZE as u64,
529            2 * PAGE_SIZE as u64,
530            Access::ReadWrite,
531            None,
532        )
533        .unwrap();
534        assert_eq!(m.flat_mem.len(), 4 * PAGE_SIZE as usize);
535        // Pages 0..2 are unmapped; pages 2..4 are RW.
536        assert_eq!(m.perm_of(0), perm::NONE);
537        assert_eq!(m.perm_of(PAGE_SIZE), perm::NONE);
538        assert_eq!(m.perm_of(2 * PAGE_SIZE), perm::RW);
539        assert_eq!(m.perm_of(3 * PAGE_SIZE), perm::RW);
540    }
541
542    #[test]
543    fn map_region_copies_init_bytes_and_zero_fills_tail() {
544        let mut m = Mem::new();
545        m.map_region(
546            0,
547            PAGE_SIZE as u64,
548            Access::ReadOnly,
549            Some(&[0xAA, 0xBB, 0xCC]),
550        )
551        .unwrap();
552        assert_eq!(m.flat_mem[0], 0xAA);
553        assert_eq!(m.flat_mem[1], 0xBB);
554        assert_eq!(m.flat_mem[2], 0xCC);
555        assert_eq!(m.flat_mem[3], 0x00);
556        assert_eq!(m.perm_of(0), perm::RO);
557    }
558
559    #[test]
560    fn map_region_truncates_oversize_init() {
561        let mut m = Mem::new();
562        let init = vec![0x77u8; (PAGE_SIZE as usize) * 3];
563        // Only one page declared; init is bigger.
564        m.map_region(0, PAGE_SIZE as u64, Access::ReadWrite, Some(&init))
565            .unwrap();
566        assert_eq!(m.flat_mem.len(), PAGE_SIZE as usize);
567        assert_eq!(m.flat_mem[PAGE_SIZE as usize - 1], 0x77);
568    }
569
570    #[test]
571    fn map_region_rejects_unaligned_start() {
572        let mut m = Mem::new();
573        assert_eq!(
574            m.map_region(123, PAGE_SIZE as u64, Access::ReadOnly, None),
575            Err(MapError::UnalignedStart(123))
576        );
577    }
578
579    #[test]
580    fn map_region_rejects_unaligned_size() {
581        let mut m = Mem::new();
582        assert_eq!(
583            m.map_region(0, 123, Access::ReadOnly, None),
584            Err(MapError::UnalignedSize(123))
585        );
586    }
587
588    #[test]
589    fn map_region_overlapping_overwrites_perms() {
590        let mut m = Mem::with_pages(2, perm::RO);
591        m.map_region(0, 2 * PAGE_SIZE as u64, Access::ReadWrite, None)
592            .unwrap();
593        assert_eq!(m.perm_of(0), perm::RW);
594        assert_eq!(m.perm_of(PAGE_SIZE), perm::RW);
595    }
596}