1use crate::{
4 BinaryError, BinaryFormatParser, BinaryFormatTrait, Result,
5 types::{
6 Architecture, BinaryFormat as Format, BinaryMetadata, Endianness, Export, Import, Section,
7 SectionPermissions, SectionType, SecurityFeatures, Symbol, SymbolBinding, SymbolType,
8 SymbolVisibility,
9 },
10};
11use goblin::mach::{Mach, MachO};
12use goblin::mach::{
13 exports::{
14 EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE, EXPORT_SYMBOL_FLAGS_KIND_MASK,
15 ExportInfo as MachExportInfo,
16 },
17 header,
18 load_command::CommandVariant,
19};
20
21mod dyld_preflight;
22mod load_command_preflight;
23
24const FAT_MAGIC_32: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
25const FAT_CIGAM_32: [u8; 4] = [0xbe, 0xba, 0xfe, 0xca];
26const FAT_MAGIC_64: [u8; 4] = [0xca, 0xfe, 0xba, 0xbf];
27const FAT_CIGAM_64: [u8; 4] = [0xbf, 0xba, 0xfe, 0xca];
28const FAT_HEADER_SIZE: usize = 8;
29const FAT_ARCH_32_SIZE: usize = 20;
30const FAT_ARCH_64_SIZE: usize = 32;
31const MAX_FAT_ARCHITECTURES: usize = 32;
32
33pub struct MachOParser;
35
36impl BinaryFormatParser for MachOParser {
37 fn parse(data: &[u8]) -> Result<Box<dyn BinaryFormatTrait>> {
38 if is_universal_macho(data) {
39 return Err(BinaryError::unsupported_format(
40 "Universal (fat) Mach-O binaries are not supported",
41 ));
42 }
43
44 load_command_preflight::validate(data)?;
48 let mach = Mach::parse(data)?;
49 match mach {
50 Mach::Binary(macho) => Ok(Box::new(MachOBinary::new(macho, data)?)),
51 Mach::Fat(_) => Err(BinaryError::unsupported_format(
52 "Universal (fat) Mach-O binaries are not supported",
53 )),
54 }
55 }
56
57 fn can_parse(data: &[u8]) -> bool {
58 if data.len() < 4 {
59 return false;
60 }
61
62 let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
63 matches!(
64 magic,
65 goblin::mach::header::MH_MAGIC
66 | goblin::mach::header::MH_CIGAM
67 | goblin::mach::header::MH_MAGIC_64
68 | goblin::mach::header::MH_CIGAM_64
69 ) || is_universal_macho(data)
70 }
71}
72
73fn is_universal_macho(data: &[u8]) -> bool {
74 let Some(header) = data.get(..FAT_HEADER_SIZE) else {
75 return false;
76 };
77
78 let magic = [header[0], header[1], header[2], header[3]];
79 let count_bytes = [header[4], header[5], header[6], header[7]];
80 let (architecture_count, architecture_size) = match magic {
81 FAT_MAGIC_32 => (u32::from_be_bytes(count_bytes), FAT_ARCH_32_SIZE),
82 FAT_CIGAM_32 => (u32::from_le_bytes(count_bytes), FAT_ARCH_32_SIZE),
83 FAT_MAGIC_64 => (u32::from_be_bytes(count_bytes), FAT_ARCH_64_SIZE),
84 FAT_CIGAM_64 => (u32::from_le_bytes(count_bytes), FAT_ARCH_64_SIZE),
85 _ => return false,
86 };
87
88 let Ok(architecture_count) = usize::try_from(architecture_count) else {
89 return false;
90 };
91 if !(1..=MAX_FAT_ARCHITECTURES).contains(&architecture_count) {
92 return false;
93 }
94
95 architecture_size
96 .checked_mul(architecture_count)
97 .and_then(|table_size| FAT_HEADER_SIZE.checked_add(table_size))
98 .is_some_and(|table_end| table_end <= data.len())
99}
100
101pub struct MachOBinary {
103 metadata: BinaryMetadata,
104 sections: Vec<Section>,
105 symbols: Vec<Symbol>,
106 imports: Vec<Import>,
107 exports: Vec<Export>,
108}
109
110impl MachOBinary {
111 fn new(macho: MachO<'_>, data: &[u8]) -> Result<Self> {
112 let mut output_budget = super::ParseOutputBudget::default();
113 dyld_preflight::validate(&macho, data)?;
114
115 let architecture = match macho.header.cputype() {
117 goblin::mach::constants::cputype::CPU_TYPE_X86 => Architecture::X86,
118 goblin::mach::constants::cputype::CPU_TYPE_X86_64 => Architecture::X86_64,
119 goblin::mach::constants::cputype::CPU_TYPE_ARM => Architecture::Arm,
120 goblin::mach::constants::cputype::CPU_TYPE_ARM64 => Architecture::Arm64,
121 goblin::mach::constants::cputype::CPU_TYPE_POWERPC => Architecture::PowerPC,
122 goblin::mach::constants::cputype::CPU_TYPE_POWERPC64 => Architecture::PowerPC64,
123 _ => Architecture::Unknown,
124 };
125
126 let endian = if data.len() >= 4 {
129 let raw_magic_be = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
130
131 if raw_magic_be == header::MH_MAGIC || raw_magic_be == header::MH_MAGIC_64 {
133 Endianness::Big
134 } else {
135 Endianness::Little
137 }
138 } else {
139 Endianness::Little };
141
142 let security_features = analyze_security_features(&macho);
144
145 let metadata = BinaryMetadata {
146 size: data.len(),
147 format: Format::MachO,
148 architecture,
149 entry_point: find_entry_point(&macho),
150 base_address: preferred_base_address(&macho),
151 timestamp: None, compiler_info: None,
153 endian,
154 security_features,
155 };
156
157 let sections = parse_sections(&macho, data, &mut output_budget)?;
159
160 let symbols = parse_symbols(&macho, &mut output_budget)?;
162
163 let (imports, exports) = parse_imports_exports(&macho, &mut output_budget)?;
165
166 Ok(Self {
167 metadata,
168 sections,
169 symbols,
170 imports,
171 exports,
172 })
173 }
174}
175
176impl BinaryFormatTrait for MachOBinary {
177 fn format_type(&self) -> Format {
178 Format::MachO
179 }
180
181 fn architecture(&self) -> Architecture {
182 self.metadata.architecture
183 }
184
185 fn entry_point(&self) -> Option<u64> {
186 self.metadata.entry_point
187 }
188
189 fn sections(&self) -> &[Section] {
190 &self.sections
191 }
192
193 fn symbols(&self) -> &[Symbol] {
194 &self.symbols
195 }
196
197 fn imports(&self) -> &[Import] {
198 &self.imports
199 }
200
201 fn exports(&self) -> &[Export] {
202 &self.exports
203 }
204
205 fn metadata(&self) -> &BinaryMetadata {
206 &self.metadata
207 }
208}
209
210fn parse_sections(
211 macho: &MachO,
212 data: &[u8],
213 output_budget: &mut super::ParseOutputBudget,
214) -> Result<Vec<Section>> {
215 use goblin::mach::constants::{
216 S_ATTR_DEBUG, S_GB_ZEROFILL, S_THREAD_LOCAL_ZEROFILL, S_ZEROFILL, SECTION_TYPE,
217 };
218
219 let mut sections = Vec::new();
220
221 for segment in &macho.segments {
222 for (section, _) in &segment.sections()? {
223 output_budget.reserve_record(&mut sections, "Mach-O sections")?;
224 let name = output_budget
225 .copy_name(section.name().unwrap_or("unknown"), "Mach-O section name")?;
226
227 let native_section_type = section.flags & SECTION_TYPE;
230 let is_zero_fill = matches!(
231 native_section_type,
232 S_ZEROFILL | S_GB_ZEROFILL | S_THREAD_LOCAL_ZEROFILL
233 );
234 let section_type = if is_zero_fill {
235 SectionType::Bss
236 } else if section.flags & S_ATTR_DEBUG != 0 {
237 SectionType::Debug
238 } else if section.flags & goblin::mach::constants::S_ATTR_PURE_INSTRUCTIONS != 0
239 || name.starts_with("__text")
240 {
241 SectionType::Code
242 } else if name.starts_with("__data") {
243 SectionType::Data
244 } else if name.starts_with("__const") || name.starts_with("__rodata") {
245 SectionType::ReadOnlyData
246 } else if name.starts_with("__bss") {
247 SectionType::Bss
248 } else if name.starts_with("__debug") {
249 SectionType::Debug
250 } else {
251 SectionType::Other("MACHO_SECTION".to_string())
252 };
253
254 let permissions = SectionPermissions {
256 read: segment.initprot & 0x1 != 0, write: segment.initprot & 0x2 != 0, execute: segment.initprot & 0x4 != 0, };
260
261 let has_file_data = !is_zero_fill && section.size != 0;
262 if has_file_data
263 && super::checked_file_range(data, u64::from(section.offset), section.size)
264 .is_none()
265 {
266 return Err(BinaryError::invalid_data(format!(
267 "Mach-O section '{name}' range {}..+{} exceeds the file",
268 section.offset, section.size
269 )));
270 }
271
272 let section_data = if has_file_data {
274 super::inline_section_data(data, u64::from(section.offset), section.size)
275 } else {
276 None
277 };
278
279 sections.push(Section {
280 name,
281 address: section.addr,
282 size: section.size,
283 offset: section.offset as u64,
284 file_size: if has_file_data { section.size } else { 0 },
285 permissions,
286 section_type,
287 data: section_data,
288 });
289 }
290 }
291
292 Ok(sections)
293}
294
295fn parse_symbols(
296 macho: &MachO,
297 output_budget: &mut super::ParseOutputBudget,
298) -> Result<Vec<Symbol>> {
299 use goblin::mach::symbols::{N_FUN, N_OSO, N_PEXT, N_SECT, N_SO, N_SOL, N_STAB, N_UNDF};
300
301 let mut symbols = Vec::new();
302
303 for entry in macho.symbols() {
304 let (name, native) = entry?;
305 if name.is_empty() {
306 continue;
307 }
308 output_budget.reserve_record(&mut symbols, "Mach-O symbols")?;
309
310 let symbol_type = if native.n_type & N_STAB != 0 {
311 match native.n_type {
312 N_FUN => SymbolType::Function,
313 N_SO | N_OSO | N_SOL => SymbolType::File,
314 other => SymbolType::Other(format!("N_STAB_{other:#04x}")),
315 }
316 } else {
317 match native.get_type() {
318 N_SECT => SymbolType::Other("N_SECT".to_string()),
319 N_UNDF => SymbolType::Other("N_UNDF".to_string()),
320 other => SymbolType::Other(format!("MACH_N_TYPE_{other:#04x}")),
321 }
322 };
323
324 let binding = if native.is_weak() {
325 SymbolBinding::Weak
326 } else if native.is_global() {
327 SymbolBinding::Global
328 } else {
329 SymbolBinding::Local
330 };
331
332 symbols.push(Symbol {
333 name: output_budget.copy_name(name, "Mach-O symbol name")?,
334 demangled_name: None,
335 address: native.n_value,
336 size: 0,
338 symbol_type,
339 binding,
340 visibility: if native.n_type & N_PEXT != 0 {
341 SymbolVisibility::Internal
342 } else {
343 SymbolVisibility::Default
344 },
345 section_index: native.n_sect.checked_sub(1),
348 });
349 }
350
351 Ok(symbols)
352}
353
354fn parse_imports_exports(
355 macho: &MachO,
356 output_budget: &mut super::ParseOutputBudget,
357) -> crate::types::ImportExportResult {
358 let mut imports = Vec::new();
359 let mut exports = Vec::new();
360 let image_base = preferred_base_address(macho).unwrap_or(0);
361
362 for import in &macho.imports()? {
364 output_budget.reserve_record(&mut imports, "Mach-O imports")?;
365 imports.push(Import {
366 name: output_budget.copy_name(import.name, "Mach-O import name")?,
367 library: Some(output_budget.copy_name(import.dylib, "Mach-O import library")?),
368 address: Some(import.address),
369 ordinal: None,
370 });
371 }
372
373 for export in &macho.exports()? {
375 output_budget.reserve_record(&mut exports, "Mach-O exports")?;
376 let name = output_budget.copy_name(&export.name, "Mach-O export name")?;
377 let (relative_address, should_rebase, forwarded_name) = match &export.info {
378 MachExportInfo::Regular { flags, .. } => (
379 export.offset,
380 flags & EXPORT_SYMBOL_FLAGS_KIND_MASK != EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE,
381 None,
382 ),
383 MachExportInfo::Stub { stub_offset, .. } => (u64::from(*stub_offset), true, None),
384 MachExportInfo::Reexport {
385 lib,
386 lib_symbol_name,
387 ..
388 } => (
389 0,
390 false,
391 Some(macho_reexport_name(
392 output_budget,
393 lib,
394 *lib_symbol_name,
395 name.as_str(),
396 )?),
397 ),
398 };
399 let address = if forwarded_name.is_some() {
400 0
401 } else {
402 resolved_export_address(image_base, relative_address, should_rebase, &name)?
403 };
404
405 exports.push(Export {
406 name,
407 address,
408 ordinal: None,
409 forwarded_name,
410 });
411 }
412
413 Ok((imports, exports))
414}
415
416fn resolved_export_address(
417 image_base: u64,
418 address: u64,
419 should_rebase: bool,
420 name: &str,
421) -> Result<u64> {
422 if !should_rebase {
423 return Ok(address);
424 }
425
426 image_base.checked_add(address).ok_or_else(|| {
427 BinaryError::invalid_data(format!("Mach-O export '{name}' address overflows u64"))
428 })
429}
430
431fn macho_reexport_name(
432 output_budget: &mut super::ParseOutputBudget,
433 lib: &str,
434 symbol_name: Option<&str>,
435 exported_name: &str,
436) -> Result<String> {
437 output_budget.copy_name_parts(
438 &[lib, ":", symbol_name.unwrap_or(exported_name)],
439 "Mach-O reexport name",
440 )
441}
442
443fn analyze_security_features(macho: &MachO) -> SecurityFeatures {
444 let mut features = SecurityFeatures::default();
445
446 let flags = macho.header.flags;
448
449 features.pie = flags & goblin::mach::header::MH_PIE != 0;
451
452 features.aslr = features.pie;
454
455 features.nx_bit =
458 flags & header::MH_ALLOW_STACK_EXECUTION == 0 || flags & header::MH_NO_HEAP_EXECUTION != 0;
459
460 features.stack_canary = false; features.signed = macho
466 .load_commands
467 .iter()
468 .any(|load_command| matches!(&load_command.command, CommandVariant::CodeSignature(_)));
469
470 features
471}
472
473fn find_entry_point(macho: &MachO) -> Option<u64> {
474 (macho.entry != 0).then_some(macho.entry)
475}
476
477fn preferred_base_address(macho: &MachO) -> Option<u64> {
478 macho
479 .segments
480 .iter()
481 .find_map(|segment| {
482 (segment.filesize != 0 && segment.name().is_ok_and(|name| name == "__TEXT"))
483 .then(|| segment.vmaddr.checked_sub(segment.fileoff))
484 .flatten()
485 })
486 .or_else(|| {
487 macho
488 .segments
489 .iter()
490 .filter(|segment| segment.filesize != 0)
491 .filter_map(|segment| segment.vmaddr.checked_sub(segment.fileoff))
492 .min()
493 })
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 fn universal_header(magic: [u8; 4], count: u32, little_endian: bool, is_64: bool) -> Vec<u8> {
501 let record_size = if is_64 {
502 FAT_ARCH_64_SIZE
503 } else {
504 FAT_ARCH_32_SIZE
505 };
506 let mut data = vec![0; FAT_HEADER_SIZE + record_size * count as usize];
507 data[..4].copy_from_slice(&magic);
508 let count_bytes = if little_endian {
509 count.to_le_bytes()
510 } else {
511 count.to_be_bytes()
512 };
513 data[4..8].copy_from_slice(&count_bytes);
514 data
515 }
516
517 fn macho_with_segments(segments: &[(&str, u64, u64, u64)]) -> Vec<u8> {
518 const HEADER_SIZE: usize = 32;
519 const SEGMENT_COMMAND_SIZE: usize = 72;
520
521 let commands_size = SEGMENT_COMMAND_SIZE * segments.len();
522 let mut data = vec![0_u8; HEADER_SIZE + commands_size];
523 data[..4].copy_from_slice(&header::MH_MAGIC_64.to_le_bytes());
524 data[4..8].copy_from_slice(&0x0100_0007_u32.to_le_bytes());
525 data[8..12].copy_from_slice(&3_u32.to_le_bytes());
526 data[12..16].copy_from_slice(&2_u32.to_le_bytes());
527 data[16..20].copy_from_slice(&(segments.len() as u32).to_le_bytes());
528 data[20..24].copy_from_slice(&(commands_size as u32).to_le_bytes());
529
530 for (index, (name, vmaddr, fileoff, filesize)) in segments.iter().enumerate() {
531 let offset = HEADER_SIZE + index * SEGMENT_COMMAND_SIZE;
532 data[offset..offset + 4].copy_from_slice(&0x19_u32.to_le_bytes());
533 data[offset + 4..offset + 8]
534 .copy_from_slice(&(SEGMENT_COMMAND_SIZE as u32).to_le_bytes());
535 data[offset + 8..offset + 8 + name.len()].copy_from_slice(name.as_bytes());
536 data[offset + 24..offset + 32].copy_from_slice(&vmaddr.to_le_bytes());
537 data[offset + 32..offset + 40].copy_from_slice(&0x1000_u64.to_le_bytes());
538 data[offset + 40..offset + 48].copy_from_slice(&fileoff.to_le_bytes());
539 data[offset + 48..offset + 56].copy_from_slice(&filesize.to_le_bytes());
540 }
541
542 let required_file_size = segments
543 .iter()
544 .filter_map(|(_, _, fileoff, filesize)| fileoff.checked_add(*filesize))
545 .filter_map(|end| usize::try_from(end).ok())
546 .max()
547 .unwrap_or(data.len());
548 data.resize(data.len().max(required_file_size), 0);
549
550 data
551 }
552
553 fn parse_thin_macho(data: &[u8]) -> MachO<'_> {
554 match Mach::parse(data).expect("test Mach-O must parse") {
555 Mach::Binary(macho) => macho,
556 Mach::Fat(_) => panic!("expected a thin Mach-O"),
557 }
558 }
559
560 #[test]
561 fn universal_header_recognition_is_bounded_and_supports_both_widths() {
562 assert!(is_universal_macho(&universal_header(
563 FAT_MAGIC_32,
564 2,
565 false,
566 false
567 )));
568 assert!(is_universal_macho(&universal_header(
569 FAT_CIGAM_32,
570 2,
571 true,
572 false
573 )));
574 assert!(is_universal_macho(&universal_header(
575 FAT_MAGIC_64,
576 1,
577 false,
578 true
579 )));
580 assert!(is_universal_macho(&universal_header(
581 FAT_CIGAM_64,
582 1,
583 true,
584 true
585 )));
586
587 assert!(!is_universal_macho(&FAT_MAGIC_32));
588 assert!(!is_universal_macho(&universal_header(
589 FAT_MAGIC_32,
590 0,
591 false,
592 false
593 )));
594 assert!(!is_universal_macho(&universal_header(
595 FAT_MAGIC_32,
596 (MAX_FAT_ARCHITECTURES + 1) as u32,
597 false,
598 false
599 )));
600 }
601
602 #[test]
603 fn preferred_base_uses_file_backed_text_segment() {
604 let data = macho_with_segments(&[
605 ("__PAGEZERO", 0, 0, 0),
606 ("__DATA", 0x9000, 0x1000, 0x100),
607 ("__TEXT", 0x1_0000_4000, 0x4000, 0x100),
608 ]);
609 let macho = parse_thin_macho(&data);
610
611 assert_eq!(preferred_base_address(&macho), Some(0x1_0000_0000));
612 }
613
614 #[test]
615 fn preferred_base_falls_back_to_lowest_file_backed_segment() {
616 let data = macho_with_segments(&[
617 ("__PAGEZERO", 0, 0, 0),
618 ("__LINKEDIT", 0xa000, 0x1000, 0x100),
619 ("__DATA", 0x9000, 0x1000, 0x100),
620 ]);
621 let macho = parse_thin_macho(&data);
622
623 assert_eq!(preferred_base_address(&macho), Some(0x8000));
624 }
625
626 #[test]
627 fn export_addresses_are_rebased_with_checked_arithmetic() {
628 assert_eq!(
629 resolved_export_address(0x1_0000_0000, 0x1234, true, "_entry").unwrap(),
630 0x1_0000_1234
631 );
632 assert_eq!(
633 resolved_export_address(0x1_0000_0000, 0x1234, false, "_absolute").unwrap(),
634 0x1234
635 );
636 assert!(resolved_export_address(u64::MAX, 1, true, "_overflow").is_err());
637 }
638
639 #[test]
640 fn reexports_retain_library_and_target_symbol() {
641 let mut output_budget = crate::formats::ParseOutputBudget::default();
642 assert_eq!(
643 macho_reexport_name(
644 &mut output_budget,
645 "/usr/lib/libSystem.B.dylib",
646 Some("_malloc"),
647 "_alloc"
648 )
649 .unwrap(),
650 "/usr/lib/libSystem.B.dylib:_malloc"
651 );
652 assert_eq!(
653 macho_reexport_name(
654 &mut output_budget,
655 "/usr/lib/libSystem.B.dylib",
656 None,
657 "_malloc"
658 )
659 .unwrap(),
660 "/usr/lib/libSystem.B.dylib:_malloc"
661 );
662 }
663}