wazabin_binary/lib.rs
1//! Binary container formats and the [`BinaryFormat`] trait.
2//!
3//! This is a leaf crate below `qcode`: it holds the [`Arch`] enum, the
4//! [`TargetOs`] enum, and the ELF/PE/blob container parsers behind a single
5//! [`BinaryFormat`] trait. `qcode` re-exports [`TargetOs`] and (in Stage 2b)
6//! implements [`BinaryFormat`] for its serializable `MemoryImage`; `harbinger`
7//! re-exports this crate as `harbinger::format` and `harbinger::arch::Arch`.
8
9mod arch;
10mod target_os;
11
12pub mod blob;
13pub mod elf;
14pub mod pe;
15
16pub use arch::Arch;
17pub use target_os::TargetOs;
18
19/// Map a byte to its printable ASCII representation.
20///
21/// Returns `b` unchanged for bytes in the printable ASCII range (0x20–0x7e),
22/// and `b'.'` for everything else. Used by hex viewers for the ASCII column.
23pub const fn printable_byte(b: u8) -> u8 {
24 if b >= 0x20 && b <= 0x7e { b } else { b'.' }
25}
26
27/// Return `true` when `b` is a printable ASCII byte.
28///
29/// This intentionally excludes ASCII control characters such as newline and
30/// tab. It is meant for filtering C strings while solving binaries, where
31/// accepting control bytes tends to produce noisy false positives.
32pub const fn is_printable_ascii_byte(b: u8) -> bool {
33 b >= 0x20 && b <= 0x7e
34}
35
36/// A trait representing a loaded binary image with one or more mapped regions
37/// and one or more known entry points.
38///
39/// Both [`blob::Blob`] (raw bytes) and [`elf::ElfBinary`] implement this trait
40/// so that arch disassemblers can accept either format through a single
41/// interface, even when the backing image is sparse.
42///
43/// Impls are immutable after parse, so the trait carries `Send + Sync` bounds:
44/// a parsed binary is shared by reference (`Arc<dyn BinaryFormat>`) across the
45/// analysis pipeline's worker threads.
46pub trait BinaryFormat: Send + Sync {
47 /// The lowest virtual address mapped by this binary image.
48 fn load_address(&self) -> u64;
49
50 /// Return the byte mapped at `addr`, or `None` if the address is unmapped.
51 fn byte_at(&self, addr: u64) -> Option<u8>;
52
53 /// Return the contiguous bytes available at `addr`.
54 ///
55 /// The returned slice covers the current mapped region from `addr` onward.
56 /// Implementations may return an owned buffer when the region is backed by
57 /// implicit zero-fill rather than file bytes.
58 fn bytes_at(&self, addr: u64) -> Option<&[u8]>;
59
60 /// Known entry points into this binary (entrypoint, symbol table functions, etc.).
61 /// The recursive disassembler should be seeded with these addresses.
62 fn entry_points(&self) -> Vec<u64>;
63
64 /// The binary's primary entry point (e.g. the ELF `e_entry`), if the format
65 /// designates one. Unlike [`entry_points`], this is the single address where
66 /// execution begins. Returns `None` for formats with no distinguished entry.
67 ///
68 /// [`entry_points`]: Self::entry_points
69 fn entrypoint(&self) -> Option<u64> {
70 None
71 }
72
73 /// Name of the architecture, should use embedded information from known binary
74 /// format, or raw values from the blob.
75 fn architecture(&self) -> Arch;
76
77 /// The operating system this binary targets, inferred from the container
78 /// format. Defaults to [`TargetOs::Unknown`]; PE returns Windows, ELF Linux.
79 fn os(&self) -> TargetOs {
80 TargetOs::Unknown
81 }
82
83 /// Library names this binary links against: ELF `DT_NEEDED` sonames,
84 /// PE import-directory DLL names (original case; matching is
85 /// case-insensitive). Empty when the format has no such notion (Blob).
86 fn linked_libraries(&self) -> Vec<String> {
87 Vec::new()
88 }
89
90 /// Return the symbol name for the function starting at `addr`, if the
91 /// binary format has one (e.g. from an ELF symbol table).
92 /// Returns `None` for formats with no symbol information.
93 fn symbol_name(&self, _addr: u64) -> Option<&str> {
94 None
95 }
96
97 /// Returns `true` if `addr` is an external (imported) function stub,
98 /// e.g. a PLT thunk. The recursive disassembler will not lift the body
99 /// of external functions. Defaults to `false`.
100 fn is_external_symbol(&self, _addr: u64) -> bool {
101 false
102 }
103
104 /// Return the name of the library providing the external function stub at
105 /// `addr`: the PE import-directory DLL, or the ELF `.gnu.version_r` soname
106 /// the symbol's version requirement points at. `None` when the format does
107 /// not record a per-symbol source library (e.g. an unversioned ELF import).
108 fn import_library(&self, _addr: u64) -> Option<&str> {
109 None
110 }
111
112 /// Return the imported symbol whose resolver slot lives at `addr`, if any.
113 ///
114 /// ELF uses this for GOT / PLT relocation slots such as `R_X86_64_GLOB_DAT`
115 /// and `R_X86_64_JUMP_SLOT`.
116 fn import_symbol_name(&self, _addr: u64) -> Option<&str> {
117 None
118 }
119
120 /// Return rows of bytes suitable for a hex viewer.
121 ///
122 /// The first row is aligned down to the nearest `width`-byte boundary.
123 /// Each row is `(row_address, bytes)` where a byte is `None` when the
124 /// address is unmapped (e.g. a hole in a sparse ELF image).
125 ///
126 /// # Panics
127 /// Panics if `width` is zero.
128 fn hex_rows(&self, addr: u64, len: usize, width: usize) -> Vec<(u64, Vec<Option<u8>>)> {
129 assert!(width > 0, "hex row width must be non-zero");
130 let row_start = addr - addr % width as u64;
131 let end = addr.saturating_add(len as u64);
132 let mut rows = Vec::new();
133 let mut cur = row_start;
134 while cur < end {
135 let bytes = (0..width as u64).map(|i| self.byte_at(cur + i)).collect();
136 rows.push((cur, bytes));
137 cur = cur.saturating_add(width as u64);
138 }
139 rows
140 }
141
142 /// Return `true` if `addr` is mapped by this binary image.
143 fn contains(&self, addr: u64) -> bool {
144 self.byte_at(addr).is_some()
145 }
146
147 /// Enumerate the binary's mapped regions as
148 /// `(start, bytes, executable, writable)`.
149 ///
150 /// At snapshot-save time the persistence layer copies these into a
151 /// serializable `MemoryImage` so a reloaded session is self-describing.
152 /// Each region's `bytes` should cover its full in-memory size (zero-filled
153 /// tail included), mirroring [`byte_at`].
154 ///
155 /// Defaults to empty for formats that do not expose their segments.
156 ///
157 /// [`byte_at`]: Self::byte_at
158 fn mapped_regions(&self) -> Vec<(u64, Vec<u8>, bool, bool)> {
159 Vec::new()
160 }
161
162 /// Return `true` if `addr` lies in an executable region of this binary
163 /// image. Defaults to "mapped" for formats that don't track per-region
164 /// permissions; formats with permission information (e.g. ELF segment
165 /// flags) should override this.
166 fn is_executable(&self, addr: u64) -> bool {
167 self.contains(addr)
168 }
169
170 /// The `[start, end)` bounds of the mapped region containing `addr`, if
171 /// any. Used to key per-segment facts (e.g. executability propositions)
172 /// so repeated queries in one region collapse to a single entry. Defaults
173 /// to `None` for formats that do not expose their segments.
174 fn segment_bounds(&self, _addr: u64) -> Option<(u64, u64)> {
175 None
176 }
177
178 /// Return `true` only if `addr` lies in a region *known* to be writable
179 /// (from the container's segment flags). Passes that fold a value out of
180 /// initialized memory use this to refuse mutable memory — e.g. a GOT slot
181 /// the dynamic linker overwrites at load time. Defaults to `false`
182 /// ("not proven writable"); `Blob` keeps the default.
183 fn is_known_writable(&self, _addr: u64) -> bool {
184 false
185 }
186
187 /// Return `true` only if `addr` lies in a region *proven* read-only (mapped,
188 /// and the container's own permission data says the region is not writable).
189 ///
190 /// This is **not** the negation of [`is_known_writable`](Self::is_known_writable): a format that
191 /// records no permissions answers `false` to both, which reads as "unknown"
192 /// rather than "read-only". Consumers that reconstruct a *value* out of
193 /// initialized memory — the decompiler rendering a `.rodata` string constant
194 /// as a named object — need the positive proof, because a writable byte may
195 /// be a different byte at run time.
196 ///
197 /// Defaults to `false` ("not proven read-only"); `Blob` and `PeBinary` keep
198 /// the default, the latter because it does not parse section permissions.
199 fn is_known_read_only(&self, _addr: u64) -> bool {
200 false
201 }
202
203 /// Read `n` bytes at virtual address `addr`.
204 ///
205 /// Returns `None` if any byte in the requested range is unmapped.
206 fn read_bytes(&self, addr: u64, n: usize) -> Option<Vec<u8>> {
207 let mut out = Vec::with_capacity(n);
208 for offset in 0..n {
209 out.push(self.byte_at(addr.checked_add(offset as u64)?)?);
210 }
211 Some(out)
212 }
213
214 /// Read a little-endian unsigned integer of `size` bytes (1..=8) at `addr`.
215 ///
216 /// Returns `None` if the size is out of range or any byte is unmapped.
217 fn read_uint(&self, addr: u64, size: usize) -> Option<u64> {
218 if size == 0 || size > 8 {
219 return None;
220 }
221 let bytes = self.read_bytes(addr, size)?;
222 let mut value = 0u64;
223 for (i, &b) in bytes.iter().enumerate() {
224 value |= (b as u64) << (i * 8);
225 }
226 Some(value)
227 }
228
229 /// Read a null-terminated C string at virtual address `addr`, returning the
230 /// bytes up to (but not including) the null terminator.
231 ///
232 /// If `max_len` is `Some(limit)`, only the first `limit` bytes are searched
233 /// for the null terminator. Returns `None` if the address is out of range or
234 /// no null terminator is found within the (optionally limited) region.
235 fn read_cstring(&self, addr: u64, max_len: Option<usize>) -> Option<Vec<u8>> {
236 let limit = max_len.unwrap_or(usize::MAX);
237 let mut out = Vec::new();
238 for offset in 0..limit {
239 let byte = self.byte_at(addr.checked_add(offset as u64)?)?;
240 if byte == 0 {
241 return Some(out);
242 }
243 out.push(byte);
244 }
245 None
246 }
247
248 /// Read a null-terminated C string at virtual address `addr`, requiring
249 /// every byte before the null terminator to be printable ASCII.
250 ///
251 /// Printable bytes are in the inclusive range `0x20..=0x7e`. Returns
252 /// `None` if the address is out of range, no null terminator is found
253 /// within the optional search window, or a non-printable byte appears
254 /// before the terminator.
255 fn read_printable_cstring(&self, addr: u64, max_len: Option<usize>) -> Option<Vec<u8>> {
256 let limit = max_len.unwrap_or(usize::MAX);
257 let mut out = Vec::new();
258 for offset in 0..limit {
259 let byte = self.byte_at(addr.checked_add(offset as u64)?)?;
260 if byte == 0 {
261 return Some(out);
262 }
263 if !is_printable_ascii_byte(byte) {
264 return None;
265 }
266 out.push(byte);
267 }
268 None
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::{BinaryFormat, blob::Blob};
275
276 #[test]
277 fn read_printable_cstring_accepts_printable_ascii() {
278 let blob = Blob::new(0x1000, b"hello, world!\0next".to_vec());
279
280 assert_eq!(
281 blob.read_printable_cstring(0x1000, None),
282 Some(b"hello, world!".to_vec())
283 );
284 }
285
286 #[test]
287 fn read_printable_cstring_rejects_control_bytes() {
288 let blob = Blob::new(0x1000, b"line\nbreak\0".to_vec());
289
290 assert_eq!(
291 blob.read_cstring(0x1000, None),
292 Some(b"line\nbreak".to_vec())
293 );
294 assert_eq!(blob.read_printable_cstring(0x1000, None), None);
295 }
296
297 #[test]
298 fn read_printable_cstring_rejects_non_ascii_bytes() {
299 let blob = Blob::new(0x1000, b"caf\xe9\0".to_vec());
300
301 assert_eq!(blob.read_printable_cstring(0x1000, None), None);
302 }
303
304 #[test]
305 fn read_printable_cstring_honors_max_len() {
306 let blob = Blob::new(0x1000, b"hello\0".to_vec());
307
308 assert_eq!(blob.read_printable_cstring(0x1000, Some(3)), None);
309 assert_eq!(
310 blob.read_printable_cstring(0x1000, Some(6)),
311 Some(b"hello".to_vec())
312 );
313 }
314}