From c348fd587434627d72f47eac57589efb093830aa Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Sun, 26 Apr 2026 20:36:54 +0800 Subject: [PATCH 1/2] refactor(macho): abstract bind/rebase resolution behind protocol Introduce MachOBindRebaseResolving so indirect symbolic reference readers no longer hard-cast to MachOFile. Wrapper types (UI-layer projections that compose a MachOFile, etc.) can opt in by conforming and forwarding to their underlying MachOFile. - Add MachOBindRebaseResolving in MachOExtensions; MachOFile conforms - Expose bindRebaseResolver on ReadingContext (default nil) and on MachOContext (cast-through when the underlying MachO conforms) - SymbolOrElementPointer.resolve consults the protocol via context lookup instead of casting to MachOFile / MachOContext - Add generic contextDescriptor(in:) overload on TypeMetadataRecord --- .../MachOBindRebaseResolving.swift | 26 ++++++++++++++ .../ReadingContext/MachOContext.swift | 9 +++++ .../ReadingContext/ReadingContext.swift | 17 +++++++++ .../MachOSwiftSection/MachOFile+Swift.swift | 9 ++++- .../Models/Type/TypeMetadataRecord.swift | 15 ++++++++ .../SymbolOrElementPointer.swift | 35 +++++++------------ 6 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 Sources/MachOExtensions/MachOBindRebaseResolving.swift diff --git a/Sources/MachOExtensions/MachOBindRebaseResolving.swift b/Sources/MachOExtensions/MachOBindRebaseResolving.swift new file mode 100644 index 00000000..dbe3cfd1 --- /dev/null +++ b/Sources/MachOExtensions/MachOBindRebaseResolving.swift @@ -0,0 +1,26 @@ +import MachOKit + +/// Exposes dyld bind / rebase resolution capabilities required when decoding +/// indirect symbolic references in Swift metadata. +/// +/// `SymbolOrElementPointer` and other indirect-pointer readers must consult +/// this protocol before treating the bytes at a relocation site as a literal +/// virtual address. For files coming straight from disk the bytes are still +/// chained-fixup encoded; only after binding/rebasing through the dyld +/// metadata do they become a usable address. +/// +/// The protocol exists so the resolver code can stay generic over the reading +/// context and not hard-code `MachOFile`. Wrapper types (e.g. UI-layer +/// projections that compose a `MachOFile` plus extra state) just forward to +/// the underlying `MachOFile` to participate in the same dispatch. +public protocol MachOBindRebaseResolving: Sendable { + /// Resolves a bind operation at the given file offset and returns the + /// imported symbol name, or `nil` when the offset is not bound. + func resolveBind(fileOffset: Int) -> String? + + /// Resolves a rebase operation at the given file offset and returns the + /// rebased absolute address, or `nil` when no rebase is recorded. + func resolveRebase(fileOffset: Int) -> UInt64? +} + +extension MachOFile: MachOBindRebaseResolving {} diff --git a/Sources/MachOReading/ReadingContext/MachOContext.swift b/Sources/MachOReading/ReadingContext/MachOContext.swift index 44e41e04..5b1dce86 100644 --- a/Sources/MachOReading/ReadingContext/MachOContext.swift +++ b/Sources/MachOReading/ReadingContext/MachOContext.swift @@ -81,6 +81,15 @@ public struct MachOContext: Readi public func offsetFromAddress(_ address: Int) throws -> Int { address } + + /// Vends the underlying MachO object as a bind/rebase resolver when it + /// conforms to `MachOBindRebaseResolving`. The runtime cast keeps the + /// generic parameter `MachO` unconstrained, so wrapper types (e.g. + /// UI-layer projections that compose a `MachOFile`) can opt in by + /// declaring conformance themselves without changing this site. + public var bindRebaseResolver: (any MachOBindRebaseResolving)? { + machO as? any MachOBindRebaseResolving + } } // MARK: - Convenience Extensions diff --git a/Sources/MachOReading/ReadingContext/ReadingContext.swift b/Sources/MachOReading/ReadingContext/ReadingContext.swift index d2c56aee..f54a82bd 100644 --- a/Sources/MachOReading/ReadingContext/ReadingContext.swift +++ b/Sources/MachOReading/ReadingContext/ReadingContext.swift @@ -239,4 +239,21 @@ public protocol ReadingContext: Sendable { /// - Returns: The integer offset representation /// - Throws: If the address cannot be converted func offsetFromAddress(_ address: Address) throws -> Int + + /// Optional bridge to dyld bind / rebase resolution. + /// + /// Indirect symbolic references encoded in Swift mangled names point at + /// relocation sites whose bytes are still chained-fixup encoded on disk. + /// Readers must consult bind / rebase tables before treating those bytes + /// as a real address. Contexts that wrap a `MachOFile` return the + /// underlying file (or any `MachOBindRebaseResolving` adapter) here; + /// contexts that read already-relocated memory (in-process images, etc.) + /// return `nil` and the caller falls back to a direct read. + var bindRebaseResolver: (any MachOBindRebaseResolving)? { get } +} + +extension ReadingContext { + /// Default: no bind/rebase support. Concrete contexts override when they + /// can vend a resolver (see `MachOContext`'s implementation). + public var bindRebaseResolver: (any MachOBindRebaseResolving)? { nil } } diff --git a/Sources/MachOSwiftSection/MachOFile+Swift.swift b/Sources/MachOSwiftSection/MachOFile+Swift.swift index ac5c20ac..66f733d1 100644 --- a/Sources/MachOSwiftSection/MachOFile+Swift.swift +++ b/Sources/MachOSwiftSection/MachOFile+Swift.swift @@ -128,7 +128,14 @@ extension MachOFile.Swift { } let recordSize = MemoryLayout.size let records: [TypeMetadataRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize) - return try records.compactMap { try $0.contextDescriptor(in: machO) } + return try records.compactMap { + do { + return try $0.contextDescriptor(in: machO) + } catch { + print(error) + throw error + } + } } private func _readProtocolRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOFile) throws -> [ProtocolDescriptor] { diff --git a/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift b/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift index ae510848..e3b78333 100644 --- a/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift +++ b/Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift @@ -49,4 +49,19 @@ extension TypeMetadataRecord { return nil } } + + public func contextDescriptor(in context: Context) throws -> ContextDescriptorWrapper? { + let fieldOffset = offset(of: \.nominalTypeDescriptor) + let relativeOffset = layout.nominalTypeDescriptor.relativeOffset + switch typeKind { + case .directTypeDescriptor: + let pointer = RelativeDirectPointer(relativeOffset: relativeOffset) + return try pointer.resolve(at: context.addressFromOffset(fieldOffset), in: context) + case .indirectTypeDescriptor: + let pointer = RelativeIndirectPointer>(relativeOffset: relativeOffset) + return try pointer.resolve(at: context.addressFromOffset(fieldOffset), in: context) + case .directObjCClassName, .indirectObjCClass: + return nil + } + } } diff --git a/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift b/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift index f92160a4..0bcf9bef 100644 --- a/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift +++ b/Sources/MachOSymbolPointers/SymbolOrElementPointer.swift @@ -75,20 +75,15 @@ public enum SymbolOrElementPointer: RelativeIndirectType { } public static func resolve(from offset: Int, in machO: MachO) throws -> Self { - if let machOFile = machO as? MachOFile { - if let symbol = machOFile.resolveBind(fileOffset: offset) { + if let resolver = machO as? any MachOBindRebaseResolving { + if let symbol = resolver.resolveBind(fileOffset: offset) { return .symbol(.init(offset: offset, name: symbol)) - } else { - let resolvedFileOffset = offset - if let rebase = machOFile.resolveRebase(fileOffset: resolvedFileOffset) { - return .address(rebase) - } else { - return try .address(machOFile.readElement(offset: resolvedFileOffset)) - } } - } else { - return try .address(machO.readElement(offset: offset)) + if let rebase = resolver.resolveRebase(fileOffset: offset) { + return .address(rebase) + } } + return try .address(machO.readElement(offset: offset)) } public static func resolve(from ptr: UnsafeRawPointer) throws -> Self { @@ -96,21 +91,15 @@ public enum SymbolOrElementPointer: RelativeIndirectType { } public static func resolve(at address: Context.Address, in context: Context) throws -> Self { - if let machOFileContext = context as? MachOContext { - let machOFile = machOFileContext.machO + if let resolver = context.bindRebaseResolver { let offset = try context.offsetFromAddress(address) - if let symbol = machOFile.resolveBind(fileOffset: offset) { + if let symbol = resolver.resolveBind(fileOffset: offset) { return .symbol(.init(offset: offset, name: symbol)) - } else { - let resolvedFileOffset = offset - if let rebase = machOFile.resolveRebase(fileOffset: resolvedFileOffset) { - return .address(rebase) - } else { - return try .address(machOFile.readElement(offset: resolvedFileOffset)) - } } - } else { - return try .address(context.readElement(at: address)) + if let rebase = resolver.resolveRebase(fileOffset: offset) { + return .address(rebase) + } } + return try .address(context.readElement(at: address)) } } From a4d5ea2161ae471e11de66dda7a386d7cece0e7e Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Sun, 26 Apr 2026 22:15:12 +0800 Subject: [PATCH 2/2] chore(macho): drop leftover debug print in type record reader The debug `print(error)` in `_readTypeMetadataRecords` was a leftover from local debugging and not part of the bind/rebase abstraction. --- Sources/MachOSwiftSection/MachOFile+Swift.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/MachOSwiftSection/MachOFile+Swift.swift b/Sources/MachOSwiftSection/MachOFile+Swift.swift index 66f733d1..ac5c20ac 100644 --- a/Sources/MachOSwiftSection/MachOFile+Swift.swift +++ b/Sources/MachOSwiftSection/MachOFile+Swift.swift @@ -128,14 +128,7 @@ extension MachOFile.Swift { } let recordSize = MemoryLayout.size let records: [TypeMetadataRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize) - return try records.compactMap { - do { - return try $0.contextDescriptor(in: machO) - } catch { - print(error) - throw error - } - } + return try records.compactMap { try $0.contextDescriptor(in: machO) } } private func _readProtocolRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOFile) throws -> [ProtocolDescriptor] {