Skip to main content

macho_dyld_cache/
lib.rs

1#![deny(missing_docs)]
2//! Dyld shared-cache parsing and extraction-bytes APIs.
3
4use serde::Serialize;
5
6pub use macho_core::format;
7
8/// The error module.
9pub mod error;
10pub(crate) use error::Error;
11pub use error::{DyldCacheError, DyldCacheErrorKind, Result};
12
13const DYLD_CACHE_MAGIC_PREFIX: &[u8] = b"dyld_v1";
14const HEADER_MIN_SIZE: usize = 32;
15const MAPPING_INFO_SIZE: usize = 32;
16const IMAGE_INFO_SIZE: usize = 32;
17const IMAGE_TEXT_INFO_SIZE: usize = 32;
18
19// Header offsets for the modern imagesText fields (dyld_cache_format.h)
20const IMAGES_TEXT_OFFSET_OFF: usize = 0x88;
21const IMAGES_TEXT_COUNT_OFF: usize = 0x90;
22// Minimum header size to contain the imagesText fields
23const MODERN_HEADER_MIN: usize = IMAGES_TEXT_COUNT_OFF + 8;
24
25/// Read-only index of a dyld shared cache file.
26///
27/// Supports enumeration and extraction of embedded Mach-O images without
28/// modifying the cache. Extracted image slices can be fed directly into
29/// [`crate::format::parse`] for normal inspection.
30#[derive(Debug, Clone, Serialize)]
31pub struct DyldCache {
32    /// The header field.
33    pub header: DyldCacheHeader,
34    /// The mappings field.
35    pub mappings: Vec<CacheMapping>,
36    /// The images field.
37    pub images: Vec<CacheImage>,
38}
39
40#[derive(Debug, Clone, Serialize)]
41/// The DyldCacheHeader type.
42pub struct DyldCacheHeader {
43    /// The magic field.
44    pub magic: String,
45    /// The arch field.
46    pub arch: String,
47    /// The mapping_offset field.
48    pub mapping_offset: u32,
49    /// The mapping_count field.
50    pub mapping_count: u32,
51}
52
53#[derive(Debug, Clone, Serialize)]
54/// The CacheMapping type.
55pub struct CacheMapping {
56    /// The address field.
57    pub address: u64,
58    /// The size field.
59    pub size: u64,
60    /// The file_offset field.
61    pub file_offset: u64,
62    /// The max_prot field.
63    pub max_prot: u32,
64    /// The init_prot field.
65    pub init_prot: u32,
66}
67
68#[derive(Debug, Clone, Serialize)]
69/// The CacheImage type.
70pub struct CacheImage {
71    /// The address field.
72    pub address: u64,
73    /// The path field.
74    pub path: String,
75    /// The text_size field.
76    pub text_size: u32,
77}
78
79/// Parse a dyld shared cache from a memory-mapped buffer.
80pub fn parse_dyld_cache(data: &[u8]) -> Result<DyldCache> {
81    if data.len() < HEADER_MIN_SIZE {
82        return Err(Error::format("file too small for dyld cache header"));
83    }
84
85    // Validate magic: first 7 bytes must be "dyld_v1"
86    if &data[..7] != DYLD_CACHE_MAGIC_PREFIX {
87        return Err(Error::format("not a dyld shared cache (bad magic)"));
88    }
89
90    // The full 16-byte magic encodes the architecture, e.g. "dyld_v1  x86_64\0"
91    let magic_bytes = &data[..16];
92    let magic = std::str::from_utf8(magic_bytes)
93        .unwrap_or("")
94        .trim_end_matches('\0')
95        .to_string();
96
97    let arch = magic
98        .strip_prefix("dyld_v1")
99        .unwrap_or("")
100        .trim()
101        .to_string();
102
103    let mapping_offset = read_u32_le(data, 16)?;
104    let mapping_count = read_u32_le(data, 20)?;
105    let images_offset_old = read_u32_le(data, 24)?;
106    let images_count_old = read_u32_le(data, 28)?;
107
108    let header = DyldCacheHeader {
109        magic,
110        arch,
111        mapping_offset,
112        mapping_count,
113    };
114
115    // Parse mappings
116    let mappings = parse_mappings(data, mapping_offset as usize, mapping_count as usize)?;
117
118    // Parse images: try old format first, fall back to modern imagesText format
119    let images = if images_count_old > 0 && images_offset_old > 0 {
120        parse_images_old(data, images_offset_old as usize, images_count_old as usize)?
121    } else if data.len() >= MODERN_HEADER_MIN {
122        let images_text_offset = read_u64_le(data, IMAGES_TEXT_OFFSET_OFF)?;
123        let images_text_count = read_u64_le(data, IMAGES_TEXT_COUNT_OFF)?;
124        if images_text_count > 0 && images_text_offset > 0 {
125            let off = usize::try_from(images_text_offset).map_err(|_| {
126                Error::format(format!(
127                    "dyld cache imagesText offset {images_text_offset:#x} exceeds addressable memory"
128                ))
129            })?;
130            let cnt = usize::try_from(images_text_count).map_err(|_| {
131                Error::format(format!(
132                    "dyld cache imagesText count {images_text_count} exceeds addressable memory"
133                ))
134            })?;
135            parse_images_text(data, off, cnt)?
136        } else {
137            Vec::new()
138        }
139    } else {
140        Vec::new()
141    };
142
143    Ok(DyldCache {
144        header,
145        mappings,
146        images,
147    })
148}
149
150fn parse_mappings(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheMapping>> {
151    validate_table_extent(data, offset, count, MAPPING_INFO_SIZE, "mapping")?;
152    let mut mappings = Vec::with_capacity(count);
153    for i in 0..count {
154        let off = offset
155            .checked_add(
156                i.checked_mul(MAPPING_INFO_SIZE)
157                    .ok_or_else(|| Error::format(format!("mapping[{i}] stride overflows")))?,
158            )
159            .ok_or_else(|| Error::format(format!("mapping[{i}] offset overflows")))?;
160        if off + MAPPING_INFO_SIZE > data.len() {
161            return Err(Error::bounds(
162                off as u64,
163                MAPPING_INFO_SIZE as u64,
164                data.len() as u64,
165            ));
166        }
167        mappings.push(CacheMapping {
168            address: read_u64_le(data, off)?,
169            size: read_u64_le(data, off + 8)?,
170            file_offset: read_u64_le(data, off + 16)?,
171            max_prot: read_u32_le(data, off + 24)?,
172            init_prot: read_u32_le(data, off + 28)?,
173        });
174    }
175    Ok(mappings)
176}
177
178/// Parse the old-style dyld_cache_image_info entries.
179/// Layout: address(u64) + modTime(u64) + inode(u64) + pathFileOffset(u32) + pad(u32)
180fn parse_images_old(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheImage>> {
181    validate_table_extent(data, offset, count, IMAGE_INFO_SIZE, "old image")?;
182    let mut images = Vec::with_capacity(count);
183    for i in 0..count {
184        let off = offset
185            .checked_add(
186                i.checked_mul(IMAGE_INFO_SIZE)
187                    .ok_or_else(|| Error::format(format!("old image[{i}] stride overflows")))?,
188            )
189            .ok_or_else(|| Error::format(format!("old image[{i}] offset overflows")))?;
190        if off + IMAGE_INFO_SIZE > data.len() {
191            return Err(Error::bounds(
192                off as u64,
193                IMAGE_INFO_SIZE as u64,
194                data.len() as u64,
195            ));
196        }
197        let address = read_u64_le(data, off)?;
198        let path_offset = read_u32_le(data, off + 24)?;
199        let path = read_c_string(data, path_offset as usize);
200
201        images.push(CacheImage {
202            address,
203            path,
204            text_size: 0,
205        });
206    }
207    Ok(images)
208}
209
210/// Parse the modern dyld_cache_image_text_info entries.
211/// Layout: uuid(16) + loadAddress(u64) + textSegmentSize(u32) + pathOffset(u32)
212fn parse_images_text(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheImage>> {
213    validate_table_extent(data, offset, count, IMAGE_TEXT_INFO_SIZE, "imagesText")?;
214    let mut images = Vec::with_capacity(count);
215    for i in 0..count {
216        let off = offset
217            .checked_add(
218                i.checked_mul(IMAGE_TEXT_INFO_SIZE)
219                    .ok_or_else(|| Error::format(format!("imagesText[{i}] stride overflows")))?,
220            )
221            .ok_or_else(|| Error::format(format!("imagesText[{i}] offset overflows")))?;
222        if off + IMAGE_TEXT_INFO_SIZE > data.len() {
223            return Err(Error::bounds(
224                off as u64,
225                IMAGE_TEXT_INFO_SIZE as u64,
226                data.len() as u64,
227            ));
228        }
229        // Skip uuid (16 bytes)
230        let address = read_u64_le(data, off + 16)?;
231        let text_size = read_u32_le(data, off + 24)?;
232        let path_offset = read_u32_le(data, off + 28)?;
233        let path = read_c_string(data, path_offset as usize);
234
235        images.push(CacheImage {
236            address,
237            path,
238            text_size,
239        });
240    }
241    Ok(images)
242}
243
244fn validate_table_extent(
245    data: &[u8],
246    offset: usize,
247    count: usize,
248    stride: usize,
249    label: &str,
250) -> Result<()> {
251    let size = count
252        .checked_mul(stride)
253        .ok_or_else(|| Error::format(format!("{label} table size overflows")))?;
254    let end = offset
255        .checked_add(size)
256        .ok_or_else(|| Error::format(format!("{label} table extent overflows")))?;
257    if end > data.len() {
258        return Err(Error::bounds(
259            offset as u64,
260            u64::try_from(size).unwrap_or(u64::MAX),
261            data.len() as u64,
262        ));
263    }
264    Ok(())
265}
266
267impl DyldCache {
268    /// List all embedded images.
269    pub fn images(&self) -> &[CacheImage] {
270        &self.images
271    }
272
273    /// List cache memory mappings.
274    pub fn mappings(&self) -> &[CacheMapping] {
275        &self.mappings
276    }
277
278    /// Architecture string extracted from the magic field.
279    pub fn arch(&self) -> &str {
280        &self.header.arch
281    }
282
283    /// Convert a virtual address to a file offset using the mapping table.
284    pub fn va_to_file_offset(&self, va: u64) -> Option<u64> {
285        for m in &self.mappings {
286            let mapping_end = m.address.checked_add(m.size)?;
287            if va >= m.address && va < mapping_end {
288                return m.file_offset.checked_add(va - m.address);
289            }
290        }
291        None
292    }
293
294    /// Extract the Mach-O bytes for the image at the given index.
295    ///
296    /// Returns a slice into the original cache data that starts at the image's
297    /// mapped location. When the image has a known `text_size`, that is used
298    /// as the slice length. Otherwise the slice extends to the next image
299    /// boundary or mapping end. The returned slice can be passed to
300    /// [`crate::format::parse`].
301    pub fn extract_image<'data>(&self, index: usize, data: &'data [u8]) -> Result<&'data [u8]> {
302        let image = self
303            .images
304            .get(index)
305            .ok_or_else(|| Error::format(format!("image index {index} out of range")))?;
306
307        let file_offset = self.va_to_file_offset(image.address).ok_or_else(|| {
308            Error::address(format!("image VA {:#x} not in any mapping", image.address))
309        })?;
310
311        let start = file_offset as usize;
312        if start >= data.len() {
313            return Err(Error::bounds(file_offset, 1, data.len() as u64));
314        }
315
316        // If we know the text segment size, use it as the extraction length
317        if image.text_size > 0 {
318            let end = (start + image.text_size as usize).min(data.len());
319            return Ok(&data[start..end]);
320        }
321
322        // Fallback: find the next image boundary or mapping end
323        let mapping_end = self
324            .mappings
325            .iter()
326            .find_map(|m| {
327                let end = m.file_offset.checked_add(m.size)?;
328                (file_offset >= m.file_offset && file_offset < end)
329                    .then(|| usize::try_from(end).ok())
330                    .flatten()
331            })
332            .unwrap_or(data.len());
333
334        let mut next_offset = mapping_end;
335        for other in &self.images {
336            if let Some(other_fo) = self.va_to_file_offset(other.address) {
337                let other_start = other_fo as usize;
338                if other_start > start && other_start < next_offset {
339                    next_offset = other_start;
340                }
341            }
342        }
343
344        let end = next_offset.min(data.len());
345        Ok(&data[start..end])
346    }
347}
348
349fn read_u32_le(data: &[u8], offset: usize) -> Result<u32> {
350    let end = offset
351        .checked_add(4)
352        .ok_or_else(|| Error::bounds(offset as u64, 4, data.len() as u64))?;
353    if end > data.len() {
354        return Err(Error::bounds(offset as u64, 4, data.len() as u64));
355    }
356    let bytes: [u8; 4] = data[offset..end]
357        .try_into()
358        .expect("slice length guaranteed by bounds check");
359    Ok(u32::from_le_bytes(bytes))
360}
361
362fn read_u64_le(data: &[u8], offset: usize) -> Result<u64> {
363    let end = offset
364        .checked_add(8)
365        .ok_or_else(|| Error::bounds(offset as u64, 8, data.len() as u64))?;
366    if end > data.len() {
367        return Err(Error::bounds(offset as u64, 8, data.len() as u64));
368    }
369    let bytes: [u8; 8] = data[offset..end]
370        .try_into()
371        .expect("slice length guaranteed by bounds check");
372    Ok(u64::from_le_bytes(bytes))
373}
374
375fn read_c_string(data: &[u8], offset: usize) -> String {
376    if offset >= data.len() {
377        return String::new();
378    }
379    let end = data[offset..]
380        .iter()
381        .position(|&b| b == 0)
382        .map(|p| offset + p)
383        .unwrap_or(data.len());
384    String::from_utf8_lossy(&data[offset..end]).into_owned()
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    fn make_minimal_cache(arch: &str) -> Vec<u8> {
392        // Build a minimal old-format dyld cache with 1 mapping and 1 image
393        let mut magic = [0u8; 16];
394        let prefix = format!("dyld_v1  {arch}");
395        let bytes = prefix.as_bytes();
396        let len = bytes.len().min(15);
397        magic[..len].copy_from_slice(&bytes[..len]);
398
399        let mapping_offset: u32 = 32; // right after header
400        let mapping_count: u32 = 1;
401        let images_offset: u32 = mapping_offset + MAPPING_INFO_SIZE as u32;
402        let images_count: u32 = 1;
403
404        let image_va: u64 = 0x1_0000_0000;
405        let image_file_offset: u64 = 4096;
406
407        // Path will be placed after the image info
408        let path_offset: u32 = images_offset + IMAGE_INFO_SIZE as u32;
409        let path = b"/usr/lib/libSystem.B.dylib\0";
410
411        let total_size = path_offset as usize + path.len() + image_file_offset as usize + 4096;
412        let mut data = vec![0u8; total_size];
413
414        // Header
415        data[..16].copy_from_slice(&magic);
416        data[16..20].copy_from_slice(&mapping_offset.to_le_bytes());
417        data[20..24].copy_from_slice(&mapping_count.to_le_bytes());
418        data[24..28].copy_from_slice(&images_offset.to_le_bytes());
419        data[28..32].copy_from_slice(&images_count.to_le_bytes());
420
421        // Mapping: covers the image region
422        let m_off = mapping_offset as usize;
423        data[m_off..m_off + 8].copy_from_slice(&image_va.to_le_bytes());
424        data[m_off + 8..m_off + 16].copy_from_slice(&4096u64.to_le_bytes());
425        data[m_off + 16..m_off + 24].copy_from_slice(&image_file_offset.to_le_bytes());
426        data[m_off + 24..m_off + 28].copy_from_slice(&5u32.to_le_bytes());
427        data[m_off + 28..m_off + 32].copy_from_slice(&5u32.to_le_bytes());
428
429        // Image info (old format: address + modTime + inode + pathOffset + pad)
430        let i_off = images_offset as usize;
431        data[i_off..i_off + 8].copy_from_slice(&image_va.to_le_bytes());
432        data[i_off + 8..i_off + 16].copy_from_slice(&0u64.to_le_bytes());
433        data[i_off + 16..i_off + 24].copy_from_slice(&0u64.to_le_bytes());
434        data[i_off + 24..i_off + 28].copy_from_slice(&path_offset.to_le_bytes());
435
436        // Path string
437        let p_off = path_offset as usize;
438        data[p_off..p_off + path.len()].copy_from_slice(path);
439
440        data
441    }
442
443    fn make_modern_cache(arch: &str) -> Vec<u8> {
444        // Build a minimal modern-format cache (old image fields zeroed, uses imagesText)
445        let mut magic = [0u8; 16];
446        let prefix = format!("dyld_v1  {arch}");
447        let bytes = prefix.as_bytes();
448        let len = bytes.len().min(15);
449        magic[..len].copy_from_slice(&bytes[..len]);
450
451        let mapping_offset: u32 = 0x98; // after header (needs room for imagesText fields)
452        let mapping_count: u32 = 1;
453
454        let image_va: u64 = 0x1_8000_0000;
455        let image_file_offset: u64 = 4096;
456
457        // imagesText entries start after the mapping
458        let images_text_offset: u64 = (mapping_offset as u64) + MAPPING_INFO_SIZE as u64;
459        let images_text_count: u64 = 1;
460
461        // Path after the image text info
462        let path_offset: u32 = (images_text_offset as u32) + IMAGE_TEXT_INFO_SIZE as u32;
463        let path = b"/usr/lib/libSystem.B.dylib\0";
464
465        let total_size = path_offset as usize + path.len() + image_file_offset as usize + 4096;
466        let mut data = vec![0u8; total_size];
467
468        // Header
469        data[..16].copy_from_slice(&magic);
470        data[16..20].copy_from_slice(&mapping_offset.to_le_bytes());
471        data[20..24].copy_from_slice(&mapping_count.to_le_bytes());
472        // Old image fields: zero
473        data[24..28].copy_from_slice(&0u32.to_le_bytes());
474        data[28..32].copy_from_slice(&0u32.to_le_bytes());
475
476        // imagesText fields at offsets 0x88/0x90
477        data[0x88..0x90].copy_from_slice(&images_text_offset.to_le_bytes());
478        data[0x90..0x98].copy_from_slice(&images_text_count.to_le_bytes());
479
480        // Mapping
481        let m_off = mapping_offset as usize;
482        data[m_off..m_off + 8].copy_from_slice(&image_va.to_le_bytes());
483        data[m_off + 8..m_off + 16].copy_from_slice(&0x100000u64.to_le_bytes());
484        data[m_off + 16..m_off + 24].copy_from_slice(&image_file_offset.to_le_bytes());
485        data[m_off + 24..m_off + 28].copy_from_slice(&5u32.to_le_bytes());
486        data[m_off + 28..m_off + 32].copy_from_slice(&5u32.to_le_bytes());
487
488        // Image text info: uuid(16) + loadAddress(8) + textSegmentSize(4) + pathOffset(4)
489        let i_off = images_text_offset as usize;
490        // uuid: leave as zeros
491        data[i_off + 16..i_off + 24].copy_from_slice(&image_va.to_le_bytes());
492        data[i_off + 24..i_off + 28].copy_from_slice(&0x50000u32.to_le_bytes()); // text_size
493        data[i_off + 28..i_off + 32].copy_from_slice(&path_offset.to_le_bytes());
494
495        // Path string
496        let p_off = path_offset as usize;
497        data[p_off..p_off + path.len()].copy_from_slice(path);
498
499        data
500    }
501
502    #[test]
503    fn parse_old_format_cache() {
504        let data = make_minimal_cache("arm64e");
505        let cache = parse_dyld_cache(&data).expect("failed to parse");
506        assert_eq!(cache.arch(), "arm64e");
507        assert_eq!(cache.mappings().len(), 1);
508        assert_eq!(cache.images().len(), 1);
509        assert_eq!(cache.images()[0].path, "/usr/lib/libSystem.B.dylib");
510    }
511
512    #[test]
513    fn parse_modern_format_cache() {
514        let data = make_modern_cache("arm64e");
515        let cache = parse_dyld_cache(&data).expect("failed to parse");
516        assert_eq!(cache.arch(), "arm64e");
517        assert_eq!(cache.mappings().len(), 1);
518        assert_eq!(cache.images().len(), 1);
519        assert_eq!(cache.images()[0].path, "/usr/lib/libSystem.B.dylib");
520        assert_eq!(cache.images()[0].text_size, 0x50000);
521        assert_eq!(cache.images()[0].address, 0x1_8000_0000);
522    }
523
524    #[test]
525    fn va_to_file_offset_works() {
526        let data = make_minimal_cache("x86_64");
527        let cache = parse_dyld_cache(&data).expect("failed to parse");
528        let fo = cache.va_to_file_offset(0x1_0000_0000);
529        assert_eq!(fo, Some(4096));
530        assert_eq!(cache.va_to_file_offset(0x1_0000_0100), Some(4096 + 0x100));
531        assert_eq!(cache.va_to_file_offset(0xDEAD), None);
532    }
533
534    #[test]
535    fn extract_image_returns_slice() {
536        let data = make_minimal_cache("arm64e");
537        let cache = parse_dyld_cache(&data).expect("failed to parse");
538        let slice = cache.extract_image(0, &data).expect("failed to extract");
539        assert!(!slice.is_empty());
540    }
541
542    #[test]
543    fn extract_modern_image_uses_text_size() {
544        let data = make_modern_cache("arm64e");
545        let cache = parse_dyld_cache(&data).expect("failed to parse");
546        let slice = cache.extract_image(0, &data).expect("failed to extract");
547        // text_size is 0x50000, but data may be smaller — should be clamped
548        assert!(!slice.is_empty());
549    }
550
551    #[test]
552    fn bad_magic_rejected() {
553        let data = b"not a dyld cache file at all!!";
554        assert!(parse_dyld_cache(data).is_err());
555    }
556
557    #[test]
558    fn too_small_rejected() {
559        let data = b"dyld_v1";
560        assert!(parse_dyld_cache(data).is_err());
561    }
562
563    #[test]
564    fn truncated_mapping_is_rejected_before_allocation() {
565        let data = macho_test_support::dyld_cache_truncated_mapping();
566        let error = parse_dyld_cache(&data).expect_err("mapping table is truncated");
567        assert_eq!(error.kind, DyldCacheErrorKind::OutOfBounds);
568        assert_eq!(error.code(), "dyld_cache.bounds.exceeded");
569        assert!(error.location.is_some());
570    }
571
572    #[test]
573    fn out_of_bounds_image_offset_is_rejected_on_extraction() {
574        let data = macho_test_support::dyld_cache_out_of_bounds_image();
575        let cache = parse_dyld_cache(&data).expect("table itself remains structurally valid");
576        let error = cache
577            .extract_image(0, &data)
578            .expect_err("image address is outside every mapping");
579        assert_eq!(error.kind, DyldCacheErrorKind::InvalidAddress);
580        assert_eq!(error.code(), "dyld_cache.address.invalid");
581    }
582}