1use super::*;
2use crate::completeness::completeness_for;
3use crate::materialize::materialize_cache_image;
4
5impl DyldCache {
6 pub fn images(&self) -> &[CacheImage] {
8 &self.images
9 }
10
11 pub fn mappings(&self) -> &[CacheMapping] {
13 &self.mappings
14 }
15
16 pub fn arch(&self) -> &str {
18 &self.header.arch
19 }
20
21 pub fn subcaches(&self) -> &[SubCacheEntry] {
23 &self.subcaches
24 }
25
26 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#[derive(Debug, Clone, Copy)]
40pub struct CacheMemberInput<'data> {
41 pub name: &'data str,
43 pub data: &'data [u8],
45}
46
47#[derive(Debug)]
49pub struct CacheFamilyMember<'data> {
50 name: String,
51 cache: DyldCache,
52 data: &'data [u8],
53}
54
55impl CacheFamilyMember<'_> {
56 pub fn name(&self) -> &str {
58 &self.name
59 }
60
61 pub fn cache(&self) -> &DyldCache {
63 &self.cache
64 }
65}
66
67#[derive(Debug)]
69pub struct DyldCacheFamily<'data> {
70 members: Vec<CacheFamilyMember<'data>>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
75#[serde(rename_all = "snake_case")]
76pub enum CompletenessState {
77 Complete,
79 Absent,
81 Unresolved,
83 Rejected,
85 Unsupported,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
91pub struct ComponentCompleteness {
92 pub state: CompletenessState,
94 pub detail: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
100pub struct ReconstructionCompleteness {
101 pub segments: ComponentCompleteness,
103 pub linkedit: ComponentCompleteness,
105 pub symbols: ComponentCompleteness,
107 pub exports: ComponentCompleteness,
109 pub imports: ComponentCompleteness,
111 pub fixups: ComponentCompleteness,
113 pub local_symbols: ComponentCompleteness,
115 pub code_signature: ComponentCompleteness,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
121pub struct ReconstructionMapping {
122 pub file_start: u64,
124 pub file_end: u64,
126 pub rva_start: u64,
128 pub rva_end: u64,
130 pub source_members: Vec<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
136pub struct ReconstructedImage {
137 pub image_path: String,
139 pub arch: String,
141 #[serde(skip)]
143 bytes: Vec<u8>,
144 pub byte_len: usize,
146 pub mappings: Vec<ReconstructionMapping>,
148 pub synthetic_padding: Vec<SerializableRange>,
150 pub completeness: ReconstructionCompleteness,
152}
153
154impl ReconstructedImage {
155 pub fn bytes(&self) -> &[u8] {
157 &self.bytes
158 }
159
160 pub fn into_bytes(self) -> Vec<u8> {
162 self.bytes
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168pub struct SerializableRange {
169 pub start: u64,
171 pub end: u64,
173}
174
175impl<'data> DyldCacheFamily<'data> {
176 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 pub fn primary(&self) -> &DyldCache {
285 &self.members[0].cache
286 }
287
288 pub fn members(&self) -> &[CacheFamilyMember<'data>] {
290 &self.members
291 }
292
293 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 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 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 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}