1#![deny(missing_docs)]
2use serde::Serialize;
5
6pub use macho_core::format;
7
8pub 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
19const IMAGES_TEXT_OFFSET_OFF: usize = 0x88;
21const IMAGES_TEXT_COUNT_OFF: usize = 0x90;
22const MODERN_HEADER_MIN: usize = IMAGES_TEXT_COUNT_OFF + 8;
24
25#[derive(Debug, Clone, Serialize)]
31pub struct DyldCache {
32 pub header: DyldCacheHeader,
34 pub mappings: Vec<CacheMapping>,
36 pub images: Vec<CacheImage>,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct DyldCacheHeader {
43 pub magic: String,
45 pub arch: String,
47 pub mapping_offset: u32,
49 pub mapping_count: u32,
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct CacheMapping {
56 pub address: u64,
58 pub size: u64,
60 pub file_offset: u64,
62 pub max_prot: u32,
64 pub init_prot: u32,
66}
67
68#[derive(Debug, Clone, Serialize)]
69pub struct CacheImage {
71 pub address: u64,
73 pub path: String,
75 pub text_size: u32,
77}
78
79pub 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 if &data[..7] != DYLD_CACHE_MAGIC_PREFIX {
87 return Err(Error::format("not a dyld shared cache (bad magic)"));
88 }
89
90 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 let mappings = parse_mappings(data, mapping_offset as usize, mapping_count as usize)?;
117
118 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
178fn 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
210fn 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 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 pub fn images(&self) -> &[CacheImage] {
270 &self.images
271 }
272
273 pub fn mappings(&self) -> &[CacheMapping] {
275 &self.mappings
276 }
277
278 pub fn arch(&self) -> &str {
280 &self.header.arch
281 }
282
283 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 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 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 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 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; 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 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 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 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 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 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 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; let mapping_count: u32 = 1;
453
454 let image_va: u64 = 0x1_8000_0000;
455 let image_file_offset: u64 = 4096;
456
457 let images_text_offset: u64 = (mapping_offset as u64) + MAPPING_INFO_SIZE as u64;
459 let images_text_count: u64 = 1;
460
461 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 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 data[24..28].copy_from_slice(&0u32.to_le_bytes());
474 data[28..32].copy_from_slice(&0u32.to_le_bytes());
475
476 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 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 let i_off = images_text_offset as usize;
490 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()); data[i_off + 28..i_off + 32].copy_from_slice(&path_offset.to_le_bytes());
494
495 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 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}