1use std::collections::HashMap;
8
9use crate::native::{
10 collect_sections, collect_text_symbols, collect_undefined_imports, symbol_fingerprint,
11};
12use crate::x86::X86_NORMALIZATION_VERSION;
13use crate::{
14 ArtifactBackend, ArtifactCapabilities, ArtifactError, ArtifactFingerprint, ArtifactFormat,
15 ArtifactIr,
16};
17use object::Object;
18use object::read::macho::{FatArch, MachOFatFile32, MachOFatFile64};
19
20#[cfg(test)]
21use crate::ArtifactImportKind;
22
23#[derive(Debug, Default, Clone, Copy)]
25pub struct MachOBackend;
26
27pub const MACHO_NORMALIZATION_VERSION: &str = X86_NORMALIZATION_VERSION;
29
30impl ArtifactBackend for MachOBackend {
31 fn format(&self) -> ArtifactFormat {
32 ArtifactFormat::MachO
33 }
34
35 fn detects(&self, bytes: &[u8]) -> bool {
36 matches!(
37 bytes.get(..4),
38 Some(
39 [0xfe, 0xed, 0xfa, 0xce | 0xcf]
40 | [0xce | 0xcf, 0xfa, 0xed, 0xfe]
41 | [0xca, 0xfe, 0xba, 0xbe | 0xbf]
42 )
43 )
44 }
45
46 fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
47 self.parse_with_architecture(bytes, None, None)
48 }
49
50 fn capabilities(&self) -> ArtifactCapabilities {
51 ArtifactCapabilities {
52 symbols: true,
53 call_graph: false,
54 source_mapping: false,
55 debug_info_unreadable: false,
56 normalized_duplicates: false,
57 independent_data_segments: false,
58 relocations: true,
59 data_segments: true,
60 }
61 }
62}
63
64impl MachOBackend {
65 pub fn parse_with_debug_companion(
76 &self,
77 bytes: &[u8],
78 debug_companion: Option<&[u8]>,
79 ) -> Result<ArtifactIr, ArtifactError> {
80 self.parse_with_architecture(bytes, debug_companion, None)
81 }
82
83 pub fn parse_with_architecture(
93 &self,
94 bytes: &[u8],
95 debug_companion: Option<&[u8]>,
96 architecture: Option<&str>,
97 ) -> Result<ArtifactIr, ArtifactError> {
98 if !self.detects(bytes) {
99 return Err(ArtifactError::WrongFormat {
100 expected: ArtifactFormat::MachO,
101 });
102 }
103 let selection = mach_o_slice(bytes, architecture)?;
104 let artifact = selection.bytes;
105 let offset = selection.offset;
106 let file = object::File::parse(artifact).map_err(|error| malformed(error.to_string()))?;
107 if file.format() != object::BinaryFormat::MachO {
108 return Err(ArtifactError::WrongFormat {
109 expected: ArtifactFormat::MachO,
110 });
111 }
112 if let Some(requested) = architecture
113 && !architecture_selector_matches(architecture_name(file.architecture()), requested)
114 {
115 return Err(malformed(format!(
116 "Mach-O architecture is {}, not requested {requested}",
117 architecture_name(file.architecture())
118 )));
119 }
120 let debug_file = debug_companion
121 .map(|companion| {
122 let companion = mach_o_slice(companion, architecture)?;
123 let companion = object::File::parse(companion.bytes)
124 .map_err(|error| malformed(error.to_string()))?;
125 if companion.format() != object::BinaryFormat::MachO
126 || !matching_uuid(&file, &companion)
127 {
128 return Err(malformed(
129 "external debug companion does not have the artifact's Mach-O UUID"
130 .to_owned(),
131 ));
132 }
133 Ok(companion)
134 })
135 .transpose()?;
136 let selected_architecture = if selection.architecture == "unknown" {
137 architecture_name(file.architecture()).to_owned()
138 } else {
139 selection.architecture
140 };
141 let mut ir = ArtifactIr::empty(ArtifactFormat::MachO, bytes);
142 ir.architecture = Some(selected_architecture);
143 ir.skipped_architectures = selection.skipped_architectures;
144 collect_sections(&file, &mut ir).map_err(|error| malformed(error.to_string()))?;
145 collect_undefined_imports(file.symbols(), &mut ir);
146 let collected_addresses = collect_symbols(&file, &mut ir)?;
147 let symbol_addresses = if ir.symbols.is_empty() {
148 infer_text_regions(&file, &mut ir)?
149 } else {
150 collected_addresses
151 };
152 crate::dwarf::attach_dwarf_frames(
153 debug_file.as_ref().unwrap_or(&file),
154 &symbol_addresses,
155 &mut ir,
156 );
157 shift_file_offsets(&mut ir, offset);
158 ir.capabilities = ArtifactCapabilities {
159 symbols: !ir.symbols.is_empty(),
160 call_graph: false,
161 source_mapping: !ir.source_mappings.is_empty(),
162 debug_info_unreadable: ir.capabilities.debug_info_unreadable,
163 normalized_duplicates: crate::x86::supports_normalized_duplicates(file.architecture()),
164 independent_data_segments: false,
165 relocations: !ir.relocations.is_empty(),
166 data_segments: !ir.data_segments.is_empty(),
167 };
168 Ok(ir)
169 }
170}
171
172struct MachOSlice<'a> {
173 bytes: &'a [u8],
174 offset: u64,
175 architecture: String,
176 skipped_architectures: Vec<String>,
177}
178
179fn mach_o_slice<'a>(
180 bytes: &'a [u8],
181 requested_architecture: Option<&str>,
182) -> Result<MachOSlice<'a>, ArtifactError> {
183 match object::FileKind::parse(bytes).map_err(|error| malformed(error.to_string()))? {
184 object::FileKind::MachO32 | object::FileKind::MachO64 => Ok(MachOSlice {
185 bytes,
186 offset: 0,
187 architecture: "unknown".to_owned(),
188 skipped_architectures: Vec::new(),
189 }),
190 object::FileKind::MachOFat32 => {
191 let fat = MachOFatFile32::parse(bytes).map_err(|error| malformed(error.to_string()))?;
192 fat_slice_with_architecture(&fat, bytes, requested_architecture)
193 }
194 object::FileKind::MachOFat64 => {
195 let fat = MachOFatFile64::parse(bytes).map_err(|error| malformed(error.to_string()))?;
196 fat_slice_with_architecture(&fat, bytes, requested_architecture)
197 }
198 _ => Err(ArtifactError::WrongFormat {
199 expected: ArtifactFormat::MachO,
200 }),
201 }
202}
203
204fn fat_slice_with_architecture<'a, Fat: FatArch>(
205 fat: &object::read::macho::MachOFatFile<'a, Fat>,
206 bytes: &'a [u8],
207 requested_architecture: Option<&str>,
208) -> Result<MachOSlice<'a>, ArtifactError> {
209 let arches = fat.arches();
210 let available = fat_architecture_labels(arches);
211 let selected_index = match (arches, requested_architecture) {
212 ([], _) => {
213 return Err(malformed(
214 "fat Mach-O has no architecture slices".to_owned(),
215 ));
216 }
217 ([_], None) => 0,
218 (_, Some(requested)) => {
219 let matches: Vec<_> = available
220 .iter()
221 .enumerate()
222 .filter_map(|(index, name)| (name == requested).then_some(index))
223 .collect();
224 match matches.as_slice() {
225 [index] => *index,
226 [] => {
227 return Err(malformed(format!(
228 "fat Mach-O has no {requested} slice (available: {})",
229 available.join(", ")
230 )));
231 }
232 _ => {
233 return Err(malformed(
234 "fat Mach-O architecture selector is ambiguous".to_owned(),
235 ));
236 }
237 }
238 }
239 (_, None) => {
240 return Err(malformed(format!(
241 "fat Mach-O has multiple architecture slices (available: {}); select one with --arch",
242 available.join(", ")
243 )));
244 }
245 };
246 let arch = &arches[selected_index];
247 let (offset, _) = arch.file_range();
248 let slice = arch
249 .data(bytes)
250 .map_err(|error| malformed(error.to_string()))?;
251 let selected = available[selected_index].clone();
252 let skipped_architectures = available
253 .into_iter()
254 .filter(|available| available != &selected)
255 .collect();
256 Ok(MachOSlice {
257 bytes: slice,
258 offset,
259 architecture: selected,
260 skipped_architectures,
261 })
262}
263
264fn architecture_selector_matches(architecture: &str, selector: &str) -> bool {
265 selector.split_once(':').map_or(selector, |(base, _)| base) == architecture
266}
267
268fn fat_architecture_labels<Fat: FatArch>(arches: &[Fat]) -> Vec<String> {
269 let bases: Vec<_> = arches
270 .iter()
271 .map(|arch| architecture_name(arch.architecture()))
272 .collect();
273 arches
274 .iter()
275 .zip(&bases)
276 .map(|(arch, base)| {
277 if bases.iter().filter(|other| *other == base).count() == 1 {
278 (*base).to_owned()
279 } else {
280 format!("{base}:{}", arch.cpusubtype())
281 }
282 })
283 .collect()
284}
285
286const fn architecture_name(architecture: object::Architecture) -> &'static str {
287 match architecture {
288 object::Architecture::Aarch64 => "aarch64",
289 object::Architecture::Arm => "arm",
290 object::Architecture::I386 => "i386",
291 object::Architecture::X86_64 => "x86_64",
292 object::Architecture::PowerPc => "powerpc",
293 object::Architecture::PowerPc64 => "powerpc64",
294 _ => "unknown",
295 }
296}
297
298fn shift_file_offsets(ir: &mut ArtifactIr, offset: u64) {
299 if offset == 0 {
300 return;
301 }
302 for section in &mut ir.sections {
303 section.offset = section.offset.saturating_add(offset);
304 }
305 for segment in &mut ir.data_segments {
306 segment.offset = segment.offset.saturating_add(offset);
307 }
308 for symbol in &mut ir.symbols {
309 symbol.offset = symbol.offset.saturating_add(offset);
310 }
311 for relocation in &mut ir.relocations {
312 relocation.offset = relocation.offset.saturating_add(offset);
313 }
314}
315
316fn matching_uuid(artifact: &object::File<'_>, companion: &object::File<'_>) -> bool {
317 matches!(
318 (artifact.mach_uuid(), companion.mach_uuid()),
319 (Ok(Some(artifact)), Ok(Some(companion))) if artifact == companion
320 )
321}
322
323fn collect_symbols(
324 file: &object::File<'_>,
325 ir: &mut ArtifactIr,
326) -> Result<HashMap<ArtifactFingerprint, (u64, u64)>, ArtifactError> {
327 collect_text_symbols(file, ir)
328 .map(|ranges| {
329 ranges
330 .into_iter()
331 .map(|range| (range.fingerprint, (range.address, range.size)))
332 .collect()
333 })
334 .map_err(|error| malformed(error.to_string()))
335}
336fn infer_text_regions(
342 file: &object::File<'_>,
343 ir: &mut ArtifactIr,
344) -> Result<HashMap<ArtifactFingerprint, (u64, u64)>, ArtifactError> {
345 let ranges = crate::native::infer_text_regions(file, ir, |section, normalized, data| {
346 symbol_fingerprint(None, section, normalized, data)
347 })
348 .map_err(|error| malformed(error.to_string()))?;
349 Ok(ranges
350 .into_iter()
351 .map(|(fingerprint, address, size)| (fingerprint, (address, size)))
352 .collect())
353}
354
355const fn malformed(message: String) -> ArtifactError {
356 ArtifactError::Malformed {
357 format: ArtifactFormat::MachO,
358 message,
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 #![allow(clippy::expect_used, clippy::unwrap_used)]
365
366 use super::*;
367 use object::write::{Object as WriteObject, StandardSection, Symbol, SymbolSection};
368 use object::{Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope};
369 use proptest::prelude::*;
370
371 fn macho_fixture() -> Vec<u8> {
372 let mut object = WriteObject::new(
373 BinaryFormat::MachO,
374 Architecture::X86_64,
375 Endianness::Little,
376 );
377 let text = object.section_id(StandardSection::Text);
378 let offset = object.append_section_data(text, &[0x90, 0xc3], 1);
379 object.add_symbol(Symbol {
380 name: b"render".to_vec(),
381 value: offset,
382 size: 2,
383 kind: SymbolKind::Text,
384 scope: SymbolScope::Dynamic,
385 weak: false,
386 section: SymbolSection::Section(text),
387 flags: SymbolFlags::None,
388 });
389 object.write().expect("write Mach-O fixture")
390 }
391
392 fn macho_fixture_without_symbols() -> Vec<u8> {
393 let mut object = WriteObject::new(
394 BinaryFormat::MachO,
395 Architecture::X86_64,
396 Endianness::Little,
397 );
398 let text = object.section_id(StandardSection::Text);
399 object.append_section_data(text, &[0x90, 0xc3], 1);
400 object.write().expect("write symbol-free Mach-O fixture")
401 }
402
403 fn macho_zero_sized_alias_fixture() -> Vec<u8> {
404 let mut object = WriteObject::new(
405 BinaryFormat::MachO,
406 Architecture::X86_64,
407 Endianness::Little,
408 );
409 let text = object.section_id(StandardSection::Text);
410 let offset = object.append_section_data(text, &[0x90, 0xc3], 1);
411 for (name, size) in [(b"implementation".as_slice(), 2), (b"alias", 0)] {
412 object.add_symbol(Symbol {
413 name: name.to_vec(),
414 value: offset,
415 size,
416 kind: SymbolKind::Text,
417 scope: SymbolScope::Dynamic,
418 weak: false,
419 section: SymbolSection::Section(text),
420 flags: SymbolFlags::None,
421 });
422 }
423 object
424 .write()
425 .expect("write zero-sized Mach-O alias fixture")
426 }
427
428 fn macho_undefined_import_fixture() -> Vec<u8> {
429 let mut object = WriteObject::new(
430 BinaryFormat::MachO,
431 Architecture::X86_64,
432 Endianness::Little,
433 );
434 object.add_symbol(Symbol {
435 name: b"_external_call".to_vec(),
436 value: 0,
437 size: 0,
438 kind: SymbolKind::Text,
439 scope: SymbolScope::Dynamic,
440 weak: false,
441 section: SymbolSection::Undefined,
442 flags: SymbolFlags::None,
443 });
444 object
445 .write()
446 .expect("write Mach-O undefined import fixture")
447 }
448
449 fn fat_macho_fixture() -> Vec<u8> {
450 let inner = macho_fixture();
451 let offset = 256_u32;
452 let mut bytes = Vec::new();
453 bytes.extend([0xca, 0xfe, 0xba, 0xbe]);
454 bytes.extend(1_u32.to_be_bytes());
455 bytes.extend(0x0100_0007_u32.to_be_bytes());
456 bytes.extend(3_u32.to_be_bytes());
457 bytes.extend(offset.to_be_bytes());
458 bytes.extend(
459 u32::try_from(inner.len())
460 .expect("fixture slice length fits")
461 .to_be_bytes(),
462 );
463 bytes.extend(8_u32.to_be_bytes());
464 bytes.resize(offset as usize, 0);
465 bytes.extend(inner);
466 bytes
467 }
468
469 fn universal_macho_fixture() -> Vec<u8> {
470 let x86_64 = macho_fixture();
471 let aarch64 = {
472 let mut object = WriteObject::new(
473 BinaryFormat::MachO,
474 Architecture::Aarch64,
475 Endianness::Little,
476 );
477 let text = object.section_id(StandardSection::Text);
478 let offset = object.append_section_data(text, &[0x1f, 0x20, 0x03, 0xd5], 4);
479 object.add_symbol(Symbol {
480 name: b"render_arm64".to_vec(),
481 value: offset,
482 size: 4,
483 kind: SymbolKind::Text,
484 scope: SymbolScope::Dynamic,
485 weak: false,
486 section: SymbolSection::Section(text),
487 flags: SymbolFlags::None,
488 });
489 object.write().expect("write AArch64 Mach-O fixture")
490 };
491 let x86_64_offset = 256_u32;
492 let aarch64_offset = 512_u32;
493 let mut bytes = Vec::new();
494 bytes.extend([0xca, 0xfe, 0xba, 0xbe]);
495 bytes.extend(2_u32.to_be_bytes());
496 for (cpu_type, cpu_subtype, offset, inner) in [
497 (0x0100_0007_u32, 3_u32, x86_64_offset, &x86_64),
498 (0x0100_000c_u32, 0_u32, aarch64_offset, &aarch64),
499 ] {
500 bytes.extend(cpu_type.to_be_bytes());
501 bytes.extend(cpu_subtype.to_be_bytes());
502 bytes.extend(offset.to_be_bytes());
503 bytes.extend(
504 u32::try_from(inner.len())
505 .expect("fixture slice length fits")
506 .to_be_bytes(),
507 );
508 bytes.extend(8_u32.to_be_bytes());
509 }
510 bytes.resize(x86_64_offset as usize, 0);
511 bytes.extend(x86_64);
512 bytes.resize(aarch64_offset as usize, 0);
513 bytes.extend(aarch64);
514 bytes
515 }
516
517 #[test]
518 fn parses_a_macho_function_without_executing_it() {
519 let ir = MachOBackend
520 .parse(&macho_fixture())
521 .expect("parse Mach-O fixture");
522 assert_eq!(ir.format, ArtifactFormat::MachO);
523 assert_eq!(ir.symbols.len(), 1, "{ir:#?}");
524 assert!(ir.capabilities.symbols);
525 assert_eq!(ir.symbols[0].name.as_deref(), Some("_render"));
526 assert_eq!(ir.symbols[0].code, vec![0x90, 0xc3]);
527 assert_eq!(
528 ir.symbols[0]
529 .normalized
530 .as_ref()
531 .map(|value| value.version.as_str()),
532 Some(MACHO_NORMALIZATION_VERSION)
533 );
534 }
535
536 #[test]
537 fn symbol_free_macho_keeps_an_explicitly_inferred_text_region() {
538 let ir = MachOBackend
539 .parse(&macho_fixture_without_symbols())
540 .expect("parse symbol-free Mach-O fixture");
541 assert_eq!(ir.symbols.len(), 1, "{ir:#?}");
542 assert!(ir.capabilities.symbols);
543 assert!(ir.symbols[0].size_inferred);
544 assert_eq!(ir.symbols[0].name, None);
545 assert_eq!(ir.symbols[0].code, vec![0x90, 0xc3]);
546 }
547
548 #[test]
549 fn zero_sized_macho_alias_is_retained_without_claiming_implementation_bytes() {
550 let ir = MachOBackend
551 .parse(&macho_zero_sized_alias_fixture())
552 .expect("parse zero-sized Mach-O alias fixture");
553 let at_start: Vec<_> = ir
554 .symbols
555 .iter()
556 .filter(|symbol| symbol.offset == ir.symbols[0].offset)
557 .collect();
558 assert_eq!(at_start.len(), 2, "{ir:#?}");
559 assert_eq!(
560 at_start
561 .iter()
562 .filter(|symbol| symbol.code.is_empty())
563 .count(),
564 1,
565 "{ir:#?}"
566 );
567 assert_eq!(
568 at_start
569 .iter()
570 .filter(|symbol| symbol.code == [0x90, 0xc3])
571 .count(),
572 1,
573 "{ir:#?}"
574 );
575 let alias = at_start
576 .iter()
577 .find(|symbol| symbol.code.is_empty())
578 .expect("empty alias record");
579 assert!(alias.size_inferred);
580 assert_eq!(alias.size, 0);
581 }
582
583 #[test]
584 fn records_undefined_macho_symbols_as_imports() {
585 let ir = MachOBackend
586 .parse(&macho_undefined_import_fixture())
587 .expect("parse Mach-O undefined import fixture");
588 assert_eq!(ir.imports.len(), 1, "{ir:#?}");
589 assert_eq!(ir.imports[0].name.as_deref(), Some("__external_call"));
590 assert_eq!(ir.imports[0].kind, ArtifactImportKind::Function);
591 }
592
593 #[test]
594 fn parses_a_fat_macho_slice_without_losing_outer_identity() {
595 let bytes = fat_macho_fixture();
596 assert!(MachOBackend.detects(&bytes));
597 let ir = MachOBackend
598 .parse(&bytes)
599 .expect("fat Mach-O fixture parses");
600 assert_eq!(ir.observed_bytes, bytes.len() as u64);
601 assert_eq!(
602 ir.fingerprint,
603 ArtifactFingerprint::from_content("artifact", &bytes)
604 );
605 assert_eq!(ir.symbols.len(), 1, "{ir:#?}");
606 assert!(ir.symbols[0].offset >= 256);
607 }
608
609 #[test]
610 fn universal_macho_requires_an_explicit_architecture_selection() {
611 let bytes = universal_macho_fixture();
612 let error = MachOBackend
613 .parse(&bytes)
614 .expect_err("multi-slice input is ambiguous");
615 assert!(error.to_string().contains("--arch"), "{error}");
616 assert!(error.to_string().contains("x86_64"), "{error}");
617 assert!(error.to_string().contains("aarch64"), "{error}");
618 }
619
620 #[test]
621 fn universal_macho_records_the_selected_and_skipped_architectures() {
622 let bytes = universal_macho_fixture();
623 let ir = MachOBackend
624 .parse_with_architecture(&bytes, None, Some("aarch64"))
625 .expect("explicit AArch64 slice parses");
626 assert_eq!(ir.architecture.as_deref(), Some("aarch64"));
627 assert_eq!(ir.skipped_architectures, ["x86_64"]);
628 assert_eq!(ir.symbols[0].name.as_deref(), Some("_render_arm64"));
629 assert!(ir.symbols[0].offset >= 512);
630 }
631
632 #[test]
633 fn universal_macho_rejects_a_missing_architecture_selection() {
634 let error = MachOBackend
635 .parse_with_architecture(&universal_macho_fixture(), None, Some("i386"))
636 .expect_err("unavailable slice is rejected");
637 assert!(error.to_string().contains("no i386 slice"), "{error}");
638 }
639
640 #[test]
641 fn thin_macho_rejects_an_architecture_that_does_not_match_its_header() {
642 let error = MachOBackend
643 .parse_with_architecture(&macho_fixture(), None, Some("aarch64"))
644 .expect_err("thin architecture mismatch is rejected");
645 assert!(
646 error.to_string().contains("not requested aarch64"),
647 "{error}"
648 );
649 }
650
651 #[test]
652 fn other_bytes_do_not_claim_the_backend() {
653 assert!(!MachOBackend.detects(b"not an object"));
654 assert!(matches!(
655 MachOBackend.parse(b"not an object"),
656 Err(ArtifactError::WrongFormat { .. })
657 ));
658 }
659
660 proptest::proptest! {
661 #[test]
662 fn arbitrary_bytes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
663 let _ = MachOBackend.parse(&bytes);
664 }
665 }
666}