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 hardware mapping for read-only
14//! enforcement; this layer just bounds-checks.
15//!
16//! The fast-path read/write helpers use `read_unaligned` /
17//! `write_unaligned` via raw pointers — single MOV on x86.
18
19use alloc::vec::Vec;
20
21use crate::gas::GasCounter;
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/// A category-#3 first-touch (`touch_read`/`touch_write`) that cannot be
56/// satisfied: a write to a read-only (pinned) page, or a data access
57/// straddling outside the declared region. The caller must `PageFault`,
58/// charging nothing — the touch is all-or-nothing. (Accesses whose base
59/// page is *not* a declared data page — a code-region PIC load or a
60/// fully-unmapped address — are skipped, not faulted: they return `Ok`
61/// so the caller's normal load/store path resolves them.)
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub struct TouchFault;
64
65/// Setup-time error for [`Memory::map_region`].
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum MapError {
68    /// `start` is not page-aligned.
69    UnalignedStart(u64),
70    /// `size` is not a multiple of [`PAGE_SIZE`].
71    UnalignedSize(u64),
72    /// `start + size` overflows `usize` on this platform, or exceeds
73    /// the addressable range supported by `Mem`.
74    Overflow,
75}
76
77/// Memory backend abstraction.
78///
79/// The interpreter is generic over `M: Memory` so the same source
80/// compiles for two substrates:
81///
82/// - **Software-copy**: [`CopyingMemory`] (this module) — an owning
83///   `Vec<u8>` with per-page permissions. Runs in-process.
84/// - **Hardware-paged**: a future bare-metal impl in
85///   `nub-arch-x86` that maps PVM pages onto real CPU pages
86///   via the in-guest IDT + page tables.
87///
88/// Hot-path methods (`read_u*`/`write_u*`) return `Option<T>` or
89/// `bool` to keep the interpreter loop branch-free. Implementations
90/// should mark these `#[inline]` (or `#[inline(always)]`) — the
91/// interpreter calls them through trait dispatch, and we want
92/// monomorphisation to collapse to direct function calls.
93pub trait Memory {
94    // ---- Hot-path width-typed reads. ----
95    fn read_u8(&self, addr: u32) -> Option<u8>;
96    fn read_u16_le(&self, addr: u32) -> Option<u16>;
97    fn read_u32_le(&self, addr: u32) -> Option<u32>;
98    fn read_u64_le(&self, addr: u32) -> Option<u64>;
99
100    // ---- Hot-path width-typed writes. ----
101    fn write_u8(&mut self, addr: u32, val: u8) -> bool;
102    fn write_u16_le(&mut self, addr: u32, val: u16) -> bool;
103    fn write_u32_le(&mut self, addr: u32, val: u32) -> bool;
104    fn write_u64_le(&mut self, addr: u32, val: u64) -> bool;
105
106    // ---- Setup-time + cold-path. ----
107
108    /// Declare a mapped region. See [`CopyingMemory::map_region`] for
109    /// the canonical semantics.
110    fn map_region(
111        &mut self,
112        start: u64,
113        size: u64,
114        access: Access,
115        init: Option<&[u8]>,
116    ) -> Result<(), MapError>;
117
118    /// Per-page permission byte for the page containing `addr`.
119    /// Returns [`perm::NONE`] if `addr` is out of range.
120    fn perm_of(&self, addr: u32) -> u8;
121
122    /// Read `dst.len()` bytes starting at `addr` into `dst`.
123    fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess>;
124
125    /// Write `data.len()` bytes starting at `addr`. Per-page perm
126    /// checks apply; out-of-range or RO-page writes return `Err`.
127    fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess>;
128
129    // ---- Category-#3 lazy-materialization accounting. ----
130
131    /// Charge category-#3 first-touch gas for a `width`-byte **read** at
132    /// `addr` (page-in on the first read of a page), advancing the
133    /// per-page materialization state. See [`CopyingMemory::touch_read`]
134    /// for the canonical semantics. The default is a no-op (`Ok`) for
135    /// backends that don't model lazy materialization.
136    fn touch_read(
137        &mut self,
138        addr: u32,
139        width: u32,
140        gas: &mut GasCounter,
141    ) -> Result<(), TouchFault> {
142        let _ = (addr, width, gas);
143        Ok(())
144    }
145
146    /// Charge category-#3 first-touch gas for a `width`-byte **write** at
147    /// `addr` (page-in + copy-on-write on the first write of a page),
148    /// advancing the per-page materialization state. See
149    /// [`CopyingMemory::touch_write`] for the canonical semantics. The
150    /// default is a no-op (`Ok`).
151    fn touch_write(
152        &mut self,
153        addr: u32,
154        width: u32,
155        gas: &mut GasCounter,
156    ) -> Result<(), TouchFault> {
157        let _ = (addr, width, gas);
158        Ok(())
159    }
160}
161
162/// Address-space mapping for one execution context.
163///
164/// Flat-buffer layout matching v2 javm. The buffer's length defines
165/// the upper bound of valid addresses; per-page permissions live in
166/// `perms`. Implements [`Memory`] via inherent-method delegation so
167/// concrete callers don't need to import the trait.
168#[derive(Clone, Debug)]
169pub struct CopyingMemory {
170    /// Base guest address `flat_mem[0]` corresponds to. Guest address
171    /// `addr` indexes `flat_mem[addr - base]`; accesses below `base` (or
172    /// past the buffer) fault. Lets the buffer cover only the high data
173    /// region `[DATA_BASE, …)` without allocating the `[0, DATA_BASE)`
174    /// null-guard hole — matching the recompiler's page table, which
175    /// leaves that range unmapped. `0` for the addr-0-based memories used
176    /// in unit tests.
177    pub base: u32,
178    /// Contiguous byte buffer covering `[base, base + flat_mem.len())`.
179    pub flat_mem: Vec<u8>,
180    /// One permission byte per `PAGE_SIZE`-page in `flat_mem`.
181    /// `perms.len() == flat_mem.len() / PAGE_SIZE` (rounded up). Indexed
182    /// by page *relative to `base`*.
183    pub perms: Vec<u8>,
184    /// Category-#3 per-page materialization state ([`crate::mat::PageState`]
185    /// as a `u8`), one byte per page, kept the same length as `perms`.
186    /// Software first-touch accounting: a never-touched page is
187    /// `NotPresent`; the first read pages it in, the first write CoWs it.
188    /// The interpreter's #3 charge is kind-independent (the only
189    /// kind-sensitive case — a write to a pinned page — is already gated
190    /// by the `perm::RW` write check), so no per-page `PageKind` is kept;
191    /// the recompiler tracks kinds itself for page sourcing.
192    pub mat_state: Vec<u8>,
193    /// Guest VA base of the read-only CODE region (`PinnedCapRo`). Guest
194    /// data loads (PIC `auipc`+load) of the program's own bytecode page it
195    /// in on first read (read-only page-in is charged eagerly at the CALL,
196    /// not at this fault, but a PIC read still records residency), identical
197    /// to the recompiler. `code_top == code_base` (the default) means no code
198    /// region is declared — code reads then skip #3 (unit tests).
199    pub code_base: u32,
200    /// Exclusive top of the code region, **page-rounded**:
201    /// `code_base + round_up(code_len)`. The last code page's zero-padded
202    /// tail is readable (matching the recompiler, which maps whole pages).
203    pub code_top: u32,
204    /// Heap base address (for sbrk).
205    pub heap_base: u32,
206    /// Current heap top.
207    pub heap_top: u32,
208    /// Maximum heap pages (sbrk refuses beyond this).
209    pub max_heap_pages: u32,
210}
211
212impl Default for CopyingMemory {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218/// Compatibility alias for the pre-trait name. Consumers can keep
219/// writing `Mem`; new code should prefer `CopyingMemory` (when the
220/// concrete impl is wanted) or be generic over `M: Memory`.
221pub type Mem = CopyingMemory;
222
223impl CopyingMemory {
224    /// Empty memory; no pages allocated. `base = 0` (addr-0-based).
225    pub fn new() -> Self {
226        Self {
227            base: 0,
228            flat_mem: Vec::new(),
229            perms: Vec::new(),
230            mat_state: Vec::new(),
231            code_base: 0,
232            code_top: 0,
233            heap_base: 0,
234            heap_top: 0,
235            max_heap_pages: 0,
236        }
237    }
238
239    /// Byte offset into `flat_mem` for guest address `addr`. Wraps for
240    /// `addr < base` so the subsequent `… < flat_mem.len()` bounds check
241    /// rejects sub-`base` accesses (the null-guard / code region) as a
242    /// fault — no separate check needed.
243    #[inline(always)]
244    fn off(&self, addr: u32) -> usize {
245        addr.wrapping_sub(self.base) as usize
246    }
247
248    /// Construct with a pre-sized flat buffer (zero-initialized).
249    /// `n_pages` is the number of `PAGE_SIZE`-pages. `base = 0`.
250    pub fn with_pages(n_pages: u32, default_perm: u8) -> Self {
251        let bytes = (n_pages as usize) * (PAGE_SIZE as usize);
252        Self {
253            base: 0,
254            flat_mem: vec![0u8; bytes],
255            perms: vec![default_perm; n_pages as usize],
256            mat_state: vec![crate::mat::PageState::NotPresent.as_u8(); n_pages as usize],
257            code_base: 0,
258            code_top: 0,
259            heap_base: 0,
260            heap_top: 0,
261            max_heap_pages: 0,
262        }
263    }
264
265    /// Declare the read-only CODE region for category-#3 accounting:
266    /// `[code_base, code_base + round_up(code_len))`. Guest data loads of
267    /// the program's own bytecode (PIC) then read each touched code page
268    /// with no per-fault charge (read-only page-in is accounted eagerly at
269    /// the CALL) and hard-fault on any write — matching the recompiler,
270    /// which lazily materializes code pages too. `code_len` is the exact
271    /// byte length; the region is page-rounded.
272    pub fn set_code_region(&mut self, code_base: u32, code_len: u32) {
273        let rounded = code_len.next_multiple_of(PAGE_SIZE);
274        self.code_base = code_base;
275        self.code_top = code_base.saturating_add(rounded);
276    }
277
278    /// Whether the page-aligned address `page_addr` lies inside the declared
279    /// read-only code region.
280    #[inline]
281    fn is_code_page(&self, page_addr: u32) -> bool {
282        self.code_top > self.code_base && page_addr >= self.code_base && page_addr < self.code_top
283    }
284
285    /// Per-page permission for the page containing `addr`. Returns
286    /// `perm::NONE` if the address is out of range (incl. below `base`).
287    pub fn perm_of(&self, addr: u32) -> u8 {
288        let page = self.off(addr) / (PAGE_SIZE as usize);
289        self.perms.get(page).copied().unwrap_or(perm::NONE)
290    }
291
292    /// Page index into `perms` / `mat_state` for the page-aligned address
293    /// `page_addr`, or `None` if it lies outside the flat buffer (below
294    /// `base` or past its end). `perms`/`mat_state` always cover every
295    /// page of `flat_mem`, so a `Some(i)` is always a valid index.
296    #[inline]
297    fn page_index(&self, page_addr: u32) -> Option<usize> {
298        let o = self.off(page_addr);
299        if o < self.flat_mem.len() {
300            Some(o / PAGE_SIZE as usize)
301        } else {
302            None
303        }
304    }
305
306    /// Category-#3 first-touch accounting for a `width`-byte access at
307    /// `addr` (`width` in `1..=8`).
308    ///
309    /// Charges page-in / copy-on-write for each declared data page the
310    /// access touches, advancing its [`crate::mat::PageState`]. The set
311    /// of touched pages (≤ 2, the base page plus a straddle page) is
312    /// [`crate::mat::access_pages`] — the *same* rule the recompiler's
313    /// fault handler uses, so the charged page set and total match
314    /// bit-for-bit.
315    ///
316    /// **All-or-nothing.** Accessibility is checked over the whole page
317    /// set *before* any charge: a write to a read-only (pinned) page, or
318    /// a straddle leaving the declared region, charges nothing and
319    /// returns [`TouchFault`] (the caller `PageFault`s).
320    ///
321    /// **Code / unmapped accesses are skipped** (`Ok`, no charge): if the
322    /// base page is not a declared data page (a code-region PIC load, the
323    /// null guard, or a fully-unmapped address), `#3` does not apply and
324    /// the caller's normal load/store path resolves it — the code-region
325    /// fallback succeeds; a truly unmapped address `PageFault`s there.
326    ///
327    /// The charge is kind-independent: the only kind-sensitive rule (a
328    /// write to a pinned page) is excluded by the `perm::RW` gate below,
329    /// so [`crate::mat::charge_for`] is driven with a fixed CoW kind. The
330    /// block reserve guarantees `gas` covers the worst case, so the final
331    /// charge cannot underflow — mirroring the recompiler, which
332    /// decrements its gas register in the fault handler without an OOG
333    /// check.
334    fn touch(
335        &mut self,
336        addr: u32,
337        width: u32,
338        is_write: bool,
339        gas: &mut GasCounter,
340    ) -> Result<(), TouchFault> {
341        let set = crate::mat::access_pages(addr, width);
342        let pages = set.as_slice();
343
344        // Region dispatch by the base page. CODE and DATA are far apart
345        // (CODE_BASE=4 MiB, DATA_BASE=256 MiB), so a single ≤8-byte access
346        // lies wholly in one region — the base page decides which.
347        if self
348            .page_index(pages[0])
349            .is_some_and(|i| self.perms[i] != perm::NONE)
350        {
351            self.touch_data(pages, is_write, gas)
352        } else if self.is_code_page(pages[0]) {
353            self.touch_code(pages, is_write)
354        } else {
355            // Null guard / inter-region gap / fully-unmapped: `#3` does not
356            // apply — the caller's normal load/store path resolves it (the
357            // code fallback or a `PageFault`).
358            Ok(())
359        }
360    }
361
362    /// `#3` for a DATA-region access (ephemeral / CoW). Accessibility-all
363    /// then materialize-all; see [`touch`](CopyingMemory::touch).
364    fn touch_data(
365        &mut self,
366        pages: &[u32],
367        is_write: bool,
368        gas: &mut GasCounter,
369    ) -> Result<(), TouchFault> {
370        // Accessibility-all (before any charge): every page must be a
371        // declared data page; a write additionally needs it writable.
372        for &p in pages {
373            match self.page_index(p) {
374                Some(i) => {
375                    let pm = self.perms[i];
376                    if pm == perm::NONE || (is_write && pm != perm::RW) {
377                        return Err(TouchFault);
378                    }
379                }
380                None => return Err(TouchFault),
381            }
382        }
383
384        // Materialize-all. Read-only (pinned-cap) pages charge no per-fault
385        // #3 — read-only page-in is accounted eagerly at the CALL; only their
386        // residency is implicit. Copy-on-write (writable) pages stay per-page:
387        // a write copies one page and charges `COW_COST`. A write to a
388        // read-only page was excluded by the perm gate above.
389        let mut total: u64 = 0;
390        for &p in pages {
391            let i = self.page_index(p).expect("checked accessible above");
392            if self.perms[i] != perm::RO {
393                let state = crate::mat::PageState::from_u8(self.mat_state[i]);
394                let (charge, next) =
395                    crate::mat::charge_for(state, crate::mat::PageKind::UnpinnedCapCow, is_write)
396                        .expect("non-pinned access pre-checked accessible");
397                total += charge;
398                self.mat_state[i] = next.as_u8();
399            }
400        }
401        gas.charge(total)
402            .expect("#3 materialization charge within block reserve");
403        Ok(())
404    }
405
406    /// `#3` for a CODE-region access: `PinnedCapRo`, read-only. A read
407    /// charges nothing (read-only page-in is accounted eagerly at the CALL),
408    /// a write hard-faults, and a read straddling out of the region faults
409    /// charging nothing (all-or-nothing). Mirrors the recompiler's lazy code
410    /// materialization (which likewise maps code pages without a per-fault
411    /// charge).
412    fn touch_code(&self, pages: &[u32], is_write: bool) -> Result<(), TouchFault> {
413        // Code is read-only: any write hard-faults (charging nothing).
414        if is_write {
415            return Err(TouchFault);
416        }
417        // Accessibility-all: every page in the set must be a code page. A
418        // read charges no #3 (read-only page-in is accounted at the CALL).
419        for &p in pages {
420            if !self.is_code_page(p) {
421                return Err(TouchFault);
422            }
423        }
424        Ok(())
425    }
426
427    /// Category-#3 first-touch for a `width`-byte **read**. See `touch`.
428    #[inline]
429    pub fn touch_read(
430        &mut self,
431        addr: u32,
432        width: u32,
433        gas: &mut GasCounter,
434    ) -> Result<(), TouchFault> {
435        self.touch(addr, width, false, gas)
436    }
437
438    /// Category-#3 first-touch for a `width`-byte **write**. See `touch`.
439    #[inline]
440    pub fn touch_write(
441        &mut self,
442        addr: u32,
443        width: u32,
444        gas: &mut GasCounter,
445    ) -> Result<(), TouchFault> {
446        self.touch(addr, width, true, gas)
447    }
448
449    // ---- Fast-path read helpers (inline; single bounds check + raw pointer load). ----
450
451    #[inline(always)]
452    pub fn read_u8(&self, addr: u32) -> Option<u8> {
453        let a = self.off(addr);
454        if a < self.flat_mem.len() {
455            // SAFETY: bounds-checked.
456            Some(unsafe { *self.flat_mem.get_unchecked(a) })
457        } else {
458            None
459        }
460    }
461
462    #[inline(always)]
463    pub fn read_u16_le(&self, addr: u32) -> Option<u16> {
464        let a = self.off(addr);
465        if a + 2 <= self.flat_mem.len() {
466            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u16>().read_unaligned() })
467        } else {
468            None
469        }
470    }
471
472    #[inline(always)]
473    pub fn read_u32_le(&self, addr: u32) -> Option<u32> {
474        let a = self.off(addr);
475        if a + 4 <= self.flat_mem.len() {
476            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u32>().read_unaligned() })
477        } else {
478            None
479        }
480    }
481
482    #[inline(always)]
483    pub fn read_u64_le(&self, addr: u32) -> Option<u64> {
484        let a = self.off(addr);
485        if a + 8 <= self.flat_mem.len() {
486            Some(unsafe { self.flat_mem.as_ptr().add(a).cast::<u64>().read_unaligned() })
487        } else {
488            None
489        }
490    }
491
492    // ---- Fast-path write helpers. ----
493
494    #[inline(always)]
495    pub fn write_u8(&mut self, addr: u32, val: u8) -> bool {
496        let a = self.off(addr);
497        if a < self.flat_mem.len() {
498            unsafe {
499                *self.flat_mem.get_unchecked_mut(a) = val;
500            }
501            true
502        } else {
503            false
504        }
505    }
506
507    #[inline(always)]
508    pub fn write_u16_le(&mut self, addr: u32, val: u16) -> bool {
509        let a = self.off(addr);
510        if a + 2 <= self.flat_mem.len() {
511            unsafe {
512                self.flat_mem
513                    .as_mut_ptr()
514                    .add(a)
515                    .cast::<u16>()
516                    .write_unaligned(val);
517            }
518            true
519        } else {
520            false
521        }
522    }
523
524    #[inline(always)]
525    pub fn write_u32_le(&mut self, addr: u32, val: u32) -> bool {
526        let a = self.off(addr);
527        if a + 4 <= self.flat_mem.len() {
528            unsafe {
529                self.flat_mem
530                    .as_mut_ptr()
531                    .add(a)
532                    .cast::<u32>()
533                    .write_unaligned(val);
534            }
535            true
536        } else {
537            false
538        }
539    }
540
541    #[inline(always)]
542    pub fn write_u64_le(&mut self, addr: u32, val: u64) -> bool {
543        let a = self.off(addr);
544        if a + 8 <= self.flat_mem.len() {
545            unsafe {
546                self.flat_mem
547                    .as_mut_ptr()
548                    .add(a)
549                    .cast::<u64>()
550                    .write_unaligned(val);
551            }
552            true
553        } else {
554            false
555        }
556    }
557
558    /// Declare a mapped region at `[start, start + size)` with
559    /// per-page permissions `access` and optional initial bytes.
560    ///
561    /// - `start` and `size` must each be multiples of [`PAGE_SIZE`].
562    /// - Grows `flat_mem` to cover `start + size` if necessary
563    ///   (newly-grown bytes are zero-initialized; their pages
564    ///   default to [`perm::NONE`] before this call sets them).
565    /// - Sets pages in `[start / PAGE_SIZE, (start + size) /
566    ///   PAGE_SIZE)` to the permission byte for `access`.
567    /// - If `init` is `Some(bytes)`, copies `bytes[..bytes.len()
568    ///   .min(size)]` into `flat_mem[start..]`; the rest of the
569    ///   region remains zero-filled (matches the DataCap canonical
570    ///   form: trailing zeros are stripped from `content`, but the
571    ///   logical `size` may be larger).
572    pub fn map_region(
573        &mut self,
574        start: u64,
575        size: u64,
576        access: Access,
577        init: Option<&[u8]>,
578    ) -> Result<(), MapError> {
579        let page = PAGE_SIZE as u64;
580        if !start.is_multiple_of(page) {
581            return Err(MapError::UnalignedStart(start));
582        }
583        if !size.is_multiple_of(page) {
584            return Err(MapError::UnalignedSize(size));
585        }
586        if start < u64::from(self.base) {
587            return Err(MapError::UnalignedStart(start));
588        }
589        // Work in offsets relative to `base` (the buffer covers
590        // `[base, base + flat_mem.len())`).
591        let rel_start = start - u64::from(self.base);
592        let rel_end = rel_start.checked_add(size).ok_or(MapError::Overflow)?;
593        let rel_end_usize: usize = rel_end.try_into().map_err(|_| MapError::Overflow)?;
594
595        // Grow flat_mem + perms (+ mat_state) to cover [0, rel_end)
596        // (relative to base). New pages default to NONE perm and the
597        // `NotPresent` materialization state (tag 0).
598        if rel_end_usize > self.flat_mem.len() {
599            self.flat_mem.resize(rel_end_usize, 0);
600            let needed_pages = rel_end_usize.div_ceil(PAGE_SIZE as usize);
601            if self.perms.len() < needed_pages {
602                self.perms.resize(needed_pages, perm::NONE);
603            }
604            if self.mat_state.len() < needed_pages {
605                self.mat_state
606                    .resize(needed_pages, crate::mat::PageState::NotPresent.as_u8());
607            }
608        }
609
610        // Set permissions on the affected pages.
611        let perm_byte = match access {
612            Access::ReadOnly => perm::RO,
613            Access::ReadWrite => perm::RW,
614        };
615        let first_page = (rel_start / page) as usize;
616        let last_page = ((rel_end / page) as usize).saturating_sub(1);
617        if size > 0 {
618            for p in first_page..=last_page {
619                self.perms[p] = perm_byte;
620            }
621        }
622
623        // Copy initial bytes if any. The destination starts as zero
624        // either from initial allocation or the grow above, so any
625        // trailing region beyond `init` is implicitly zero.
626        if let Some(bytes) = init {
627            let n = bytes.len().min(size as usize);
628            let s = rel_start as usize;
629            self.flat_mem[s..s + n].copy_from_slice(&bytes[..n]);
630        }
631
632        Ok(())
633    }
634
635    // ---- Slow-path helpers (for tests / non-hot paths). ----
636
637    /// Read `len` bytes from `addr`. Returns `Err` on out-of-range.
638    pub fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess> {
639        let a = self.off(addr);
640        let end = a
641            .checked_add(len)
642            .ok_or(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)))?;
643        if end > self.flat_mem.len() {
644            return Err(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)));
645        }
646        Ok(self.flat_mem[a..end].to_vec())
647    }
648
649    /// Write `data` starting at `addr`. Returns `Err` on out-of-range
650    /// or write-protected page. Writes are NOT rolled back on partial
651    /// failure (test-only API).
652    pub fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess> {
653        let a = self.off(addr);
654        let end = a
655            .checked_add(data.len())
656            .ok_or(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)))?;
657        if end > self.flat_mem.len() {
658            return Err(MemAccess::PageFault(addr & !(PAGE_SIZE - 1)));
659        }
660        // Check perms per page touched.
661        let start_page = a / (PAGE_SIZE as usize);
662        let last_page = (end - 1) / (PAGE_SIZE as usize);
663        for p in start_page..=last_page {
664            if self.perms.get(p).copied().unwrap_or(perm::NONE) != perm::RW {
665                return Err(MemAccess::WriteProtected((p as u32) * PAGE_SIZE));
666            }
667        }
668        self.flat_mem[a..end].copy_from_slice(data);
669        Ok(())
670    }
671}
672
673// `Memory` impl delegates to inherent methods via UFCS (no name
674// clash, no recursion). All bodies are `#[inline(always)]` so trait
675// dispatch is zero-cost after monomorphisation.
676impl Memory for CopyingMemory {
677    #[inline(always)]
678    fn read_u8(&self, addr: u32) -> Option<u8> {
679        CopyingMemory::read_u8(self, addr)
680    }
681    #[inline(always)]
682    fn read_u16_le(&self, addr: u32) -> Option<u16> {
683        CopyingMemory::read_u16_le(self, addr)
684    }
685    #[inline(always)]
686    fn read_u32_le(&self, addr: u32) -> Option<u32> {
687        CopyingMemory::read_u32_le(self, addr)
688    }
689    #[inline(always)]
690    fn read_u64_le(&self, addr: u32) -> Option<u64> {
691        CopyingMemory::read_u64_le(self, addr)
692    }
693    #[inline(always)]
694    fn write_u8(&mut self, addr: u32, val: u8) -> bool {
695        CopyingMemory::write_u8(self, addr, val)
696    }
697    #[inline(always)]
698    fn write_u16_le(&mut self, addr: u32, val: u16) -> bool {
699        CopyingMemory::write_u16_le(self, addr, val)
700    }
701    #[inline(always)]
702    fn write_u32_le(&mut self, addr: u32, val: u32) -> bool {
703        CopyingMemory::write_u32_le(self, addr, val)
704    }
705    #[inline(always)]
706    fn write_u64_le(&mut self, addr: u32, val: u64) -> bool {
707        CopyingMemory::write_u64_le(self, addr, val)
708    }
709    #[inline]
710    fn map_region(
711        &mut self,
712        start: u64,
713        size: u64,
714        access: Access,
715        init: Option<&[u8]>,
716    ) -> Result<(), MapError> {
717        CopyingMemory::map_region(self, start, size, access, init)
718    }
719    #[inline]
720    fn perm_of(&self, addr: u32) -> u8 {
721        CopyingMemory::perm_of(self, addr)
722    }
723    #[inline]
724    fn read(&self, addr: u32, len: usize) -> Result<Vec<u8>, MemAccess> {
725        CopyingMemory::read(self, addr, len)
726    }
727    #[inline]
728    fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), MemAccess> {
729        CopyingMemory::write(self, addr, data)
730    }
731    #[inline]
732    fn touch_read(
733        &mut self,
734        addr: u32,
735        width: u32,
736        gas: &mut GasCounter,
737    ) -> Result<(), TouchFault> {
738        CopyingMemory::touch_read(self, addr, width, gas)
739    }
740    #[inline]
741    fn touch_write(
742        &mut self,
743        addr: u32,
744        width: u32,
745        gas: &mut GasCounter,
746    ) -> Result<(), TouchFault> {
747        CopyingMemory::touch_write(self, addr, width, gas)
748    }
749}