use core::convert::TryFrom;
use goblin::mach::{
Mach, MachO,
load_command::{
CommandVariant, LC_VERSION_MIN_IPHONEOS, LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_TVOS,
LC_VERSION_MIN_WATCHOS, LinkeditDataCommand, PLATFORM_IOS, PLATFORM_MACOS, PLATFORM_TVOS,
PLATFORM_WATCHOS,
},
};
use core::marker::PhantomData;
use crate::{
block::BlockRuntime,
cfstring::CFStringRuntime,
codesign::Signature,
dataincode::DataInCodeIter,
dylib::{DylibIter, LoadCommandIter},
error::{Error, Result},
export::ExportIter,
fixup::{
BindIter, ChainedFixups, ChainedImport, RebaseIter, SegmentLoc, build_bind_iter,
build_rebase_iter,
},
import::{Import, ImportIter},
objc::ObjcRuntime,
segment::{SectionIter, SegmentIter},
swift::SwiftRuntime,
symbol::SymbolIter,
unwind::UnwindInfo,
util::{read_u32_le_at, read_uleb128, vm_to_file_offset_in},
};
const SIZEOF_BUILD_VERSION_COMMAND: usize = 24;
const SIZEOF_BUILD_TOOL_VERSION: usize = 8;
pub const CPU_SUBTYPE_ANY: u32 = u32::MAX;
#[derive(Debug)]
pub struct MachoBinary<'a> {
data: &'a [u8],
macho: MachO<'a>,
fat_arch_count: u32,
uuid: Option<[u8; 16]>,
min_os: Option<MinOsVersion>,
sdk_version: Option<Version>,
source_version: Option<SourceVersion>,
build_tools: Vec<BuildTool>,
dylinker: Option<&'a str>,
function_starts_cmd: Option<LinkeditDataCommand>,
function_starts_count: Option<u32>,
data_in_code_cmd: Option<LinkeditDataCommand>,
chained_fixups_cmd: Option<LinkeditDataCommand>,
code_signature_cmd: Option<LinkeditDataCommand>,
}
impl<'a> MachoBinary<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self> {
Self::parse_predicate(data, |_, _| true)
}
pub fn parse_with_arch(data: &'a [u8], cputype: u32, cpusubtype: u32) -> Result<Self> {
Self::parse_predicate(data, |actual_cputype, actual_subtype| {
arch_matches(actual_cputype, actual_subtype, cputype, cpusubtype)
})
}
fn parse_predicate(data: &'a [u8], pred: impl Fn(u32, u32) -> bool) -> Result<Self> {
match Mach::parse(data)? {
Mach::Binary(macho) => {
if pred(macho.header.cputype, macho.header.cpusubtype) {
Ok(Self::new(data, macho, 1))
} else {
Err(Error::NoMatchingArchSlice)
}
}
Mach::Fat(multi) => {
let fat_count = u32_from_usize(multi.narches)?;
for arch in multi.iter_arches() {
let Ok(arch) = arch else { continue };
let slice_bytes = arch.slice(data);
if slice_bytes.is_empty() {
continue;
}
let Ok(macho) = MachO::parse(slice_bytes, 0) else {
continue;
};
if pred(macho.header.cputype, macho.header.cpusubtype) {
return Ok(Self::new(slice_bytes, macho, fat_count));
}
}
Err(Error::NoMatchingArchSlice)
}
}
}
fn new(data: &'a [u8], macho: MachO<'a>, fat_arch_count: u32) -> Self {
let mut uuid = None;
let mut min_os: Option<MinOsVersion> = None;
let mut sdk_version: Option<Version> = None;
let mut source_version = None;
let mut build_tools: Vec<BuildTool> = Vec::new();
let mut dylinker = None;
let mut function_starts_cmd = None;
let mut data_in_code_cmd = None;
let mut chained_fixups_cmd = None;
let mut code_signature_cmd = None;
for lc in &macho.load_commands {
match &lc.command {
CommandVariant::Uuid(c) => {
uuid = Some(c.uuid);
}
CommandVariant::BuildVersion(c) => {
min_os = Some(MinOsVersion {
platform: c.platform,
version: Version::from_packed_u32(c.minos),
});
sdk_version = Some(Version::from_packed_u32(c.sdk));
let Some(base) = lc.offset.checked_add(SIZEOF_BUILD_VERSION_COMMAND) else {
continue;
};
for i in 0..(c.ntools as usize) {
let Some(stride) = i.checked_mul(SIZEOF_BUILD_TOOL_VERSION) else {
break;
};
let Some(off) = base.checked_add(stride) else {
break;
};
let Some(off_ver) = off.checked_add(4) else {
break;
};
let (Some(tool), Some(version_packed)) =
(read_u32_le_at(data, off), read_u32_le_at(data, off_ver))
else {
break;
};
build_tools.push(BuildTool {
tool,
version: Version::from_packed_u32(version_packed),
});
}
}
CommandVariant::VersionMinMacosx(c)
| CommandVariant::VersionMinIphoneos(c)
| CommandVariant::VersionMinTvos(c)
| CommandVariant::VersionMinWatchos(c) => {
if min_os.is_none() {
let platform = match c.cmd {
LC_VERSION_MIN_MACOSX => PLATFORM_MACOS,
LC_VERSION_MIN_IPHONEOS => PLATFORM_IOS,
LC_VERSION_MIN_TVOS => PLATFORM_TVOS,
LC_VERSION_MIN_WATCHOS => PLATFORM_WATCHOS,
_ => 0,
};
min_os = Some(MinOsVersion {
platform,
version: Version::from_packed_u32(c.version),
});
}
if sdk_version.is_none() && c.sdk != 0 {
sdk_version = Some(Version::from_packed_u32(c.sdk));
}
}
CommandVariant::SourceVersion(c) => {
source_version = Some(SourceVersion::from_packed_u64(c.version));
}
CommandVariant::LoadDylinker(c) => {
let name_off = c.name as usize;
let cmdsize = lc.command.cmdsize();
if let (Some(off), Some(end)) = (
lc.offset.checked_add(name_off),
lc.offset.checked_add(cmdsize),
) && let Some(s) = read_lc_str(data, off, end)
{
dylinker = Some(s);
}
}
CommandVariant::FunctionStarts(c) => {
function_starts_cmd = Some(*c);
}
CommandVariant::DataInCode(c) => {
data_in_code_cmd = Some(*c);
}
CommandVariant::DyldChainedFixups(c) => {
chained_fixups_cmd = Some(*c);
}
CommandVariant::CodeSignature(c) => {
code_signature_cmd = Some(*c);
}
_ => {}
}
}
let mut bin = Self {
data,
macho,
fat_arch_count,
uuid,
min_os,
sdk_version,
source_version,
build_tools,
dylinker,
function_starts_cmd,
function_starts_count: None,
data_in_code_cmd,
chained_fixups_cmd,
code_signature_cmd,
};
bin.function_starts_count = bin
.function_starts_cmd
.map(|_| u32::try_from(bin.function_starts().count()).unwrap_or(u32::MAX));
bin
}
pub fn fat_arch_count(&self) -> u32 {
self.fat_arch_count
}
pub fn raw(&self) -> &'a [u8] {
self.data
}
pub fn segments(&self) -> SegmentIter<'a, '_> {
SegmentIter::new(&self.macho.segments)
}
pub fn sections(&self) -> SectionIter<'a, '_> {
SectionIter::new(&self.macho.segments)
}
pub fn symbols(&self) -> SymbolIter<'a, '_> {
SymbolIter::new(self.macho.symbols.as_ref().map(|s| s.into_iter()))
}
pub fn dylibs(&self) -> DylibIter<'a, '_> {
DylibIter::new(self.data, &self.macho.load_commands)
}
pub fn load_commands(&self) -> LoadCommandIter<'a, '_> {
LoadCommandIter::new(self.data, &self.macho.load_commands)
}
pub fn function_starts(&self) -> FunctionStartIter<'a, '_> {
let Some(cmd) = self.function_starts_cmd else {
return FunctionStartIter::empty();
};
let dataoff = cmd.dataoff as usize;
let datasize = cmd.datasize as usize;
let Some(end) = dataoff.checked_add(datasize) else {
return FunctionStartIter::empty();
};
let Some(stream) = self.data.get(dataoff..end) else {
return FunctionStartIter::empty();
};
let Some(text_vmaddr) = self
.macho
.segments
.iter()
.find(|s| &s.segname[..7] == b"__TEXT\0")
.map(|s| s.vmaddr)
else {
return FunctionStartIter::empty();
};
FunctionStartIter {
stream,
cursor: 0,
running: text_vmaddr,
done: false,
_parent: PhantomData,
}
}
pub fn data_in_code(&self) -> DataInCodeIter<'a, '_> {
let Some(cmd) = self.data_in_code_cmd else {
return DataInCodeIter::empty();
};
let dataoff = cmd.dataoff as usize;
let Some(end) = dataoff.checked_add(cmd.datasize as usize) else {
return DataInCodeIter::empty();
};
let Some(stream) = self.data.get(dataoff..end) else {
return DataInCodeIter::empty();
};
let segments = self
.macho
.segments
.iter()
.map(|s| (s.vmaddr, s.vmsize, s.fileoff, s.filesize))
.collect();
DataInCodeIter::new(stream, self.macho.little_endian, segments)
}
pub fn unwind_info(&self) -> Option<UnwindInfo<'a>> {
let section = self.sections().find(|section| {
section.segname() == "__TEXT" && section.sectname() == "__unwind_info"
})?;
let image_base = self
.segments()
.find(|segment| segment.name() == "__TEXT")?
.vmaddr();
UnwindInfo::parse(section.body(), image_base)
}
pub fn exports(&self) -> ExportIter<'_> {
let items = self.macho.exports().unwrap_or_default();
ExportIter::new(items)
}
pub fn imports(&self) -> ImportIter<'_> {
let mut all: Vec<Import<'_>> = Vec::new();
if let Ok(items) = self.macho.imports() {
for g in items {
all.push(Import {
name: g.name,
dylib: g.dylib,
is_lazy: g.is_lazy,
is_weak: g.is_weak,
offset: g.offset,
size: g.size,
address: g.address,
addend: g.addend,
bind_offset: g.start_of_sequence_offset,
});
}
}
for b in self.chained_binds() {
let offset = self.vm_to_file_offset(b.vm_address()).unwrap_or(0);
all.push(Import {
name: b.name(),
dylib: b.dylib(),
is_lazy: false,
is_weak: b.is_weak(),
offset,
size: 8,
address: b.vm_address(),
addend: b.addend(),
bind_offset: 0,
});
}
ImportIter::new(all)
}
pub fn chained_fixups(&self) -> Option<ChainedFixups<'a>> {
let cmd = self.chained_fixups_cmd?;
ChainedFixups::parse(self.data, cmd.dataoff as usize, cmd.datasize as usize)
}
pub fn chained_rebases(&self) -> RebaseIter<'a> {
let Some(cf) = self.chained_fixups() else {
return RebaseIter::empty();
};
let (locs, image_base) = self.collect_chained_segments(&cf);
if locs.is_empty() {
return RebaseIter::empty();
}
build_rebase_iter(self.data, &cf, &locs, image_base)
}
pub fn chained_binds(&self) -> BindIter<'a> {
let Some(cf) = self.chained_fixups() else {
return BindIter::empty();
};
let (locs, image_base) = self.collect_chained_segments(&cf);
if locs.is_empty() {
return BindIter::empty();
}
let imports: Vec<ChainedImport<'a>> = cf.imports().collect();
let dylib_names: Vec<&'a str> = self.dylibs().map(|d| d.name).collect();
build_bind_iter(self.data, &cf, &locs, image_base, &imports, &dylib_names)
}
pub fn signature(&self) -> Option<Signature<'a>> {
let cmd = self.code_signature_cmd?;
Signature::parse(self.data, cmd.dataoff as usize)
}
pub fn objc(&self) -> Option<ObjcRuntime<'a>> {
ObjcRuntime::build(self)
}
pub fn cfstrings(&self) -> Option<CFStringRuntime<'a>> {
CFStringRuntime::build(self)
}
pub fn blocks(&self) -> Option<BlockRuntime<'a>> {
BlockRuntime::build(self)
}
pub fn swift(&self) -> Option<SwiftRuntime<'a>> {
SwiftRuntime::build(self)
}
fn collect_chained_segments(&self, cf: &ChainedFixups<'a>) -> (Vec<SegmentLoc>, u64) {
let image_base = self
.macho
.segments
.iter()
.filter(|s| s.filesize > 0)
.map(|s| s.vmaddr)
.min()
.unwrap_or(0);
let segs: Vec<_> = self.macho.segments.iter().collect();
let mut out: Vec<SegmentLoc> = Vec::new();
for chained in cf.segments() {
let idx = chained.seg_index as usize;
let Some(seg) = segs.get(idx) else { continue };
out.push(SegmentLoc {
chained,
vmaddr: seg.vmaddr,
fileoff: seg.fileoff,
filesize: seg.filesize,
});
}
(out, image_base)
}
pub fn vm_to_file_offset(&self, vmaddr: u64) -> Option<u64> {
vm_to_file_offset_in(
self.macho
.segments
.iter()
.map(|s| (s.vmaddr, s.vmsize, s.fileoff, s.filesize)),
vmaddr,
)
}
pub fn header(&self) -> Header<'_> {
Header {
raw: &self.macho.header,
is_64: self.macho.is_64,
uuid: self.uuid,
min_os: self.min_os,
sdk_version: self.sdk_version,
source_version: self.source_version,
build_tools: &self.build_tools,
dylinker: self.dylinker,
function_starts_count: self.function_starts_count,
}
}
}
pub struct FunctionStartIter<'a, 'p> {
stream: &'a [u8],
cursor: usize,
running: u64,
done: bool,
_parent: PhantomData<&'p ()>,
}
impl<'a, 'p> FunctionStartIter<'a, 'p> {
fn empty() -> Self {
Self {
stream: &[],
cursor: 0,
running: 0,
done: true,
_parent: PhantomData,
}
}
}
impl<'a, 'p> Iterator for FunctionStartIter<'a, 'p> {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.done {
return None;
}
let remaining = self.stream.get(self.cursor..)?;
let (delta, used) = read_uleb128(remaining)?;
if delta == 0 {
self.done = true;
return None;
}
let new_cursor = self.cursor.checked_add(used)?;
let new_running = self.running.checked_add(delta)?;
self.cursor = new_cursor;
self.running = new_running;
Some(self.running)
}
}
#[derive(Debug)]
pub struct Header<'p> {
raw: &'p goblin::mach::header::Header,
is_64: bool,
uuid: Option<[u8; 16]>,
min_os: Option<MinOsVersion>,
sdk_version: Option<Version>,
source_version: Option<SourceVersion>,
build_tools: &'p [BuildTool],
dylinker: Option<&'p str>,
function_starts_count: Option<u32>,
}
impl<'p> Header<'p> {
pub fn magic(&self) -> u32 {
self.raw.magic
}
pub fn cputype(&self) -> u32 {
self.raw.cputype
}
pub fn cpusubtype(&self) -> u32 {
self.raw.cpusubtype
}
pub fn filetype(&self) -> u32 {
self.raw.filetype
}
pub fn ncmds(&self) -> u32 {
self.raw.ncmds as u32
}
pub fn sizeofcmds(&self) -> u32 {
self.raw.sizeofcmds
}
pub fn flags(&self) -> u32 {
self.raw.flags
}
pub fn reserved(&self) -> u32 {
self.raw.reserved
}
pub fn is_64(&self) -> bool {
self.is_64
}
pub fn uuid(&self) -> Option<[u8; 16]> {
self.uuid
}
pub fn min_os(&self) -> Option<MinOsVersion> {
self.min_os
}
pub fn sdk_version(&self) -> Option<Version> {
self.sdk_version
}
pub fn source_version(&self) -> Option<SourceVersion> {
self.source_version
}
pub fn build_tools(&self) -> &'p [BuildTool] {
self.build_tools
}
pub fn dylinker(&self) -> Option<&'p str> {
self.dylinker
}
pub fn function_starts_count(&self) -> Option<u32> {
self.function_starts_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinOsVersion {
pub platform: u32,
pub version: Version,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Version {
pub major: u16,
pub minor: u8,
pub patch: u8,
}
impl Version {
pub fn from_packed_u32(v: u32) -> Self {
Self {
major: v.wrapping_shr(16) as u16,
minor: v.wrapping_shr(8) as u8,
patch: v as u8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceVersion {
pub a: u32,
pub b: u16,
pub c: u16,
pub d: u16,
pub e: u16,
}
impl SourceVersion {
pub fn from_packed_u64(v: u64) -> Self {
Self {
a: (v.wrapping_shr(40) & 0x00ff_ffff) as u32,
b: (v.wrapping_shr(30) & 0x3ff) as u16,
c: (v.wrapping_shr(20) & 0x3ff) as u16,
d: (v.wrapping_shr(10) & 0x3ff) as u16,
e: (v & 0x3ff) as u16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuildTool {
pub tool: u32,
pub version: Version,
}
fn arch_matches(
actual_cputype: u32,
actual_subtype: u32,
want_cputype: u32,
want_subtype: u32,
) -> bool {
if actual_cputype != want_cputype {
return false;
}
if want_subtype == CPU_SUBTYPE_ANY {
return true;
}
const CPU_SUBTYPE_MASK: u32 = 0xff00_0000;
(actual_subtype & !CPU_SUBTYPE_MASK) == (want_subtype & !CPU_SUBTYPE_MASK)
}
fn u32_from_usize(n: usize) -> Result<u32> {
u32::try_from(n).map_err(|_| Error::Structural(format!("count {n} exceeds u32")))
}
pub(crate) fn read_lc_str(data: &[u8], off: usize, end: usize) -> Option<&str> {
let limit = end.min(data.len());
let slice = data.get(off..limit)?;
let len = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
let body = slice.get(..len)?;
core::str::from_utf8(body).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_from_packed_decodes_max_minor_patch() {
let v = Version::from_packed_u32(0x000e_0502);
assert_eq!(v.major, 14);
assert_eq!(v.minor, 5);
assert_eq!(v.patch, 2);
}
#[test]
fn version_from_packed_handles_high_major() {
let v = Version::from_packed_u32(0x001a_0400);
assert_eq!(v.major, 26);
assert_eq!(v.minor, 4);
assert_eq!(v.patch, 0);
}
#[test]
fn version_from_packed_zero() {
let v = Version::from_packed_u32(0);
assert_eq!((v.major, v.minor, v.patch), (0, 0, 0));
}
#[test]
fn source_version_round_trip_packed_u64() {
let packed: u64 = (0x00AB_CDEFu64 << 40)
| (0x123u64 << 30)
| (0x234u64 << 20)
| (0x345u64 << 10)
| 0x3FFu64;
let sv = SourceVersion::from_packed_u64(packed);
assert_eq!(sv.a, 0x00AB_CDEF);
assert_eq!(sv.b, 0x123);
assert_eq!(sv.c, 0x234);
assert_eq!(sv.d, 0x345);
assert_eq!(sv.e, 0x3FF);
}
#[test]
fn arch_matches_strips_subtype_capability_bits() {
const ARM64: u32 = 0x0100_000c;
assert!(arch_matches(ARM64, 0x8100_0000, ARM64, 0));
assert!(!arch_matches(ARM64, 0, 0x0100_0007, 0));
assert!(arch_matches(ARM64, 0xdead_beef, ARM64, CPU_SUBTYPE_ANY));
}
#[test]
fn read_lc_str_truncates_at_nul() {
let data = b"abc\0def";
assert_eq!(read_lc_str(data, 0, data.len()), Some("abc"));
}
#[test]
fn read_lc_str_respects_end_limit() {
let data = b"abcdefghi";
assert_eq!(read_lc_str(data, 0, 5), Some("abcde"));
}
#[test]
fn read_lc_str_out_of_range_returns_none() {
let data = b"abc";
assert_eq!(read_lc_str(data, 99, 200), None);
}
}