1use macho_core::format::io::pod::{
2 self, RawClassRoT64, RawMethodListHeader, RawMethodT, RawRelativeMethodT,
3};
4use macho_core::model::addr::{ThinFileOffset, Va};
5use macho_core::model::macho_file::MachoFile;
6use macho_core::model::section::Section;
7
8use crate::error::{Error, Result};
9use crate::resolve::{ObjCPointerProvenance, ObjCResolver};
10use crate::types::{
11 METHOD_LIST_ENTSIZE_MASK, METHOD_LIST_USES_DIRECT_SELECTOR_OFFSETS,
12 METHOD_LIST_USES_RELATIVE_OFFSETS,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ObjCMethodKind {
18 Instance,
20 Class,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ObjCMethodRecordProvenance {
27 pub record_file_offset: ThinFileOffset,
29 pub record_va: Va,
31 pub record_size: u64,
33 pub encoding: ObjCMethodRecordEncoding,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ObjCMethodRecordEncoding {
40 Absolute {
42 selector_pointer: ObjCPointerProvenance,
44 type_encoding_pointer: ObjCPointerProvenance,
46 implementation_pointer: ObjCPointerProvenance,
48 },
49 Relative {
51 selector_field_va: Va,
53 type_encoding_field_va: Va,
55 implementation_field_va: Va,
57 selector: ObjCRelativeSelectorEncoding,
59 },
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ObjCRelativeSelectorEncoding {
65 DirectString {
67 selector_string_file_offset: ThinFileOffset,
69 },
70 IndirectReference {
72 selector_reference_file_offset: ThinFileOffset,
74 selector_reference_pointer: ObjCPointerProvenance,
76 },
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ObjCMethodRecord {
82 pub owner_va: Va,
84 pub class_name: String,
86 pub category_name: Option<String>,
88 pub method_name: String,
90 pub type_encoding: String,
92 pub kind: ObjCMethodKind,
94 pub imp: Va,
96 pub provenance: ObjCMethodRecordProvenance,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ObjCMethodImp {
103 pub class_name: String,
105 pub category_name: Option<String>,
107 pub method_name: String,
109 pub kind: ObjCMethodKind,
111 pub imp: Va,
113}
114
115pub fn fold_method_imps<State>(
123 macho: &MachoFile<'_>,
124 state: State,
125 mut folder: impl FnMut(&mut State, ObjCMethodImp) -> Result<()>,
126) -> Result<State> {
127 fold_method_records(macho, state, |state, record| {
128 folder(
129 state,
130 ObjCMethodImp {
131 class_name: record.class_name,
132 category_name: record.category_name,
133 method_name: record.method_name,
134 kind: record.kind,
135 imp: record.imp,
136 },
137 )
138 })
139}
140
141pub fn fold_method_records<State>(
143 macho: &MachoFile<'_>,
144 mut state: State,
145 mut folder: impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
146) -> Result<State> {
147 if !macho.is_64bit() {
148 return Err(Error::unsupported(
149 "ObjC method implementation parsing is only supported for 64-bit binaries",
150 ));
151 }
152 let resolver = ObjCResolver::new(macho)?;
153 let (class_list, category_list) = runtime_lists(macho);
154 if let Some(section) = class_list {
155 fold_pointer_list(macho, &resolver, section, |class_va| {
156 fold_class(&resolver, class_va, &mut state, &mut folder)
157 })?;
158 }
159 if let Some(section) = category_list {
160 fold_pointer_list(macho, &resolver, section, |category_va| {
161 fold_category(&resolver, category_va, &mut state, &mut folder)
162 })?;
163 }
164 Ok(state)
165}
166
167pub fn fold_method_imps_from_source<S, State>(
173 source: &S,
174 state: State,
175 folder: impl FnMut(&mut State, ObjCMethodImp) -> Result<()>,
176) -> Result<State>
177where
178 S: AsRef<[u8]> + ?Sized,
179{
180 let macho = crate::parse_source(source)?;
181 fold_method_imps(&macho, state, folder)
182}
183
184pub fn fold_method_records_from_source<S, State>(
186 source: &S,
187 state: State,
188 folder: impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
189) -> Result<State>
190where
191 S: AsRef<[u8]> + ?Sized,
192{
193 let macho = crate::parse_source(source)?;
194 fold_method_records(&macho, state, folder)
195}
196
197fn runtime_lists<'macho>(
198 macho: &'macho MachoFile<'_>,
199) -> (Option<&'macho Section>, Option<&'macho Section>) {
200 let mut classes = None;
201 let mut categories = None;
202 for section in macho.all_sections() {
203 if section.section_name() == "__objc_classlist" && classes.is_none() {
204 classes = Some(section);
205 } else if section.section_name() == "__objc_catlist" && categories.is_none() {
206 categories = Some(section);
207 }
208 }
209 (classes, categories)
210}
211
212fn fold_pointer_list(
213 macho: &MachoFile<'_>,
214 resolver: &ObjCResolver<'_>,
215 section: &Section,
216 mut visitor: impl FnMut(Va) -> Result<()>,
217) -> Result<()> {
218 let offset = section.offset().0;
219 let size = section.size();
220 if size % 8 != 0 {
221 return Err(Error::format(format!(
222 "section {} size {size:#x} is not pointer-aligned",
223 section.section_name()
224 )));
225 }
226 let end = offset
227 .checked_add(size)
228 .ok_or_else(|| Error::address("Objective-C pointer-list range overflows"))?;
229 if end > macho.file_size() as u64 {
230 return Err(Error::bounds(offset, size, macho.file_size() as u64));
231 }
232 for ordinal in 0..size / 8 {
233 let pointer_offset = offset
234 .checked_add(ordinal * 8)
235 .ok_or_else(|| Error::address("Objective-C pointer offset overflows"))?;
236 let runtime_va = resolver
237 .read_pointer_at_offset(pointer_offset)?
238 .filter(|va| va.0 != 0)
239 .ok_or_else(|| {
240 Error::format(format!(
241 "{}[{ordinal}] has a null or unresolved runtime pointer",
242 section.section_name()
243 ))
244 })?;
245 visitor(runtime_va)?;
246 }
247 Ok(())
248}
249
250fn fold_class<State>(
251 resolver: &ObjCResolver<'_>,
252 class_va: Va,
253 state: &mut State,
254 folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
255) -> Result<()> {
256 let class_offset = resolver.va_to_offset(class_va)?.0;
257 let data_va = required_pointer(
258 resolver,
259 checked_field_offset(class_offset, 32, "class data")?,
260 "class data",
261 )?;
262 let ro_offset = resolver
263 .va_to_offset(Va(data_va.0 & crate::types::CLASS_DATA_POINTER_MASK))?
264 .0;
265 let _: RawClassRoT64 = pod::read_pod(resolver.macho().bytes(), ro_offset as usize)?;
266 let class_name = required_cstring_pointer(
267 resolver,
268 checked_field_offset(ro_offset, 24, "class name")?,
269 "class name",
270 )?;
271
272 fold_optional_method_list(
273 resolver,
274 checked_field_offset(ro_offset, 32, "class methods")?,
275 class_va,
276 &class_name,
277 None,
278 ObjCMethodKind::Instance,
279 state,
280 folder,
281 )?;
282 if let Some(meta_va) = resolver.read_pointer_at_offset(class_offset)? {
283 let meta_offset = resolver.va_to_offset(meta_va)?.0;
284 let meta_data = required_pointer(
285 resolver,
286 checked_field_offset(meta_offset, 32, "metaclass data")?,
287 "metaclass data",
288 )?;
289 let meta_ro_offset = resolver
290 .va_to_offset(Va(meta_data.0 & crate::types::CLASS_DATA_POINTER_MASK))?
291 .0;
292 let _: RawClassRoT64 = pod::read_pod(resolver.macho().bytes(), meta_ro_offset as usize)?;
293 fold_optional_method_list(
294 resolver,
295 checked_field_offset(meta_ro_offset, 32, "metaclass methods")?,
296 meta_va,
297 &class_name,
298 None,
299 ObjCMethodKind::Class,
300 state,
301 folder,
302 )?;
303 }
304 Ok(())
305}
306
307fn fold_category<State>(
308 resolver: &ObjCResolver<'_>,
309 category_va: Va,
310 state: &mut State,
311 folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
312) -> Result<()> {
313 let offset = resolver.va_to_offset(category_va)?.0;
314 let category_name = required_cstring_pointer(resolver, offset, "category name")?;
315 let class_name =
316 strict_class_ref_name(resolver, checked_field_offset(offset, 8, "category class")?)?;
317 fold_optional_method_list(
318 resolver,
319 checked_field_offset(offset, 16, "category instance methods")?,
320 category_va,
321 &class_name,
322 Some(&category_name),
323 ObjCMethodKind::Instance,
324 state,
325 folder,
326 )?;
327 fold_optional_method_list(
328 resolver,
329 checked_field_offset(offset, 24, "category class methods")?,
330 category_va,
331 &class_name,
332 Some(&category_name),
333 ObjCMethodKind::Class,
334 state,
335 folder,
336 )
337}
338
339#[allow(clippy::too_many_arguments)]
340fn fold_optional_method_list<State>(
341 resolver: &ObjCResolver<'_>,
342 pointer_offset: u64,
343 owner_va: Va,
344 class_name: &str,
345 category_name: Option<&str>,
346 kind: ObjCMethodKind,
347 state: &mut State,
348 folder: &mut impl FnMut(&mut State, ObjCMethodRecord) -> Result<()>,
349) -> Result<()> {
350 let Some(list_va) = resolver.read_pointer_at_offset(pointer_offset)? else {
351 return Ok(());
352 };
353 if list_va.0 == 0 {
354 return Ok(());
355 }
356 fold_method_list(resolver, list_va, |record| {
357 folder(
358 state,
359 ObjCMethodRecord {
360 owner_va,
361 class_name: class_name.to_owned(),
362 category_name: category_name.map(str::to_owned),
363 method_name: record.method_name,
364 type_encoding: record.type_encoding,
365 kind,
366 imp: record.imp,
367 provenance: record.provenance,
368 },
369 )
370 })
371}
372
373struct ParsedMethodRecord {
374 method_name: String,
375 type_encoding: String,
376 imp: Va,
377 provenance: ObjCMethodRecordProvenance,
378}
379
380fn fold_method_list(
381 resolver: &ObjCResolver<'_>,
382 list_va: Va,
383 mut visitor: impl FnMut(ParsedMethodRecord) -> Result<()>,
384) -> Result<()> {
385 let offset = resolver.va_to_offset(list_va)?.as_usize();
386 let data = resolver.macho().bytes();
387 let endian = resolver.endian();
388 let header: RawMethodListHeader = pod::read_pod(data, offset)?;
389 let flags = endian.interpret_u32(header.entsize_and_flags);
390 let count = endian.interpret_u32(header.count) as usize;
391 let relative = flags & METHOD_LIST_USES_RELATIVE_OFFSETS != 0;
392 let direct_selectors = flags & METHOD_LIST_USES_DIRECT_SELECTOR_OFFSETS != 0;
393 let minimum = if relative {
394 size_of::<RawRelativeMethodT>()
395 } else {
396 size_of::<RawMethodT>()
397 };
398 let encoded_size = (flags & METHOD_LIST_ENTSIZE_MASK) as usize;
399 let entry_size = if encoded_size == 0 {
400 minimum
401 } else {
402 encoded_size
403 };
404 if entry_size < minimum {
405 return Err(Error::format(format!(
406 "method entry size {entry_size} is smaller than {minimum}"
407 )));
408 }
409 let entries_start = offset
410 .checked_add(size_of::<RawMethodListHeader>())
411 .ok_or_else(|| Error::address("method-list header range overflows"))?;
412 let entries_size = count
413 .checked_mul(entry_size)
414 .ok_or_else(|| Error::address("method-list entry range overflows"))?;
415 let entries_end = entries_start
416 .checked_add(entries_size)
417 .ok_or_else(|| Error::address("method-list end overflows"))?;
418 if entries_end > data.len() {
419 return Err(Error::bounds(
420 entries_start as u64,
421 entries_size as u64,
422 data.len() as u64,
423 ));
424 }
425
426 for ordinal in 0..count {
427 let entry_offset = entries_start + ordinal * entry_size;
428 let record = if relative {
429 parse_relative_method(resolver, entry_offset, entry_size, direct_selectors)?
430 } else {
431 parse_absolute_method(resolver, entry_offset, entry_size)?
432 };
433 visitor(record)?;
434 }
435 Ok(())
436}
437
438fn parse_relative_method(
439 resolver: &ObjCResolver<'_>,
440 entry_offset: usize,
441 entry_size: usize,
442 direct_selectors: bool,
443) -> Result<ParsedMethodRecord> {
444 let data = resolver.macho().bytes();
445 let endian = resolver.endian();
446 let raw: RawRelativeMethodT = pod::read_pod(data, entry_offset)?;
447 let name_relative = endian.interpret_i32(raw.name_offset) as isize;
448 let types_relative = endian.interpret_i32(raw.types_offset) as isize;
449 let imp_relative = endian.interpret_i32(raw.imp_offset) as i64;
450 let selector_offset = entry_offset
451 .checked_add_signed(name_relative)
452 .ok_or_else(|| Error::address("relative method selector offset overflows"))?;
453 let (name, selector) = if direct_selectors {
454 (
455 read_cstring_at_file_offset(data, selector_offset)?.to_owned(),
456 ObjCRelativeSelectorEncoding::DirectString {
457 selector_string_file_offset: ThinFileOffset(selector_offset as u64),
458 },
459 )
460 } else {
461 let string_va = resolver
462 .read_pointer_at_offset(selector_offset as u64)?
463 .ok_or_else(|| Error::address("relative method selector reference is unresolved"))?;
464 (
465 resolver.read_cstring(string_va)?.to_owned(),
466 ObjCRelativeSelectorEncoding::IndirectReference {
467 selector_reference_file_offset: ThinFileOffset(selector_offset as u64),
468 selector_reference_pointer: resolver
469 .pointer_provenance_at_offset(selector_offset as u64),
470 },
471 )
472 };
473 let types_field_offset = entry_offset
474 .checked_add(4)
475 .ok_or_else(|| Error::address("relative method type-encoding field overflows"))?;
476 let types_offset = types_field_offset
477 .checked_add_signed(types_relative)
478 .ok_or_else(|| Error::address("relative method type-encoding offset overflows"))?;
479 let type_encoding = read_cstring_at_file_offset(data, types_offset)?.to_owned();
480 let imp_field_offset = entry_offset
481 .checked_add(8)
482 .ok_or_else(|| Error::address("relative method IMP field overflows"))?;
483 let imp_field_va = resolver
484 .macho()
485 .address_map()
486 .thin_offset_to_va(ThinFileOffset(imp_field_offset as u64))?;
487 let imp = imp_field_va
488 .0
489 .checked_add_signed(imp_relative)
490 .filter(|value| *value != 0)
491 .map(Va)
492 .ok_or_else(|| Error::address("relative method IMP address is invalid"))?;
493 let address_map = resolver.macho().address_map();
494 let record_file_offset = ThinFileOffset(entry_offset as u64);
495 let record_va = address_map.thin_offset_to_va(record_file_offset)?;
496 Ok(ParsedMethodRecord {
497 method_name: name,
498 type_encoding,
499 imp,
500 provenance: ObjCMethodRecordProvenance {
501 record_file_offset,
502 record_va,
503 record_size: entry_size as u64,
504 encoding: ObjCMethodRecordEncoding::Relative {
505 selector_field_va: record_va,
506 type_encoding_field_va: address_map
507 .thin_offset_to_va(ThinFileOffset(types_field_offset as u64))?,
508 implementation_field_va: imp_field_va,
509 selector,
510 },
511 },
512 })
513}
514
515fn parse_absolute_method(
516 resolver: &ObjCResolver<'_>,
517 entry_offset: usize,
518 entry_size: usize,
519) -> Result<ParsedMethodRecord> {
520 let name = required_cstring_pointer(resolver, entry_offset as u64, "method name")?;
521 let types_offset = checked_field_offset(entry_offset as u64, 8, "method type encoding")?;
522 let type_encoding = required_cstring_pointer(resolver, types_offset, "method type encoding")?;
523 let imp_offset = checked_field_offset(entry_offset as u64, 16, "method IMP")?;
524 let imp = required_pointer(resolver, imp_offset, "method IMP")?;
525 if imp.0 == 0 {
526 return Err(Error::address("method IMP is null"));
527 }
528 let record_file_offset = ThinFileOffset(entry_offset as u64);
529 Ok(ParsedMethodRecord {
530 method_name: name,
531 type_encoding,
532 imp,
533 provenance: ObjCMethodRecordProvenance {
534 record_file_offset,
535 record_va: resolver
536 .macho()
537 .address_map()
538 .thin_offset_to_va(record_file_offset)?,
539 record_size: entry_size as u64,
540 encoding: ObjCMethodRecordEncoding::Absolute {
541 selector_pointer: resolver.pointer_provenance_at_offset(entry_offset as u64),
542 type_encoding_pointer: resolver.pointer_provenance_at_offset(types_offset),
543 implementation_pointer: resolver.pointer_provenance_at_offset(imp_offset),
544 },
545 },
546 })
547}
548
549fn strict_class_ref_name(resolver: &ObjCResolver<'_>, pointer_offset: u64) -> Result<String> {
550 if let Some(bind_name) = resolver.bind_name_at_offset(pointer_offset) {
551 return Ok(bind_name
552 .strip_prefix("_OBJC_CLASS_$_")
553 .unwrap_or(bind_name)
554 .to_owned());
555 }
556 let class_va = required_pointer(resolver, pointer_offset, "category class")?;
557 let class_offset = resolver.va_to_offset(class_va)?.0;
558 let data_va = required_pointer(
559 resolver,
560 checked_field_offset(class_offset, 32, "category class data")?,
561 "category class data",
562 )?;
563 let ro_offset = resolver
564 .va_to_offset(Va(data_va.0 & crate::types::CLASS_DATA_POINTER_MASK))?
565 .0;
566 required_cstring_pointer(
567 resolver,
568 checked_field_offset(ro_offset, 24, "category class name")?,
569 "category class name",
570 )
571}
572
573fn checked_field_offset(base: u64, delta: u64, field: &str) -> Result<u64> {
574 base.checked_add(delta)
575 .ok_or_else(|| Error::address(format!("{field} offset overflows")))
576}
577
578fn required_pointer(resolver: &ObjCResolver<'_>, offset: u64, field: &str) -> Result<Va> {
579 resolver
580 .read_pointer_at_offset(offset)?
581 .filter(|va| va.0 != 0)
582 .ok_or_else(|| Error::format(format!("{field} pointer is null or unresolved")))
583}
584
585fn required_cstring_pointer(
586 resolver: &ObjCResolver<'_>,
587 offset: u64,
588 field: &str,
589) -> Result<String> {
590 let va = required_pointer(resolver, offset, field)?;
591 resolver.read_cstring(va).map(str::to_owned)
592}
593
594fn read_cstring_at_file_offset(data: &[u8], offset: usize) -> Result<&str> {
595 let slice = data
596 .get(offset..)
597 .ok_or_else(|| Error::bounds(offset as u64, 1, data.len() as u64))?;
598 let end = slice
599 .iter()
600 .position(|byte| *byte == 0)
601 .ok_or_else(|| Error::format(format!("unterminated string at file offset {offset:#x}")))?;
602 std::str::from_utf8(&slice[..end])
603 .map_err(|error| Error::format(format!("invalid UTF-8 at offset {offset:#x}: {error}")))
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 fn thin(bytes: &[u8]) -> &MachoFile<'_> {
611 let container = Box::leak(Box::new(macho_core::parse(bytes).unwrap()));
612 match container {
613 macho_core::model::container::MachoContainer::Thin(macho) => macho,
614 macho_core::model::container::MachoContainer::Fat(_) => panic!("expected thin image"),
615 }
616 }
617
618 #[test]
619 fn folds_class_and_category_method_imps_without_collecting_a_graph() {
620 let class_bytes = macho_test_support::disassembly_objc_boundary();
621 let class_imps = fold_method_imps(thin(&class_bytes), Vec::new(), |items, item| {
622 items.push(item);
623 Ok(())
624 })
625 .unwrap();
626 assert!(
627 class_imps.iter().any(|item| {
628 item.class_name == "Fixture"
629 && item.category_name.is_none()
630 && item.method_name == "next"
631 && item.kind == ObjCMethodKind::Instance
632 }),
633 "{class_imps:?}"
634 );
635 let class_records = fold_method_records(thin(&class_bytes), Vec::new(), |items, item| {
636 items.push(item);
637 Ok(())
638 })
639 .unwrap();
640 let class_method = class_records
641 .iter()
642 .find(|item| item.method_name == "next")
643 .unwrap();
644 assert_eq!(class_method.type_encoding, "v@:");
645 assert_eq!(class_method.provenance.record_file_offset.0, 0x2c8);
646 assert_eq!(class_method.provenance.record_va.0, 0x1_0000_02c8);
647 assert_eq!(class_method.provenance.record_size, 24);
648 assert_eq!(
649 class_method.provenance.encoding,
650 ObjCMethodRecordEncoding::Absolute {
651 selector_pointer: ObjCPointerProvenance::Direct,
652 type_encoding_pointer: ObjCPointerProvenance::Direct,
653 implementation_pointer: ObjCPointerProvenance::Direct,
654 }
655 );
656
657 let category_bytes = macho_test_support::disassembly_objc_category_labels();
658 let category_imps = fold_method_imps(thin(&category_bytes), Vec::new(), |items, item| {
659 items.push(item);
660 Ok(())
661 })
662 .unwrap();
663 assert!(
664 category_imps
665 .iter()
666 .any(|item| item.category_name.as_deref() == Some("Fixture")
667 && item.kind == ObjCMethodKind::Instance)
668 );
669 assert!(
670 category_imps
671 .iter()
672 .any(|item| item.category_name.as_deref() == Some("Fixture")
673 && item.kind == ObjCMethodKind::Class)
674 );
675 }
676
677 #[test]
678 fn relative_method_records_retain_each_field_basis_and_selector_reference() {
679 let mut bytes = macho_test_support::disassembly_objc_boundary();
680 const IMAGE_BASE: u64 = 0x1_0000_0000;
681 const ENTRY: usize = 0x2c8;
682 const SELECTOR_REFERENCE: usize = 0x2f8;
683 bytes[0x2c0..0x2c4].copy_from_slice(&0x8000_000cu32.to_le_bytes());
684 bytes[ENTRY..ENTRY + 4].copy_from_slice(
685 &i32::try_from(SELECTOR_REFERENCE - ENTRY)
686 .unwrap()
687 .to_le_bytes(),
688 );
689 bytes[ENTRY + 4..ENTRY + 8]
690 .copy_from_slice(&i32::try_from(0x2f0 - (ENTRY + 4)).unwrap().to_le_bytes());
691 bytes[ENTRY + 8..ENTRY + 12].copy_from_slice(&(-0xcci32).to_le_bytes());
692 bytes[SELECTOR_REFERENCE..SELECTOR_REFERENCE + 8]
693 .copy_from_slice(&(IMAGE_BASE + 0x2e8).to_le_bytes());
694
695 let records = fold_method_records(thin(&bytes), Vec::new(), |items, item| {
696 items.push(item);
697 Ok(())
698 })
699 .unwrap();
700 let record = records
701 .iter()
702 .find(|item| item.method_name == "next")
703 .unwrap();
704 assert_eq!(record.type_encoding, "v@:");
705 assert_eq!(record.imp.0, IMAGE_BASE + 0x204);
706 assert_eq!(record.provenance.record_file_offset.0, ENTRY as u64);
707 assert_eq!(record.provenance.record_size, 12);
708 assert_eq!(
709 record.provenance.encoding,
710 ObjCMethodRecordEncoding::Relative {
711 selector_field_va: Va(IMAGE_BASE + ENTRY as u64),
712 type_encoding_field_va: Va(IMAGE_BASE + ENTRY as u64 + 4),
713 implementation_field_va: Va(IMAGE_BASE + ENTRY as u64 + 8),
714 selector: ObjCRelativeSelectorEncoding::IndirectReference {
715 selector_reference_file_offset: ThinFileOffset(SELECTOR_REFERENCE as u64),
716 selector_reference_pointer: ObjCPointerProvenance::Direct,
717 },
718 }
719 );
720
721 bytes[0x2c0..0x2c4].copy_from_slice(&0xc000_000cu32.to_le_bytes());
722 bytes[ENTRY..ENTRY + 4]
723 .copy_from_slice(&i32::try_from(0x2e8 - ENTRY).unwrap().to_le_bytes());
724 let direct_records = fold_method_records(thin(&bytes), Vec::new(), |items, item| {
725 items.push(item);
726 Ok(())
727 })
728 .unwrap();
729 let direct = direct_records
730 .iter()
731 .find(|item| item.method_name == "next")
732 .unwrap();
733 assert!(matches!(
734 direct.provenance.encoding,
735 ObjCMethodRecordEncoding::Relative {
736 selector: ObjCRelativeSelectorEncoding::DirectString {
737 selector_string_file_offset: ThinFileOffset(0x2e8),
738 },
739 ..
740 }
741 ));
742 }
743
744 #[test]
745 fn damaged_chained_fixups_reject_instead_of_falling_back_to_legacy_metadata() {
746 let mut bytes = macho_test_support::disassembly_objc_boundary();
747 const HEADER_SIZE: usize = 32;
748 const SEGMENT_COMMAND_SIZE: usize = 72 + 2 * 80;
749 const SYMTAB_COMMAND_SIZE: usize = 24;
750 const CHAINED_COMMAND_SIZE: usize = 16;
751 const COMMAND_OFFSET: usize = HEADER_SIZE + SEGMENT_COMMAND_SIZE + SYMTAB_COMMAND_SIZE;
752
753 bytes[16..20].copy_from_slice(&3u32.to_le_bytes());
754 bytes[20..24].copy_from_slice(
755 &((SEGMENT_COMMAND_SIZE + SYMTAB_COMMAND_SIZE + CHAINED_COMMAND_SIZE) as u32)
756 .to_le_bytes(),
757 );
758 bytes[COMMAND_OFFSET..COMMAND_OFFSET + 4].copy_from_slice(&0x8000_0034u32.to_le_bytes());
759 bytes[COMMAND_OFFSET + 4..COMMAND_OFFSET + 8]
760 .copy_from_slice(&(CHAINED_COMMAND_SIZE as u32).to_le_bytes());
761 bytes[COMMAND_OFFSET + 8..COMMAND_OFFSET + 12].copy_from_slice(&u32::MAX.to_le_bytes());
762 bytes[COMMAND_OFFSET + 12..COMMAND_OFFSET + 16].copy_from_slice(&28u32.to_le_bytes());
763
764 let error = fold_method_records(thin(&bytes), (), |_, _| Ok(()))
765 .expect_err("damaged chained-fixup payload must reject");
766 assert!(error.to_string().contains("offset"), "{error}");
767 }
768
769 #[test]
770 fn malformed_suffix_drops_an_accumulator_after_a_valid_imp() {
771 let mut bytes = macho_test_support::disassembly_objc_boundary();
772 bytes[224..232].copy_from_slice(&16u64.to_le_bytes());
776 bytes[0x248..0x250].copy_from_slice(&u64::MAX.to_le_bytes());
777 let callback_count = std::cell::Cell::new(0usize);
778 let result = fold_method_imps(thin(&bytes), 0usize, |count, _| {
779 *count += 1;
780 callback_count.set(callback_count.get() + 1);
781 Ok(())
782 });
783 assert_eq!(callback_count.get(), 1);
784 assert!(result.is_err());
785 }
786}