Skip to main content

macho_core/
mapped.rs

1//! Reconstruction of a parseable file image from an immutable mapped RVA view.
2
3use std::ops::Range;
4
5use crate::format::io::Endian;
6use crate::model::header::{Bitness, MagicNumber};
7use crate::{ParseError, ParseErrorKind, ParseResult};
8
9const LC_SEGMENT: u32 = 0x1;
10const LC_SEGMENT_64: u32 = 0x19;
11
12/// Immutable random-access reader over one mapped Mach-O module.
13pub trait MappedImageReader {
14    /// Copy exactly `range.len()` bytes from module-relative virtual addresses.
15    fn read_exact(&self, range: Range<u64>) -> ParseResult<Vec<u8>>;
16
17    /// Total number of captured bytes available to reconstruction.
18    fn captured_len(&self) -> ParseResult<u64>;
19}
20
21/// Resource limits for mapped-image reconstruction.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct MaterializationLimits {
24    /// Maximum reconstructed file length.
25    pub max_file_bytes: u64,
26    /// Maximum total load-command bytes.
27    pub max_load_command_bytes: u64,
28    /// Maximum number of load commands.
29    pub max_load_commands: u32,
30}
31
32impl Default for MaterializationLimits {
33    fn default() -> Self {
34        Self {
35            max_file_bytes: 512 * 1024 * 1024,
36            max_load_command_bytes: 16 * 1024 * 1024,
37            max_load_commands: 65_536,
38        }
39    }
40}
41
42/// Mapping from reconstructed file bytes to the mapped RVA bytes that supplied them.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FileRvaMapping {
45    /// Reconstructed thin-file byte range.
46    pub file: Range<u64>,
47    /// Module-relative virtual byte range.
48    pub rva: Range<u64>,
49}
50
51/// A reconstructed Mach-O file and its source-coordinate map.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct MaterializedImage {
54    bytes: Vec<u8>,
55    mappings: Vec<FileRvaMapping>,
56    synthetic_padding: Vec<Range<u64>>,
57}
58
59impl MaterializedImage {
60    /// Parseable thin Mach-O bytes.
61    pub fn bytes(&self) -> &[u8] {
62        &self.bytes
63    }
64
65    /// Consume the result and return its parseable bytes.
66    pub fn into_bytes(self) -> Vec<u8> {
67        self.bytes
68    }
69
70    /// File-to-RVA mappings in file order.
71    pub fn mappings(&self) -> &[FileRvaMapping] {
72        &self.mappings
73    }
74
75    /// Unbacked file ranges filled with zero solely to preserve file layout.
76    ///
77    /// Consumers must not cite these ranges as captured evidence.
78    pub fn synthetic_padding(&self) -> &[Range<u64>] {
79        &self.synthetic_padding
80    }
81}
82
83#[derive(Debug, Clone, Copy)]
84struct RawSegment {
85    vmaddr: u64,
86    fileoff: u64,
87    filesize: u64,
88}
89
90/// Reconstruct one parseable thin file from an immutable mapped-module snapshot.
91pub fn materialize_mapped_image(
92    reader: &impl MappedImageReader,
93    limits: MaterializationLimits,
94) -> ParseResult<MaterializedImage> {
95    let prefix = reader.read_exact(0..32)?;
96    let magic_bytes: [u8; 4] = prefix
97        .get(..4)
98        .and_then(|bytes| bytes.try_into().ok())
99        .ok_or_else(|| ParseError::bounds(0, 4, prefix.len() as u64))?;
100    let magic = MagicNumber::from_u32(u32::from_ne_bytes(magic_bytes))?;
101    let endian = magic.endian();
102    let bitness = magic.bitness();
103    let header_size = bitness.header_size();
104    let ncmds = read_u32(&prefix, 16, endian, "Mach-O command count")?;
105    let sizeofcmds = u64::from(read_u32(&prefix, 20, endian, "Mach-O load-command bytes")?);
106    if ncmds > limits.max_load_commands {
107        return Err(ParseError::limit(format!(
108            "Mach-O command count {ncmds} exceeds {}",
109            limits.max_load_commands
110        )));
111    }
112    if sizeofcmds > limits.max_load_command_bytes {
113        return Err(ParseError::limit(format!(
114            "Mach-O load-command bytes {sizeofcmds} exceed {}",
115            limits.max_load_command_bytes
116        )));
117    }
118    let commands_end = (header_size as u64)
119        .checked_add(sizeofcmds)
120        .ok_or_else(|| ParseError::command("Mach-O load-command extent overflows"))?;
121    let header_and_commands = reader.read_exact(0..commands_end)?;
122    let commands_end_usize = usize::try_from(commands_end)
123        .map_err(|_| ParseError::limit("Mach-O load commands exceed host limits"))?;
124    let mut cursor = header_size;
125    let mut segments = Vec::new();
126    for index in 0..ncmds {
127        let command = read_u32(&header_and_commands, cursor, endian, "Mach-O load command")?;
128        let command_size = usize::try_from(read_u32(
129            &header_and_commands,
130            cursor
131                .checked_add(4)
132                .ok_or_else(|| ParseError::command("load-command offset overflows"))?,
133            endian,
134            "Mach-O load-command size",
135        )?)
136        .map_err(|_| ParseError::limit("Mach-O load-command size exceeds host limits"))?;
137        let command_end = cursor
138            .checked_add(command_size)
139            .ok_or_else(|| ParseError::command("Mach-O load-command extent overflows"))?;
140        if command_size < 8 || command_end > commands_end_usize {
141            return Err(ParseError::command(format!(
142                "Mach-O load command {index} is truncated"
143            )));
144        }
145        let segment = match (command, bitness) {
146            (LC_SEGMENT_64, Bitness::Bits64) if command_size >= 72 => Some(RawSegment {
147                vmaddr: read_u64(&header_and_commands, cursor + 24, endian, "segment vmaddr")?,
148                fileoff: read_u64(&header_and_commands, cursor + 40, endian, "segment fileoff")?,
149                filesize: read_u64(
150                    &header_and_commands,
151                    cursor + 48,
152                    endian,
153                    "segment filesize",
154                )?,
155            }),
156            (LC_SEGMENT, Bitness::Bits32) if command_size >= 56 => Some(RawSegment {
157                vmaddr: u64::from(read_u32(
158                    &header_and_commands,
159                    cursor + 24,
160                    endian,
161                    "segment vmaddr",
162                )?),
163                fileoff: u64::from(read_u32(
164                    &header_and_commands,
165                    cursor + 32,
166                    endian,
167                    "segment fileoff",
168                )?),
169                filesize: u64::from(read_u32(
170                    &header_and_commands,
171                    cursor + 36,
172                    endian,
173                    "segment filesize",
174                )?),
175            }),
176            (LC_SEGMENT_64, Bitness::Bits64) | (LC_SEGMENT, Bitness::Bits32) => {
177                return Err(ParseError::command(format!(
178                    "Mach-O segment command {index} is truncated"
179                )));
180            }
181            _ => None,
182        };
183        if let Some(segment) = segment.filter(|segment| segment.filesize != 0) {
184            segments.push(segment);
185        }
186        cursor = command_end;
187    }
188    if cursor != commands_end_usize || segments.is_empty() {
189        return Err(ParseError::new(
190            ParseErrorKind::InvalidLoadCommand,
191            "Mach-O load commands or file-backed segments are incomplete",
192        ));
193    }
194    let image_base = segments
195        .iter()
196        .map(|segment| segment.vmaddr)
197        .min()
198        .ok_or_else(|| ParseError::format("Mach-O image base is absent"))?;
199    segments.sort_by_key(|segment| segment.fileoff);
200    let file_len = segments.iter().try_fold(0_u64, |length, segment| {
201        segment
202            .fileoff
203            .checked_add(segment.filesize)
204            .map(|end| length.max(end))
205            .ok_or_else(|| ParseError::address("Mach-O file extent overflows"))
206    })?;
207    if file_len == 0 || file_len > limits.max_file_bytes {
208        return Err(ParseError::limit(format!(
209            "reconstructed Mach-O length {file_len} exceeds {}",
210            limits.max_file_bytes
211        )));
212    }
213    let source_len = segments.iter().try_fold(0_u64, |total, segment| {
214        total
215            .checked_add(segment.filesize)
216            .ok_or_else(|| ParseError::limit("Mach-O mapped source length overflows"))
217    })?;
218    let captured_len = reader.captured_len()?;
219    if source_len > captured_len {
220        return Err(ParseError::bounds(0, source_len, captured_len));
221    }
222    let mut bytes = vec![
223        0_u8;
224        usize::try_from(file_len).map_err(|_| ParseError::limit(
225            "reconstructed Mach-O exceeds host limits"
226        ))?
227    ];
228    let mut mappings = Vec::with_capacity(segments.len());
229    let mut synthetic_padding = Vec::new();
230    let mut prior_file_end = 0_u64;
231    for segment in segments {
232        if segment.fileoff < prior_file_end {
233            return Err(ParseError::validation(
234                "Mach-O file-backed segments overlap",
235            ));
236        }
237        if prior_file_end < segment.fileoff {
238            synthetic_padding.push(prior_file_end..segment.fileoff);
239        }
240        let rva_start = segment
241            .vmaddr
242            .checked_sub(image_base)
243            .ok_or_else(|| ParseError::address("Mach-O segment precedes image base"))?;
244        let rva_end = rva_start
245            .checked_add(segment.filesize)
246            .ok_or_else(|| ParseError::address("Mach-O segment RVA extent overflows"))?;
247        let source = reader.read_exact(rva_start..rva_end)?;
248        let file_end = segment
249            .fileoff
250            .checked_add(segment.filesize)
251            .ok_or_else(|| ParseError::address("Mach-O segment file extent overflows"))?;
252        let start = usize::try_from(segment.fileoff)
253            .map_err(|_| ParseError::limit("Mach-O segment file offset exceeds host limits"))?;
254        let end = usize::try_from(file_end)
255            .map_err(|_| ParseError::limit("Mach-O segment file end exceeds host limits"))?;
256        bytes
257            .get_mut(start..end)
258            .ok_or_else(|| ParseError::bounds(segment.fileoff, segment.filesize, file_len))?
259            .copy_from_slice(&source);
260        mappings.push(FileRvaMapping {
261            file: segment.fileoff..file_end,
262            rva: rva_start..rva_end,
263        });
264        prior_file_end = file_end;
265    }
266    crate::format::parse_macho_file(&bytes)?;
267    Ok(MaterializedImage {
268        bytes,
269        mappings,
270        synthetic_padding,
271    })
272}
273
274fn read_u32(bytes: &[u8], offset: usize, endian: Endian, subject: &str) -> ParseResult<u32> {
275    let value: [u8; 4] = bytes
276        .get(offset..offset.saturating_add(4))
277        .and_then(|value| value.try_into().ok())
278        .ok_or_else(|| ParseError::command(format!("{subject} is truncated")))?;
279    Ok(endian.read_u32(value))
280}
281
282fn read_u64(bytes: &[u8], offset: usize, endian: Endian, subject: &str) -> ParseResult<u64> {
283    let value: [u8; 8] = bytes
284        .get(offset..offset.saturating_add(8))
285        .and_then(|value| value.try_into().ok())
286        .ok_or_else(|| ParseError::command(format!("{subject} is truncated")))?;
287    Ok(endian.read_u64(value))
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    struct BytesReader(Vec<u8>);
295
296    impl MappedImageReader for BytesReader {
297        fn read_exact(&self, range: Range<u64>) -> ParseResult<Vec<u8>> {
298            self.0
299                .get(range.start as usize..range.end as usize)
300                .map(<[u8]>::to_vec)
301                .ok_or_else(|| {
302                    ParseError::bounds(
303                        range.start,
304                        range.end.saturating_sub(range.start),
305                        self.0.len() as u64,
306                    )
307                })
308        }
309
310        fn captured_len(&self) -> ParseResult<u64> {
311            Ok(self.0.len() as u64)
312        }
313    }
314
315    fn mapped_thin64() -> Vec<u8> {
316        let mut bytes = vec![0_u8; 104];
317        bytes[0..4].copy_from_slice(&0xfeed_facfu32.to_le_bytes());
318        bytes[4..8].copy_from_slice(&0x0100_0007i32.to_le_bytes());
319        bytes[8..12].copy_from_slice(&3_i32.to_le_bytes());
320        bytes[12..16].copy_from_slice(&2_u32.to_le_bytes());
321        bytes[16..20].copy_from_slice(&1_u32.to_le_bytes());
322        bytes[20..24].copy_from_slice(&72_u32.to_le_bytes());
323        bytes[32..36].copy_from_slice(&LC_SEGMENT_64.to_le_bytes());
324        bytes[36..40].copy_from_slice(&72_u32.to_le_bytes());
325        bytes[40..46].copy_from_slice(b"__TEXT");
326        bytes[56..64].copy_from_slice(&0x1000_u64.to_le_bytes());
327        bytes[64..72].copy_from_slice(&104_u64.to_le_bytes());
328        bytes[72..80].copy_from_slice(&0_u64.to_le_bytes());
329        bytes[80..88].copy_from_slice(&104_u64.to_le_bytes());
330        bytes[88..92].copy_from_slice(&7_u32.to_le_bytes());
331        bytes[92..96].copy_from_slice(&5_u32.to_le_bytes());
332        bytes
333    }
334
335    #[test]
336    fn materialization_retains_file_rva_provenance() {
337        let source = mapped_thin64();
338        let image = materialize_mapped_image(
339            &BytesReader(source.clone()),
340            MaterializationLimits::default(),
341        )
342        .expect("mapped image");
343        assert_eq!(image.bytes(), source);
344        assert_eq!(
345            image.mappings(),
346            [FileRvaMapping {
347                file: 0..104,
348                rva: 0..104,
349            }]
350        );
351        assert!(image.synthetic_padding().is_empty());
352    }
353}