Skip to main content

macho_dyld_cache/
family.rs

1use super::*;
2use crate::completeness::completeness_for;
3use crate::materialize::materialize_cache_image;
4
5impl DyldCache {
6    /// List all embedded images.
7    pub fn images(&self) -> &[CacheImage] {
8        &self.images
9    }
10
11    /// List cache memory mappings.
12    pub fn mappings(&self) -> &[CacheMapping] {
13        &self.mappings
14    }
15
16    /// Architecture string extracted from the magic field.
17    pub fn arch(&self) -> &str {
18        &self.header.arch
19    }
20
21    /// Required sibling-cache declarations in deterministic header order.
22    pub fn subcaches(&self) -> &[SubCacheEntry] {
23        &self.subcaches
24    }
25
26    /// Convert a virtual address to a file offset using the mapping table.
27    pub fn va_to_file_offset(&self, va: u64) -> Option<u64> {
28        for m in &self.mappings {
29            let mapping_end = m.address.checked_add(m.size)?;
30            if va >= m.address && va < mapping_end {
31                return m.file_offset.checked_add(va - m.address);
32            }
33        }
34        None
35    }
36}
37
38/// Borrowed bytes and a stable member name supplied to family parsing.
39#[derive(Debug, Clone, Copy)]
40pub struct CacheMemberInput<'data> {
41    /// Filename or exact declared suffix used to identify this member.
42    pub name: &'data str,
43    /// Complete bytes of this cache family member.
44    pub data: &'data [u8],
45}
46
47/// One validated cache family member.
48#[derive(Debug)]
49pub struct CacheFamilyMember<'data> {
50    name: String,
51    cache: DyldCache,
52    data: &'data [u8],
53}
54
55impl CacheFamilyMember<'_> {
56    /// Stable member name supplied by the caller.
57    pub fn name(&self) -> &str {
58        &self.name
59    }
60
61    /// Parsed member metadata.
62    pub fn cache(&self) -> &DyldCache {
63        &self.cache
64    }
65}
66
67/// Fully validated offline dyld cache family.
68#[derive(Debug)]
69pub struct DyldCacheFamily<'data> {
70    members: Vec<CacheFamilyMember<'data>>,
71}
72
73/// Typed state for one reconstruction evidence domain.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
75#[serde(rename_all = "snake_case")]
76pub enum CompletenessState {
77    /// All referenced bytes were reconstructed and remain addressable.
78    Complete,
79    /// The source image did not declare this domain.
80    Absent,
81    /// Source evidence exists outside the reconstructed image or was unavailable.
82    Unresolved,
83    /// Source evidence was deliberately excluded because it would be misleading.
84    Rejected,
85    /// A valid source layout for this domain is not implemented.
86    Unsupported,
87}
88
89/// State and human-readable reason for one evidence domain.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
91pub struct ComponentCompleteness {
92    /// Typed state.
93    pub state: CompletenessState,
94    /// Precise explanation of the state.
95    pub detail: String,
96}
97
98/// Evidence ledger for one reconstructed standalone image.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
100pub struct ReconstructionCompleteness {
101    /// File-backed Mach-O segments.
102    pub segments: ComponentCompleteness,
103    /// The `__LINKEDIT` segment and its file-coordinate references.
104    pub linkedit: ComponentCompleteness,
105    /// Mach-O symbol table.
106    pub symbols: ComponentCompleteness,
107    /// Export trie or legacy export stream.
108    pub exports: ComponentCompleteness,
109    /// Bind/import streams or chained-import table.
110    pub imports: ComponentCompleteness,
111    /// Rebase/bind or chained-fixup evidence.
112    pub fixups: ComponentCompleteness,
113    /// Cache-level local symbols, which are not ordinary image segments.
114    pub local_symbols: ComponentCompleteness,
115    /// Image-level code-signature evidence.
116    pub code_signature: ComponentCompleteness,
117}
118
119/// Provenance for one reconstructed segment range.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
121pub struct ReconstructionMapping {
122    /// Byte start in the standalone output.
123    pub file_start: u64,
124    /// Exclusive byte end in the standalone output.
125    pub file_end: u64,
126    /// RVA start in the mapped image.
127    pub rva_start: u64,
128    /// Exclusive RVA end in the mapped image.
129    pub rva_end: u64,
130    /// Cache family members that supplied this range, in read order.
131    pub source_members: Vec<String>,
132}
133
134/// Owned, parseable standalone Mach-O plus reconstruction evidence.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
136pub struct ReconstructedImage {
137    /// Exact image path selected from the cache index.
138    pub image_path: String,
139    /// Architecture declared by the cache family.
140    pub arch: String,
141    /// Owned standalone Mach-O bytes.
142    #[serde(skip)]
143    bytes: Vec<u8>,
144    /// Reconstructed byte length (also serialized when `bytes` are skipped).
145    pub byte_len: usize,
146    /// File/RVA/cache-member provenance.
147    pub mappings: Vec<ReconstructionMapping>,
148    /// File gaps filled with zero only to preserve Mach-O file coordinates.
149    pub synthetic_padding: Vec<SerializableRange>,
150    /// Structured completeness ledger.
151    pub completeness: ReconstructionCompleteness,
152}
153
154impl ReconstructedImage {
155    /// Parseable standalone thin Mach-O bytes.
156    pub fn bytes(&self) -> &[u8] {
157        &self.bytes
158    }
159
160    /// Consume the result and return its standalone bytes.
161    pub fn into_bytes(self) -> Vec<u8> {
162        self.bytes
163    }
164}
165
166/// Serializable half-open byte range.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168pub struct SerializableRange {
169    /// Inclusive range start.
170    pub start: u64,
171    /// Exclusive range end.
172    pub end: u64,
173}
174
175impl<'data> DyldCacheFamily<'data> {
176    /// Parse and validate a primary cache plus exactly its declared siblings.
177    pub fn parse(
178        primary: CacheMemberInput<'data>,
179        siblings: impl IntoIterator<Item = CacheMemberInput<'data>>,
180    ) -> Result<Self> {
181        let primary_cache = parse_dyld_cache(primary.data)?;
182        let mut image_paths = BTreeMap::new();
183        for (index, image) in primary_cache.images.iter().enumerate() {
184            if let Some(prior) = image_paths.insert(image.path.as_str(), index) {
185                return Err(Error::format(format!(
186                    "duplicate image path {:?} at indexes {prior} and {index}",
187                    image.path
188                )));
189            }
190        }
191        let mut supplied = BTreeMap::new();
192        for sibling in siblings {
193            if supplied.insert(sibling.name, sibling.data).is_some() {
194                return Err(Error::format(format!(
195                    "duplicate cache family member {:?}",
196                    sibling.name
197                )));
198            }
199        }
200        let expected_names = primary_cache
201            .subcaches
202            .iter()
203            .map(|entry| entry.file_suffix.as_str())
204            .collect::<BTreeSet<_>>();
205        if let Some(unexpected) = supplied
206            .keys()
207            .find(|name| !expected_names.contains(**name))
208        {
209            return Err(Error::format(format!(
210                "unexpected cache family member {unexpected:?}"
211            )));
212        }
213
214        let primary_base = primary_cache
215            .mappings
216            .iter()
217            .map(|mapping| mapping.address)
218            .min()
219            .ok_or_else(|| Error::unsupported("dyld cache has no mapped regions"))?;
220        let arch = primary_cache.arch().to_owned();
221        let mut members = vec![CacheFamilyMember {
222            name: primary.name.to_owned(),
223            cache: primary_cache,
224            data: primary.data,
225        }];
226        let declarations = members[0].cache.subcaches.clone();
227        for declaration in declarations {
228            let data = supplied
229                .get(declaration.file_suffix.as_str())
230                .ok_or_else(|| {
231                    Error::format(format!(
232                        "missing required cache family member {:?}",
233                        declaration.file_suffix
234                    ))
235                })?;
236            let cache = parse_dyld_cache(data)?;
237            if cache.header.uuid != declaration.uuid {
238                return Err(Error::format(format!(
239                    "cache family member {:?} UUID mismatch: expected {}, found {}",
240                    declaration.file_suffix,
241                    format_uuid(declaration.uuid),
242                    format_uuid(cache.header.uuid)
243                )));
244            }
245            if cache.arch() != arch {
246                return Err(Error::unsupported(format!(
247                    "cache family member {:?} architecture {:?} does not match {:?}",
248                    declaration.file_suffix,
249                    cache.arch(),
250                    arch
251                )));
252            }
253            let actual_base = cache
254                .mappings
255                .iter()
256                .map(|mapping| mapping.address)
257                .min()
258                .ok_or_else(|| {
259                    Error::unsupported(format!(
260                        "cache family member {:?} has no mapped regions",
261                        declaration.file_suffix
262                    ))
263                })?;
264            let expected_base = primary_base
265                .checked_add(declaration.cache_vm_offset)
266                .ok_or_else(|| Error::address("subcache VM base overflows"))?;
267            if actual_base != expected_base {
268                return Err(Error::format(format!(
269                    "cache family member {:?} VM base mismatch: expected {expected_base:#x}, found {actual_base:#x}",
270                    declaration.file_suffix
271                )));
272            }
273            members.push(CacheFamilyMember {
274                name: declaration.file_suffix,
275                cache,
276                data,
277            });
278        }
279        validate_family_mappings(&members)?;
280        Ok(Self { members })
281    }
282
283    /// Primary-cache metadata and image index.
284    pub fn primary(&self) -> &DyldCache {
285        &self.members[0].cache
286    }
287
288    /// Validated family members, primary first and siblings in declaration order.
289    pub fn members(&self) -> &[CacheFamilyMember<'data>] {
290        &self.members
291    }
292
293    /// Find image indexes whose path contains `query`, in cache index order.
294    pub fn search_images(&self, query: &str) -> Vec<usize> {
295        self.primary()
296            .images
297            .iter()
298            .enumerate()
299            .filter_map(|(index, image)| image.path.contains(query).then_some(index))
300            .collect()
301    }
302
303    /// Find one image by exact install path.
304    pub fn image_index_by_path(&self, path: &str) -> Option<usize> {
305        self.primary()
306            .images
307            .iter()
308            .position(|image| image.path == path)
309    }
310
311    /// Reconstruct one indexed image as an owned standalone Mach-O.
312    pub fn reconstruct_image(
313        &self,
314        index: usize,
315        limits: MaterializationLimits,
316    ) -> Result<ReconstructedImage> {
317        let image = self
318            .primary()
319            .images
320            .get(index)
321            .ok_or_else(|| Error::format(format!("image index {index} out of range")))?;
322        if image.path.is_empty() {
323            return Err(Error::format(format!("image index {index} has no path")));
324        }
325        let materialized = materialize_cache_image(self, image.address, limits)?;
326        // Strict parsing is the delivery boundary: never return an artifact
327        // whose rewritten load-command references fail core validation.
328        let container = macho_core::format::parse(&materialized.bytes).map_err(Error::from)?;
329        let macho = container
330            .first_macho()
331            .ok_or_else(|| Error::format("reconstructed image has no thin Mach-O"))?;
332        let completeness = completeness_for(self, macho);
333        let mappings = materialized
334            .mappings
335            .iter()
336            .map(|mapping| {
337                let va_start = image
338                    .address
339                    .checked_add(mapping.rva.start)
340                    .ok_or_else(|| Error::address("reconstruction provenance VA overflows"))?;
341                let va_end = image
342                    .address
343                    .checked_add(mapping.rva.end)
344                    .ok_or_else(|| Error::address("reconstruction provenance VA overflows"))?;
345                Ok(ReconstructionMapping {
346                    file_start: mapping.file.start,
347                    file_end: mapping.file.end,
348                    rva_start: mapping.rva.start,
349                    rva_end: mapping.rva.end,
350                    source_members: self.source_members_for(va_start..va_end)?,
351                })
352            })
353            .collect::<Result<Vec<_>>>()?;
354        let synthetic_padding = materialized
355            .synthetic_padding
356            .iter()
357            .map(|range| SerializableRange {
358                start: range.start,
359                end: range.end,
360            })
361            .collect();
362        let bytes = materialized.bytes;
363        Ok(ReconstructedImage {
364            image_path: image.path.clone(),
365            arch: self.primary().arch().to_owned(),
366            byte_len: bytes.len(),
367            bytes,
368            mappings,
369            synthetic_padding,
370            completeness,
371        })
372    }
373
374    pub(super) fn read_va_exact(&self, range: Range<u64>) -> Result<Vec<u8>> {
375        if range.start > range.end {
376            return Err(Error::address("cache read range is reversed"));
377        }
378        let length = range.end - range.start;
379        let capacity = usize::try_from(length)
380            .map_err(|_| Error::unsupported("cache read exceeds host address space"))?;
381        let mut result = Vec::with_capacity(capacity);
382        let mut cursor = range.start;
383        while cursor < range.end {
384            let (member, mapping) = self.mapping_for_va(cursor).ok_or_else(|| {
385                Error::address(format!("VA {cursor:#x} is not mapped by the cache family"))
386            })?;
387            let mapping_end = mapping
388                .address
389                .checked_add(mapping.size)
390                .ok_or_else(|| Error::address("validated cache mapping extent overflows"))?;
391            let chunk_end = mapping_end.min(range.end);
392            let source_start = mapping
393                .file_offset
394                .checked_add(cursor - mapping.address)
395                .ok_or_else(|| Error::address("cache source offset overflows"))?;
396            let source_end = source_start
397                .checked_add(chunk_end - cursor)
398                .ok_or_else(|| Error::address("cache source extent overflows"))?;
399            let start = usize::try_from(source_start)
400                .map_err(|_| Error::unsupported("cache source offset exceeds host limits"))?;
401            let end = usize::try_from(source_end)
402                .map_err(|_| Error::unsupported("cache source extent exceeds host limits"))?;
403            let bytes = member.data.get(start..end).ok_or_else(|| {
404                Error::bounds(
405                    source_start,
406                    source_end - source_start,
407                    member.data.len() as u64,
408                )
409            })?;
410            result.extend_from_slice(bytes);
411            cursor = chunk_end;
412        }
413        Ok(result)
414    }
415
416    pub(super) fn read_c_string_va(
417        &self,
418        start: u64,
419        available: u64,
420        subject: &str,
421    ) -> Result<Vec<u8>> {
422        const MAX_SYMBOL_NAME: u64 = 1024 * 1024;
423        let limit = available.min(MAX_SYMBOL_NAME);
424        let mut result = Vec::new();
425        let mut cursor = 0_u64;
426        while cursor < limit {
427            let chunk_len = (limit - cursor).min(256);
428            let chunk_start = start
429                .checked_add(cursor)
430                .ok_or_else(|| Error::address(format!("{subject} address overflows")))?;
431            let chunk_end = chunk_start
432                .checked_add(chunk_len)
433                .ok_or_else(|| Error::address(format!("{subject} extent overflows")))?;
434            let chunk = self.read_va_exact(chunk_start..chunk_end)?;
435            if let Some(end) = chunk.iter().position(|byte| *byte == 0) {
436                result.extend_from_slice(&chunk[..end]);
437                return Ok(result);
438            }
439            result.extend_from_slice(&chunk);
440            cursor += chunk_len;
441        }
442        Err(Error::unsupported(format!(
443            "{subject} is not NUL-terminated within {limit} bytes"
444        )))
445    }
446
447    fn mapping_for_va(&self, va: u64) -> Option<(&CacheFamilyMember<'data>, &CacheMapping)> {
448        self.members.iter().find_map(|member| {
449            member.cache.mappings.iter().find_map(|mapping| {
450                let end = mapping.address.checked_add(mapping.size)?;
451                (va >= mapping.address && va < end).then_some((member, mapping))
452            })
453        })
454    }
455
456    fn source_members_for(&self, range: Range<u64>) -> Result<Vec<String>> {
457        let mut result = Vec::new();
458        let mut cursor = range.start;
459        while cursor < range.end {
460            let (member, mapping) = self.mapping_for_va(cursor).ok_or_else(|| {
461                Error::address(format!("VA {cursor:#x} is not mapped by the cache family"))
462            })?;
463            if result.last().is_none_or(|name| name != member.name()) {
464                result.push(member.name.clone());
465            }
466            cursor = mapping
467                .address
468                .checked_add(mapping.size)
469                .ok_or_else(|| Error::address("validated cache mapping extent overflows"))?
470                .min(range.end);
471        }
472        Ok(result)
473    }
474}
475
476fn validate_family_mappings(members: &[CacheFamilyMember<'_>]) -> Result<()> {
477    let mut mappings = Vec::new();
478    for member in members {
479        for mapping in &member.cache.mappings {
480            let end = mapping
481                .address
482                .checked_add(mapping.size)
483                .ok_or_else(|| Error::address("validated cache mapping extent overflows"))?;
484            if mappings
485                .iter()
486                .any(|(start, prior_end): &(u64, u64)| mapping.address < *prior_end && *start < end)
487            {
488                return Err(Error::format(format!(
489                    "cache family member {:?} has a mapping that overlaps another member",
490                    member.name
491                )));
492            }
493            mappings.push((mapping.address, end));
494        }
495    }
496    Ok(())
497}