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}