use std::fs::File;
use std::mem::offset_of;
use std::path::{Path, PathBuf};
use memmap2::Mmap;
use zerocopy::FromBytes;
use super::structs::*;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct MappingEntry {
pub address: u64,
pub size: u64,
pub file_offset: u64,
pub max_prot: u32,
pub init_prot: u32,
pub slide_info_offset: u64,
pub slide_info_size: u64,
pub flags: u64,
pub subcache_index: usize,
}
impl MappingEntry {
pub fn from_basic(info: &DyldCacheMappingInfo, subcache_index: usize) -> Self {
Self {
address: info.address,
size: info.size,
file_offset: info.file_offset,
max_prot: info.max_prot,
init_prot: info.init_prot,
slide_info_offset: 0,
slide_info_size: 0,
flags: 0,
subcache_index,
}
}
pub fn from_extended(info: &DyldCacheMappingAndSlideInfo, subcache_index: usize) -> Self {
Self {
address: info.address,
size: info.size,
file_offset: info.file_offset,
max_prot: info.max_prot,
init_prot: info.init_prot,
slide_info_offset: info.slide_info_file_offset,
slide_info_size: info.slide_info_file_size,
flags: info.flags,
subcache_index,
}
}
#[inline]
pub fn contains_addr(&self, addr: u64) -> bool {
addr >= self.address && addr < self.address + self.size
}
#[inline]
pub fn contains_offset(&self, offset: u64) -> bool {
offset >= self.file_offset && offset < self.file_offset + self.size
}
#[inline]
pub fn addr_to_offset(&self, addr: u64) -> u64 {
self.file_offset + (addr - self.address)
}
#[inline]
pub fn offset_to_addr(&self, offset: u64) -> u64 {
self.address + (offset - self.file_offset)
}
#[inline]
pub fn is_readable(&self) -> bool {
(self.init_prot & 1) != 0
}
#[inline]
pub fn is_writable(&self) -> bool {
(self.init_prot & 2) != 0
}
#[inline]
pub fn is_executable(&self) -> bool {
(self.init_prot & 4) != 0
}
#[inline]
pub fn has_slide_info(&self) -> bool {
self.slide_info_size > 0
}
}
#[derive(Debug, Clone)]
pub struct ImageEntry {
pub index: usize,
pub address: u64,
pub file_offset: u64,
pub path: String,
pub mod_time: u64,
pub inode: u64,
pub subcache_index: usize,
}
impl ImageEntry {
pub fn basename(&self) -> &str {
self.path.rsplit('/').next().unwrap_or(&self.path)
}
pub fn matches_filter(&self, filter: &str) -> bool {
self.path.contains(filter) || self.basename().contains(filter)
}
}
#[derive(Debug)]
pub struct SubcacheFile {
pub mmap: Mmap,
pub path: PathBuf,
pub uuid: [u8; 16],
pub vm_offset: u64,
pub header: DyldCacheHeader,
}
#[derive(Debug)]
pub struct DyldContext {
pub mmap: Mmap,
pub path: PathBuf,
pub header: DyldCacheHeader,
pub mappings: Vec<MappingEntry>,
pub images: Vec<ImageEntry>,
pub subcaches: Vec<SubcacheFile>,
pub symbols_file: Option<SubcacheFile>,
pub local_symbols_info: Option<DyldCacheLocalSymbolsInfo>,
pub shared_region_start: u64,
}
impl DyldContext {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let file = File::open(&path).map_err(|e| Error::FileOpen {
path: path.clone(),
source: e,
})?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| Error::MemoryMap {
path: path.clone(),
source: e,
})?;
let header = Self::parse_header(&mmap)?;
let mappings = Self::parse_mappings(&mmap, &header, 0)?;
let mut ctx = Self {
mmap,
path: path.clone(),
header,
mappings,
images: Vec::new(),
subcaches: Vec::new(),
symbols_file: None,
local_symbols_info: None,
shared_region_start: 0,
};
if ctx
.header
.contains_field(offset_of!(super::DyldCacheHeader, shared_region_start))
{
ctx.shared_region_start = ctx.header.shared_region_start;
}
ctx.load_subcaches(&path)?;
ctx.load_symbols_file(&path)?;
ctx.images = ctx.parse_images()?;
ctx.parse_local_symbols()?;
Ok(ctx)
}
fn parse_header(data: &[u8]) -> Result<DyldCacheHeader> {
if data.len() < std::mem::size_of::<DyldCacheHeader>() {
return Err(Error::BufferTooSmall {
needed: std::mem::size_of::<DyldCacheHeader>(),
available: data.len(),
});
}
let header = DyldCacheHeader::read_from_prefix(data)
.map_err(|_| Error::Parse {
offset: 0,
reason: "failed to parse dyld cache header".into(),
})?
.0;
if &header.magic[..4] != DYLD_CACHE_MAGIC_PREFIX {
return Err(Error::InvalidMagic([
header.magic[0],
header.magic[1],
header.magic[2],
header.magic[3],
]));
}
Ok(header.clone())
}
fn parse_mappings(
data: &[u8],
header: &DyldCacheHeader,
subcache_index: usize,
) -> Result<Vec<MappingEntry>> {
let mut mappings = Vec::with_capacity(header.mapping_count as usize);
let use_extended = header.contains_field(offset_of!(
super::DyldCacheHeader,
mapping_with_slide_offset
)) && header.mapping_with_slide_offset != 0;
if use_extended {
let offset = header.mapping_with_slide_offset as usize;
for i in 0..header.mapping_with_slide_count as usize {
let entry_offset = offset + i * std::mem::size_of::<DyldCacheMappingAndSlideInfo>();
let info = DyldCacheMappingAndSlideInfo::read_from_prefix(&data[entry_offset..])
.map_err(|_| Error::Parse {
offset: entry_offset,
reason: "failed to parse extended mapping".into(),
})?
.0;
mappings.push(MappingEntry::from_extended(&info, subcache_index));
}
} else {
let offset = header.mapping_offset as usize;
for i in 0..header.mapping_count as usize {
let entry_offset = offset + i * std::mem::size_of::<DyldCacheMappingInfo>();
let info = DyldCacheMappingInfo::read_from_prefix(&data[entry_offset..])
.map_err(|_| Error::Parse {
offset: entry_offset,
reason: "failed to parse mapping".into(),
})?
.0;
mappings.push(MappingEntry::from_basic(&info, subcache_index));
}
}
Ok(mappings)
}
fn load_subcaches(&mut self, main_path: &Path) -> Result<()> {
if !self.header.has_subcaches() {
return Ok(());
}
let parent_dir = main_path.parent().unwrap_or(Path::new("."));
let main_name = main_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let count = self.header.sub_cache_array_count as usize;
let offset = self.header.sub_cache_array_offset as usize;
let entry_size = if self.has_v2_subcache_entries() {
std::mem::size_of::<DyldSubcacheEntry2>()
} else {
std::mem::size_of::<DyldSubcacheEntry>()
};
for i in 0..count {
let entry_offset = offset + i * entry_size;
let (uuid, vm_offset, suffix) = if self.has_v2_subcache_entries() {
let entry = DyldSubcacheEntry2::read_from_prefix(&self.mmap[entry_offset..])
.map_err(|_| Error::Parse {
offset: entry_offset,
reason: "failed to parse subcache entry v2".into(),
})?
.0;
(
entry.uuid,
entry.cache_vm_offset,
entry.suffix_str().to_string(),
)
} else {
let entry = DyldSubcacheEntry::read_from_prefix(&self.mmap[entry_offset..])
.map_err(|_| Error::Parse {
offset: entry_offset,
reason: "failed to parse subcache entry".into(),
})?
.0;
(entry.uuid, entry.cache_vm_offset, format!(".{}", i + 1))
};
let subcache_path = parent_dir.join(format!("{}{}", main_name, suffix));
self.load_subcache_file(&subcache_path, uuid, vm_offset, i + 1)?;
}
Ok(())
}
fn load_subcache_file(
&mut self,
path: &Path,
expected_uuid: [u8; 16],
vm_offset: u64,
subcache_index: usize,
) -> Result<()> {
let file = File::open(path).map_err(|_| Error::SubcacheNotFound {
path: path.to_path_buf(),
})?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| Error::MemoryMap {
path: path.to_path_buf(),
source: e,
})?;
let header = Self::parse_header(&mmap)?;
if header.uuid != expected_uuid {
return Err(Error::SubcacheUuidMismatch {
path: path.to_path_buf(),
expected: format!("{:02x?}", expected_uuid),
actual: format!("{:02x?}", header.uuid),
});
}
let subcache_mappings = Self::parse_mappings(&mmap, &header, subcache_index)?;
self.mappings.extend(subcache_mappings);
self.subcaches.push(SubcacheFile {
mmap,
path: path.to_path_buf(),
uuid: header.uuid,
vm_offset,
header,
});
Ok(())
}
fn load_symbols_file(&mut self, main_path: &Path) -> Result<()> {
if !self.header.has_symbol_file() {
return Ok(());
}
let parent_dir = main_path.parent().unwrap_or(Path::new("."));
let main_name = main_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let symbols_path = parent_dir.join(format!("{}.symbols", main_name));
if !symbols_path.exists() {
return Ok(());
}
let file = File::open(&symbols_path).map_err(|e| Error::FileOpen {
path: symbols_path.clone(),
source: e,
})?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| Error::MemoryMap {
path: symbols_path.clone(),
source: e,
})?;
let header = Self::parse_header(&mmap)?;
if header.uuid != self.header.symbol_file_uuid {
return Err(Error::SubcacheUuidMismatch {
path: symbols_path.clone(),
expected: format!("{:02x?}", self.header.symbol_file_uuid),
actual: format!("{:02x?}", header.uuid),
});
}
self.symbols_file = Some(SubcacheFile {
mmap,
path: symbols_path,
uuid: header.uuid,
vm_offset: 0,
header,
});
Ok(())
}
fn parse_images(&self) -> Result<Vec<ImageEntry>> {
let count = self.header.actual_images_count() as usize;
let offset = self.header.actual_images_offset() as usize;
let mut images = Vec::with_capacity(count);
for i in 0..count {
let entry_offset = offset + i * std::mem::size_of::<DyldCacheImageInfo>();
let info = DyldCacheImageInfo::read_from_prefix(&self.mmap[entry_offset..])
.map_err(|_| Error::Parse {
offset: entry_offset,
reason: "failed to parse image info".into(),
})?
.0;
let path = self.read_string(info.path_file_offset as usize)?;
let subcache_index = self.find_subcache_for_addr(info.address);
let file_offset = self.addr_to_offset(info.address).unwrap_or(0);
images.push(ImageEntry {
index: i,
address: info.address,
file_offset,
path,
mod_time: info.mod_time,
inode: info.inode,
subcache_index,
});
}
Ok(images)
}
fn parse_local_symbols(&mut self) -> Result<()> {
let (data, offset) =
if self.header.local_symbols_offset != 0 && self.header.local_symbols_size != 0 {
(&self.mmap[..], self.header.local_symbols_offset as usize)
} else if let Some(ref symbols_file) = self.symbols_file {
if symbols_file.header.local_symbols_offset != 0 {
(
&symbols_file.mmap[..],
symbols_file.header.local_symbols_offset as usize,
)
} else {
return Ok(());
}
} else {
return Ok(());
};
if offset + std::mem::size_of::<DyldCacheLocalSymbolsInfo>() > data.len() {
return Ok(());
}
let info = DyldCacheLocalSymbolsInfo::read_from_prefix(&data[offset..])
.map_err(|_| Error::Parse {
offset,
reason: "failed to parse local symbols info".into(),
})?
.0;
self.local_symbols_info = Some(info.clone());
Ok(())
}
fn has_v2_subcache_entries(&self) -> bool {
if self.header.sub_cache_array_count == 0 {
return false;
}
let offset = self.header.sub_cache_array_offset as usize;
if offset + std::mem::size_of::<DyldSubcacheEntry2>() > self.mmap.len() {
return false;
}
if let Ok((entry, _)) = DyldSubcacheEntry2::read_from_prefix(&self.mmap[offset..]) {
entry.file_suffix[0] == b'.'
} else {
false
}
}
fn find_subcache_for_addr(&self, addr: u64) -> usize {
for mapping in &self.mappings {
if mapping.contains_addr(addr) {
return mapping.subcache_index;
}
}
0 }
pub fn read_string(&self, offset: usize) -> Result<String> {
if offset >= self.mmap.len() {
return Err(Error::Parse {
offset,
reason: "string offset out of bounds".into(),
});
}
let bytes = &self.mmap[offset..];
let end = crate::util::memchr_null(bytes);
String::from_utf8(bytes[..end].to_vec()).map_err(|_| Error::Parse {
offset,
reason: "invalid UTF-8 string".into(),
})
}
#[inline]
pub fn addr_to_offset(&self, addr: u64) -> Option<u64> {
let idx = self
.mappings
.partition_point(|m| m.address + m.size <= addr);
if idx < self.mappings.len() {
let mapping = &self.mappings[idx];
if mapping.contains_addr(addr) {
return Some(mapping.addr_to_offset(addr));
}
}
self.mappings
.iter()
.find(|m| m.contains_addr(addr))
.map(|m| m.addr_to_offset(addr))
}
#[inline]
pub fn offset_to_addr(&self, offset: u64) -> Option<u64> {
self.mappings
.iter()
.find(|m| m.contains_offset(offset))
.map(|m| m.offset_to_addr(offset))
}
#[inline]
pub fn data_at_addr(&self, addr: u64, len: usize) -> Result<&[u8]> {
let idx = self
.mappings
.partition_point(|m| m.address + m.size <= addr);
if idx < self.mappings.len() {
let mapping = &self.mappings[idx];
if mapping.contains_addr(addr) {
let offset = mapping.addr_to_offset(addr) as usize;
let data = self.data_for_subcache(mapping.subcache_index);
if offset + len > data.len() {
return Err(Error::BufferTooSmall {
needed: offset + len,
available: data.len(),
});
}
return Ok(&data[offset..offset + len]);
}
}
Err(Error::AddressNotFound { addr })
}
#[inline]
pub fn data_for_subcache(&self, index: usize) -> &[u8] {
if index == 0 {
&self.mmap[..]
} else if let Some(subcache) = self.subcaches.get(index - 1) {
&subcache.mmap[..]
} else {
&[]
}
}
#[inline]
pub fn mapping_for_addr(&self, addr: u64) -> Option<&MappingEntry> {
let idx = self
.mappings
.partition_point(|m| m.address + m.size <= addr);
if idx < self.mappings.len() {
let mapping = &self.mappings[idx];
if mapping.contains_addr(addr) {
return Some(mapping);
}
}
None
}
pub fn iter_images(&self) -> impl Iterator<Item = &ImageEntry> {
self.images.iter()
}
pub fn find_image(&self, name: &str) -> Option<&ImageEntry> {
self.images.iter().find(|img| img.matches_filter(name))
}
pub fn image_count(&self) -> usize {
self.images.len()
}
pub fn has_subcaches(&self) -> bool {
!self.subcaches.is_empty()
}
pub fn total_size(&self) -> u64 {
let main_size = self.mmap.len() as u64;
let subcache_size: u64 = self.subcaches.iter().map(|s| s.mmap.len() as u64).sum();
let symbols_size = self
.symbols_file
.as_ref()
.map(|s| s.mmap.len() as u64)
.unwrap_or(0);
main_size + subcache_size + symbols_size
}
pub fn architecture(&self) -> &str {
self.header.architecture()
}
pub fn symbols_cache_data(&self) -> Option<&[u8]> {
if let Some(ref symbols_file) = self.symbols_file {
if symbols_file.header.local_symbols_offset != 0 {
return Some(&symbols_file.mmap[..]);
}
}
if self.header.local_symbols_offset != 0 && self.header.local_symbols_size != 0 {
return Some(&self.mmap[..]);
}
None
}
pub fn local_symbols_offset(&self) -> Option<u64> {
if let Some(ref symbols_file) = self.symbols_file {
if symbols_file.header.local_symbols_offset != 0 {
return Some(symbols_file.header.local_symbols_offset);
}
}
if self.header.local_symbols_offset != 0 {
return Some(self.header.local_symbols_offset);
}
None
}
pub fn uses_64bit_local_symbol_entries(&self) -> bool {
self.header.has_symbol_file()
}
pub fn read_symbols_string(&self, offset: usize) -> Result<String> {
let data = self.symbols_cache_data().ok_or(Error::Parse {
offset: 0,
reason: "no symbols cache available".into(),
})?;
if offset >= data.len() {
return Err(Error::Parse {
offset,
reason: "string offset out of bounds in symbols cache".into(),
});
}
let bytes = &data[offset..];
let end = crate::util::memchr_null(bytes);
String::from_utf8(bytes[..end].to_vec()).map_err(|_| Error::Parse {
offset,
reason: "invalid UTF-8 string in symbols cache".into(),
})
}
pub fn slide_info_value_add(&self) -> Option<u64> {
for mapping in &self.mappings {
if !mapping.has_slide_info() {
continue;
}
let cache_data = self.data_for_subcache(mapping.subcache_index);
let offset = mapping.slide_info_offset as usize;
if offset + 4 > cache_data.len() {
continue;
}
let version = crate::util::read_u32_le(&cache_data[offset..]);
match version {
2 => {
if offset + 40 <= cache_data.len() {
return Some(crate::util::read_u64_le(&cache_data[offset + 32..]));
}
}
3 => {
if offset + 16 <= cache_data.len() {
return Some(crate::util::read_u64_le(&cache_data[offset + 8..]));
}
}
5 => {
if offset + 16 <= cache_data.len() {
return Some(crate::util::read_u64_le(&cache_data[offset + 8..]));
}
}
_ => {}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mapping_entry_contains() {
let mapping = MappingEntry {
address: 0x1000,
size: 0x1000,
file_offset: 0x0,
max_prot: 7,
init_prot: 5,
slide_info_offset: 0,
slide_info_size: 0,
flags: 0,
subcache_index: 0,
};
assert!(mapping.contains_addr(0x1000));
assert!(mapping.contains_addr(0x1FFF));
assert!(!mapping.contains_addr(0x2000));
assert!(!mapping.contains_addr(0x0FFF));
}
#[test]
fn test_mapping_conversion() {
let mapping = MappingEntry {
address: 0x1_0000_0000,
size: 0x1000_0000,
file_offset: 0x1000,
max_prot: 7,
init_prot: 5,
slide_info_offset: 0,
slide_info_size: 0,
flags: 0,
subcache_index: 0,
};
assert_eq!(mapping.addr_to_offset(0x1_0000_0000), 0x1000);
assert_eq!(mapping.addr_to_offset(0x1_0001_0000), 0x11000);
assert_eq!(mapping.offset_to_addr(0x1000), 0x1_0000_0000);
}
}