Skip to main content

qcode/
memory_image.rs

1//! A serializable snapshot of a binary's initialized memory.
2//!
3//! [`MemoryImage`] is the persistence/test form of the byte surface: it
4//! implements [`wazabin_binary::BinaryFormat`], so a snapshot (built from a live
5//! format's `mapped_regions()` at save time) or a test-seeded image can be
6//! `Arc`-wrapped and handed to the pipeline as `PipelineEnv.binary`, exactly
7//! like a live ELF/PE handle. During a live lift the bytes stay in the loader's
8//! format object only — nothing is copied into the `Context`.
9//!
10//! The motivating consumer is the jump-table pass, which reads table entries
11//! straight out of `.rodata` through the shared handle.
12
13/// One contiguous mapped region of the binary image.
14#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15struct Segment {
16    /// Virtual address of the first byte.
17    start: u64,
18    /// The mapped bytes (file contents plus any zero-fill the loader materialized).
19    bytes: Vec<u8>,
20    /// Whether the region is executable (so resolved jump targets can be sanity
21    /// checked against code memory).
22    executable: bool,
23    /// Whether the region is writable. A writable region's initialized bytes are
24    /// not a reliable constant — the runtime (e.g. the dynamic linker populating
25    /// the GOT) may overwrite them — so passes that resolve control flow or fold
26    /// constants out of memory must not trust it. Defaults to `false` for images
27    /// deserialized from before this field existed.
28    #[serde(default)]
29    writable: bool,
30}
31
32impl Segment {
33    /// Inclusive-exclusive end of the region.
34    fn end(&self) -> u64 {
35        self.start + self.bytes.len() as u64
36    }
37
38    /// True if `addr` falls within `[start, end)`.
39    fn contains(&self, addr: u64) -> bool {
40        addr >= self.start && addr < self.end()
41    }
42}
43
44/// The initialized memory of a loaded binary, as a set of mapped segments kept
45/// sorted by start address.
46#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
47pub struct MemoryImage {
48    segments: Vec<Segment>,
49    /// Whether the per-segment `executable` flags are authoritative. Until the
50    /// `memory_protections` pass establishes them, the lifter treats every mapped
51    /// byte as potentially executable (default r/x); once known, it narrows to the
52    /// real flags. See [`may_be_executable`](Self::may_be_executable).
53    #[serde(default)]
54    protections_known: bool,
55}
56
57impl MemoryImage {
58    /// Add a mapped region. Segments are kept sorted by start address; callers
59    /// need not insert in order.
60    pub fn add_segment(&mut self, start: u64, bytes: Vec<u8>, executable: bool, writable: bool) {
61        if bytes.is_empty() {
62            return;
63        }
64        let seg = Segment {
65            start,
66            bytes,
67            executable,
68            writable,
69        };
70        let pos = self.segments.partition_point(|s| s.start < seg.start);
71        self.segments.insert(pos, seg);
72    }
73
74    /// The segment containing `addr`, if any.
75    fn segment_at(&self, addr: u64) -> Option<&Segment> {
76        // Largest start <= addr, then a range check (segments are sorted and,
77        // for a sane loader, non-overlapping).
78        let pos = self.segments.partition_point(|s| s.start <= addr);
79        self.segments
80            .get(pos.checked_sub(1)?)
81            .filter(|s| s.contains(addr))
82    }
83
84    /// Read `n` bytes starting at `addr`, all from a single mapped segment.
85    /// Returns `None` if any byte in `[addr, addr + n)` is unmapped.
86    pub fn read_bytes(&self, addr: u64, n: usize) -> Option<Vec<u8>> {
87        let seg = self.segment_at(addr)?;
88        let off = (addr - seg.start) as usize;
89        let end = off.checked_add(n)?;
90        seg.bytes.get(off..end).map(|s| s.to_vec())
91    }
92
93    /// Read a little-endian unsigned integer of `size` bytes (1..=8) at `addr`.
94    ///
95    /// Endianness is fixed to little-endian for now (x86/x64); a big-endian /
96    /// arch-driven variant is a TODO.
97    pub fn read_uint(&self, addr: u64, size: usize) -> Option<u64> {
98        if size == 0 || size > 8 {
99            return None;
100        }
101        let bytes = self.read_bytes(addr, size)?;
102        let mut value = 0u64;
103        for (i, &b) in bytes.iter().enumerate() {
104            value |= (b as u64) << (i * 8);
105        }
106        Some(value)
107    }
108
109    /// True if `addr` lies in an executable mapped region (per the raw segment
110    /// flag, regardless of whether protections have been established).
111    pub fn is_executable(&self, addr: u64) -> bool {
112        self.segment_at(addr).is_some_and(|s| s.executable)
113    }
114
115    /// True only if `addr` is mapped in a region *known* to be writable: the
116    /// per-segment protection flags must be established (see
117    /// [`protections_known`](Self::protections_known)) and the containing segment
118    /// writable. Before protections are known the segment flags are not
119    /// authoritative, so this conservatively returns `false` ("not proven
120    /// writable"). Callers use it to refuse to treat mutable memory (e.g. a GOT
121    /// slot the dynamic linker rewrites) as a constant.
122    pub fn is_known_writable(&self, addr: u64) -> bool {
123        self.protections_known && self.segment_at(addr).is_some_and(|s| s.writable)
124    }
125
126    /// True if `addr` is mapped by any segment.
127    pub fn contains(&self, addr: u64) -> bool {
128        self.segment_at(addr).is_some()
129    }
130
131    /// The `[start, end)` bounds of the segment containing `addr`, if any.
132    pub fn segment_bounds(&self, addr: u64) -> Option<(u64, u64)> {
133        self.segment_at(addr).map(|s| (s.start, s.end()))
134    }
135
136    /// Whether the per-segment executable flags are authoritative (the
137    /// `memory_protections` pass has run).
138    pub fn protections_known(&self) -> bool {
139        self.protections_known
140    }
141
142    /// Mark the per-segment protection flags as authoritative, so the lifter
143    /// narrows from the permissive default to the real flags.
144    pub fn mark_protections_known(&mut self) {
145        self.protections_known = true;
146    }
147
148    /// True if no segments have been loaded yet. Used to make binary memory
149    /// loading idempotent across fixpoint rounds.
150    pub fn is_empty(&self) -> bool {
151        self.segments.is_empty()
152    }
153}
154
155/// The persistence/test backing of the byte surface: a reloaded `.harbinger`
156/// snapshot (or a `qcode!`-DSL test that seeded segments) wraps its
157/// `MemoryImage` in an `Arc` and hands it to `PipelineEnv.binary`, so passes
158/// read initialized memory through one trait regardless of whether a live
159/// container format is behind it.
160impl wazabin_binary::BinaryFormat for MemoryImage {
161    fn load_address(&self) -> u64 {
162        self.segments.first().map(|s| s.start).unwrap_or(0)
163    }
164
165    fn byte_at(&self, addr: u64) -> Option<u8> {
166        let seg = self.segment_at(addr)?;
167        seg.bytes.get((addr - seg.start) as usize).copied()
168    }
169
170    fn bytes_at(&self, addr: u64) -> Option<&[u8]> {
171        let seg = self.segment_at(addr)?;
172        seg.bytes.get((addr - seg.start) as usize..)
173    }
174
175    /// An image records mapped bytes, not entry metadata.
176    fn entry_points(&self) -> Vec<u64> {
177        Vec::new()
178    }
179
180    /// Images do not record an architecture; x86-64 is the workspace default
181    /// (mirrors [`Blob`]'s placeholder). Consumers of `PipelineEnv.binary`
182    /// read bytes and permissions, never the architecture.
183    ///
184    /// [`Blob`]: wazabin_binary::blob::Blob
185    fn architecture(&self) -> wazabin_binary::Arch {
186        wazabin_binary::Arch::X86_64
187    }
188
189    fn segment_bounds(&self, addr: u64) -> Option<(u64, u64)> {
190        MemoryImage::segment_bounds(self, addr)
191    }
192
193    fn is_executable(&self, addr: u64) -> bool {
194        MemoryImage::is_executable(self, addr)
195    }
196
197    /// Answers straight from the per-segment flag: an image is only ever built
198    /// from an authoritative container format's `mapped_regions` (or a test's
199    /// explicit `add_segment`), so the flags need no separate establishment
200    /// step. (The inherent [`MemoryImage::is_known_writable`] keeps the legacy
201    /// `protections_known` gate for its remaining callers.)
202    fn is_known_writable(&self, addr: u64) -> bool {
203        self.segment_at(addr).is_some_and(|s| s.writable)
204    }
205
206    /// The mirror of [`is_known_writable`](Self::is_known_writable): a mapped
207    /// segment whose recorded flag says "not writable" is proven read-only. The
208    /// flags come from the container format's `mapped_regions`, so this is only
209    /// as authoritative as the format that filled them.
210    ///
211    /// [`is_known_writable`]: wazabin_binary::BinaryFormat::is_known_writable
212    fn is_known_read_only(&self, addr: u64) -> bool {
213        self.segment_at(addr).is_some_and(|s| !s.writable)
214    }
215
216    fn mapped_regions(&self) -> Vec<(u64, Vec<u8>, bool, bool)> {
217        self.segments
218            .iter()
219            .map(|s| (s.start, s.bytes.clone(), s.executable, s.writable))
220            .collect()
221    }
222
223    fn read_bytes(&self, addr: u64, n: usize) -> Option<Vec<u8>> {
224        MemoryImage::read_bytes(self, addr, n)
225    }
226
227    fn read_uint(&self, addr: u64, size: usize) -> Option<u64> {
228        MemoryImage::read_uint(self, addr, size)
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    fn image() -> MemoryImage {
237        let mut img = MemoryImage::default();
238        // Insert out of order to exercise the sorted insert.
239        img.add_segment(0x2000, vec![0xaa, 0xbb, 0xcc, 0xdd], true, false);
240        img.add_segment(0x1000, vec![0x01, 0x02, 0x03, 0x04], false, false);
241        img
242    }
243
244    #[test]
245    fn read_uint_little_endian() {
246        let img = image();
247        assert_eq!(img.read_uint(0x1000, 4), Some(0x04030201));
248        assert_eq!(img.read_uint(0x1000, 2), Some(0x0201));
249        assert_eq!(img.read_uint(0x1001, 1), Some(0x02));
250    }
251
252    #[test]
253    fn read_bytes_within_segment() {
254        let img = image();
255        assert_eq!(img.read_bytes(0x2001, 2), Some(vec![0xbb, 0xcc]));
256    }
257
258    #[test]
259    fn unmapped_and_straddling_are_none() {
260        let img = image();
261        // Fully unmapped.
262        assert_eq!(img.read_uint(0x500, 1), None);
263        // Runs off the end of the segment.
264        assert_eq!(img.read_bytes(0x1003, 2), None);
265        // Gap between the two segments.
266        assert_eq!(img.read_uint(0x1004, 1), None);
267    }
268
269    #[test]
270    fn executability() {
271        let img = image();
272        assert!(img.is_executable(0x2002));
273        assert!(!img.is_executable(0x1002));
274        assert!(!img.is_executable(0x9999));
275    }
276
277    #[test]
278    fn protections_known_is_off_by_default() {
279        let mut img = image();
280        assert!(!img.protections_known());
281        img.mark_protections_known();
282        assert!(img.protections_known());
283    }
284
285    #[test]
286    fn known_writable_requires_established_protections() {
287        let mut img = MemoryImage::default();
288        img.add_segment(0x1000, vec![0u8; 4], false, true); // writable data
289        img.add_segment(0x2000, vec![0u8; 4], false, false); // read-only data
290
291        // Until protections are established, writability is not authoritative, so
292        // nothing is *known* writable.
293        assert!(!img.is_known_writable(0x1000));
294        assert!(!img.is_known_writable(0x2000));
295
296        img.mark_protections_known();
297        assert!(img.is_known_writable(0x1000), "writable segment now known");
298        assert!(!img.is_known_writable(0x2000), "read-only stays read-only");
299        assert!(!img.is_known_writable(0x9999), "unmapped is not writable");
300    }
301}