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}