Skip to content

Commit 2c8c703

Browse files
authored
Accept arm64e Mach-O images (#1403)
This PR extends the Mach-O loader to accept arm64e images, which are with Pointer Authentication Code (PAC), to deal with system binaries like `dyld`.
1 parent 5c7b31f commit 2c8c703

3 files changed

Lines changed: 110 additions & 25 deletions

File tree

litebox_common_macos/src/loader.rs

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ pub fn is_thin_arm64_macho_header(data: &[u8]) -> bool {
114114
header.is_little_endian()
115115
&& header.cputype(LE) == macho::CPU_TYPE_ARM64
116116
&& matches!(
117-
header.cpusubtype(LE),
118-
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8
117+
header.cpusubtype(LE) & !macho::CPU_SUBTYPE_MASK,
118+
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8 | macho::CPU_SUBTYPE_ARM64E
119119
)
120120
}
121121

@@ -215,13 +215,15 @@ impl MachoParsedFile {
215215
if !header.is_little_endian()
216216
|| header.cputype(LE) != macho::CPU_TYPE_ARM64
217217
|| !matches!(
218-
header.cpusubtype(LE),
219-
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8
218+
header.cpusubtype(LE) & !macho::CPU_SUBTYPE_MASK,
219+
macho::CPU_SUBTYPE_ARM64_ALL
220+
| macho::CPU_SUBTYPE_ARM64_V8
221+
| macho::CPU_SUBTYPE_ARM64E
220222
)
221223
|| header.filetype(LE) != macho::MH_EXECUTE
222224
{
223225
return Err(Unsupported(
224-
"unexpected file type or architecture (requires arm64, not arm64e)",
226+
"unexpected file type or architecture (requires arm64/arm64e, executable only)",
225227
));
226228
}
227229
if header.flags(LE) & (macho::MH_DYLDLINK | macho::MH_DYLIB_IN_CACHE) != 0 {
@@ -376,17 +378,14 @@ impl MachoParsedFile {
376378
}
377379
}
378380

379-
/// Return the container-relative range of the unique arm64 (not arm64e) slice.
381+
/// Return the container-relative range of the selected arm64 or arm64e slice.
380382
pub fn arm64_slice_range(data: &[u8]) -> Result<Range<usize>, MachoLoaderError> {
381383
let slice = arm64_slice(data)?;
382384
let offset = slice.as_ptr().addr() - data.as_ptr().addr();
383385
Ok(offset..offset + slice.len())
384386
}
385387

386-
/// Select the unique arm64 (not arm64e) slice, or return thin input unchanged.
387-
///
388-
/// Pass the returned slice to the parser and rewriter: their file offsets are
389-
/// slice-relative. Malformed universal headers are rejected, not treated as thin.
388+
/// Select an arm64 or arm64e slice, or return thin input unchanged.
390389
pub fn arm64_slice(data: &[u8]) -> Result<&[u8], MachoLoaderError> {
391390
use MachoLoaderError::{Invalid, Unsupported};
392391
const HEADER_SIZE: usize = size_of::<macho::FatHeader>();
@@ -442,7 +441,10 @@ fn select_arm64<'a, A: object::read::macho::FatArch>(
442441
table_end: usize,
443442
) -> Result<&'a [u8], MachoLoaderError> {
444443
use MachoLoaderError::{Invalid, Unsupported};
445-
let mut selected = None;
444+
let mut arm64 = None;
445+
let mut arm64e = None;
446+
let mut ambiguous_arm64 = false;
447+
let mut ambiguous_arm64e = false;
446448
for arch in arches {
447449
let offset = usize::try_from(arch.offset().into()).map_err(|_| Invalid("fat offset"))?;
448450
let size = usize::try_from(arch.size().into()).map_err(|_| Invalid("fat size"))?;
@@ -453,19 +455,26 @@ fn select_arm64<'a, A: object::read::macho::FatArch>(
453455
if offset < table_end || size == 0 || !offset.is_multiple_of(align) || end > data.len() {
454456
return Err(Invalid("fat slice bounds/alignment"));
455457
}
456-
if arch.cputype() == macho::CPU_TYPE_ARM64
457-
&& matches!(
458-
arch.cpusubtype(),
459-
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8
460-
)
461-
{
462-
if selected.is_some() {
463-
return Err(Invalid("ambiguous arm64 slices"));
458+
if arch.cputype() != macho::CPU_TYPE_ARM64 {
459+
continue;
460+
}
461+
let slice = &data[offset..end];
462+
match arch.cpusubtype() & !macho::CPU_SUBTYPE_MASK {
463+
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8 => {
464+
ambiguous_arm64 |= arm64.replace(slice).is_some();
465+
}
466+
macho::CPU_SUBTYPE_ARM64E => {
467+
ambiguous_arm64e |= arm64e.replace(slice).is_some();
464468
}
465-
selected = Some(&data[offset..end]);
469+
_ => {}
466470
}
467471
}
468-
selected.ok_or(Unsupported("universal binary has no supported arm64 slice"))
472+
if ambiguous_arm64 || (arm64.is_none() && ambiguous_arm64e) {
473+
return Err(Invalid("ambiguous arm64 slices"));
474+
}
475+
arm64
476+
.or(arm64e)
477+
.ok_or(Unsupported("universal binary has no supported arm64 slice"))
469478
}
470479

471480
#[cfg(test)]
@@ -549,6 +558,69 @@ mod tests {
549558
data
550559
}
551560

561+
fn image_with_subtype(subtype: u32) -> Vec<u8> {
562+
let mut data = image();
563+
put32(&mut data, offset_of!(Header, cpusubtype), subtype);
564+
data
565+
}
566+
567+
fn fat32(subtypes: &[u32]) -> Vec<u8> {
568+
use object::{endian::U32, pod::bytes_of};
569+
570+
let header = macho::FatHeader {
571+
magic: U32::new(BE, macho::FAT_MAGIC),
572+
nfat_arch: U32::new(BE, u32::try_from(subtypes.len()).unwrap()),
573+
};
574+
let mut fat = bytes_of(&header).to_vec();
575+
for (index, subtype) in subtypes.iter().copied().enumerate() {
576+
let offset = (index + 1) * PAGE_SIZE;
577+
let arch = macho::FatArch32 {
578+
cputype: U32::new(BE, macho::CPU_TYPE_ARM64),
579+
cpusubtype: U32::new(BE, subtype),
580+
offset: U32::new(BE, u32::try_from(offset).unwrap()),
581+
size: U32::new(BE, u32::try_from(PAGE_SIZE).unwrap()),
582+
align: U32::new(BE, PAGE_SIZE.ilog2()),
583+
};
584+
fat.extend_from_slice(bytes_of(&arch));
585+
}
586+
for subtype in subtypes {
587+
fat.resize(fat.len().next_multiple_of(PAGE_SIZE), 0);
588+
fat.extend_from_slice(&image_with_subtype(*subtype));
589+
}
590+
fat
591+
}
592+
593+
#[test]
594+
fn mixed_arm64_fat_binaries_prefer_plain_arm64() {
595+
for subtypes in [
596+
[macho::CPU_SUBTYPE_ARM64E, macho::CPU_SUBTYPE_ARM64_ALL],
597+
[macho::CPU_SUBTYPE_ARM64_V8, macho::CPU_SUBTYPE_ARM64E],
598+
] {
599+
let fat = fat32(&subtypes);
600+
let plain_index = subtypes
601+
.iter()
602+
.position(|subtype| *subtype != macho::CPU_SUBTYPE_ARM64E)
603+
.unwrap();
604+
let start = (plain_index + 1) * PAGE_SIZE;
605+
assert_eq!(arm64_slice_range(&fat).unwrap(), start..start + PAGE_SIZE);
606+
}
607+
608+
assert!(matches!(
609+
arm64_slice(&fat32(&[
610+
macho::CPU_SUBTYPE_ARM64_ALL,
611+
macho::CPU_SUBTYPE_ARM64_V8,
612+
])),
613+
Err(MachoLoaderError::Invalid("ambiguous arm64 slices"))
614+
));
615+
assert!(matches!(
616+
arm64_slice(&fat32(&[
617+
macho::CPU_SUBTYPE_ARM64E,
618+
macho::CPU_SUBTYPE_ARM64E,
619+
])),
620+
Err(MachoLoaderError::Invalid("ambiguous arm64 slices"))
621+
));
622+
}
623+
552624
#[test]
553625
fn universal_slices_are_bounded_and_architecture_specific() {
554626
use object::{
@@ -623,7 +695,7 @@ mod tests {
623695
}
624696
for (offset, value) in [
625697
(COUNT, u32::MAX), // table multiplication / bounds
626-
(HEADER_SIZE + subtype, macho::CPU_SUBTYPE_ARM64E),
698+
(HEADER_SIZE + subtype, u32::MAX),
627699
(HEADER_SIZE + offset_field, u32::MAX), // slice past EOF
628700
(HEADER_SIZE + alignment, usize::BITS), // alignment shift overflow
629701
] {

litebox_syscall_rewriter/src/macho.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,12 @@ fn parse_image(input: &[u8]) -> Result<Option<CodeMetadata>> {
215215
let header = macho::MachHeader64::<LE>::parse(&*bytes, 0).map_err(parse_error)?;
216216
if header.cputype(LE) != macho::CPU_TYPE_ARM64
217217
|| !matches!(
218-
header.cpusubtype(LE),
219-
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8
218+
header.cpusubtype(LE) & !macho::CPU_SUBTYPE_MASK,
219+
macho::CPU_SUBTYPE_ARM64_ALL | macho::CPU_SUBTYPE_ARM64_V8 | macho::CPU_SUBTYPE_ARM64E
220220
)
221221
{
222222
return Err(Error::UnsupportedExecutable(
223-
"Mach-O requires AArch64, not arm64e".into(),
223+
"Mach-O requires AArch64 or arm64e".into(),
224224
));
225225
}
226226
if header.filetype(LE) == macho::MH_OBJECT {

litebox_syscall_rewriter/tests/macho_tests.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,19 @@ fn footer(bytes: &[u8]) -> (usize, u64, usize) {
107107
)
108108
}
109109

110+
#[test]
111+
fn arm64e_images_and_capability_bits_are_supported() {
112+
for subtype in [
113+
macho::CPU_SUBTYPE_ARM64E,
114+
macho::CPU_SUBTYPE_ARM64E | macho::CPU_SUBTYPE_PTRAUTH_ABI,
115+
] {
116+
let mut input = image(&[SVC]);
117+
put32(&mut input, 8, subtype);
118+
CodeMetadata::parse(&input).unwrap();
119+
hook_syscalls_in_macho(&input, None).unwrap();
120+
}
121+
}
122+
110123
#[test]
111124
fn load_time_rewriting_uses_mapping_addresses_and_finalizes_gates() {
112125
let rewriter = Rewriter::new(TargetHost::MacOs).unwrap();

0 commit comments

Comments
 (0)