use super::*;
use crate::completeness::completeness_for;
use crate::materialize::materialize_cache_image;
impl DyldCache {
pub fn images(&self) -> &[CacheImage] {
&self.images
}
pub fn mappings(&self) -> &[CacheMapping] {
&self.mappings
}
pub fn arch(&self) -> &str {
&self.header.arch
}
pub fn subcaches(&self) -> &[SubCacheEntry] {
&self.subcaches
}
pub fn va_to_file_offset(&self, va: u64) -> Option<u64> {
for m in &self.mappings {
let mapping_end = m.address.checked_add(m.size)?;
if va >= m.address && va < mapping_end {
return m.file_offset.checked_add(va - m.address);
}
}
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct CacheMemberInput<'data> {
pub name: &'data str,
pub data: &'data [u8],
}
#[derive(Debug)]
pub struct CacheFamilyMember<'data> {
name: String,
cache: DyldCache,
data: &'data [u8],
}
impl CacheFamilyMember<'_> {
pub fn name(&self) -> &str {
&self.name
}
pub fn cache(&self) -> &DyldCache {
&self.cache
}
}
#[derive(Debug)]
pub struct DyldCacheFamily<'data> {
members: Vec<CacheFamilyMember<'data>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CompletenessState {
Complete,
Absent,
Unresolved,
Rejected,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ComponentCompleteness {
pub state: CompletenessState,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReconstructionCompleteness {
pub segments: ComponentCompleteness,
pub linkedit: ComponentCompleteness,
pub symbols: ComponentCompleteness,
pub exports: ComponentCompleteness,
pub imports: ComponentCompleteness,
pub fixups: ComponentCompleteness,
pub local_symbols: ComponentCompleteness,
pub code_signature: ComponentCompleteness,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReconstructionMapping {
pub file_start: u64,
pub file_end: u64,
pub rva_start: u64,
pub rva_end: u64,
pub source_members: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReconstructedImage {
pub image_path: String,
pub arch: String,
#[serde(skip)]
bytes: Vec<u8>,
pub byte_len: usize,
pub mappings: Vec<ReconstructionMapping>,
pub synthetic_padding: Vec<SerializableRange>,
pub completeness: ReconstructionCompleteness,
}
impl ReconstructedImage {
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SerializableRange {
pub start: u64,
pub end: u64,
}
impl<'data> DyldCacheFamily<'data> {
pub fn parse(
primary: CacheMemberInput<'data>,
siblings: impl IntoIterator<Item = CacheMemberInput<'data>>,
) -> Result<Self> {
let primary_cache = parse_dyld_cache(primary.data)?;
let mut image_paths = BTreeMap::new();
for (index, image) in primary_cache.images.iter().enumerate() {
if let Some(prior) = image_paths.insert(image.path.as_str(), index) {
return Err(Error::format(format!(
"duplicate image path {:?} at indexes {prior} and {index}",
image.path
)));
}
}
let mut supplied = BTreeMap::new();
for sibling in siblings {
if supplied.insert(sibling.name, sibling.data).is_some() {
return Err(Error::format(format!(
"duplicate cache family member {:?}",
sibling.name
)));
}
}
let expected_names = primary_cache
.subcaches
.iter()
.map(|entry| entry.file_suffix.as_str())
.collect::<BTreeSet<_>>();
if let Some(unexpected) = supplied
.keys()
.find(|name| !expected_names.contains(**name))
{
return Err(Error::format(format!(
"unexpected cache family member {unexpected:?}"
)));
}
let primary_base = primary_cache
.mappings
.iter()
.map(|mapping| mapping.address)
.min()
.ok_or_else(|| Error::unsupported("dyld cache has no mapped regions"))?;
let arch = primary_cache.arch().to_owned();
let mut members = vec![CacheFamilyMember {
name: primary.name.to_owned(),
cache: primary_cache,
data: primary.data,
}];
let declarations = members[0].cache.subcaches.clone();
for declaration in declarations {
let data = supplied
.get(declaration.file_suffix.as_str())
.ok_or_else(|| {
Error::format(format!(
"missing required cache family member {:?}",
declaration.file_suffix
))
})?;
let cache = parse_dyld_cache(data)?;
if cache.header.uuid != declaration.uuid {
return Err(Error::format(format!(
"cache family member {:?} UUID mismatch: expected {}, found {}",
declaration.file_suffix,
format_uuid(declaration.uuid),
format_uuid(cache.header.uuid)
)));
}
if cache.arch() != arch {
return Err(Error::unsupported(format!(
"cache family member {:?} architecture {:?} does not match {:?}",
declaration.file_suffix,
cache.arch(),
arch
)));
}
let actual_base = cache
.mappings
.iter()
.map(|mapping| mapping.address)
.min()
.ok_or_else(|| {
Error::unsupported(format!(
"cache family member {:?} has no mapped regions",
declaration.file_suffix
))
})?;
let expected_base = primary_base
.checked_add(declaration.cache_vm_offset)
.ok_or_else(|| Error::address("subcache VM base overflows"))?;
if actual_base != expected_base {
return Err(Error::format(format!(
"cache family member {:?} VM base mismatch: expected {expected_base:#x}, found {actual_base:#x}",
declaration.file_suffix
)));
}
members.push(CacheFamilyMember {
name: declaration.file_suffix,
cache,
data,
});
}
validate_family_mappings(&members)?;
Ok(Self { members })
}
pub fn primary(&self) -> &DyldCache {
&self.members[0].cache
}
pub fn members(&self) -> &[CacheFamilyMember<'data>] {
&self.members
}
pub fn search_images(&self, query: &str) -> Vec<usize> {
self.primary()
.images
.iter()
.enumerate()
.filter_map(|(index, image)| image.path.contains(query).then_some(index))
.collect()
}
pub fn image_index_by_path(&self, path: &str) -> Option<usize> {
self.primary()
.images
.iter()
.position(|image| image.path == path)
}
pub fn reconstruct_image(
&self,
index: usize,
limits: MaterializationLimits,
) -> Result<ReconstructedImage> {
let image = self
.primary()
.images
.get(index)
.ok_or_else(|| Error::format(format!("image index {index} out of range")))?;
if image.path.is_empty() {
return Err(Error::format(format!("image index {index} has no path")));
}
let materialized = materialize_cache_image(self, image.address, limits)?;
let container = macho_core::format::parse(&materialized.bytes).map_err(Error::from)?;
let macho = container
.first_macho()
.ok_or_else(|| Error::format("reconstructed image has no thin Mach-O"))?;
let completeness = completeness_for(self, macho);
let mappings = materialized
.mappings
.iter()
.map(|mapping| {
let va_start = image
.address
.checked_add(mapping.rva.start)
.ok_or_else(|| Error::address("reconstruction provenance VA overflows"))?;
let va_end = image
.address
.checked_add(mapping.rva.end)
.ok_or_else(|| Error::address("reconstruction provenance VA overflows"))?;
Ok(ReconstructionMapping {
file_start: mapping.file.start,
file_end: mapping.file.end,
rva_start: mapping.rva.start,
rva_end: mapping.rva.end,
source_members: self.source_members_for(va_start..va_end)?,
})
})
.collect::<Result<Vec<_>>>()?;
let synthetic_padding = materialized
.synthetic_padding
.iter()
.map(|range| SerializableRange {
start: range.start,
end: range.end,
})
.collect();
let bytes = materialized.bytes;
Ok(ReconstructedImage {
image_path: image.path.clone(),
arch: self.primary().arch().to_owned(),
byte_len: bytes.len(),
bytes,
mappings,
synthetic_padding,
completeness,
})
}
pub(super) fn read_va_exact(&self, range: Range<u64>) -> Result<Vec<u8>> {
if range.start > range.end {
return Err(Error::address("cache read range is reversed"));
}
let length = range.end - range.start;
let capacity = usize::try_from(length)
.map_err(|_| Error::unsupported("cache read exceeds host address space"))?;
let mut result = Vec::with_capacity(capacity);
let mut cursor = range.start;
while cursor < range.end {
let (member, mapping) = self.mapping_for_va(cursor).ok_or_else(|| {
Error::address(format!("VA {cursor:#x} is not mapped by the cache family"))
})?;
let mapping_end = mapping
.address
.checked_add(mapping.size)
.ok_or_else(|| Error::address("validated cache mapping extent overflows"))?;
let chunk_end = mapping_end.min(range.end);
let source_start = mapping
.file_offset
.checked_add(cursor - mapping.address)
.ok_or_else(|| Error::address("cache source offset overflows"))?;
let source_end = source_start
.checked_add(chunk_end - cursor)
.ok_or_else(|| Error::address("cache source extent overflows"))?;
let start = usize::try_from(source_start)
.map_err(|_| Error::unsupported("cache source offset exceeds host limits"))?;
let end = usize::try_from(source_end)
.map_err(|_| Error::unsupported("cache source extent exceeds host limits"))?;
let bytes = member.data.get(start..end).ok_or_else(|| {
Error::bounds(
source_start,
source_end - source_start,
member.data.len() as u64,
)
})?;
result.extend_from_slice(bytes);
cursor = chunk_end;
}
Ok(result)
}
pub(super) fn read_c_string_va(
&self,
start: u64,
available: u64,
subject: &str,
) -> Result<Vec<u8>> {
const MAX_SYMBOL_NAME: u64 = 1024 * 1024;
let limit = available.min(MAX_SYMBOL_NAME);
let mut result = Vec::new();
let mut cursor = 0_u64;
while cursor < limit {
let chunk_len = (limit - cursor).min(256);
let chunk_start = start
.checked_add(cursor)
.ok_or_else(|| Error::address(format!("{subject} address overflows")))?;
let chunk_end = chunk_start
.checked_add(chunk_len)
.ok_or_else(|| Error::address(format!("{subject} extent overflows")))?;
let chunk = self.read_va_exact(chunk_start..chunk_end)?;
if let Some(end) = chunk.iter().position(|byte| *byte == 0) {
result.extend_from_slice(&chunk[..end]);
return Ok(result);
}
result.extend_from_slice(&chunk);
cursor += chunk_len;
}
Err(Error::unsupported(format!(
"{subject} is not NUL-terminated within {limit} bytes"
)))
}
fn mapping_for_va(&self, va: u64) -> Option<(&CacheFamilyMember<'data>, &CacheMapping)> {
self.members.iter().find_map(|member| {
member.cache.mappings.iter().find_map(|mapping| {
let end = mapping.address.checked_add(mapping.size)?;
(va >= mapping.address && va < end).then_some((member, mapping))
})
})
}
fn source_members_for(&self, range: Range<u64>) -> Result<Vec<String>> {
let mut result = Vec::new();
let mut cursor = range.start;
while cursor < range.end {
let (member, mapping) = self.mapping_for_va(cursor).ok_or_else(|| {
Error::address(format!("VA {cursor:#x} is not mapped by the cache family"))
})?;
if result.last().is_none_or(|name| name != member.name()) {
result.push(member.name.clone());
}
cursor = mapping
.address
.checked_add(mapping.size)
.ok_or_else(|| Error::address("validated cache mapping extent overflows"))?
.min(range.end);
}
Ok(result)
}
}
fn validate_family_mappings(members: &[CacheFamilyMember<'_>]) -> Result<()> {
let mut mappings = Vec::new();
for member in members {
for mapping in &member.cache.mappings {
let end = mapping
.address
.checked_add(mapping.size)
.ok_or_else(|| Error::address("validated cache mapping extent overflows"))?;
if mappings
.iter()
.any(|(start, prior_end): &(u64, u64)| mapping.address < *prior_end && *start < end)
{
return Err(Error::format(format!(
"cache family member {:?} has a mapping that overlaps another member",
member.name
)));
}
mappings.push((mapping.address, end));
}
}
Ok(())
}