Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Sources/MachOExtensions/MachOBindRebaseResolving.swift
Original file line number Diff line number Diff line change
@@ -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 {}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a retroactive conformance of an external type (MachOFile from MachOKit) to a local protocol. The codebase already uses @retroactive for similar conformances; adding it here will avoid Swift’s retroactive-conformance warnings and makes the intent explicit.

Copilot uses AI. Check for mistakes.
9 changes: 9 additions & 0 deletions Sources/MachOReading/ReadingContext/MachOContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ public struct MachOContext<MachO: MachORepresentableWithCache & Readable>: 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
Expand Down
17 changes: 17 additions & 0 deletions Sources/MachOReading/ReadingContext/ReadingContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
15 changes: 15 additions & 0 deletions Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,19 @@ extension TypeMetadataRecord {
return nil
}
}

public func contextDescriptor<Context: ReadingContext>(in context: Context) throws -> ContextDescriptorWrapper? {
let fieldOffset = offset(of: \.nominalTypeDescriptor)
let relativeOffset = layout.nominalTypeDescriptor.relativeOffset
switch typeKind {
case .directTypeDescriptor:
let pointer = RelativeDirectPointer<ContextDescriptorWrapper>(relativeOffset: relativeOffset)
return try pointer.resolve(at: context.addressFromOffset(fieldOffset), in: context)
case .indirectTypeDescriptor:
let pointer = RelativeIndirectPointer<ContextDescriptorWrapper, Pointer<ContextDescriptorWrapper>>(relativeOffset: relativeOffset)
return try pointer.resolve(at: context.addressFromOffset(fieldOffset), in: context)
case .directObjCClassName, .indirectObjCClass:
return nil
}
}
}
35 changes: 12 additions & 23 deletions Sources/MachOSymbolPointers/SymbolOrElementPointer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,42 +75,31 @@ public enum SymbolOrElementPointer<Element: Resolvable>: RelativeIndirectType {
}

public static func resolve<MachO: MachORepresentableWithCache & Readable>(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))
Comment on lines 77 to +86
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says indirect symbolic-reference readers “stop hard-casting to MachOFile”, but there are still hard casts elsewhere (e.g. Sources/MachOSymbols/SymbolOrElement.swift). Either expand this refactor to cover those remaining call sites, or adjust the PR description to reflect the narrower scope.

Copilot uses AI. Check for mistakes.
}

public static func resolve(from ptr: UnsafeRawPointer) throws -> Self {
return try .address(ptr.stripPointerTags().assumingMemoryBound(to: UInt64.self).pointee)
}

public static func resolve<Context: ReadingContext>(at address: Context.Address, in context: Context) throws -> Self {
if let machOFileContext = context as? MachOContext<MachOFile> {
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))
}
}
Loading