1use 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
12pub trait MappedImageReader {
14 fn read_exact(&self, range: Range<u64>) -> ParseResult<Vec<u8>>;
16
17 fn captured_len(&self) -> ParseResult<u64>;
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct MaterializationLimits {
24 pub max_file_bytes: u64,
26 pub max_load_command_bytes: u64,
28 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#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct FileRvaMapping {
45 pub file: Range<u64>,
47 pub rva: Range<u64>,
49}
50
51#[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 pub fn bytes(&self) -> &[u8] {
62 &self.bytes
63 }
64
65 pub fn into_bytes(self) -> Vec<u8> {
67 self.bytes
68 }
69
70 pub fn mappings(&self) -> &[FileRvaMapping] {
72 &self.mappings
73 }
74
75 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
90pub 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}