use std::ops::Range;
use crate::format::io::Endian;
use crate::model::header::{Bitness, MagicNumber};
use crate::{ParseError, ParseErrorKind, ParseResult};
const LC_SEGMENT: u32 = 0x1;
const LC_SEGMENT_64: u32 = 0x19;
pub trait MappedImageReader {
fn read_exact(&self, range: Range<u64>) -> ParseResult<Vec<u8>>;
fn captured_len(&self) -> ParseResult<u64>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaterializationLimits {
pub max_file_bytes: u64,
pub max_load_command_bytes: u64,
pub max_load_commands: u32,
}
impl Default for MaterializationLimits {
fn default() -> Self {
Self {
max_file_bytes: 512 * 1024 * 1024,
max_load_command_bytes: 16 * 1024 * 1024,
max_load_commands: 65_536,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileRvaMapping {
pub file: Range<u64>,
pub rva: Range<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterializedImage {
bytes: Vec<u8>,
mappings: Vec<FileRvaMapping>,
synthetic_padding: Vec<Range<u64>>,
}
impl MaterializedImage {
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
pub fn mappings(&self) -> &[FileRvaMapping] {
&self.mappings
}
pub fn synthetic_padding(&self) -> &[Range<u64>] {
&self.synthetic_padding
}
}
#[derive(Debug, Clone, Copy)]
struct RawSegment {
vmaddr: u64,
fileoff: u64,
filesize: u64,
}
pub fn materialize_mapped_image(
reader: &impl MappedImageReader,
limits: MaterializationLimits,
) -> ParseResult<MaterializedImage> {
let prefix = reader.read_exact(0..32)?;
let magic_bytes: [u8; 4] = prefix
.get(..4)
.and_then(|bytes| bytes.try_into().ok())
.ok_or_else(|| ParseError::bounds(0, 4, prefix.len() as u64))?;
let magic = MagicNumber::from_u32(u32::from_ne_bytes(magic_bytes))?;
let endian = magic.endian();
let bitness = magic.bitness();
let header_size = bitness.header_size();
let ncmds = read_u32(&prefix, 16, endian, "Mach-O command count")?;
let sizeofcmds = u64::from(read_u32(&prefix, 20, endian, "Mach-O load-command bytes")?);
if ncmds > limits.max_load_commands {
return Err(ParseError::limit(format!(
"Mach-O command count {ncmds} exceeds {}",
limits.max_load_commands
)));
}
if sizeofcmds > limits.max_load_command_bytes {
return Err(ParseError::limit(format!(
"Mach-O load-command bytes {sizeofcmds} exceed {}",
limits.max_load_command_bytes
)));
}
let commands_end = (header_size as u64)
.checked_add(sizeofcmds)
.ok_or_else(|| ParseError::command("Mach-O load-command extent overflows"))?;
let header_and_commands = reader.read_exact(0..commands_end)?;
let commands_end_usize = usize::try_from(commands_end)
.map_err(|_| ParseError::limit("Mach-O load commands exceed host limits"))?;
let mut cursor = header_size;
let mut segments = Vec::new();
for index in 0..ncmds {
let command = read_u32(&header_and_commands, cursor, endian, "Mach-O load command")?;
let command_size = usize::try_from(read_u32(
&header_and_commands,
cursor
.checked_add(4)
.ok_or_else(|| ParseError::command("load-command offset overflows"))?,
endian,
"Mach-O load-command size",
)?)
.map_err(|_| ParseError::limit("Mach-O load-command size exceeds host limits"))?;
let command_end = cursor
.checked_add(command_size)
.ok_or_else(|| ParseError::command("Mach-O load-command extent overflows"))?;
if command_size < 8 || command_end > commands_end_usize {
return Err(ParseError::command(format!(
"Mach-O load command {index} is truncated"
)));
}
let segment = match (command, bitness) {
(LC_SEGMENT_64, Bitness::Bits64) if command_size >= 72 => Some(RawSegment {
vmaddr: read_u64(&header_and_commands, cursor + 24, endian, "segment vmaddr")?,
fileoff: read_u64(&header_and_commands, cursor + 40, endian, "segment fileoff")?,
filesize: read_u64(
&header_and_commands,
cursor + 48,
endian,
"segment filesize",
)?,
}),
(LC_SEGMENT, Bitness::Bits32) if command_size >= 56 => Some(RawSegment {
vmaddr: u64::from(read_u32(
&header_and_commands,
cursor + 24,
endian,
"segment vmaddr",
)?),
fileoff: u64::from(read_u32(
&header_and_commands,
cursor + 32,
endian,
"segment fileoff",
)?),
filesize: u64::from(read_u32(
&header_and_commands,
cursor + 36,
endian,
"segment filesize",
)?),
}),
(LC_SEGMENT_64, Bitness::Bits64) | (LC_SEGMENT, Bitness::Bits32) => {
return Err(ParseError::command(format!(
"Mach-O segment command {index} is truncated"
)));
}
_ => None,
};
if let Some(segment) = segment.filter(|segment| segment.filesize != 0) {
segments.push(segment);
}
cursor = command_end;
}
if cursor != commands_end_usize || segments.is_empty() {
return Err(ParseError::new(
ParseErrorKind::InvalidLoadCommand,
"Mach-O load commands or file-backed segments are incomplete",
));
}
let image_base = segments
.iter()
.map(|segment| segment.vmaddr)
.min()
.ok_or_else(|| ParseError::format("Mach-O image base is absent"))?;
segments.sort_by_key(|segment| segment.fileoff);
let file_len = segments.iter().try_fold(0_u64, |length, segment| {
segment
.fileoff
.checked_add(segment.filesize)
.map(|end| length.max(end))
.ok_or_else(|| ParseError::address("Mach-O file extent overflows"))
})?;
if file_len == 0 || file_len > limits.max_file_bytes {
return Err(ParseError::limit(format!(
"reconstructed Mach-O length {file_len} exceeds {}",
limits.max_file_bytes
)));
}
let source_len = segments.iter().try_fold(0_u64, |total, segment| {
total
.checked_add(segment.filesize)
.ok_or_else(|| ParseError::limit("Mach-O mapped source length overflows"))
})?;
let captured_len = reader.captured_len()?;
if source_len > captured_len {
return Err(ParseError::bounds(0, source_len, captured_len));
}
let mut bytes = vec![
0_u8;
usize::try_from(file_len).map_err(|_| ParseError::limit(
"reconstructed Mach-O exceeds host limits"
))?
];
let mut mappings = Vec::with_capacity(segments.len());
let mut synthetic_padding = Vec::new();
let mut prior_file_end = 0_u64;
for segment in segments {
if segment.fileoff < prior_file_end {
return Err(ParseError::validation(
"Mach-O file-backed segments overlap",
));
}
if prior_file_end < segment.fileoff {
synthetic_padding.push(prior_file_end..segment.fileoff);
}
let rva_start = segment
.vmaddr
.checked_sub(image_base)
.ok_or_else(|| ParseError::address("Mach-O segment precedes image base"))?;
let rva_end = rva_start
.checked_add(segment.filesize)
.ok_or_else(|| ParseError::address("Mach-O segment RVA extent overflows"))?;
let source = reader.read_exact(rva_start..rva_end)?;
let file_end = segment
.fileoff
.checked_add(segment.filesize)
.ok_or_else(|| ParseError::address("Mach-O segment file extent overflows"))?;
let start = usize::try_from(segment.fileoff)
.map_err(|_| ParseError::limit("Mach-O segment file offset exceeds host limits"))?;
let end = usize::try_from(file_end)
.map_err(|_| ParseError::limit("Mach-O segment file end exceeds host limits"))?;
bytes
.get_mut(start..end)
.ok_or_else(|| ParseError::bounds(segment.fileoff, segment.filesize, file_len))?
.copy_from_slice(&source);
mappings.push(FileRvaMapping {
file: segment.fileoff..file_end,
rva: rva_start..rva_end,
});
prior_file_end = file_end;
}
crate::format::parse_macho_file(&bytes)?;
Ok(MaterializedImage {
bytes,
mappings,
synthetic_padding,
})
}
fn read_u32(bytes: &[u8], offset: usize, endian: Endian, subject: &str) -> ParseResult<u32> {
let value: [u8; 4] = bytes
.get(offset..offset.saturating_add(4))
.and_then(|value| value.try_into().ok())
.ok_or_else(|| ParseError::command(format!("{subject} is truncated")))?;
Ok(endian.read_u32(value))
}
fn read_u64(bytes: &[u8], offset: usize, endian: Endian, subject: &str) -> ParseResult<u64> {
let value: [u8; 8] = bytes
.get(offset..offset.saturating_add(8))
.and_then(|value| value.try_into().ok())
.ok_or_else(|| ParseError::command(format!("{subject} is truncated")))?;
Ok(endian.read_u64(value))
}
#[cfg(test)]
mod tests {
use super::*;
struct BytesReader(Vec<u8>);
impl MappedImageReader for BytesReader {
fn read_exact(&self, range: Range<u64>) -> ParseResult<Vec<u8>> {
self.0
.get(range.start as usize..range.end as usize)
.map(<[u8]>::to_vec)
.ok_or_else(|| {
ParseError::bounds(
range.start,
range.end.saturating_sub(range.start),
self.0.len() as u64,
)
})
}
fn captured_len(&self) -> ParseResult<u64> {
Ok(self.0.len() as u64)
}
}
fn mapped_thin64() -> Vec<u8> {
let mut bytes = vec![0_u8; 104];
bytes[0..4].copy_from_slice(&0xfeed_facfu32.to_le_bytes());
bytes[4..8].copy_from_slice(&0x0100_0007i32.to_le_bytes());
bytes[8..12].copy_from_slice(&3_i32.to_le_bytes());
bytes[12..16].copy_from_slice(&2_u32.to_le_bytes());
bytes[16..20].copy_from_slice(&1_u32.to_le_bytes());
bytes[20..24].copy_from_slice(&72_u32.to_le_bytes());
bytes[32..36].copy_from_slice(&LC_SEGMENT_64.to_le_bytes());
bytes[36..40].copy_from_slice(&72_u32.to_le_bytes());
bytes[40..46].copy_from_slice(b"__TEXT");
bytes[56..64].copy_from_slice(&0x1000_u64.to_le_bytes());
bytes[64..72].copy_from_slice(&104_u64.to_le_bytes());
bytes[72..80].copy_from_slice(&0_u64.to_le_bytes());
bytes[80..88].copy_from_slice(&104_u64.to_le_bytes());
bytes[88..92].copy_from_slice(&7_u32.to_le_bytes());
bytes[92..96].copy_from_slice(&5_u32.to_le_bytes());
bytes
}
#[test]
fn materialization_retains_file_rva_provenance() {
let source = mapped_thin64();
let image = materialize_mapped_image(
&BytesReader(source.clone()),
MaterializationLimits::default(),
)
.expect("mapped image");
assert_eq!(image.bytes(), source);
assert_eq!(
image.mappings(),
[FileRvaMapping {
file: 0..104,
rva: 0..104,
}]
);
assert!(image.synthetic_padding().is_empty());
}
}