From b1cdeea8acafe56acb1a108fd84d5bde641031f2 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Tue, 9 Jun 2026 07:25:21 -0700 Subject: [PATCH 1/4] [DSC] Reduce log noise when loading iOS / macOS 27 shared caches These shared caches contain symbols pointing into address ranges that are no longer mapped, such as `objc_msgSend$stub` functions that are now merged into stub island regions. --- view/sharedcache/core/MachO.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/view/sharedcache/core/MachO.cpp b/view/sharedcache/core/MachO.cpp index 7afcbd8d0a..8393b0eeef 100644 --- a/view/sharedcache/core/MachO.cpp +++ b/view/sharedcache/core/MachO.cpp @@ -531,8 +531,16 @@ std::vector SharedCacheMachOHeader::ReadSymbolTable(VirtualMemory& { if (!flags.has_value()) { - // TODO: where logger? - LogErrorF("Symbol {:?} at address {:#x} is not in any section", symbolName.c_str(), symbolAddress); + // In iOS / macOS 27 shared caches, sections such as __objc_stubs are coalesced out of + // individual dylibs, leaving a zero-size section whose symbols no longer point at + // anything in this image. These are not an error. + bool coalescedSection = nlist.n_sect > 0 && (size_t)(nlist.n_sect - 1) < sections.size() + && sections[nlist.n_sect - 1].size == 0; + if (!coalescedSection) + { + // TODO: where logger? + LogErrorF("Symbol {:?} at address {:#x} is not in any section", symbolName, symbolAddress); + } continue; } From 21e5c987fec7f343ba3a166698eb78d176c6c65f Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Tue, 9 Jun 2026 09:32:12 -0700 Subject: [PATCH 2/4] [DSC] Handle changes to how objc_msgSend is called in iOS / macOS 27 shared caches * `objc_msgSend$stub` functions no longer appear in the `__objc_stubs` section of their dylib. Instead they're coalesced across multiple dylibs and appear in a stub island region of the shared cache. This means that `AnalyzeStubFunction` can no longer determine the type of stub it is processing purely based on the containing section name. It now considers the target of the call to determine the type of the stub. * `objc_msgSend` and friends now have definitions in multiple dylibs throughout the shared cache (`/usr/lib/objc/libobjcMsgSendN.dylib`). This means that loading the target of `objc_msgSend` calls within `objc_msgSend$stub` functions is not sufficient to make selector definitions visible to analysis. Instead, we explicitly load `/usr/lib/libobjc.A.dylib` whenever we process a stub function that references `libobjcMsgSendN.dylib`. --- .../workflow/SharedCacheWorkflow.cpp | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/view/sharedcache/workflow/SharedCacheWorkflow.cpp b/view/sharedcache/workflow/SharedCacheWorkflow.cpp index 2b9cd28bbc..ff42e0c5bd 100644 --- a/view/sharedcache/workflow/SharedCacheWorkflow.cpp +++ b/view/sharedcache/workflow/SharedCacheWorkflow.cpp @@ -99,9 +99,17 @@ void IdentifyStub(BinaryView& view, const SharedCacheController& controller, uin // Ref selectedType = demangledType; Ref selectedType = nullptr; if (const auto image = controller.GetImageContaining(symbolAddr)) - if (auto typeLib = TypeLibraryFromName(view, image->name)) + { + // The objc_msgSend trampolines live in their own libobjcMsgSend* dylibs which have no type + // library. Look in libobjc.A.dylib instead. + std::string typeLibName = image->name; + if (typeLibName.rfind("/usr/lib/objc/libobjcMsgSend", 0) == 0) + typeLibName = "/usr/lib/libobjc.A.dylib"; + + if (auto typeLib = TypeLibraryFromName(view, typeLibName)) if (Ref libraryType = view.ImportTypeLibraryObject(typeLib, {symbol->name}); libraryType) selectedType = libraryType; + } if (selectedType != nullptr) targetFunc->ApplyAutoDiscoveredType(selectedType); @@ -112,7 +120,18 @@ void IdentifyStub(BinaryView& view, const SharedCacheController& controller, uin view.DefineAutoSymbol(bnSymbol); } -void AnalyzeStubFunction(Ref func, Ref mlil, SharedCacheController& controller, bool loadImage) +// Controls which images AnalyzeStubFunction may auto-load to resolve a stub's jump target. +enum class StubImageLoading +{ + // Do not auto-load any target image. + None, + // Only the dedicated objc_msgSend libraries introduced in macOS 27. + ObjCMsgSendOnly, + // Any directly referenced image. + AnyReferenced, +}; + +void AnalyzeStubFunction(Ref func, Ref mlil, SharedCacheController& controller, StubImageLoading imageLoading) { // 1. Identify the load target and load the region, resolving the load to a const pointer. // 2. We _should_ have a proper call now to the appropriate external function (external to the current image) @@ -138,6 +157,20 @@ void AnalyzeStubFunction(Ref func, Ref mlil, Sh const auto image = controller.GetImageContaining(imageAddr); if (!image.has_value() || controller.IsImageLoaded(*image)) return false; + + const bool isLibobjcMsgSend = image->name.rfind("/usr/lib/objc/libobjcMsgSend", 0) == 0; + if (imageLoading == StubImageLoading::ObjCMsgSendOnly && !isLibobjcMsgSend) + return false; + + if (isLibobjcMsgSend) + { + // The selectors referenced by `objc_msgSend` stubs still live in libobjc.A.dylib. + // Load it too so they are resolved to strings rather than `sel_` symbols. + auto libobjc = controller.GetImageWithName("/usr/lib/libobjc.A.dylib"); + if (libobjc && !controller.IsImageLoaded(*libobjc)) + controller.ApplyImage(*view, *libobjc); + } + return controller.ApplyImage(*view, *image); }; @@ -145,8 +178,7 @@ void AnalyzeStubFunction(Ref func, Ref mlil, Sh // Skip if already loaded. if (view->IsValidOffset(targetAddr)) return false; - // If the stub function is allowed to load images (for inlining) - if (loadImage && loadTargetImage(targetAddr)) + if (imageLoading != StubImageLoading::None && loadTargetImage(targetAddr)) return true; return loadStubIslandRegion(targetAddr); }; @@ -358,10 +390,12 @@ void AnalyzeFunction(Ref ctx) AnalyzeStandardFunction(func, mlilSsa, *controller); break; case StubFunction: - AnalyzeStubFunction(func, mlilSsa, *controller, false); + AnalyzeStubFunction(func, mlilSsa, *controller, + workflowState->autoLoadObjCStubRequirements ? StubImageLoading::ObjCMsgSendOnly : StubImageLoading::None); break; case ObjCStubFunction: - AnalyzeStubFunction(func, mlilSsa, *controller, workflowState->autoLoadObjCStubRequirements); + AnalyzeStubFunction(func, mlilSsa, *controller, + workflowState->autoLoadObjCStubRequirements ? StubImageLoading::AnyReferenced : StubImageLoading::None); break; } } From 0deca0aafd6f5a3ff3990825031e86d6f968b6cd Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Thu, 11 Jun 2026 11:11:04 -0700 Subject: [PATCH 3/4] [Obj-C] Add support for method type strings that are pointers relative to selector base address These show up in iOS 27 shared caches. --- objectivec/objc.cpp | 43 ++++++++++++++++++++++++++++------ objectivec/objc.h | 3 ++- view/sharedcache/core/ObjC.cpp | 19 +++++++++++---- view/sharedcache/core/ObjC.h | 3 ++- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/objectivec/objc.cpp b/objectivec/objc.cpp index d3ab8525fe..814e4c0803 100644 --- a/objectivec/objc.cpp +++ b/objectivec/objc.cpp @@ -918,8 +918,11 @@ void ObjCProcessor::LoadProtocols(ObjCReader* reader, Ref
listSection) } } -void ObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth) +void ObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase) { + // `typesAreOffsetsFromSelectorBase` is only relevant for shared caches + (void)typesAreOffsetsFromSelectorBase; + uint64_t offset = reader->GetOffset(); meth.name = offset + reader->ReadS32(); @@ -980,15 +983,16 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri uint64_t pointerSize = m_data->GetAddressSize(); bool relativeOffsets = (head.entsizeAndFlags & 0xFFFF0000) & 0x80000000; bool directSelectors = (head.entsizeAndFlags & 0xFFFF0000) & 0x40000000; + bool typesAreOffsetsFromSelectorBase = (head.entsizeAndFlags & 0xFFFF0000) & 0x20000000; auto methodSize = relativeOffsets ? 12 : pointerSize * 3; DefineObjCSymbol(DataSymbol, m_typeNames.methodList, "method_list_" + std::string(name), start, true); for (unsigned i = 0; i < head.count; i++) { + auto cursor = start + sizeof(method_list_t) + (i * methodSize); try { Method method; - auto cursor = start + sizeof(method_list_t) + (i * methodSize); reader->Seek(cursor); method_t meth; // workflow_objc support @@ -997,7 +1001,7 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri // -- if (relativeOffsets) { - GetRelativeMethod(reader, meth); + GetRelativeMethod(reader, meth, typesAreOffsetsFromSelectorBase); } else { @@ -1050,8 +1054,12 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri m_selRefToImplementations[selRefAddr].push_back(meth.imp); // -- - DefineObjCSymbol(DataSymbol, relativeOffsets ? m_typeNames.methodEntry : m_typeNames.method, - "method_" + method.name, cursor, true); + QualifiedName methodTypeName = m_typeNames.method; + if (relativeOffsets) + methodTypeName = typesAreOffsetsFromSelectorBase && !m_typeNames.methodEntryTypeOffsets.IsEmpty() + ? m_typeNames.methodEntryTypeOffsets + : m_typeNames.methodEntry; + DefineObjCSymbol(DataSymbol, methodTypeName, "method_" + method.name, cursor, true); method.imp = meth.imp; cls.methodList[cursor] = method; m_localMethods[cursor] = method; @@ -1061,10 +1069,14 @@ void ObjCProcessor::ReadMethodList(ObjCReader* reader, ClassBase& cls, std::stri if (selRefAddr) m_data->AddDataReference(selRefAddr, meth.imp); } + catch (const std::exception& ex) + { + m_logger->LogErrorF( + "Failed to process a method at offset {:#x} in method list \"{}\": {}", cursor, name, ex.what()); + } catch (...) { - m_logger->LogError( - "Failed to process a method at offset 0x%llx", start + sizeof(method_list_t) + (i * methodSize)); + m_logger->LogErrorF("Failed to process a method at offset {:#x} in method list \"{}\"", cursor, name); } } } @@ -1512,6 +1524,23 @@ void ObjCProcessor::ProcessObjCData() auto type = finalizeStructureBuilder(m_data, methodEntry, "objc_method_entry_t"); m_typeNames.methodEntry = type.first; + // Shared caches built with type offsets store the `types` field as an offset from the same base address as + // relative selectors. That base address is only known for shared caches, so the struct is only defined for them. + if (relativeSelectorBaseOffset) + { + auto relativeTypesPtrName = defineTypedef(m_data, {"rel_types"}, + TypeBuilder::PointerType(4, Type::PointerType(addrSize, Type::IntegerType(1, false))) + .SetPointerBase(RelativeToConstantPointerBaseType, relativeSelectorBaseOffset) + .Finalize()); + + StructureBuilder methodEntryTypeOffsets; + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeSelectorPtrName), "name"); + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeTypesPtrName), "types"); + methodEntryTypeOffsets.AddMember(Type::NamedType(m_data, relativeIMPPtrName), "imp"); + type = finalizeStructureBuilder(m_data, methodEntryTypeOffsets, "objc_method_entry_type_offsets_t"); + m_typeNames.methodEntryTypeOffsets = type.first; + } + StructureBuilder method; method.AddMember(Type::PointerType(addrSize, Type::IntegerType(1, true)), "name"); method.AddMember(Type::PointerType(addrSize, Type::IntegerType(1, true)), "types"); diff --git a/objectivec/objc.h b/objectivec/objc.h index e0b2167e4d..991d1daac3 100644 --- a/objectivec/objc.h +++ b/objectivec/objc.h @@ -268,6 +268,7 @@ namespace BinaryNinja { QualifiedName imageInfoSwiftVersion; QualifiedName imageInfo; QualifiedName methodEntry; + QualifiedName methodEntryTypeOffsets; QualifiedName method; QualifiedName methodList; QualifiedName classRO; @@ -337,7 +338,7 @@ namespace BinaryNinja { Ref m_logger; virtual uint64_t GetObjCRelativeMethodBaseAddress(ObjCReader* reader); - virtual void GetRelativeMethod(ObjCReader* reader, method_t& meth); + virtual void GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase); virtual std::shared_ptr GetReader() = 0; // Because an objective-c processor might have access to other non-view symbols that we want to retrieve. // By default, this will just get symbol at the address in the view. diff --git a/view/sharedcache/core/ObjC.cpp b/view/sharedcache/core/ObjC.cpp index b411ed8752..64fecb0818 100644 --- a/view/sharedcache/core/ObjC.cpp +++ b/view/sharedcache/core/ObjC.cpp @@ -92,21 +92,30 @@ std::shared_ptr SharedCacheObjCProcessor::GetReader() return std::make_shared(reader); } -void SharedCacheObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth) +void SharedCacheObjCProcessor::GetRelativeMethod(ObjCReader* reader, method_t& meth, bool typesAreOffsetsFromSelectorBase) { if (m_customRelativeMethodSelectorBase.has_value()) { meth.name = m_customRelativeMethodSelectorBase.value() + reader->ReadS32(); - uint64_t offset = reader->GetOffset(); - meth.types = offset + reader->ReadS32(); + // `typesAreOffsetsFromSelectorBase` indicates that `types` is an unsigned offset into the cache's + // deduplicated string pool, relative to the same base address as relative method selectors. + if (typesAreOffsetsFromSelectorBase) + { + meth.types = m_customRelativeMethodSelectorBase.value() + reader->Read32(); + } + else + { + uint64_t offset = reader->GetOffset(); + meth.types = offset + reader->ReadS32(); + } - offset += sizeof(int32_t); + uint64_t offset = reader->GetOffset(); meth.imp = offset + reader->ReadS32(); } else { - ObjCProcessor::GetRelativeMethod(reader, meth); + ObjCProcessor::GetRelativeMethod(reader, meth, typesAreOffsetsFromSelectorBase); } } diff --git a/view/sharedcache/core/ObjC.h b/view/sharedcache/core/ObjC.h index e8c47e70fe..13be2ad9b5 100644 --- a/view/sharedcache/core/ObjC.h +++ b/view/sharedcache/core/ObjC.h @@ -78,7 +78,8 @@ namespace DSCObjC { std::shared_ptr GetReader() override; - void GetRelativeMethod(BinaryNinja::ObjCReader* reader, BinaryNinja::method_t& meth) override; + void GetRelativeMethod(BinaryNinja::ObjCReader* reader, BinaryNinja::method_t& meth, + bool typesAreOffsetsFromSelectorBase) override; BinaryNinja::Ref GetSymbol(uint64_t address) override; From f2c275d91afcb9cc24baaaf2e3b14b4f96b2f2e9 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Tue, 9 Jun 2026 12:26:12 -0700 Subject: [PATCH 4/4] [ObjC] Introduce a new activity to rename objc_msgSend stub functions This helps for stripped binaries, and in cases such as the macOS 27 shared cache where the symbols are no longer accruate for stub functions since they are coalesced into stub island regions outside of any dylib. --- plugins/workflow_objc/src/activities/mod.rs | 1 + .../src/activities/name_stubs.rs | 99 +++++++++++++++++++ .../src/activities/objc_msg_send_calls.rs | 6 +- plugins/workflow_objc/src/workflow.rs | 16 ++- 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 plugins/workflow_objc/src/activities/name_stubs.rs diff --git a/plugins/workflow_objc/src/activities/mod.rs b/plugins/workflow_objc/src/activities/mod.rs index b418c1a655..2ff180710c 100644 --- a/plugins/workflow_objc/src/activities/mod.rs +++ b/plugins/workflow_objc/src/activities/mod.rs @@ -1,5 +1,6 @@ pub mod alloc_init; pub mod inline_stubs; +pub mod name_stubs; pub mod objc_msg_send_calls; pub mod remove_memory_management; pub mod super_init; diff --git a/plugins/workflow_objc/src/activities/name_stubs.rs b/plugins/workflow_objc/src/activities/name_stubs.rs new file mode 100644 index 0000000000..f39d6aa0e1 --- /dev/null +++ b/plugins/workflow_objc/src/activities/name_stubs.rs @@ -0,0 +1,99 @@ +use binaryninja::{ + low_level_il::instruction::{InstructionHandler as _, LowLevelILInstructionKind}, + symbol::{Symbol, SymbolType}, + variable::PossibleValueSet, + workflow::AnalysisContext, +}; + +use crate::{ + activities::objc_msg_send_calls::{call_target_type, selector_from_call, MessageSendType}, + error::ILLevel, + metadata::GlobalState, + Error, +}; + +// Reconstruct names for Objective-C selector stubs, e.g. `_objc_msgSend$length`. +// The compiler emits one of these stubs per selector. Each one does nothing but load the selector and tail-call +// `objc_msgSend`. +pub fn process(ac: &AnalysisContext) -> Result<(), Error> { + let view = ac.view(); + if GlobalState::should_ignore_view(&view) { + return Ok(()); + } + + let func = ac.function(); + let func_start = func.start(); + + // Don't override a name the user has assigned to this function. + if let Some(symbol) = view.symbol_by_address(func_start) { + if !symbol.auto_defined() { + return Ok(()); + } + } + + // Bail out early for functions that do not look like a stub: a single basic block of a few instructions. + const MAX_STUB_SIZE: u64 = 32; + if func.highest_address().saturating_sub(func_start) > MAX_STUB_SIZE + || func.basic_blocks().len() != 1 + { + return Ok(()); + } + + let Some(llil) = (unsafe { ac.llil_function() }) else { + return Err(Error::MissingIL { + level: ILLevel::Low, + func_start, + }); + }; + let Some(ssa) = llil.ssa_form() else { + return Err(Error::MissingSsaForm { + level: ILLevel::Low, + func_start, + }); + }; + + // The tail call terminates the stub's single basic block, so it can only be the last instruction. + let blocks = ssa.basic_blocks(); + let Some(block) = blocks.iter().next() else { + return Ok(()); + }; + let Some(insn) = block.iter().last() else { + return Ok(()); + }; + let LowLevelILInstructionKind::TailCallSsa(call_op) = insn.kind() else { + return Ok(()); + }; + + // A selector stub does nothing besides load a selector and tail-call objc_msgSend. + // Reject any function that performs a regular call or memory write before the tail call. + if block.iter().any(|insn| { + matches!( + insn.kind(), + LowLevelILInstructionKind::CallSsa(_) + | LowLevelILInstructionKind::Store(_) + | LowLevelILInstructionKind::StoreSsa(_) + ) + }) { + return Ok(()); + } + + let call_target = match call_op.target().possible_values() { + PossibleValueSet::ConstantValue { value } + | PossibleValueSet::ConstantPointerValue { value } + | PossibleValueSet::ImportedAddressValue { value } => value as u64, + _ => return Ok(()), + }; + + if call_target_type(&view, call_target) != Some(MessageSendType::Normal) { + return Ok(()); + } + let Some(selector) = selector_from_call(&view, &ssa, &call_op) else { + return Ok(()); + }; + + let name = format!("_objc_msgSend${}", selector.name); + let symbol = Symbol::builder(SymbolType::Function, &name, func_start).create(); + view.define_auto_symbol(&symbol); + + Ok(()) +} diff --git a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs index 41fc38c065..1d9c580ef4 100644 --- a/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs +++ b/plugins/workflow_objc/src/activities/objc_msg_send_calls.rs @@ -119,12 +119,12 @@ fn process_instruction( } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum MessageSendType { +pub(crate) enum MessageSendType { Normal, Super, } -fn call_target_type(bv: &BinaryView, call_target: u64) -> Option { +pub(crate) fn call_target_type(bv: &BinaryView, call_target: u64) -> Option { let name = bv .symbol_by_address(call_target) .map(|s| s.raw_name().to_string_lossy().into_owned())?; @@ -141,7 +141,7 @@ fn call_target_type(bv: &BinaryView, call_target: u64) -> Option, call_op: &Operation, diff --git a/plugins/workflow_objc/src/workflow.rs b/plugins/workflow_objc/src/workflow.rs index 8c52e0f48e..be8c1be16b 100644 --- a/plugins/workflow_objc/src/workflow.rs +++ b/plugins/workflow_objc/src/workflow.rs @@ -37,6 +37,19 @@ pub fn register_activities() -> Result<(), WorkflowRegistrationError> { run(activities::objc_msg_send_calls::process), ); + let name_stubs_activity = Activity::new_with_action( + activity::Config::action( + "core.function.objectiveC.nameSelectorStubs", + "Obj-C: Rename Message Send Stubs", + "Reconstruct names for Objective-C selector stubs, such as _objc_msgSend$foo, that have no symbol table entry.", + ) + .eligibility( + activity::Eligibility::auto().predicate( + activity::ViewType::in_(["Mach-O", "DSCView"]), + )), + run(activities::name_stubs::process), + ); + let inline_stubs_activity = Activity::new_with_action( activity::Config::action( "core.function.objectiveC.inlineStubs", @@ -93,7 +106,8 @@ pub fn register_activities() -> Result<(), WorkflowRegistrationError> { ); workflow - .activity_after(&inline_stubs_activity, "core.function.translateTailCalls")? + .activity_after(&name_stubs_activity, "core.function.translateTailCalls")? + .activity_after(&inline_stubs_activity, &name_stubs_activity.name())? .activity_after(&objc_msg_send_calls_activity, &inline_stubs_activity.name())? .activity_before( &remove_memory_management_activity,