Files
2026-03-11 00:01:54 -06:00

2032 lines
54 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cpp
import (
"encoding/binary"
"fmt"
"slices"
"sort"
"strings"
"unicode/utf8"
"github.com/blacktop/arm64-cgo/disassemble"
"github.com/blacktop/go-macho"
"github.com/blacktop/go-macho/types"
)
func normalizeEntryID(entry string) string {
switch strings.TrimSpace(entry) {
case "", "kernel", "__kernel__", kernelBundleName:
return kernelBundleName
default:
return entry
}
}
func validKernelPointer(ptr uint64) bool {
return ptr >= kernelAddrFloor
}
func validMetaPointer(ptr uint64) bool {
return ptr >= kernelAddrFloor && ptr%8 == 0
}
func addSignedOffset(addr uint64, offset int64) (uint64, bool) {
if offset >= 0 {
return addr + uint64(offset), true
}
neg := uint64(-offset)
if neg > addr {
return 0, false
}
return addr - neg, true
}
func decodeBLTarget(addr uint64, instr uint32) (uint64, bool) {
if (instr >> 26) != 0b100101 {
return 0, false
}
imm26 := int64(instr & 0x03ffffff)
if imm26&(1<<25) != 0 {
imm26 |= ^int64(0x03ffffff)
}
return addSignedOffset(addr, imm26<<2)
}
func decodeBTarget(addr uint64, instr uint32) (uint64, bool) {
if (instr >> 26) != 0b000101 {
return 0, false
}
imm26 := int64(instr & 0x03ffffff)
if imm26&(1<<25) != 0 {
imm26 |= ^int64(0x03ffffff)
}
return addSignedOffset(addr, imm26<<2)
}
func decodeADRPImmediate(pc uint64, raw uint32) (uint64, bool) {
immlo := int64((raw >> 29) & 0x3)
immhi := int64((raw >> 5) & 0x7ffff)
imm := (immhi << 2) | immlo
if (imm & (int64(1) << 20)) != 0 {
imm |= ^((int64(1) << 21) - 1)
}
offset := imm << 12
base := pc & ^uint64(0xfff)
if offset >= 0 {
return base + uint64(offset), true
}
return base - uint64(-offset), true
}
func isArm64Nop(instr uint32) bool {
switch instr {
case 0xd503201f, 0xd503205f, 0xd503207f, 0xd50320bf, 0xd50320ff:
return true
default:
return false
}
}
func isPacibsp(raw uint32) bool {
return raw == 0xd503237f
}
func (s *Scanner) decodeArm64Instruction(addr uint64, raw uint32, inst *disassemble.Inst) error {
return s.decoder.DecomposeInto(addr, raw, inst)
}
func operandCount(inst *disassemble.Inst) int {
if inst == nil {
return 0
}
return int(inst.NumOps)
}
func instPtr(ok bool, inst *disassemble.Inst) *disassemble.Inst {
if !ok {
return nil
}
return inst
}
func operandRegisterCount(op *disassemble.Op) int {
if op == nil {
return 0
}
return int(op.NumRegisters)
}
func operandRegister(op *disassemble.Op, idx int) (disassemble.Register, bool) {
if idx < 0 || idx >= operandRegisterCount(op) {
return 0, false
}
return op.Registers[idx], true
}
func operandHasRegister(op *disassemble.Op, reg disassemble.Register) bool {
if op == nil {
return false
}
for idx := 0; idx < int(op.NumRegisters); idx++ {
if op.Registers[idx] == reg {
return true
}
}
return false
}
func trackedStaticAddress(regBase [31]uint64, op *disassemble.Op) (uint64, bool) {
if op == nil {
return 0, false
}
if op.Class == disassemble.LABEL {
return op.GetImmediate(), true
}
baseReg, ok := operandRegister(op, 0)
if !ok {
return 0, false
}
baseIdx, ok := registerToIndex(baseReg)
if !ok || baseIdx >= 31 || regBase[baseIdx] == 0 {
return 0, false
}
switch op.Class {
case disassemble.MEM_OFFSET, disassemble.MEM_PRE_IDX, disassemble.MEM_POST_IDX:
return addSignedOffset(regBase[baseIdx], int64(op.GetImmediate()))
default:
return 0, false
}
}
func operandIsSPPreIndex(op *disassemble.Op) bool {
baseReg, ok := operandRegister(op, 0)
return ok && op.Class == disassemble.MEM_PRE_IDX && baseReg == disassemble.REG_SP
}
func isFrameSetupInstruction(inst *disassemble.Inst) bool {
if inst == nil {
return false
}
switch inst.Operation {
case disassemble.ARM64_PACIBSP:
return true
case disassemble.ARM64_STP:
if operandCount(inst) < 3 {
return false
}
return operandHasRegister(&inst.Operands[0], disassemble.REG_X29) &&
operandHasRegister(&inst.Operands[1], disassemble.REG_X30) &&
operandIsSPPreIndex(&inst.Operands[2])
case disassemble.ARM64_SUB:
if operandCount(inst) < 3 {
return false
}
return operandHasRegister(&inst.Operands[0], disassemble.REG_SP) &&
operandHasRegister(&inst.Operands[1], disassemble.REG_SP) &&
inst.Operands[2].Class == disassemble.IMM64 &&
inst.Operands[2].GetImmediate() > 0
default:
return false
}
}
type staticValueTrackOptions struct {
acceptAnyLoadAddr bool
propagateLoadAddrInAdd bool
handleLoadPairs bool
}
func (s *Scanner) trackStaticValueInstruction(owner *macho.File, regBase, regLoadAddr, regValue *[31]uint64, ins *disassemble.Inst, opts staticValueTrackOptions) {
if ins == nil || operandCount(ins) == 0 {
return
}
switch ins.Operation {
case disassemble.ARM64_ADD:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
srcReg, srcOK := operandRegister(&ins.Operands[1], 0)
if operandCount(ins) < 3 || !dstOK || !srcOK {
return
}
dstIdx, dstOK := registerToIndex(dstReg)
srcIdx, srcOK := registerToIndex(srcReg)
if !dstOK || !srcOK || dstIdx >= 31 || srcIdx >= 31 {
return
}
srcBase := regBase[srcIdx]
srcLoadAddr := regLoadAddr[srcIdx]
srcValue := regValue[srcIdx]
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
if srcBase != 0 {
if addr, ok := addSignedOffset(srcBase, int64(ins.Operands[2].GetImmediate())); ok {
regBase[dstIdx] = addr
regValue[dstIdx] = addr
}
return
}
if opts.propagateLoadAddrInAdd && srcLoadAddr != 0 {
if addr, ok := addSignedOffset(srcLoadAddr, int64(ins.Operands[2].GetImmediate())); ok {
regLoadAddr[dstIdx] = addr
}
return
}
if srcValue != 0 {
if value, ok := addSignedOffset(srcValue, int64(ins.Operands[2].GetImmediate())); ok {
regValue[dstIdx] = value
}
}
case disassemble.ARM64_MOV:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if !dstOK {
return
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
return
}
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
if operandCount(ins) <= 1 {
return
}
srcReg, srcOK := operandRegister(&ins.Operands[1], 0)
if !srcOK {
regValue[dstIdx] = ins.Operands[1].GetImmediate()
return
}
srcIdx, srcOK := registerToIndex(srcReg)
if !srcOK || srcIdx >= 31 {
return
}
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
regValue[dstIdx] = regValue[srcIdx]
case disassemble.ARM64_ORR:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if operandCount(ins) < 3 || !dstOK {
return
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
return
}
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
reg1, reg1OK := operandRegister(&ins.Operands[1], 0)
reg2, reg2OK := operandRegister(&ins.Operands[2], 0)
switch {
case reg1OK && reg2OK && (reg1 == disassemble.REG_XZR || reg1 == disassemble.REG_WZR):
if srcIdx, ok := registerToIndex(reg2); ok && srcIdx < 31 {
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
regValue[dstIdx] = regValue[srcIdx]
}
case reg1OK && reg2OK && (reg2 == disassemble.REG_XZR || reg2 == disassemble.REG_WZR):
if srcIdx, ok := registerToIndex(reg1); ok && srcIdx < 31 {
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
regValue[dstIdx] = regValue[srcIdx]
}
}
case disassemble.ARM64_LDR, disassemble.ARM64_LDUR:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if operandCount(ins) < 2 || !dstOK {
return
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
return
}
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
addr, ok := trackedStaticAddress(*regBase, &ins.Operands[1])
if !ok {
return
}
regLoadAddr[dstIdx] = addr
if ptr, ok := s.resolvePointerAt(owner, addr); ok {
regValue[dstIdx] = ptr
return
}
if opts.acceptAnyLoadAddr || validKernelPointer(addr) {
regValue[dstIdx] = addr
}
case disassemble.ARM64_LDP:
if !opts.handleLoadPairs {
return
}
if operandCount(ins) < 3 {
return
}
if reg, ok := operandRegister(&ins.Operands[0], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
}
}
if reg, ok := operandRegister(&ins.Operands[1], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0
}
}
addr, ok := trackedStaticAddress(*regBase, &ins.Operands[2])
if !ok {
return
}
if reg, ok := operandRegister(&ins.Operands[0], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regLoadAddr[dstIdx] = addr
if ptr, ok := s.resolvePointerAt(owner, addr); ok {
regValue[dstIdx] = ptr
} else if opts.acceptAnyLoadAddr || validKernelPointer(addr) {
regValue[dstIdx] = addr
}
}
}
if reg, ok := operandRegister(&ins.Operands[1], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
if addr2, ok := addSignedOffset(addr, 8); ok {
regLoadAddr[dstIdx] = addr2
if ptr, ok := s.resolvePointerAt(owner, addr2); ok {
regValue[dstIdx] = ptr
} else if opts.acceptAnyLoadAddr || validKernelPointer(addr2) {
regValue[dstIdx] = addr2
}
}
}
}
}
}
func findFunctionStartInSection(sectionAddr uint64, data []byte, targetAddr uint64) (uint64, error) {
if targetAddr < sectionAddr || targetAddr >= sectionAddr+uint64(len(data)) {
return 0, fmt.Errorf("target %#x not inside section [%#x, %#x)", targetAddr, sectionAddr, sectionAddr+uint64(len(data)))
}
cursor := int(targetAddr - sectionAddr)
cursor -= cursor % 4
const maxScan = 256
var decoder disassemble.Decoder
for steps := 0; steps < maxScan && cursor >= 0; steps++ {
if cursor+4 > len(data) {
break
}
raw := binary.LittleEndian.Uint32(data[cursor : cursor+4])
addr := sectionAddr + uint64(cursor)
if isPacibsp(raw) {
return addr, nil
}
var inst disassemble.Inst
if err := decoder.DecomposeInto(addr, raw, &inst); err == nil && isFrameSetupInstruction(&inst) {
return addr, nil
}
cursor -= 4
}
return 0, fmt.Errorf("no prologue found before %#x", targetAddr)
}
func (s *Scanner) includeEntry(entry string) bool {
if len(s.cfg.Entries) == 0 {
return true
}
norm := normalizeEntryID(entry)
for _, candidate := range s.cfg.Entries {
want := normalizeEntryID(candidate)
if norm == want || strings.EqualFold(entry, candidate) || strings.HasSuffix(strings.ToLower(norm), strings.ToLower(candidate)) {
return true
}
}
return false
}
func (s *Scanner) buildTargets() ([]scanTarget, error) {
targets := make([]scanTarget, 0, 1+len(s.root.FileSets()))
if s.root.FileHeader.Type != types.MH_FILESET {
// Non-fileset: the root file IS the kernel.
if s.includeEntry(kernelBundleName) {
targets = append(targets, scanTarget{file: s.root, entryID: kernelBundleName})
}
return targets, nil
}
// MH_FILESET: each fileset entry (including com.apple.kernel)
// is a separate Mach-O scoped to its own code. Using the root
// file as a kernel target would scan every kext's functions and
// __mod_init_func pointers, mis-attributing them to com.apple.kernel.
for _, fs := range s.root.FileSets() {
entryID := normalizeEntryID(fs.EntryID)
if !s.includeEntry(entryID) {
continue
}
m, err := s.root.GetFileSetFileByName(fs.EntryID)
if err != nil {
return nil, fmt.Errorf("open fileset entry %s: %w", fs.EntryID, err)
}
s.fileEntries[m] = entryID
targets = append(targets, scanTarget{file: m, entryID: entryID})
}
return targets, nil
}
func (s *Scanner) buildVMRangeIndex() {
ranges := make([]vmRangeOwner, 0, len(s.root.Sections)+len(s.root.Segments()))
appendFileRanges := func(m *macho.File) {
if m == nil {
return
}
for _, seg := range m.Segments() {
if seg == nil || seg.Filesz == 0 || seg.Addr == 0 {
continue
}
ranges = append(ranges, vmRangeOwner{
start: seg.Addr,
end: seg.Addr + seg.Memsz,
file: m,
})
}
for _, sec := range m.Sections {
if sec == nil || sec.Size == 0 {
continue
}
ranges = append(ranges, vmRangeOwner{
start: sec.Addr,
end: sec.Addr + sec.Size,
file: m,
})
}
}
for _, target := range s.targets {
if target.file == nil || target.file == s.root {
continue
}
appendFileRanges(target.file)
}
appendFileRanges(s.root)
sort.SliceStable(ranges, func(i, j int) bool {
if ranges[i].start != ranges[j].start {
return ranges[i].start < ranges[j].start
}
return ranges[i].end > ranges[j].end
})
s.vmRanges = ranges
}
func (s *Scanner) fileForVMAddr(addr uint64) *macho.File {
if s.root == nil {
return nil
}
if len(s.vmRanges) == 0 {
s.buildVMRangeIndex()
}
idx := sort.Search(len(s.vmRanges), func(i int) bool {
return s.vmRanges[i].start > addr
})
for i := idx - 1; i >= 0; i-- {
rng := s.vmRanges[i]
if rng.end <= addr {
break
}
if rng.start <= addr && addr < rng.end {
return rng.file
}
}
return nil
}
func (s *Scanner) pointerLookupOwner(owner *macho.File, addr uint64) (*macho.File, pointerOwnerSource) {
if owner != nil && fileOwnsVMAddr(owner, addr) {
return owner, pointerOwnerSourceProvided
}
if s.root != nil {
if resolved := s.fileForVMAddr(addr); resolved != nil {
return resolved, pointerOwnerSourceIndexed
}
}
if owner != nil {
return owner, pointerOwnerSourceOwnerFallback
}
return s.root, pointerOwnerSourceRootFallback
}
func (s *Scanner) entryForFile(m *macho.File) string {
if entry, ok := s.fileEntries[m]; ok {
return entry
}
if m == s.root {
return kernelBundleName
}
return ""
}
func (s *Scanner) readSectionData(m *macho.File, sec *types.Section) ([]byte, error) {
key := sectionKey{file: m, addr: sec.Addr}
if data, ok := s.sectionData[key]; ok {
return data, nil
}
data, err := sec.Data()
if err != nil {
return nil, err
}
s.sectionData[key] = data
return data, nil
}
func (s *Scanner) functionsForFile(m *macho.File) ([]types.Function, error) {
if funcs, ok := s.functions[m]; ok {
return funcs, nil
}
funcs := m.GetFunctions()
if len(funcs) == 0 {
var err error
funcs, err = m.GenerateFunctionStarts()
if err != nil {
return nil, err
}
}
sort.Slice(funcs, func(i, j int) bool {
return funcs[i].StartAddr < funcs[j].StartAddr
})
s.functions[m] = funcs
return funcs, nil
}
func (s *Scanner) functionForAddr(m *macho.File, addr uint64) (types.Function, error) {
funcs, err := s.functionsForFile(m)
if err == nil {
idx := sort.Search(len(funcs), func(i int) bool {
return funcs[i].StartAddr > addr
})
if idx > 0 {
fn := funcs[idx-1]
if addr >= fn.StartAddr && addr < fn.EndAddr {
return fn, nil
}
}
}
sec := m.FindSectionForVMAddr(addr)
if sec == nil {
return types.Function{}, fmt.Errorf("address %#x not inside executable section", addr)
}
data, err := s.readSectionData(m, sec)
if err != nil {
return types.Function{}, err
}
start, err := findFunctionStartInSection(sec.Addr, data, addr)
if err != nil {
return types.Function{}, err
}
funcs, err = s.functionsForFile(m)
if err != nil {
return types.Function{}, err
}
end := sec.Addr + sec.Size
idx := sort.Search(len(funcs), func(i int) bool {
return funcs[i].StartAddr > start
})
if idx < len(funcs) {
end = funcs[idx].StartAddr
}
fn := types.Function{StartAddr: start, EndAddr: end}
funcs = append(funcs, fn)
sort.Slice(funcs, func(i, j int) bool {
return funcs[i].StartAddr < funcs[j].StartAddr
})
s.functions[m] = slices.CompactFunc(funcs, func(a, b types.Function) bool {
return a.StartAddr == b.StartAddr && a.EndAddr == b.EndAddr
})
delete(s.callerIndex, m)
delete(s.pointerIndex, m)
return fn, nil
}
func (s *Scanner) functionForAddrInAnyFile(preferred *macho.File, addr uint64) (types.Function, *macho.File, error) {
if preferred != nil {
if fn, err := s.functionForAddr(preferred, addr); err == nil {
return fn, preferred, nil
}
}
if owner := s.fileForVMAddr(addr); owner != nil && owner != preferred {
if fn, err := s.functionForAddr(owner, addr); err == nil {
return fn, owner, nil
}
}
return types.Function{}, nil, fmt.Errorf("address %#x not inside known function", addr)
}
func (s *Scanner) functionDataFor(m *macho.File, fn types.Function) ([]byte, error) {
key := fileFuncKey{file: m, start: fn.StartAddr}
if data, ok := s.functionData[key]; ok {
return data, nil
}
if fn.EndAddr > fn.StartAddr {
if sec := m.FindSectionForVMAddr(fn.StartAddr); sec != nil {
if fn.EndAddr <= sec.Addr+sec.Size {
if data, err := s.readSectionData(m, sec); err == nil {
start := int(fn.StartAddr - sec.Addr)
end := int(fn.EndAddr - sec.Addr)
if start >= 0 && end <= len(data) && start < end {
window := data[start:end:end]
s.functionData[key] = window
return window, nil
}
}
}
}
}
data, err := m.GetFunctionData(fn)
if err != nil {
return nil, err
}
s.functionData[key] = data
return data, nil
}
func getCStringFromAny(root *macho.File, owner *macho.File, addr uint64) (string, error) {
if owner != nil {
if str, err := owner.GetCString(addr); err == nil && str != "" {
return str, nil
}
}
return root.GetCString(addr)
}
func (s *Scanner) cachedCStringAt(addr uint64) (string, error) {
if addr == 0 {
return "", fmt.Errorf("zero cstring address")
}
if cached, ok := s.nameStrings[addr]; ok {
if !cached.ok {
return "", fmt.Errorf("cached cstring miss at %#x", addr)
}
return cached.value, nil
}
value, err := getCStringFromAny(s.root, s.fileForVMAddr(addr), addr)
s.nameStrings[addr] = cachedCString{value: value, ok: err == nil && value != ""}
return value, err
}
func looksLikeRecoveredClassName(name string) bool {
if name == "" || !utf8.ValidString(name) {
return false
}
first := true
hasUpper := false
leadingUnderscore := false
for _, r := range name {
if r < 0x20 || r == 0x7f {
return false
}
if first {
first = false
leadingUnderscore = r == '_'
}
switch {
case r >= 'a' && r <= 'z':
case r >= 'A' && r <= 'Z':
hasUpper = true
case r >= '0' && r <= '9':
case r == '_', r == ':', r == '<', r == '>', r == '*', r == '·':
default:
return false
}
}
return hasUpper || leadingUnderscore || looksLikeRecoveredLowercaseClassName(name)
}
func looksLikeRecoveredLowercaseClassName(name string) bool {
switch {
case strings.HasPrefix(name, "com_"):
return true
case strings.HasSuffix(name, "_t"):
return true
case name == "cache":
return true
case strings.HasSuffix(name, "_init"),
strings.HasSuffix(name, "_bootstrap"),
strings.HasSuffix(name, "_event"):
return false
default:
return false
}
}
func recoveredClassNameScore(name string) int {
if name == "" {
return 0
}
if strings.HasPrefix(name, "UnknownClass_") {
return 1
}
if !looksLikeRecoveredClassName(name) {
return 1
}
score := 2
if strings.ContainsAny(name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ:<>*·") {
score++
}
switch {
case strings.HasSuffix(name, "_init"),
strings.HasSuffix(name, "_bootstrap"),
strings.HasSuffix(name, "_event"):
score--
}
if score < 1 {
return 1
}
return score
}
func hasStrongClassEvidence(class discoveredClass) bool {
return class.SuperMeta != 0 || class.MetaVtableAddr != 0 || class.VtableAddr != 0
}
func fileOwnsVMAddr(m *macho.File, addr uint64) bool {
for _, seg := range m.Segments() {
if seg == nil || seg.Memsz == 0 {
continue
}
if seg.Addr <= addr && addr < seg.Addr+seg.Memsz {
return true
}
}
for _, sec := range m.Sections {
if sec.Addr <= addr && addr < sec.Addr+sec.Size {
return true
}
}
return false
}
func (s *Scanner) clearDiscoveryCaches() {
clear(s.functionData)
clear(s.sectionData)
clear(s.callerIndex)
clear(s.callsiteCtx)
clear(s.getMetaCands)
clear(s.metaPtrInfer)
clear(s.metaPtrBusy)
clear(s.staticCalls)
}
func (s *Scanner) resolvePointerAtReason(owner *macho.File, addr uint64, reason pointerReason) (uint64, bool) {
stats := &s.stats.pointerReasons[reason]
stats.lookups++
// Fast path: check owner and root forward caches before doing
// any expensive owner lookup.
if owner != nil {
if fwd := s.forwardPointers[owner]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
s.stats.ptrCacheHits++
stats.forwardHits++
stats.successes++
return ptr, true
}
}
}
if s.root != nil && s.root != owner {
if fwd := s.forwardPointers[s.root]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
s.stats.ptrCacheHits++
stats.forwardHits++
stats.successes++
return ptr, true
}
}
}
// After root fixup seeding, all chained fixup pointers are in the
// per-file forward caches. Skip expensive owner lookup and pread.
if s.rootFixupsSeeded {
s.stats.ptrCacheMisses++
stats.misses++
return 0, false
}
// Slow path: find the owner via linear scan and fall through to
// pread if no cache entry exists (only reached before fixup seed).
lookupOwner, ownerSource := s.pointerLookupOwner(owner, addr)
if ownerSource == pointerOwnerSourceIndexed {
stats.ownerIndexHits++
}
if lookupOwner != nil && lookupOwner != owner && lookupOwner != s.root {
if fwd := s.forwardPointers[lookupOwner]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
s.stats.ptrCacheHits++
stats.forwardHits++
stats.successes++
return ptr, true
}
}
}
s.stats.ptrCacheMisses++
if lookupOwner != nil && s.forwardPointers[lookupOwner] != nil && pointerCacheCoversAddress(lookupOwner, addr) {
stats.misses++
return 0, false
}
if lookupOwner != nil && lookupOwner != owner && s.forwardPointers[owner] != nil && pointerCacheCoversAddress(owner, addr) {
stats.misses++
return 0, false
}
if lookupOwner != nil && s.root != nil && lookupOwner != s.root && s.forwardPointers[s.root] != nil && fileOwnsVMAddr(s.root, addr) && pointerCacheCoversAddress(s.root, addr) {
stats.misses++
return 0, false
}
if lookupOwner != nil {
stats.slidAttempts++
if ptr, err := lookupOwner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
stats.rawAttempts++
if ptr, err := lookupOwner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
}
if owner != nil && owner != lookupOwner {
stats.slidAttempts++
if ptr, err := owner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
stats.rawAttempts++
if ptr, err := owner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
}
if s.root != nil && s.root != lookupOwner && s.root != owner {
stats.slidAttempts++
if ptr, err := s.root.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
stats.rawAttempts++
if ptr, err := s.root.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
stats.successes++
return ptr, true
}
}
stats.misses++
return 0, false
}
func (s *Scanner) repairClassesFromCallsite(classes []discoveredClass) {
for i := range classes {
owner := classes[i].file
if owner == nil {
owner = s.fileForVMAddr(classes[i].Ctor)
classes[i].file = owner
}
if owner == nil || classes[i].Ctor == 0 {
continue
}
if validMetaPointer(classes[i].SuperMeta) && classes[i].MetaVtableAddr != 0 {
continue
}
ctx, ok := s.recoverCallsiteContext(owner, &classes[i])
if !ok {
continue
}
if classes[i].MetaPtr == 0 && validKernelPointer(ctx.x0) {
classes[i].MetaPtr = ctx.x0
}
if !validMetaPointer(classes[i].SuperMeta) {
if super := s.recoverSuperMetaFromCtorPattern(owner, &classes[i]); validMetaPointer(super) {
classes[i].SuperMeta = super
}
}
if !validMetaPointer(classes[i].SuperMeta) {
if super := s.normalizeLoadedPointer(owner, ctx.x2); validMetaPointer(super) {
classes[i].SuperMeta = super
}
}
if classes[i].Size == 0 && ctx.x3 > 0 && ctx.x3 <= 0xffffffff {
classes[i].Size = uint32(ctx.x3)
}
if classes[i].MetaVtableAddr == 0 && validKernelPointer(ctx.metaVtab) {
classes[i].MetaVtableAddr = ctx.metaVtab
}
}
}
// recoverSuperMetaFromModInit fills in missing SuperMeta values by
// statically analyzing mod_init_func wrapper functions. Each wrapper
// sets up x0 (metaclass ptr) and x2 (parent metaclass ptr) before
// calling the constructor. When x2 is loaded from a GOT slot
// (cross-kext parent), the pointer is resolved via chained fixups.
func (s *Scanner) recoverSuperMetaFromModInit(classes []discoveredClass) {
needsRecovery := false
for i := range classes {
if classes[i].SuperMeta == 0 && classes[i].MetaPtr != 0 {
needsRecovery = true
break
}
}
if !needsRecovery {
return
}
metaPtrToIdx := make(map[uint64]int, len(classes))
for i := range classes {
if classes[i].MetaPtr != 0 && classes[i].SuperMeta == 0 {
metaPtrToIdx[classes[i].MetaPtr] = i
}
}
for _, target := range s.targets {
if len(metaPtrToIdx) == 0 {
break
}
ptrs, err := s.modInitPointers(target.file)
if err != nil {
continue
}
for _, ptr := range ptrs {
x0, x2 := s.extractModInitPair(target.file, ptr)
if x0 == 0 || x2 == 0 {
continue
}
idx, ok := metaPtrToIdx[x0]
if !ok {
continue
}
if validMetaPointer(x2) {
classes[idx].SuperMeta = x2
delete(metaPtrToIdx, x0)
}
}
}
}
// extractModInitPair statically traces a mod_init_func wrapper to
// extract the x0 (metaclass ptr) and x2 (parent metaclass ptr)
// values at the last BL/BRAA call that has both set. When x2 is
// loaded from a GOT slot, the chained fixup pointer is resolved.
func (s *Scanner) extractModInitPair(owner *macho.File, funcAddr uint64) (uint64, uint64) {
buf := make([]byte, 80*4)
if _, err := s.root.ReadAtAddr(buf, funcAddr); err != nil {
return 0, 0
}
type regState struct {
adrpBase uint64
computed uint64
loadAddr uint64
isLoad bool
}
var regs [31]regState
var bestX0, bestX2 uint64
for i := 0; i+4 <= len(buf); i += 4 {
pc := funcAddr + uint64(i)
raw := readUint32At(buf, i)
// ADRP
if (raw & 0x9f000000) == 0x90000000 {
rd := int(raw & 0x1f)
if rd < 31 {
if addr, ok := decodeADRPImmediate(pc, raw); ok {
regs[rd] = regState{adrpBase: addr}
}
}
continue
}
// ADD imm (64-bit, sf=1)
if (raw & 0xff800000) == 0x91000000 {
rd := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
imm12 := uint64((raw >> 10) & 0xfff)
if (raw>>22)&1 == 1 {
imm12 <<= 12
}
if rd < 31 && rn < 31 && regs[rn].adrpBase != 0 {
regs[rd] = regState{computed: regs[rn].adrpBase + imm12}
}
continue
}
// LDR unsigned offset (64-bit)
if (raw & 0xffc00000) == 0xf9400000 {
rt := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
imm12 := uint64((raw>>10)&0xfff) * 8
if rt < 31 && rn < 31 {
var la uint64
if regs[rn].adrpBase != 0 {
la = regs[rn].adrpBase + imm12
} else if regs[rn].computed != 0 {
la = regs[rn].computed + imm12
}
regs[rt] = regState{loadAddr: la, isLoad: true}
}
continue
}
// MOV Xd, Xm (ORR Xd, XZR, Xm)
if (raw & 0xffe0ffe0) == 0xaa0003e0 {
rd := int(raw & 0x1f)
rm := int((raw >> 16) & 0x1f)
if rd < 31 && rm < 31 {
regs[rd] = regs[rm]
}
continue
}
// MOVZ (64-bit)
if (raw & 0xff800000) == 0xd2800000 {
rd := int(raw & 0x1f)
if rd < 31 {
regs[rd] = regState{}
}
continue
}
// RET
if raw == 0xd65f03c0 || raw == 0xd65f0fff {
break
}
// BL or BRAA/BLRAA - capture register state
isBL := (raw >> 26) == 0b100101
isAuthBranch := (raw&0xfffff800) == 0xd71f0800 ||
(raw&0xfffff800) == 0xd63f0800
if !isBL && !isAuthBranch {
continue
}
// x0: prefer computed (ADRP+ADD), fall back to adrpBase
var x0 uint64
if regs[0].computed != 0 {
x0 = regs[0].computed
} else if regs[0].adrpBase != 0 {
x0 = regs[0].adrpBase
}
// x2: if loaded from GOT, resolve the pointer
var x2 uint64
if regs[2].computed != 0 {
x2 = regs[2].computed
} else if regs[2].adrpBase != 0 && !regs[2].isLoad {
x2 = regs[2].adrpBase
} else if regs[2].loadAddr != 0 {
if ptr, ok := s.resolvePointerAtReason(owner, regs[2].loadAddr, pointerReasonX2LoadRecovery); ok {
x2 = ptr
}
}
if x0 != 0 && x2 != 0 {
bestX0, bestX2 = x0, x2
}
}
return bestX0, bestX2
}
func (s *Scanner) validateSuperMeta(classes []discoveredClass) {
known := make(map[uint64]bool, len(classes))
for i := range classes {
if classes[i].MetaPtr != 0 {
known[classes[i].MetaPtr] = true
}
}
for i := range classes {
if validMetaPointer(classes[i].SuperMeta) {
continue
}
owner := classes[i].file
if owner == nil {
owner = s.fileForVMAddr(classes[i].Ctor)
}
if recovered := s.recoverSuperMetaFromCtorPattern(owner, &classes[i]); validMetaPointer(recovered) {
classes[i].SuperMeta = recovered
continue
}
if ctx, ok := s.recoverCallsiteContext(owner, &classes[i]); ok {
if super := s.normalizeLoadedPointer(owner, ctx.x2); validMetaPointer(super) {
classes[i].SuperMeta = super
continue
}
}
if classes[i].SuperMeta != 0 && known[classes[i].SuperMeta] {
continue
}
classes[i].SuperMeta = 0
}
}
func (s *Scanner) normalizeLoadedPointer(owner *macho.File, value uint64) uint64 {
if validKernelPointer(value) || value == 0 {
return value
}
// Try root first — in a fileset cache, the root MH_FILESET
// decodes chained fixup binds to canonical kernel addresses,
// while individual entry handles may resolve to local offsets.
if s.root != nil {
if slid := s.root.SlidePointer(value); validKernelPointer(slid) {
return slid
}
}
if owner != nil && owner != s.root {
if slid := owner.SlidePointer(value); validKernelPointer(slid) {
return slid
}
}
return value
}
func pointerCacheCoversAddress(m *macho.File, addr uint64) bool {
if m == nil {
return false
}
for _, sec := range m.Sections {
if sec == nil || sec.Size < 8 {
continue
}
if sec.Addr > addr || addr >= sec.Addr+sec.Size {
continue
}
if sec.Seg == "__TEXT" || sec.Seg == "__TEXT_EXEC" {
return false
}
if strings.HasSuffix(sec.Name, "__bss") || strings.HasSuffix(sec.Name, "__common") {
return false
}
return true
}
return false
}
func (s *Scanner) resolvePointerAt(owner *macho.File, addr uint64) (uint64, bool) {
return s.resolvePointerAtReason(owner, addr, pointerReasonOther)
}
func (s *Scanner) fallbackPointerAt(owner *macho.File, addr uint64) (uint64, bool) {
lookupOwner, _ := s.pointerLookupOwner(owner, addr)
if lookupOwner == nil {
return 0, false
}
// Fast path: check forward pointer cache built by buildPointerIndex.
if fwd := s.forwardPointers[lookupOwner]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
return ptr, true
}
}
if lookupOwner != owner {
if fwd := s.forwardPointers[owner]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
return ptr, true
}
}
}
if s.root != nil && lookupOwner != s.root {
if fwd := s.forwardPointers[s.root]; fwd != nil {
if ptr, ok := fwd[addr]; ok {
return ptr, true
}
}
}
// After root fixup seeding, all chained fixup pointers are in the
// forward cache. Any address not found is genuinely absent.
if s.rootFixupsSeeded {
return 0, false
}
// Skip slow I/O path only for addresses covered by the warmed cache.
if s.forwardPointers[lookupOwner] != nil && pointerCacheCoversAddress(lookupOwner, addr) {
return 0, false
}
if lookupOwner != owner && s.forwardPointers[owner] != nil && pointerCacheCoversAddress(owner, addr) {
return 0, false
}
if s.root != nil && lookupOwner != s.root && s.forwardPointers[s.root] != nil && fileOwnsVMAddr(s.root, addr) && pointerCacheCoversAddress(s.root, addr) {
return 0, false
}
// Slow path: I/O via go-macho (only reached before cache warm).
if ptr, err := lookupOwner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
if ptr, err := lookupOwner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
if owner != nil && owner != lookupOwner {
if ptr, err := owner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
if ptr, err := owner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
}
if s.root != nil && s.root != lookupOwner && s.root != owner {
if ptr, err := s.root.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
if ptr, err := s.root.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) {
return ptr, true
}
}
return 0, false
}
// isStubFor checks whether stubAddr is a small auth stub (ADRP+ADD+BR
// or ADRP+LDR+BR) that ultimately branches to targetFunc.
func (s *Scanner) isStubFor(m *macho.File, stubAddr, targetFunc uint64) bool {
if stubAddr == 0 || targetFunc == 0 {
return false
}
var buf [12]byte
owner := m
if s.root != nil {
if _, err := s.root.ReadAtAddr(buf[:], stubAddr); err != nil {
if owner == nil {
return false
}
if _, err := owner.ReadAtAddr(buf[:], stubAddr); err != nil {
return false
}
}
} else if owner != nil {
if _, err := owner.ReadAtAddr(buf[:], stubAddr); err != nil {
return false
}
} else {
return false
}
i0 := binary.LittleEndian.Uint32(buf[0:4])
i1 := binary.LittleEndian.Uint32(buf[4:8])
// First instruction must be ADRP Xd, #page
if i0&0x9F000000 != 0x90000000 {
return false
}
rd := i0 & 0x1F
immlo := (i0 >> 29) & 0x3
immhi := (i0 >> 5) & 0x7FFFF
imm := int64((uint64(immhi)<<2 | uint64(immlo)) << 12)
if imm&(1<<32) != 0 {
imm |= ^int64((1 << 33) - 1)
}
page := (stubAddr &^ 0xFFF) + uint64(imm)
// Second instruction: ADD Xd, Xd, #imm12 or LDR Xd, [Xd, #imm12]
if i1&0xFFC00000 == 0x91000000 && (i1&0x1F) == rd && ((i1>>5)&0x1F) == rd {
// ADD immediate
addImm := uint64((i1 >> 10) & 0xFFF)
if (i1>>22)&1 != 0 {
addImm <<= 12
}
target := page + addImm
return target == targetFunc
}
if i1&0xFFC00000 == 0xF9400000 && (i1&0x1F) == rd && ((i1>>5)&0x1F) == rd {
// LDR X, [X, #imm12] (unsigned offset, scale 8)
ldrOff := uint64((i1>>10)&0xFFF) * 8
gotSlot := page + ldrOff
ptr, ok := s.resolvePointerAtReason(m, gotSlot, pointerReasonVtableStub)
if !ok {
return false
}
return ptr == targetFunc
}
return false
}
func (s *Scanner) recoverSuperMetaFromCtorPattern(m *macho.File, class *discoveredClass) uint64 {
if class == nil || class.Ctor == 0 {
return 0
}
fn, owner, err := s.functionForAddrInAnyFile(m, class.Ctor)
if err != nil {
return 0
}
data, err := s.functionDataFor(owner, fn)
if err != nil {
return 0
}
callOffset := int(class.Ctor - fn.StartAddr)
if callOffset < 0 || callOffset >= len(data) {
return 0
}
start := max(callOffset-32*4, 0)
var regBase [31]uint64
var regLoadAddr [31]uint64
for i := start; i+4 <= callOffset; i += 4 {
pc := fn.StartAddr + uint64(i)
raw := readUint32At(data, i)
if (raw & 0x9f000000) == 0x90000000 {
rd := int(raw & 0x1f)
if rd < 31 {
if addr, ok := decodeADRPImmediate(pc, raw); ok {
regBase[rd] = addr
regLoadAddr[rd] = 0
}
}
continue
}
var ins disassemble.Inst
if err := s.decodeArm64Instruction(pc, raw, &ins); err != nil {
continue
}
switch ins.Operation {
case disassemble.ARM64_ADD:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
srcReg, srcOK := operandRegister(&ins.Operands[1], 0)
if operandCount(&ins) < 3 || !dstOK || !srcOK {
continue
}
dstIdx, dstOk := registerToIndex(dstReg)
srcIdx, srcOk := registerToIndex(srcReg)
if dstOk && srcOk && dstIdx < 31 && srcIdx < 31 {
srcBase := regBase[srcIdx]
regLoadAddr[dstIdx] = 0
regBase[dstIdx] = 0
if srcBase != 0 {
if addr, ok := addSignedOffset(srcBase, int64(ins.Operands[2].GetImmediate())); ok {
regBase[dstIdx] = addr
}
}
}
case disassemble.ARM64_SUB, disassemble.ARM64_ADR:
if dstReg, ok := operandRegister(&ins.Operands[0], 0); operandCount(&ins) > 0 && ok {
if dstIdx, ok := registerToIndex(dstReg); ok && dstIdx < 31 {
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
}
}
case disassemble.ARM64_MOV:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if operandCount(&ins) < 2 || !dstOK {
continue
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
continue
}
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
srcReg, srcOK := operandRegister(&ins.Operands[1], 0)
if !srcOK {
continue
}
srcIdx, srcOK := registerToIndex(srcReg)
if !srcOK || srcIdx >= 31 {
continue
}
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
case disassemble.ARM64_ORR:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if operandCount(&ins) < 3 || !dstOK {
continue
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
continue
}
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
reg1, reg1OK := operandRegister(&ins.Operands[1], 0)
reg2, reg2OK := operandRegister(&ins.Operands[2], 0)
switch {
case reg1OK && reg2OK && (reg1 == disassemble.REG_XZR || reg1 == disassemble.REG_WZR):
if srcIdx, ok := registerToIndex(reg2); ok && srcIdx < 31 {
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
}
case reg1OK && reg2OK && (reg2 == disassemble.REG_XZR || reg2 == disassemble.REG_WZR):
if srcIdx, ok := registerToIndex(reg1); ok && srcIdx < 31 {
regBase[dstIdx] = regBase[srcIdx]
regLoadAddr[dstIdx] = regLoadAddr[srcIdx]
}
}
case disassemble.ARM64_LDR, disassemble.ARM64_LDUR:
dstReg, dstOK := operandRegister(&ins.Operands[0], 0)
if operandCount(&ins) < 2 || !dstOK {
continue
}
dstIdx, dstOK := registerToIndex(dstReg)
if !dstOK || dstIdx >= 31 {
continue
}
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
if addr, ok := trackedStaticAddress(regBase, &ins.Operands[1]); ok {
regLoadAddr[dstIdx] = addr
}
case disassemble.ARM64_LDP:
if operandCount(&ins) < 3 {
continue
}
if reg, ok := operandRegister(&ins.Operands[0], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
}
}
if reg, ok := operandRegister(&ins.Operands[1], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regBase[dstIdx] = 0
regLoadAddr[dstIdx] = 0
}
}
addr, ok := trackedStaticAddress(regBase, &ins.Operands[2])
if !ok {
continue
}
if reg, ok := operandRegister(&ins.Operands[0], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
regLoadAddr[dstIdx] = addr
}
}
if reg, ok := operandRegister(&ins.Operands[1], 0); ok {
if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 {
if addr2, ok := addSignedOffset(addr, 8); ok {
regLoadAddr[dstIdx] = addr2
}
}
}
}
}
if regLoadAddr[2] != 0 {
if ptr, ok := s.resolvePointerAtReason(owner, regLoadAddr[2], pointerReasonX2LoadRecovery); ok {
return ptr
}
}
if validKernelPointer(regBase[2]) {
return regBase[2]
}
return 0
}
func readUint32At(data []byte, offset int) uint32 {
return binary.LittleEndian.Uint32(data[offset : offset+4])
}
func (s *Scanner) inferMetaPtrFromDirectCallers(owner *macho.File, target uint64) uint64 {
return s.inferMetaPtrFromDirectCallersDepth(owner, target, 0)
}
func (s *Scanner) inferMetaPtrFromDirectCallersDepth(owner *macho.File, target uint64, depth int) uint64 {
if owner == nil || target == 0 || depth > s.cfg.MaxWrapperDepth {
return 0
}
s.stats.inferCalls++
if depth > s.stats.inferMaxDepth {
s.stats.inferMaxDepth = depth
}
key := metaInferKey{file: owner, addr: target, depth: depth}
if cached, ok := s.metaPtrInfer[key]; ok && cached.ok {
s.stats.inferCacheHits++
return cached.value
}
if s.metaPtrBusy[key] {
s.stats.inferBusyHits++
return 0
}
s.metaPtrBusy[key] = true
defer delete(s.metaPtrBusy, key)
index, err := s.directCallerIndex(owner)
if err != nil {
s.metaPtrInfer[key] = cachedMetaPtr{ok: true}
return 0
}
for _, callerStart := range index[target] {
fn, err := s.functionForAddr(owner, callerStart)
if err != nil {
continue
}
metaPtr, found := s.metaPtrAtDirectCall(owner, fn, target)
if !found {
continue
}
if validKernelPointer(metaPtr) {
return metaPtr
}
if inferred := s.inferMetaPtrFromDirectCallersDepth(owner, callerStart, depth+1); validKernelPointer(inferred) {
s.metaPtrInfer[key] = cachedMetaPtr{value: inferred, ok: true}
return inferred
}
}
s.metaPtrInfer[key] = cachedMetaPtr{ok: true}
return 0
}
func (s *Scanner) directCallerIndex(m *macho.File) (map[uint64][]uint64, error) {
if index, ok := s.callerIndex[m]; ok {
return index, nil
}
funcs, err := s.functionsForFile(m)
if err != nil {
return nil, err
}
index := make(map[uint64][]uint64)
for _, fn := range funcs {
data, err := s.functionDataFor(m, fn)
if err != nil {
continue
}
for off := 0; off+4 <= len(data); off += 4 {
pc := fn.StartAddr + uint64(off)
raw := readUint32At(data, off)
if target, ok := decodeBLTarget(pc, raw); ok && target != 0 {
index[target] = append(index[target], fn.StartAddr)
continue
}
if target, ok := decodeBTarget(pc, raw); ok && target != 0 {
index[target] = append(index[target], fn.StartAddr)
}
}
}
for target, callers := range index {
slices.Sort(callers)
index[target] = slices.Compact(callers)
}
s.callerIndex[m] = index
return index, nil
}
func (s *Scanner) metaPtrAtDirectCall(owner *macho.File, fn types.Function, target uint64) (uint64, bool) {
data, err := s.functionDataFor(owner, fn)
if err != nil {
s.stats.metaPtrDirectMisses++
return 0, false
}
var regBase [31]uint64
var regValue [31]uint64
for i := 0; i+4 <= len(data); i += 4 {
pc := fn.StartAddr + uint64(i)
raw := readUint32At(data, i)
// BL / B to target → return tracked x0.
if callTarget, ok := decodeBLTarget(pc, raw); ok && callTarget == target {
s.stats.metaPtrDirectHits++
return metaPtrResult(regBase, regValue), true
}
if callTarget, ok := decodeBTarget(pc, raw); ok && callTarget == target {
s.stats.metaPtrDirectHits++
return metaPtrResult(regBase, regValue), true
}
trackRegisterRawWithReason(s, owner, raw, pc, &regBase, &regValue, pointerReasonMetaPtrDirectCall)
}
s.stats.metaPtrDirectMisses++
return 0, false
}
func metaPtrResult(regBase, regValue [31]uint64) uint64 {
if validKernelPointer(regValue[0]) {
return regValue[0]
}
if validKernelPointer(regBase[0]) {
return regBase[0]
}
return 0
}
// trackRegisterRaw performs register tracking using raw ARM64
// instruction bitmasks, avoiding CGo entirely. It handles the
// instruction subset needed by metaPtrAtDirectCall and similar
// callers: ADRP, ADD(imm), LDR(uoff/reg), LDUR, LDP, MOV, ORR, SUB(imm).
func trackRegisterRaw(s *Scanner, owner *macho.File, raw uint32, pc uint64, regBase, regValue *[31]uint64) bool {
return trackRegisterRawWithReason(s, owner, raw, pc, regBase, regValue, pointerReasonOther)
}
func trackRegisterRawWithReason(s *Scanner, owner *macho.File, raw uint32, pc uint64, regBase, regValue *[31]uint64, reason pointerReason) bool {
switch {
// ADRP: sf 1 0000 immlo immhi Rd
case (raw & 0x9f000000) == 0x90000000:
rd := int(raw & 0x1f)
if rd < 31 {
if addr, ok := decodeADRPImmediate(pc, raw); ok {
regBase[rd] = addr
regValue[rd] = 0
}
}
// ADR: sf 0 0000 immlo immhi Rd
case (raw & 0x9f000000) == 0x10000000:
rd := int(raw & 0x1f)
if rd < 31 {
immhi := int64((raw >> 5) & 0x7ffff)
immlo := int64((raw >> 29) & 0x3)
offset := (immhi << 2) | immlo
if offset&(1<<20) != 0 {
offset |= ^int64((1 << 21) - 1)
}
addr := uint64(int64(pc) + offset)
regBase[rd] = addr
regValue[rd] = addr
}
// ADD (immediate, 64-bit): 1 00 10001 sh imm12 Rn Rd
case (raw & 0xff000000) == 0x91000000:
rd := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
if rd < 31 && rn < 31 {
imm := uint64((raw >> 10) & 0xfff)
if (raw>>22)&1 == 1 {
imm <<= 12
}
srcBase := regBase[rn]
regBase[rd] = 0
regValue[rd] = 0
if srcBase != 0 {
regBase[rd] = srcBase + imm
regValue[rd] = srcBase + imm
} else if regValue[rn] != 0 {
regValue[rd] = regValue[rn] + imm
}
}
// SUB (immediate, 64-bit): 1 10 10001 sh imm12 Rn Rd
case (raw & 0xff000000) == 0xd1000000:
rd := int(raw & 0x1f)
if rd < 31 {
regBase[rd] = 0
regValue[rd] = 0
}
// ORR (shifted register, 64-bit): 1 01 01010 sh 0 Rm imm6 Rn Rd
// MOV register is ORR Xd, XZR, Xm (Rn == XZR, shift == 0, imm6 == 0)
case (raw & 0xff200000) == 0xaa000000:
rd := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
rm := int((raw >> 16) & 0x1f)
imm6 := (raw >> 10) & 0x3f
if rd < 31 {
regBase[rd] = 0
regValue[rd] = 0
if rn == 31 && imm6 == 0 && rm < 31 {
// MOV Xd, Xm
regBase[rd] = regBase[rm]
regValue[rd] = regValue[rm]
} else if rm == 31 && imm6 == 0 && rn < 31 {
// MOV Xd, Xn (rare but valid)
regBase[rd] = regBase[rn]
regValue[rd] = regValue[rn]
}
}
// MOVZ (32/64): sf 10 100101 hw imm16 Rd
case (raw & 0x7f800000) == 0x52800000:
rd := int(raw & 0x1f)
if rd < 31 {
imm := uint64((raw >> 5) & 0xffff)
hw := (raw >> 21) & 0x3
regBase[rd] = 0
regValue[rd] = imm << (hw * 16)
if (raw>>31)&1 == 0 {
regValue[rd] &= 0xffff_ffff
}
}
// MOVK (32/64): sf 11 100101 hw imm16 Rd
case (raw & 0x7f800000) == 0x72800000:
rd := int(raw & 0x1f)
if rd < 31 {
imm := uint64((raw >> 5) & 0xffff)
hw := (raw >> 21) & 0x3
shift := hw * 16
mask := uint64(0xffff) << shift
regBase[rd] = 0
regValue[rd] = (regValue[rd] &^ mask) | (imm << shift)
if (raw>>31)&1 == 0 {
regValue[rd] &= 0xffff_ffff
}
}
// LDR (unsigned offset, 64-bit): 11 111 00101 imm12 Rn Rt
case (raw & 0xffc00000) == 0xf9400000:
rt := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
if rt < 31 && rn < 31 {
imm := uint64((raw>>10)&0xfff) * 8
regBase[rt] = 0
regValue[rt] = 0
base := regBase[rn]
if base == 0 {
base = regValue[rn]
}
if base != 0 {
addr := base + imm
if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok {
regValue[rt] = ptr
} else if validKernelPointer(addr) {
regValue[rt] = addr
}
}
}
// LDR (unsigned offset, 32-bit): 10 111 00101 imm12 Rn Rt
case (raw & 0xffc00000) == 0xb9400000:
rt := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
if rt < 31 && rn < 31 {
imm := uint64((raw>>10)&0xfff) * 4
regBase[rt] = 0
regValue[rt] = 0
base := regBase[rn]
if base == 0 {
base = regValue[rn]
}
if base != 0 {
addr := base + imm
if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok {
regValue[rt] = ptr
} else if validKernelPointer(addr) {
regValue[rt] = addr
}
}
}
// LDUR (64-bit): 11 111000 010 imm9 00 Rn Rt
case (raw & 0xffe00c00) == 0xf8400000:
rt := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
if rt < 31 && rn < 31 {
imm9 := int64((raw >> 12) & 0x1ff)
if imm9&(1<<8) != 0 {
imm9 |= ^int64((1 << 9) - 1)
}
regBase[rt] = 0
regValue[rt] = 0
base := regBase[rn]
if base == 0 {
base = regValue[rn]
}
if base != 0 {
addr := uint64(int64(base) + imm9)
if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok {
regValue[rt] = ptr
} else if validKernelPointer(addr) {
regValue[rt] = addr
}
}
}
// LDP (signed offset, 64-bit): x0 101 0 010 1 imm7 Rt2 Rn Rt
case (raw & 0x7fc00000) == 0xa9400000:
rt := int(raw & 0x1f)
rn := int((raw >> 5) & 0x1f)
rt2 := int((raw >> 10) & 0x1f)
if rn < 31 {
imm7 := int64((raw >> 15) & 0x7f)
if imm7&(1<<6) != 0 {
imm7 |= ^int64((1 << 7) - 1)
}
base := regBase[rn]
if base == 0 {
base = regValue[rn]
}
addr := uint64(0)
if base != 0 {
addr = uint64(int64(base) + imm7*8)
}
if rt < 31 {
regBase[rt] = 0
regValue[rt] = 0
if addr != 0 {
if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok {
regValue[rt] = ptr
} else if validKernelPointer(addr) {
regValue[rt] = addr
}
}
}
if rt2 < 31 {
regBase[rt2] = 0
regValue[rt2] = 0
if addr != 0 {
addr2 := addr + 8
if ptr, ok := s.resolvePointerAtReason(owner, addr2, reason); ok {
regValue[rt2] = ptr
} else if validKernelPointer(addr2) {
regValue[rt2] = addr2
}
}
}
}
default:
return false
}
return true
}
// batchStaticCallContexts does a single linear pass through fn,
// tracking register state and capturing wrapperContext at every
// BL/B to an OSMetaClass variant. O(function_size) total instead
// of O(N × function_size) when called per-callsite.
func (s *Scanner) batchStaticCallContexts(owner *macho.File, fn types.Function, plan microPlan) map[uint64]wrapperContext {
data, err := s.functionDataFor(owner, fn)
if err != nil {
return nil
}
out := make(map[uint64]wrapperContext)
var regBase [31]uint64
var regValue [31]uint64
for i := 0; i+4 <= len(data); i += 4 {
pc := fn.StartAddr + uint64(i)
raw := readUint32At(data, i)
idx := i / 4
if idx < len(plan.tags) && plan.tags[idx]&(microTagBL|microTagB) != 0 && s.isOSMetaClassVariant(plan.targets[idx]) {
ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc)
out[pc] = ctx
}
trackRegisterRawWithReason(s, owner, raw, pc, &regBase, &regValue, pointerReasonStaticDirectCall)
}
return out
}
func (s *Scanner) staticDirectCallContext(owner *macho.File, fn types.Function, callsite uint64, target uint64) (wrapperContext, bool) {
s.stats.staticDirectCalls++
key := staticCallKey{file: owner, start: fn.StartAddr, callsite: callsite, target: target}
if cached, ok := s.staticCalls[key]; ok {
s.stats.staticDirectCache++
s.stats.recordStaticDirectResolution(cached.ctx, cached.ok)
return cached.ctx, cached.ok
}
data, err := s.functionDataFor(owner, fn)
if err != nil {
s.staticCalls[key] = cachedWrapperContext{}
return wrapperContext{}, false
}
var regBase [31]uint64
var regValue [31]uint64
for i := 0; i+4 <= len(data); i += 4 {
pc := fn.StartAddr + uint64(i)
raw := readUint32At(data, i)
if (callsite == 0 || pc == callsite) && target != 0 {
if callTarget, ok := decodeBLTarget(pc, raw); ok && callTarget == target {
ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc)
s.staticCalls[key] = cachedWrapperContext{ctx: ctx, ok: true}
s.stats.recordStaticDirectResolution(ctx, true)
return ctx, true
}
if callTarget, ok := decodeBTarget(pc, raw); ok && callTarget == target {
ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc)
s.staticCalls[key] = cachedWrapperContext{ctx: ctx, ok: true}
s.stats.recordStaticDirectResolution(ctx, true)
return ctx, true
}
}
trackRegisterRawWithReason(s, owner, raw, pc, &regBase, &regValue, pointerReasonStaticDirectCall)
}
s.staticCalls[key] = cachedWrapperContext{}
return wrapperContext{}, false
}
func (s *Scanner) staticDirectCallTrackedContext(owner *macho.File, regBase [31]uint64, regValue [31]uint64, callsite uint64) wrapperContext {
for reg := range 4 {
if regValue[reg] == 0 && regBase[reg] != 0 {
regValue[reg] = regBase[reg]
}
if reg == 2 && regValue[reg] != 0 && !validKernelPointer(regValue[reg]) {
regValue[reg] = s.normalizeLoadedPointer(owner, regValue[reg])
}
}
return wrapperContext{
x0: regValue[0],
x1: regValue[1],
x2: regValue[2],
x3: regValue[3],
callsite: callsite,
}
}
func mergeWrapperContext(dst, src wrapperContext) wrapperContext {
if dst.x0 == 0 {
dst.x0 = src.x0
}
if dst.x1 == 0 {
dst.x1 = src.x1
}
if dst.x2 == 0 {
dst.x2 = src.x2
}
if dst.x3 == 0 {
dst.x3 = src.x3
}
if dst.metaVtab == 0 {
dst.metaVtab = src.metaVtab
}
if dst.callsite == 0 {
dst.callsite = src.callsite
}
return dst
}
func wrapperContextEmpty(ctx *wrapperContext) bool {
return ctx == nil || (ctx.x0 == 0 && ctx.x1 == 0 && ctx.x2 == 0 && ctx.x3 == 0 && ctx.metaVtab == 0)
}
func (s *Scanner) recoverStaticWrapperContext(owner *macho.File, startAddr uint64, canonicalStart uint64) (*wrapperContext, bool) {
ctx, ok := s.recoverStaticWrapperContextDepth(owner, startAddr, canonicalStart, 0, wrapperContext{})
if !ok {
return nil, false
}
return &ctx, true
}
func (s *Scanner) recoverStaticWrapperContextDepth(owner *macho.File, startAddr uint64, canonicalStart uint64, depth int, accum wrapperContext) (wrapperContext, bool) {
if owner == nil || depth > s.cfg.MaxWrapperDepth {
return wrapperContext{}, false
}
fn, _, err := s.functionForAddrInAnyFile(owner, startAddr)
if err != nil {
return wrapperContext{}, false
}
return s.staticWrapperContextAlongPath(owner, fn, canonicalStart, depth, accum)
}
func (s *Scanner) staticWrapperContextAlongPath(owner *macho.File, fn types.Function, canonicalStart uint64, depth int, accum wrapperContext) (wrapperContext, bool) {
data, err := s.functionDataFor(owner, fn)
if err != nil {
return wrapperContext{}, false
}
var regBase [31]uint64
var regLoadAddr [31]uint64
var regValue [31]uint64
for i := 0; i+4 <= len(data); i += 4 {
pc := fn.StartAddr + uint64(i)
raw := readUint32At(data, i)
if (raw & 0x9f000000) == 0x90000000 {
rd := int(raw & 0x1f)
if rd < 31 {
if addr, ok := decodeADRPImmediate(pc, raw); ok {
regBase[rd] = addr
regLoadAddr[rd] = 0
regValue[rd] = 0
}
}
continue
}
var ins disassemble.Inst
if err := s.decodeArm64Instruction(pc, raw, &ins); err != nil || operandCount(&ins) == 0 {
continue
}
s.trackStaticValueInstruction(owner, &regBase, &regLoadAddr, &regValue, &ins, staticValueTrackOptions{
acceptAnyLoadAddr: true,
propagateLoadAddrInAdd: false,
handleLoadPairs: false,
})
for reg := range 4 {
if regValue[reg] == 0 && regBase[reg] != 0 {
regValue[reg] = regBase[reg]
}
if reg == 2 && regValue[reg] != 0 && !validKernelPointer(regValue[reg]) {
regValue[reg] = s.normalizeLoadedPointer(owner, regValue[reg])
}
}
var nextTarget uint64
if blTarget, ok := decodeBLTarget(pc, raw); ok {
nextTarget = blTarget
} else if bTarget, ok := decodeBTarget(pc, raw); ok {
nextTarget = bTarget
}
if nextTarget == 0 || s.isOSMetaClassVariant(nextTarget) {
continue
}
ctx := mergeWrapperContext(accum, wrapperContext{
x0: regValue[0],
x1: regValue[1],
x2: regValue[2],
x3: regValue[3],
callsite: pc,
})
if nextTarget == canonicalStart {
return ctx, true
}
if depth >= s.cfg.MaxWrapperDepth {
continue
}
if nested, ok := s.recoverStaticWrapperContextDepth(owner, nextTarget, canonicalStart, depth+1, ctx); ok {
return nested, true
}
}
return wrapperContext{}, false
}