Skip to main content

qcode_vm/
memory.rs

1//! The bridge between the interpreter's memory interface and the [`Mmu`].
2//!
3//! [`VmMemory`] is a hybrid, and deliberately so. Guest process memory — the
4//! default RAM space — goes through the MMU, where it gets mapping,
5//! permissions, and faults. Every other space keeps the flat representation the
6//! emulator already uses:
7//!
8//! * **Register** and **unique** spaces are not process memory at all. They are
9//!   architectural state and per-instruction scratch, addressed by the lifter
10//!   rather than the guest. There is no meaningful sense in which a write to
11//!   `RAX` could be "unmapped", and putting a page lookup on that path would tax
12//!   the single hottest operation in the interpreter.
13//! * **Temporary** spaces are per-function IR storage, invisible to the guest.
14//!
15//! So the MMU is applied exactly where guest-visible addressing happens, and
16//! nowhere else.
17
18use qcode::{context::Context, space::MemorySpaceId};
19use qcode_emulator::{DomainMemory, DomainValue, EmulatorErrorKind, EmulatorMemory, SizedValue};
20
21use crate::{
22    flat::FlatSpaces,
23    mmu::{MemFault, Mmu},
24};
25
26/// Memory for a VM run: an [`Mmu`] for the RAM space, flat storage elsewhere.
27#[derive(Default)]
28pub struct VmMemory {
29    /// Guest process memory.
30    pub mmu: Mmu,
31    /// Register, unique and temporary spaces, densely stored.
32    flat: FlatSpaces,
33    /// Which space the MMU backs. Resolved from the context on the first
34    /// [`configure_spaces`](EmulatorMemory::configure_spaces); `None` until then,
35    /// which routes everything to flat storage rather than guessing.
36    ram: Option<MemorySpaceId>,
37    /// The last fault produced by an MMU access.
38    ///
39    /// [`EmulatorErrorKind`] can only carry "a read failed at this address", so
40    /// the precise cause would be lost on the way out of the interpreter. The VM
41    /// layer takes the fault from here to build an exact exit, which is what
42    /// makes a fault a resumable value rather than an abort. Cleared by
43    /// [`take_fault`](Self::take_fault).
44    fault: Option<MemFault>,
45}
46
47impl VmMemory {
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Returns and clears the fault recorded by the most recent failed access.
53    pub fn take_fault(&mut self) -> Option<MemFault> {
54        self.fault.take()
55    }
56
57    /// The recorded fault, without clearing it.
58    ///
59    /// A backend that has to turn a fault into the interpreter's error type
60    /// needs the address but must leave the fault itself for the VM, which is
61    /// what turns it into a resumable exit.
62    pub fn fault(&self) -> Option<MemFault> {
63        self.fault
64    }
65
66    /// Records a fault taken by a read performed on this memory's behalf.
67    pub(crate) fn record_read_fault(&mut self, fault: MemFault) {
68        self.record(fault, false);
69    }
70
71    /// Records a fault taken by a write performed on this memory's behalf.
72    pub(crate) fn record_write_fault(&mut self, fault: MemFault) {
73        self.record(fault, true);
74    }
75
76    /// Whether `space` is the guest RAM backed by the MMU.
77    fn is_ram(&self, space: MemorySpaceId) -> bool {
78        self.ram == Some(space)
79    }
80
81    /// Whether `space` is densely stored, and so directly addressable by
82    /// compiled code. False for guest RAM, which needs the MMU's checks.
83    pub fn is_flat(&self, space: MemorySpaceId) -> bool {
84        !self.is_ram(space)
85    }
86
87    /// The flat spaces, for a backend that addresses them directly.
88    pub fn flat_mut(&mut self) -> &mut FlatSpaces {
89        &mut self.flat
90    }
91
92    /// Records `fault` and converts it into the error the interpreter
93    /// understands. The address is preserved in both, so a consumer that never
94    /// looks at [`take_fault`](Self::take_fault) still gets a truthful error.
95    fn record(&mut self, fault: MemFault, writing: bool) -> EmulatorErrorKind {
96        self.fault = Some(fault);
97        if writing {
98            EmulatorErrorKind::MemoryWriteError(fault.addr)
99        } else {
100            EmulatorErrorKind::MemoryReadError(fault.addr)
101        }
102    }
103}
104
105impl DomainMemory for VmMemory {
106    type V = SizedValue;
107
108    fn read(
109        &self,
110        space: MemorySpaceId,
111        addr: Self::V,
112        size: usize,
113    ) -> Result<Self::V, EmulatorErrorKind> {
114        if !self.is_ram(space) {
115            let bits = self.flat.read_u128(space, addr.value()?, size)?;
116            return Ok(SizedValue::from_bits(bits, size));
117        }
118        let addr = addr.value()?;
119        // A value wider than 16 bytes cannot be held by `SizedValue`; the flat
120        // backend truncates the same way, so the two agree.
121        let mut bytes = vec![0u8; size.min(16)];
122        // `&self` cannot record the fault; the error still carries the address,
123        // and every faulting path the VM actually resumes from goes through a
124        // `&mut self` write or through `read_mut` below.
125        self.mmu
126            .read(addr, &mut bytes)
127            .map_err(|fault| EmulatorErrorKind::MemoryReadError(fault.addr))?;
128        let mut bits = 0u128;
129        for (index, byte) in bytes.iter().enumerate() {
130            bits |= u128::from(*byte) << (index * 8);
131        }
132        Ok(SizedValue::from_bits(bits, size))
133    }
134
135    fn write(
136        &mut self,
137        space: MemorySpaceId,
138        addr: Self::V,
139        size: usize,
140        value: Self::V,
141    ) -> Result<(), EmulatorErrorKind> {
142        if !self.is_ram(space) {
143            return self
144                .flat
145                .entry(space)
146                .write_u128(addr.value()?, size, value.as_bits());
147        }
148        let addr = addr.value()?;
149        let bits = value.as_bits();
150        let width = size.min(16);
151        let bytes: Vec<u8> = (0..width).map(|i| (bits >> (i * 8)) as u8).collect();
152        self.mmu
153            .write(addr, &bytes)
154            .map_err(|fault| self.record(fault, true))
155    }
156}
157
158impl EmulatorMemory for VmMemory {
159    fn configure_spaces(&mut self, ctx: &Context<'_>) {
160        // The guest's RAM is the specification's default space: the one a bare
161        // `Load`/`Store` addresses, and the only one a guest pointer refers to.
162        self.ram = Some(MemorySpaceId::Shared(ctx.shared.default_space));
163        self.flat.configure(ctx);
164    }
165
166    fn read_bytes(
167        &self,
168        space: MemorySpaceId,
169        addr: u64,
170        size: usize,
171    ) -> Result<Vec<u8>, EmulatorErrorKind> {
172        if !self.is_ram(space) {
173            return self.flat.read_bytes(space, addr, size);
174        }
175        let mut bytes = vec![0u8; size];
176        self.mmu
177            .read(addr, &mut bytes)
178            .map_err(|fault| EmulatorErrorKind::MemoryReadError(fault.addr))?;
179        Ok(bytes)
180    }
181
182    fn write_bytes(
183        &mut self,
184        space: MemorySpaceId,
185        addr: u64,
186        bytes: &[u8],
187    ) -> Result<(), EmulatorErrorKind> {
188        if !self.is_ram(space) {
189            return self.flat.entry(space).write_bytes(addr, bytes);
190        }
191        self.mmu
192            .write(addr, bytes)
193            .map_err(|fault| self.record(fault, true))
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::mmu::{FaultKind, PAGE_SIZE, perm};
201    use qcode::space::SpaceId;
202
203    /// A context has a RAM default space; the register space is a second one.
204    fn context() -> Context<'static> {
205        Context::default()
206    }
207
208    fn configured() -> (Context<'static>, VmMemory) {
209        let ctx = context();
210        let mut memory = VmMemory::new();
211        memory.configure_spaces(&ctx);
212        (ctx, memory)
213    }
214
215    fn ram(ctx: &Context<'_>) -> MemorySpaceId {
216        MemorySpaceId::Shared(ctx.shared.default_space)
217    }
218
219    #[test]
220    fn ram_accesses_go_through_the_mmu() {
221        let (ctx, mut memory) = configured();
222        memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
223
224        let addr = SizedValue::from_u64(0x1000);
225        memory
226            .write(ram(&ctx), addr, 4, SizedValue::from_bits(0xdead_beef, 4))
227            .unwrap();
228        let read = memory.read(ram(&ctx), addr, 4).unwrap();
229        assert_eq!(read.as_bits(), 0xdead_beef);
230        // The bytes really are in the MMU, little-endian.
231        let mut raw = [0; 4];
232        memory.mmu.read(0x1000, &mut raw).unwrap();
233        assert_eq!(raw, [0xef, 0xbe, 0xad, 0xde]);
234    }
235
236    #[test]
237    fn unmapped_ram_write_faults_and_is_recorded() {
238        let (ctx, mut memory) = configured();
239        let addr = SizedValue::from_u64(0x1000);
240        let error = memory
241            .write(ram(&ctx), addr, 4, SizedValue::from_bits(1, 4))
242            .unwrap_err();
243        assert!(matches!(error, EmulatorErrorKind::MemoryWriteError(0x1000)));
244        assert_eq!(
245            memory.take_fault(),
246            Some(MemFault {
247                kind: FaultKind::WriteUnmapped,
248                addr: 0x1000
249            })
250        );
251        // Taking the fault clears it.
252        assert_eq!(memory.take_fault(), None);
253    }
254
255    #[test]
256    fn read_only_ram_refuses_a_write() {
257        let (ctx, mut memory) = configured();
258        memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
259        let addr = SizedValue::from_u64(0x1000);
260        memory
261            .write(ram(&ctx), addr, 1, SizedValue::from_bits(1, 1))
262            .unwrap_err();
263        assert_eq!(
264            memory.take_fault().map(|f| f.kind),
265            Some(FaultKind::WritePerm)
266        );
267    }
268
269    #[test]
270    fn non_ram_spaces_bypass_the_mmu_entirely() {
271        let ctx = context();
272        let mut memory = VmMemory::new();
273        memory.configure_spaces(&ctx);
274
275        // A space that is not the default one is flat: it needs no mapping and
276        // takes no faults.
277        let register = MemorySpaceId::Shared(SpaceId::from(1));
278        let addr = SizedValue::from_u64(0x40);
279        memory
280            .write(register, addr, 8, SizedValue::from_bits(0x1234, 8))
281            .unwrap();
282        assert_eq!(memory.read(register, addr, 8).unwrap().as_bits(), 0x1234);
283        assert_eq!(memory.mmu.resident_pages(), 0);
284        assert_eq!(memory.take_fault(), None);
285    }
286
287    #[test]
288    fn byte_level_access_routes_the_same_way() {
289        let (ctx, mut memory) = configured();
290        memory.mmu.map(0x2000, PAGE_SIZE, perm::RW_INIT).unwrap();
291        memory
292            .write_bytes(ram(&ctx), 0x2000, &[1, 2, 3, 4])
293            .unwrap();
294        assert_eq!(
295            memory.read_bytes(ram(&ctx), 0x2000, 4).unwrap(),
296            vec![1, 2, 3, 4]
297        );
298    }
299
300    #[test]
301    fn before_configuration_nothing_is_routed_to_the_mmu() {
302        // Guessing a RAM space before the context is known would silently send
303        // register traffic through the MMU and fault on it.
304        let mut memory = VmMemory::new();
305        let addr = SizedValue::from_u64(0x1000);
306        let space = MemorySpaceId::Shared(SpaceId::from(0));
307        memory
308            .write(space, addr, 4, SizedValue::from_bits(7, 4))
309            .unwrap();
310        assert_eq!(memory.mmu.resident_pages(), 0);
311    }
312}