use crate::{
objc::ObjcRuntime,
ptrauth::strip_signature,
util::{read_i32_le_at, read_u32_le_at, relative_pointer},
};
const METHOD_LIST_FLAG_MASK: u32 = 0xffff_0003;
const SMALL_METHOD_LIST_FLAG: u32 = 0x8000_0000;
const RELATIVE_METHOD_SELECTORS_ARE_DIRECT_FLAG: u32 = 0x4000_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodKind {
Legacy,
SmallIndirect,
SmallDirect,
}
#[derive(Debug, Clone)]
pub struct Method<'a> {
selector: &'a str,
types: &'a str,
imp: Option<u64>,
kind: MethodKind,
}
impl<'a> Method<'a> {
pub fn selector(&self) -> &'a str {
self.selector
}
pub fn types(&self) -> &'a str {
self.types
}
pub fn implementation(&self) -> Option<u64> {
self.imp
}
pub fn kind(&self) -> MethodKind {
self.kind
}
pub fn is_small(&self) -> bool {
matches!(
self.kind,
MethodKind::SmallIndirect | MethodKind::SmallDirect
)
}
}
pub struct MethodIter<'a, 'p> {
rt: &'p ObjcRuntime<'a>,
layout: Option<MethodListLayout>,
cursor: u32,
}
#[derive(Debug, Clone, Copy)]
struct MethodListLayout {
base_va: u64,
entsize: u32,
count: u32,
kind: MethodKind,
}
impl<'a, 'p> MethodIter<'a, 'p> {
pub(crate) fn empty(rt: &'p ObjcRuntime<'a>) -> Self {
Self {
rt,
layout: None,
cursor: 0,
}
}
}
impl<'a, 'p> Iterator for MethodIter<'a, 'p> {
type Item = Method<'a>;
fn next(&mut self) -> Option<Self::Item> {
let layout = self.layout?;
loop {
if self.cursor >= layout.count {
return None;
}
let i = self.cursor;
self.cursor = self.cursor.checked_add(1)?;
let entry_off = u64::from(i).checked_mul(u64::from(layout.entsize))?;
let entry_va = layout.base_va.checked_add(entry_off)?;
if let Some(m) = decode_method(self.rt, entry_va, layout.kind) {
return Some(m);
}
#[cfg(feature = "tracing")]
tracing::debug!(
"darwinscope::objc: method row at 0x{:x} (idx={}) skipped - decode failed",
entry_va,
i
);
}
}
}
pub(crate) fn method_list_iter<'a, 'p>(
rt: &'p ObjcRuntime<'a>,
list_va: u64,
) -> MethodIter<'a, 'p> {
if list_va == 0 || (list_va & 0x1) != 0 {
if (list_va & 0x1) != 0 {
#[cfg(feature = "tracing")]
tracing::debug!(
"darwinscope::objc: method list at 0x{:x} has list-of-lists low bit - skipped (runtime-only)",
list_va,
);
}
return MethodIter::empty(rt);
}
let Some(header) = rt.read_bytes(list_va, 8) else {
return MethodIter::empty(rt);
};
let Some(entsize_and_flags) = read_u32_le_at(header, 0) else {
return MethodIter::empty(rt);
};
let Some(count) = read_u32_le_at(header, 4) else {
return MethodIter::empty(rt);
};
let entsize = entsize_and_flags & !METHOD_LIST_FLAG_MASK;
if entsize == 0 {
return MethodIter::empty(rt);
}
let is_small = (entsize_and_flags & SMALL_METHOD_LIST_FLAG) != 0;
let is_direct = (entsize_and_flags & RELATIVE_METHOD_SELECTORS_ARE_DIRECT_FLAG) != 0;
let kind = if is_small {
if is_direct {
MethodKind::SmallDirect
} else {
MethodKind::SmallIndirect
}
} else {
MethodKind::Legacy
};
let min_entsize = match kind {
MethodKind::Legacy => 24,
MethodKind::SmallIndirect | MethodKind::SmallDirect => 12,
};
if entsize < min_entsize {
#[cfg(feature = "tracing")]
tracing::debug!(
"darwinscope::objc: method list at 0x{:x} has entsize={} (< {}) - skipped",
list_va,
entsize,
min_entsize,
);
return MethodIter::empty(rt);
}
let base_va = match list_va.checked_add(8) {
Some(v) => v,
None => return MethodIter::empty(rt),
};
MethodIter {
rt,
layout: Some(MethodListLayout {
base_va,
entsize,
count,
kind,
}),
cursor: 0,
}
}
fn decode_method<'a>(rt: &ObjcRuntime<'a>, entry_va: u64, kind: MethodKind) -> Option<Method<'a>> {
match kind {
MethodKind::Legacy => decode_legacy_method(rt, entry_va),
MethodKind::SmallIndirect => decode_small_method(rt, entry_va, false),
MethodKind::SmallDirect => decode_small_method(rt, entry_va, true),
}
}
fn decode_legacy_method<'a>(rt: &ObjcRuntime<'a>, entry_va: u64) -> Option<Method<'a>> {
rt.read_bytes(entry_va, 24)?;
let sel_va = rt.resolve_pointer(entry_va)?;
let types_va = rt.resolve_pointer(entry_va.checked_add(8)?)?;
let imp_va = rt.resolve_pointer(entry_va.checked_add(16)?).unwrap_or(0);
let selector = rt.read_cstr(sel_va)?;
let types = rt.read_cstr(types_va).unwrap_or("");
let imp = if imp_va == 0 { None } else { Some(imp_va) };
Some(Method {
selector,
types,
imp,
kind: MethodKind::Legacy,
})
}
fn decode_small_method<'a>(
rt: &ObjcRuntime<'a>,
entry_va: u64,
direct_selectors: bool,
) -> Option<Method<'a>> {
let bytes = rt.read_bytes(entry_va, 12)?;
let name_off = read_i32_le_at(bytes, 0)?;
let types_off = read_i32_le_at(bytes, 4)?;
let imp_off = read_i32_le_at(bytes, 8)?;
let name_slot_va = entry_va;
let types_slot_va = entry_va.checked_add(4)?;
let imp_slot_va = entry_va.checked_add(8)?;
let name_target = relative_pointer(name_slot_va, name_off);
let types_target = relative_pointer(types_slot_va, types_off);
let imp_target = relative_pointer(imp_slot_va, imp_off);
let selector = if direct_selectors {
rt.read_cstr(name_target)?
} else {
let sel_va = rt.resolve_pointer(name_target)?;
rt.read_cstr(sel_va)?
};
let types = rt.read_cstr(types_target).unwrap_or("");
let imp = if imp_off == 0 {
None
} else {
Some(strip_signature(imp_target))
};
Some(Method {
selector,
types,
imp,
kind: if direct_selectors {
MethodKind::SmallDirect
} else {
MethodKind::SmallIndirect
},
})
}