Skip to main content

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`]: 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}