Skip to main content

squib_core/
memory.rs

1//! Guest-physical address ranges and memory protection types.
2
3use core::fmt;
4
5use serde::{Deserialize, Serialize};
6
7// `parking_lot` is the project's standard non-poisoning lock per CLAUDE.md
8// `ยง Async & Concurrency`. Used for the test-only `SliceGuestMemory`.
9use crate::error::{Error, Result};
10
11/// A guest-physical address.
12///
13/// Newtype around `u64` so the type system stops us from confusing host-virtual and
14/// guest-physical addresses at API boundaries.
15#[derive(
16    Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
17)]
18#[serde(transparent)]
19pub struct GuestAddress(pub u64);
20
21impl GuestAddress {
22    /// Returns the underlying `u64` value.
23    #[inline]
24    pub const fn raw(self) -> u64 {
25        self.0
26    }
27
28    /// Returns the address aligned down to the given power-of-two `align`.
29    ///
30    /// # Errors
31    /// Returns [`Error::InvalidArgument`] if `align` is not a power of two.
32    pub fn align_down(self, align: u64) -> Result<Self> {
33        if !align.is_power_of_two() {
34            return Err(Error::InvalidArgument(format!(
35                "alignment must be a power of two: {align}"
36            )));
37        }
38        Ok(Self(self.0 & !(align - 1)))
39    }
40
41    /// Returns the address advanced by `offset`, saturating at `u64::MAX`.
42    #[inline]
43    #[must_use]
44    pub const fn saturating_add(self, offset: u64) -> Self {
45        Self(self.0.saturating_add(offset))
46    }
47}
48
49impl fmt::Display for GuestAddress {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "{:#018x}", self.0)
52    }
53}
54
55impl From<u64> for GuestAddress {
56    #[inline]
57    fn from(value: u64) -> Self {
58        Self(value)
59    }
60}
61
62/// A half-open guest-physical range `[base, base + size)`.
63#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
64pub struct GuestRange {
65    /// The starting guest-physical address (inclusive).
66    pub base: GuestAddress,
67    /// The size of the range in bytes.
68    pub size: u64,
69}
70
71impl GuestRange {
72    /// Construct a new [`GuestRange`].
73    ///
74    /// # Errors
75    /// Returns [`Error::InvalidArgument`] if `base + size` would overflow.
76    pub fn new(base: GuestAddress, size: u64) -> Result<Self> {
77        base.0.checked_add(size).ok_or_else(|| {
78            Error::InvalidArgument(format!("range overflow: base={base} size={size}"))
79        })?;
80        Ok(Self { base, size })
81    }
82
83    /// Returns the first address past the range (exclusive end).
84    #[inline]
85    pub fn end(self) -> GuestAddress {
86        GuestAddress(self.base.0 + self.size)
87    }
88
89    /// Returns true if `addr` falls inside the range.
90    #[inline]
91    pub fn contains(self, addr: GuestAddress) -> bool {
92        addr.0 >= self.base.0 && addr.0 < self.base.0 + self.size
93    }
94}
95
96/// Memory protection bits a backend may set on a guest range.
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
98#[allow(clippy::struct_excessive_bools)] // matches POSIX RWX semantics; bitflags would be overkill
99pub struct Protection {
100    /// Guest may read.
101    pub read: bool,
102    /// Guest may write.
103    pub write: bool,
104    /// Guest may execute.
105    pub execute: bool,
106}
107
108impl Protection {
109    /// Read-only.
110    pub const READ: Self = Self::new(true, false, false);
111    /// Read-write.
112    pub const READ_WRITE: Self = Self::new(true, true, false);
113    /// Read-execute (e.g. kernel text).
114    pub const READ_EXECUTE: Self = Self::new(true, false, true);
115    /// Read-write-execute.
116    pub const READ_WRITE_EXECUTE: Self = Self::new(true, true, true);
117
118    /// Construct a protection set from individual flags.
119    pub const fn new(read: bool, write: bool, execute: bool) -> Self {
120        Self {
121            read,
122            write,
123            execute,
124        }
125    }
126}
127
128/// A region of guest memory backed by a host-side mmap, registered with the hypervisor.
129#[derive(Debug)]
130#[non_exhaustive]
131pub struct GuestMemoryRegion {
132    /// The guest-physical range this region covers.
133    pub range: GuestRange,
134    /// The slot identifier the backend assigns; meaningful only to the backend.
135    pub slot: u32,
136    /// The default protection for the region at registration time.
137    pub protection: Protection,
138}
139
140impl GuestMemoryRegion {
141    /// Construct a new region.
142    pub fn new(range: GuestRange, slot: u32, protection: Protection) -> Self {
143        Self {
144            range,
145            slot,
146            protection,
147        }
148    }
149}
150
151/// Read / write access to guest memory across one or more registered regions.
152///
153/// Implementations are responsible for routing addresses to the underlying
154/// host-mapped buffer; callers (virtio devices, snapshot writer, debugger)
155/// only see guest-physical addresses.
156///
157/// All methods MUST be byte-accurate: a partial read or write is a hard error,
158/// not a silent truncation. This matches the virtio spec's expectation that
159/// descriptor chains either fully resolve or surface as a malformed-payload
160/// error to the device handler.
161pub trait GuestMemory: Send + Sync + fmt::Debug {
162    /// Read `buf.len()` bytes from `addr` into `buf`.
163    ///
164    /// # Errors
165    /// [`Error::MemoryOutOfRange`] if the request escapes any registered
166    /// region, including a request that straddles a region boundary.
167    fn read(&self, addr: GuestAddress, buf: &mut [u8]) -> Result<()>;
168
169    /// Write `buf.len()` bytes from `buf` into guest memory at `addr`.
170    ///
171    /// # Errors
172    /// Same as [`Self::read`].
173    fn write(&self, addr: GuestAddress, buf: &[u8]) -> Result<()>;
174
175    /// Read a little-endian `u16` from `addr`.
176    ///
177    /// # Errors
178    /// Same as [`Self::read`].
179    fn read_u16_le(&self, addr: GuestAddress) -> Result<u16> {
180        let mut b = [0u8; 2];
181        self.read(addr, &mut b)?;
182        Ok(u16::from_le_bytes(b))
183    }
184
185    /// Read a little-endian `u32` from `addr`.
186    ///
187    /// # Errors
188    /// Same as [`Self::read`].
189    fn read_u32_le(&self, addr: GuestAddress) -> Result<u32> {
190        let mut b = [0u8; 4];
191        self.read(addr, &mut b)?;
192        Ok(u32::from_le_bytes(b))
193    }
194
195    /// Read a little-endian `u64` from `addr`.
196    ///
197    /// # Errors
198    /// Same as [`Self::read`].
199    fn read_u64_le(&self, addr: GuestAddress) -> Result<u64> {
200        let mut b = [0u8; 8];
201        self.read(addr, &mut b)?;
202        Ok(u64::from_le_bytes(b))
203    }
204
205    /// Write a little-endian `u16` to `addr`.
206    ///
207    /// # Errors
208    /// Same as [`Self::write`].
209    fn write_u16_le(&self, addr: GuestAddress, value: u16) -> Result<()> {
210        self.write(addr, &value.to_le_bytes())
211    }
212
213    /// Write a little-endian `u32` to `addr`.
214    ///
215    /// # Errors
216    /// Same as [`Self::write`].
217    fn write_u32_le(&self, addr: GuestAddress, value: u32) -> Result<()> {
218        self.write(addr, &value.to_le_bytes())
219    }
220
221    /// Write a little-endian `u64` to `addr`.
222    ///
223    /// # Errors
224    /// Same as [`Self::write`].
225    fn write_u64_le(&self, addr: GuestAddress, value: u64) -> Result<()> {
226        self.write(addr, &value.to_le_bytes())
227    }
228}
229
230/// In-process [`GuestMemory`] implementation backed by a single contiguous
231/// `Vec<u8>` keyed at a fixed `base` guest address.
232///
233/// Useful for unit tests that need a real `GuestMemory` without standing up
234/// the HVF backend. Production hypervisor backends provide their own
235/// implementation that routes to `mmap`-backed regions.
236#[derive(Debug)]
237pub struct SliceGuestMemory {
238    base: GuestAddress,
239    bytes: parking_lot::RwLock<Vec<u8>>,
240}
241
242impl SliceGuestMemory {
243    /// Build a `[base, base + size)` region zero-filled at construction.
244    #[must_use]
245    pub fn new(base: GuestAddress, size: usize) -> Self {
246        Self {
247            base,
248            bytes: parking_lot::RwLock::new(vec![0u8; size]),
249        }
250    }
251
252    /// Build a region pre-populated with `bytes` starting at `base`.
253    #[must_use]
254    pub fn from_bytes(base: GuestAddress, bytes: Vec<u8>) -> Self {
255        Self {
256            base,
257            bytes: parking_lot::RwLock::new(bytes),
258        }
259    }
260
261    /// Inclusive base address.
262    #[must_use]
263    pub fn base(&self) -> GuestAddress {
264        self.base
265    }
266
267    /// Region size in bytes.
268    #[must_use]
269    pub fn size(&self) -> usize {
270        self.bytes.read().len()
271    }
272
273    fn offset_of(&self, addr: GuestAddress, len: usize) -> Result<usize> {
274        let base = self.base.raw();
275        let size = u64::try_from(self.bytes.read().len()).unwrap_or(u64::MAX);
276        let end = base.saturating_add(size);
277        let len_u64 = u64::try_from(len)
278            .map_err(|_| Error::MemoryOutOfRange(format!("addr {addr} + len {len} overflows")))?;
279        let req_end = addr
280            .raw()
281            .checked_add(len_u64)
282            .ok_or_else(|| Error::MemoryOutOfRange(format!("addr {addr} + len {len} overflows")))?;
283        if addr.raw() < base || req_end > end {
284            return Err(Error::MemoryOutOfRange(format!(
285                "addr {addr} + len {len} escapes [{base:#x}, {end:#x})"
286            )));
287        }
288        usize::try_from(addr.raw() - base).map_err(|_| {
289            Error::MemoryOutOfRange(format!("offset {} exceeds usize::MAX", addr.raw() - base))
290        })
291    }
292}
293
294impl GuestMemory for SliceGuestMemory {
295    fn read(&self, addr: GuestAddress, buf: &mut [u8]) -> Result<()> {
296        let off = self.offset_of(addr, buf.len())?;
297        let bytes = self.bytes.read();
298        buf.copy_from_slice(&bytes[off..off + buf.len()]);
299        Ok(())
300    }
301
302    fn write(&self, addr: GuestAddress, buf: &[u8]) -> Result<()> {
303        let off = self.offset_of(addr, buf.len())?;
304        let mut bytes = self.bytes.write();
305        bytes[off..off + buf.len()].copy_from_slice(buf);
306        Ok(())
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn align_down_rejects_non_power_of_two() {
316        let addr = GuestAddress(0x1234);
317        assert!(addr.align_down(7).is_err());
318    }
319
320    #[test]
321    fn align_down_rounds() {
322        let addr = GuestAddress(0x1234);
323        assert_eq!(addr.align_down(0x1000).unwrap().raw(), 0x1000);
324    }
325
326    #[test]
327    fn range_overflow_is_rejected() {
328        let err = GuestRange::new(GuestAddress(u64::MAX - 0x100), 0x200).unwrap_err();
329        matches!(err, Error::InvalidArgument(_));
330    }
331
332    #[test]
333    fn range_contains_endpoints() {
334        let r = GuestRange::new(GuestAddress(0x1000), 0x1000).unwrap();
335        assert!(r.contains(GuestAddress(0x1000)));
336        assert!(r.contains(GuestAddress(0x1FFF)));
337        assert!(!r.contains(GuestAddress(0x2000)));
338    }
339
340    #[test]
341    fn protection_constants_round_trip() {
342        let p = Protection::READ_WRITE;
343        assert!(p.read && p.write && !p.execute);
344    }
345
346    #[test]
347    fn slice_guest_memory_round_trips_typed_writes() {
348        let mem = SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x1000);
349        mem.write_u32_le(GuestAddress(0x4000_0010), 0xDEAD_BEEF)
350            .unwrap();
351        assert_eq!(
352            mem.read_u32_le(GuestAddress(0x4000_0010)).unwrap(),
353            0xDEAD_BEEF
354        );
355        mem.write_u16_le(GuestAddress(0x4000_0020), 0xABCD).unwrap();
356        assert_eq!(mem.read_u16_le(GuestAddress(0x4000_0020)).unwrap(), 0xABCD);
357    }
358
359    #[test]
360    fn slice_guest_memory_rejects_below_base() {
361        let mem = SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x1000);
362        let mut buf = [0u8; 4];
363        let err = mem.read(GuestAddress(0x3FFF_FFFF), &mut buf).unwrap_err();
364        assert!(matches!(err, Error::MemoryOutOfRange(_)));
365    }
366
367    #[test]
368    fn slice_guest_memory_rejects_straddling_top() {
369        let mem = SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x10);
370        let mut buf = [0u8; 8];
371        let err = mem.read(GuestAddress(0x4000_000C), &mut buf).unwrap_err();
372        assert!(matches!(err, Error::MemoryOutOfRange(_)));
373    }
374
375    #[test]
376    fn slice_guest_memory_rejects_overflow() {
377        let mem = SliceGuestMemory::new(GuestAddress(u64::MAX - 0x10), 0x10);
378        let mut buf = [0u8; 0x100];
379        let err = mem
380            .read(GuestAddress(u64::MAX - 0x8), &mut buf)
381            .unwrap_err();
382        assert!(matches!(err, Error::MemoryOutOfRange(_)));
383    }
384}