Skip to main content

macho_dyld_cache/
parse.rs

1use super::*;
2
3const DYLD_CACHE_MAGIC_PREFIX: &[u8] = b"dyld_v1";
4const HEADER_MIN_SIZE: usize = 32;
5pub(super) const MAPPING_INFO_SIZE: usize = 32;
6pub(super) const IMAGE_INFO_SIZE: usize = 32;
7pub(super) const IMAGE_TEXT_INFO_SIZE: usize = 32;
8
9// Header offsets for the modern imagesText fields (dyld_cache_format.h)
10const IMAGES_TEXT_OFFSET_OFF: usize = 0x88;
11const IMAGES_TEXT_COUNT_OFF: usize = 0x90;
12// Minimum header size to contain the imagesText fields
13const MODERN_HEADER_MIN: usize = IMAGES_TEXT_COUNT_OFF + 8;
14const UUID_OFF: usize = 0x58;
15const LOCAL_SYMBOLS_OFFSET_OFF: usize = 0x48;
16const LOCAL_SYMBOLS_SIZE_OFF: usize = 0x50;
17const SUBCACHE_ARRAY_OFFSET_OFF: usize = 0x188;
18const SUBCACHE_ARRAY_COUNT_OFF: usize = 0x18c;
19const SYMBOL_FILE_UUID_OFF: usize = 0x190;
20const CACHE_SUBTYPE_OFF: usize = 0x1c8;
21const IMAGES_OFFSET_OFF: usize = 0x1c0;
22const IMAGES_COUNT_OFF: usize = 0x1c4;
23const SUBCACHE_ENTRY_V1_SIZE: usize = 24;
24const SUBCACHE_ENTRY_V2_SIZE: usize = 56;
25
26/// Read-only index of a dyld shared cache file.
27///
28/// Supports enumeration and extraction of embedded Mach-O images without
29/// modifying the cache. Extracted image slices can be fed directly into
30/// [`crate::format::parse`] for normal inspection.
31#[derive(Debug, Clone, Serialize)]
32pub struct DyldCache {
33    /// The header field.
34    pub header: DyldCacheHeader,
35    /// The mappings field.
36    pub mappings: Vec<CacheMapping>,
37    /// The images field.
38    pub images: Vec<CacheImage>,
39    /// Subcache files required by this cache, in header order.
40    pub subcaches: Vec<SubCacheEntry>,
41}
42
43#[derive(Debug, Clone, Serialize)]
44/// The DyldCacheHeader type.
45pub struct DyldCacheHeader {
46    /// The magic field.
47    pub magic: String,
48    /// The arch field.
49    pub arch: String,
50    /// The mapping_offset field.
51    pub mapping_offset: u32,
52    /// The mapping_count field.
53    pub mapping_count: u32,
54    /// Unique identifier of this cache family member.
55    pub uuid: [u8; 16],
56    /// File offset of the cache-level local-symbol store, when present.
57    pub local_symbols_offset: u64,
58    /// Size of the cache-level local-symbol store, when present.
59    pub local_symbols_size: u64,
60    /// UUID of a separate local-symbol cache, or all zeroes when absent.
61    pub symbol_file_uuid: [u8; 16],
62}
63
64#[derive(Debug, Clone, Serialize)]
65/// The CacheMapping type.
66pub struct CacheMapping {
67    /// The address field.
68    pub address: u64,
69    /// The size field.
70    pub size: u64,
71    /// The file_offset field.
72    pub file_offset: u64,
73    /// The max_prot field.
74    pub max_prot: u32,
75    /// The init_prot field.
76    pub init_prot: u32,
77}
78
79#[derive(Debug, Clone, Serialize)]
80/// The CacheImage type.
81pub struct CacheImage {
82    /// The address field.
83    pub address: u64,
84    /// The path field.
85    pub path: String,
86    /// The text_size field.
87    pub text_size: u32,
88}
89
90/// One required dyld cache family member declared by the primary header.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub struct SubCacheEntry {
93    /// Expected UUID of the sibling cache file.
94    pub uuid: [u8; 16],
95    /// VM offset of the sibling from the primary cache base.
96    pub cache_vm_offset: u64,
97    /// Exact filename suffix, including the leading dot.
98    pub file_suffix: String,
99}
100
101/// Parse a dyld shared cache from a memory-mapped buffer.
102pub fn parse_dyld_cache(data: &[u8]) -> Result<DyldCache> {
103    if data.len() < HEADER_MIN_SIZE {
104        return Err(Error::format("file too small for dyld cache header"));
105    }
106
107    // Validate magic: first 7 bytes must be "dyld_v1"
108    if &data[..7] != DYLD_CACHE_MAGIC_PREFIX {
109        return Err(Error::format("not a dyld shared cache (bad magic)"));
110    }
111
112    // The full 16-byte magic encodes the architecture, e.g. "dyld_v1  x86_64\0"
113    let magic_bytes = &data[..16];
114    let magic = std::str::from_utf8(magic_bytes)
115        .unwrap_or("")
116        .trim_end_matches('\0')
117        .to_string();
118
119    let arch = magic
120        .strip_prefix("dyld_v1")
121        .unwrap_or("")
122        .trim()
123        .to_string();
124
125    let mapping_offset = read_u32_le(data, 16)?;
126    let mapping_count = read_u32_le(data, 20)?;
127    let images_offset_old = read_u32_le(data, 24)?;
128    let images_count_old = read_u32_le(data, 28)?;
129
130    let uuid = if mapping_offset as usize > UUID_OFF {
131        read_uuid(data, UUID_OFF)?
132    } else {
133        [0; 16]
134    };
135    let (local_symbols_offset, local_symbols_size) =
136        if mapping_offset as usize > LOCAL_SYMBOLS_SIZE_OFF {
137            (
138                read_u64_le(data, LOCAL_SYMBOLS_OFFSET_OFF)?,
139                read_u64_le(data, LOCAL_SYMBOLS_SIZE_OFF)?,
140            )
141        } else {
142            (0, 0)
143        };
144    let symbol_file_uuid = if mapping_offset as usize > SYMBOL_FILE_UUID_OFF {
145        read_uuid(data, SYMBOL_FILE_UUID_OFF)?
146    } else {
147        [0; 16]
148    };
149
150    let header = DyldCacheHeader {
151        magic,
152        arch,
153        mapping_offset,
154        mapping_count,
155        uuid,
156        local_symbols_offset,
157        local_symbols_size,
158        symbol_file_uuid,
159    };
160
161    // Parse mappings
162    let mappings = parse_mappings(data, mapping_offset as usize, mapping_count as usize)?;
163
164    // Parse images: current caches use the later images array, intermediate
165    // caches use imagesText, and legacy caches use the original fields.
166    let images = if images_count_old > 0 && images_offset_old > 0 {
167        parse_images_old(data, images_offset_old as usize, images_count_old as usize)?
168    } else if mapping_offset as usize > IMAGES_COUNT_OFF {
169        let images_offset = read_u32_le(data, IMAGES_OFFSET_OFF)?;
170        let images_count = read_u32_le(data, IMAGES_COUNT_OFF)?;
171        if images_count > 0 && images_offset > 0 {
172            parse_images_old(data, images_offset as usize, images_count as usize)?
173        } else {
174            parse_images_text_if_present(data)?
175        }
176    } else if data.len() >= MODERN_HEADER_MIN {
177        parse_images_text_if_present(data)?
178    } else {
179        Vec::new()
180    };
181
182    let subcaches = parse_subcaches(data, mapping_offset as usize)?;
183
184    Ok(DyldCache {
185        header,
186        mappings,
187        images,
188        subcaches,
189    })
190}
191
192fn parse_images_text_if_present(data: &[u8]) -> Result<Vec<CacheImage>> {
193    if data.len() < MODERN_HEADER_MIN {
194        return Ok(Vec::new());
195    }
196    let images_text_offset = read_u64_le(data, IMAGES_TEXT_OFFSET_OFF)?;
197    let images_text_count = read_u64_le(data, IMAGES_TEXT_COUNT_OFF)?;
198    if images_text_count == 0 || images_text_offset == 0 {
199        return Ok(Vec::new());
200    }
201    let off = usize::try_from(images_text_offset).map_err(|_| {
202        Error::format(format!(
203            "dyld cache imagesText offset {images_text_offset:#x} exceeds addressable memory"
204        ))
205    })?;
206    let cnt = usize::try_from(images_text_count).map_err(|_| {
207        Error::format(format!(
208            "dyld cache imagesText count {images_text_count} exceeds addressable memory"
209        ))
210    })?;
211    parse_images_text(data, off, cnt)
212}
213
214fn parse_subcaches(data: &[u8], header_size: usize) -> Result<Vec<SubCacheEntry>> {
215    if header_size <= SUBCACHE_ARRAY_COUNT_OFF {
216        return Ok(Vec::new());
217    }
218    let offset = read_u32_le(data, SUBCACHE_ARRAY_OFFSET_OFF)? as usize;
219    let count = read_u32_le(data, SUBCACHE_ARRAY_COUNT_OFF)? as usize;
220    if count == 0 {
221        return Ok(Vec::new());
222    }
223    if offset == 0 {
224        return Err(Error::format(
225            "dyld cache declares subcaches with a zero table offset",
226        ));
227    }
228    let has_suffix = header_size > CACHE_SUBTYPE_OFF;
229    let stride = if has_suffix {
230        SUBCACHE_ENTRY_V2_SIZE
231    } else {
232        SUBCACHE_ENTRY_V1_SIZE
233    };
234    validate_table_extent(data, offset, count, stride, "subcache")?;
235    let mut result = Vec::with_capacity(count);
236    let mut suffixes = BTreeSet::new();
237    let mut uuids = BTreeSet::new();
238    for index in 0..count {
239        let entry_offset = offset
240            .checked_add(
241                index
242                    .checked_mul(stride)
243                    .ok_or_else(|| Error::format(format!("subcache[{index}] stride overflows")))?,
244            )
245            .ok_or_else(|| Error::format(format!("subcache[{index}] offset overflows")))?;
246        let uuid = read_uuid(data, entry_offset)?;
247        let cache_vm_offset = read_u64_le(data, entry_offset + 16)?;
248        let file_suffix = if has_suffix {
249            read_fixed_c_string(data, entry_offset + 24, 32, "subcache suffix")?
250        } else {
251            format!(".{}", index + 1)
252        };
253        if !file_suffix.starts_with('.') || file_suffix.len() < 2 {
254            return Err(Error::format(format!(
255                "subcache[{index}] has invalid suffix {file_suffix:?}"
256            )));
257        }
258        if !suffixes.insert(file_suffix.clone()) {
259            return Err(Error::format(format!(
260                "duplicate subcache suffix {file_suffix:?}"
261            )));
262        }
263        if !uuids.insert(uuid) {
264            return Err(Error::format(format!(
265                "duplicate subcache UUID {}",
266                format_uuid(uuid)
267            )));
268        }
269        result.push(SubCacheEntry {
270            uuid,
271            cache_vm_offset,
272            file_suffix,
273        });
274    }
275    Ok(result)
276}
277
278fn parse_mappings(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheMapping>> {
279    validate_table_extent(data, offset, count, MAPPING_INFO_SIZE, "mapping")?;
280    let mut mappings = Vec::with_capacity(count);
281    for i in 0..count {
282        let off = offset
283            .checked_add(
284                i.checked_mul(MAPPING_INFO_SIZE)
285                    .ok_or_else(|| Error::format(format!("mapping[{i}] stride overflows")))?,
286            )
287            .ok_or_else(|| Error::format(format!("mapping[{i}] offset overflows")))?;
288        if off + MAPPING_INFO_SIZE > data.len() {
289            return Err(Error::bounds(
290                off as u64,
291                MAPPING_INFO_SIZE as u64,
292                data.len() as u64,
293            ));
294        }
295        let mapping = CacheMapping {
296            address: read_u64_le(data, off)?,
297            size: read_u64_le(data, off + 8)?,
298            file_offset: read_u64_le(data, off + 16)?,
299            max_prot: read_u32_le(data, off + 24)?,
300            init_prot: read_u32_le(data, off + 28)?,
301        };
302        if mapping.size == 0 {
303            return Err(Error::format(format!("mapping[{i}] has zero size")));
304        }
305        let va_end = mapping.address.checked_add(mapping.size).ok_or_else(|| {
306            Error::address(format!("mapping[{i}] virtual-address extent overflows"))
307        })?;
308        let file_end = mapping
309            .file_offset
310            .checked_add(mapping.size)
311            .ok_or_else(|| Error::address(format!("mapping[{i}] file extent overflows")))?;
312        if file_end > data.len() as u64 {
313            return Err(Error::bounds(
314                mapping.file_offset,
315                mapping.size,
316                data.len() as u64,
317            ));
318        }
319        if mappings.iter().any(|prior: &CacheMapping| {
320            let prior_end = prior.address + prior.size;
321            mapping.address < prior_end && prior.address < va_end
322        }) {
323            return Err(Error::format(format!(
324                "mapping[{i}] overlaps an earlier virtual-address mapping"
325            )));
326        }
327        mappings.push(mapping);
328    }
329    Ok(mappings)
330}
331
332/// Parse the old-style dyld_cache_image_info entries.
333/// Layout: address(u64) + modTime(u64) + inode(u64) + pathFileOffset(u32) + pad(u32)
334fn parse_images_old(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheImage>> {
335    validate_table_extent(data, offset, count, IMAGE_INFO_SIZE, "old image")?;
336    let mut images = Vec::with_capacity(count);
337    for i in 0..count {
338        let off = offset
339            .checked_add(
340                i.checked_mul(IMAGE_INFO_SIZE)
341                    .ok_or_else(|| Error::format(format!("old image[{i}] stride overflows")))?,
342            )
343            .ok_or_else(|| Error::format(format!("old image[{i}] offset overflows")))?;
344        if off + IMAGE_INFO_SIZE > data.len() {
345            return Err(Error::bounds(
346                off as u64,
347                IMAGE_INFO_SIZE as u64,
348                data.len() as u64,
349            ));
350        }
351        let address = read_u64_le(data, off)?;
352        let path_offset = read_u32_le(data, off + 24)?;
353        let path = read_c_string(data, path_offset as usize, "old image path")?;
354
355        images.push(CacheImage {
356            address,
357            path,
358            text_size: 0,
359        });
360    }
361    Ok(images)
362}
363
364/// Parse the modern dyld_cache_image_text_info entries.
365/// Layout: uuid(16) + loadAddress(u64) + textSegmentSize(u32) + pathOffset(u32)
366fn parse_images_text(data: &[u8], offset: usize, count: usize) -> Result<Vec<CacheImage>> {
367    validate_table_extent(data, offset, count, IMAGE_TEXT_INFO_SIZE, "imagesText")?;
368    let mut images = Vec::with_capacity(count);
369    for i in 0..count {
370        let off = offset
371            .checked_add(
372                i.checked_mul(IMAGE_TEXT_INFO_SIZE)
373                    .ok_or_else(|| Error::format(format!("imagesText[{i}] stride overflows")))?,
374            )
375            .ok_or_else(|| Error::format(format!("imagesText[{i}] offset overflows")))?;
376        if off + IMAGE_TEXT_INFO_SIZE > data.len() {
377            return Err(Error::bounds(
378                off as u64,
379                IMAGE_TEXT_INFO_SIZE as u64,
380                data.len() as u64,
381            ));
382        }
383        // Skip uuid (16 bytes)
384        let address = read_u64_le(data, off + 16)?;
385        let text_size = read_u32_le(data, off + 24)?;
386        let path_offset = read_u32_le(data, off + 28)?;
387        let path = read_c_string(data, path_offset as usize, "imagesText path")?;
388
389        images.push(CacheImage {
390            address,
391            path,
392            text_size,
393        });
394    }
395    Ok(images)
396}
397
398fn validate_table_extent(
399    data: &[u8],
400    offset: usize,
401    count: usize,
402    stride: usize,
403    label: &str,
404) -> Result<()> {
405    let size = count
406        .checked_mul(stride)
407        .ok_or_else(|| Error::format(format!("{label} table size overflows")))?;
408    let end = offset
409        .checked_add(size)
410        .ok_or_else(|| Error::format(format!("{label} table extent overflows")))?;
411    if end > data.len() {
412        return Err(Error::bounds(
413            offset as u64,
414            u64::try_from(size).unwrap_or(u64::MAX),
415            data.len() as u64,
416        ));
417    }
418    Ok(())
419}