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
9const IMAGES_TEXT_OFFSET_OFF: usize = 0x88;
11const IMAGES_TEXT_COUNT_OFF: usize = 0x90;
12const 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#[derive(Debug, Clone, Serialize)]
32pub struct DyldCache {
33 pub header: DyldCacheHeader,
35 pub mappings: Vec<CacheMapping>,
37 pub images: Vec<CacheImage>,
39 pub subcaches: Vec<SubCacheEntry>,
41}
42
43#[derive(Debug, Clone, Serialize)]
44pub struct DyldCacheHeader {
46 pub magic: String,
48 pub arch: String,
50 pub mapping_offset: u32,
52 pub mapping_count: u32,
54 pub uuid: [u8; 16],
56 pub local_symbols_offset: u64,
58 pub local_symbols_size: u64,
60 pub symbol_file_uuid: [u8; 16],
62}
63
64#[derive(Debug, Clone, Serialize)]
65pub struct CacheMapping {
67 pub address: u64,
69 pub size: u64,
71 pub file_offset: u64,
73 pub max_prot: u32,
75 pub init_prot: u32,
77}
78
79#[derive(Debug, Clone, Serialize)]
80pub struct CacheImage {
82 pub address: u64,
84 pub path: String,
86 pub text_size: u32,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub struct SubCacheEntry {
93 pub uuid: [u8; 16],
95 pub cache_vm_offset: u64,
97 pub file_suffix: String,
99}
100
101pub 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 if &data[..7] != DYLD_CACHE_MAGIC_PREFIX {
109 return Err(Error::format("not a dyld shared cache (bad magic)"));
110 }
111
112 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 let mappings = parse_mappings(data, mapping_offset as usize, mapping_count as usize)?;
163
164 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
332fn 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
364fn 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 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}