diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 75eb2eb295..ebc5e646c4 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -451,6 +451,13 @@ complete observation. an unfixed zero in a nullable ABI field is treated as null. Incomplete or conflicting fixup streams keep affected pointers unresolved; coverage reports parsing and resource limits while retaining validated prefix facts. + Conformance records preserve the raw three-bit `type_reference_kind`. Kinds + 0 and 1 identify direct and indirect nominal descriptors; kind 2 preserves + the inline Objective-C class name and its source location. Kind 3 preserves + an imported class binding or the local class-object address when available, + but class metadata/name recovery requiring the Objective-C runtime remains + partial. Unknown kinds retain the raw kind and reference location without + being treated as absent or assigned a guessed type name. So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for example `NSObject`). The `pointer_fixups` coverage facet names the mechanism. diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index fca04b09c7..57612ab16a 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -214,8 +214,14 @@ export type MachoImportBinding = z.infer; /** Swift protocol conformance and its associated witness-table location. */ export const swiftConformanceSchema = z.strictObject({ type_name: z.string().min(1).nullable(), + /** Raw three-bit Swift ABI type-reference kind; absent in older Evidence. */ + type_reference_kind: z.number().int().min(0).max(7).optional(), + /** Address of the descriptor, class-name string, or resolved ObjC class reference. */ + type_reference_target: nativeMetadataLocationSchema.optional(), type_binding: machoImportBindingSchema.nullable().optional(), protocol_name: z.string().min(1).nullable(), + /** Resolved protocol descriptor address, retained when its bytes are unavailable. */ + protocol_target: nativeMetadataLocationSchema.optional(), protocol_binding: machoImportBindingSchema.nullable().optional(), module: z.string().nullable(), witness_table: nativeMetadataLocationSchema, diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index 92250d71bc..9793811c12 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -121,7 +121,7 @@ const fixture = ( const provenance = { path: "/fixture/app", sha256: "a".repeat(64) }; const swiftExternalSwiftBindingImage = ( - boundField: "protocol" | "type" = "protocol", + boundField: "protocol" | "type" | "objc-class" = "protocol", ) => { const bytes = fixture(); const sectionCommand = 32; @@ -141,7 +141,9 @@ const swiftExternalSwiftBindingImage = ( const bind = Buffer.from([ 0x11, // SET_DYLIB_ORDINAL_IMM 1 0x40, - ...Buffer.from(`${boundField === "protocol" ? "_$sSQMp" : "_$sSiMn"}\0`), + ...Buffer.from( + `${boundField === "protocol" ? "_$sSQMp" : boundField === "objc-class" ? "_OBJC_CLASS_$_NSObject" : "_$sSiMn"}\0`, + ), 0x70, 0x80, 0x16, // SET_SEGMENT_AND_OFFSET_ULEB segment 0 + offset 0xb00 @@ -163,12 +165,15 @@ const swiftExternalSwiftBindingImage = ( bytes.writeInt32LE(0x10, 0xa00); bytes.writeInt32LE(boundField === "protocol" ? 0x1ec : 0xec, 0xa14); bytes.writeInt32LE(0, 0xa18); // Nullable witness-table pattern. - bytes.writeUInt32LE(boundField === "protocol" ? 0 : 8, 0xa1c); + bytes.writeUInt32LE( + boundField === "protocol" ? 0 : boundField === "objc-class" ? 24 : 8, + 0xa1c, + ); bytes.writeInt32LE(boundField === "protocol" ? 0xf1 : 0x2f0, 0xa10); // Protocol reference: imported indirect slot or local descriptor. bytes.writeBigUInt64LE(0n, 0xb00); // Bound slot has no in-image pointer. bytes.writeUInt32LE(16, 0xc00); bytes.writeInt32LE(0x1f8, 0xc08); - if (boundField === "type") { + if (boundField !== "protocol") { bytes.writeUInt32LE(3, 0xd00); bytes.writeInt32LE(0xf8, 0xd08); bytes.write("Equatable\0", 0xe00); @@ -237,6 +242,28 @@ const swiftBoundWitnessSlotImage = () => { return bytes; }; +const swiftConformanceTypeReferenceImage = ( + typeKind: number, + input: { + readonly typeTargetOffset?: number; + readonly indirectClassTargetOffset?: number; + } = {}, +) => { + const bytes = swiftBoundWitnessSlotImage(); + const typeTargetOffset = input.typeTargetOffset ?? 0xe60; + bytes.writeUInt32LE(typeKind << 3, 0xa1c); + bytes.writeInt32LE(typeTargetOffset - 0xa14, 0xa14); + if (typeKind === 2 && typeTargetOffset === 0xe60) { + bytes.write("NSObject\0", typeTargetOffset); + } else if (typeKind === 3 && typeTargetOffset === 0xe60) { + bytes.writeBigUInt64LE( + base + BigInt(input.indirectClassTargetOffset ?? 0xe80), + typeTargetOffset, + ); + } + return bytes; +}; + describe("Swift imported protocol references", () => { it("preserves bind identity without reading VM address zero and marks coverage partial", () => { const result = decodeAppleDispatchMetadata( @@ -282,6 +309,7 @@ describe("Swift imported protocol references", () => { expect(result.swift_conformances).toMatchObject([ { type_name: null, + type_reference_kind: 1, type_binding: { symbol: "_$sSiMn", requested_address: "0x100000b00", @@ -298,6 +326,27 @@ describe("Swift imported protocol references", () => { ).toMatchObject({ status: "partial" }); }); + it("preserves a kind-3 Objective-C class import without inventing a class name", () => { + const result = decodeAppleDispatchMetadata( + swiftExternalSwiftBindingImage("objc-class"), + 100, + provenance, + ); + expect(result.swift_conformances).toMatchObject([ + { + type_name: null, + type_reference_kind: 3, + type_reference_target: { address: null, file_offset: null }, + type_binding: { + symbol: "_OBJC_CLASS_$_NSObject", + requested_address: "0x100000b00", + }, + protocol_name: "Equatable", + decode: { status: "partial" }, + }, + ]); + }); + it("reports only the legacy stream mechanisms actually examined", () => { const bytes = swiftBoundWitnessSlotImage(); const dyldInfo = 32 + 232; @@ -325,6 +374,8 @@ describe("Swift imported protocol references", () => { expect(result.swift_conformances).toMatchObject([ { type_name: "Widget", + type_reference_kind: 0, + type_reference_target: { address: "0x100000c00", file_offset: 3072 }, protocol_name: "Equatable", witness_table: { address: "0x100000e00" }, decode: { status: "decoded" }, @@ -350,6 +401,256 @@ describe("Swift imported protocol references", () => { }); }); +describe("Swift conformance type-reference kinds", () => { + it("keeps a direct Objective-C class name and its local witness evidence", () => { + const result = decodeAppleDispatchMetadata( + swiftConformanceTypeReferenceImage(2), + 100, + provenance, + ); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + type_reference_kind: 2, + type_reference_target: { address: "0x100000e60", file_offset: 3680 }, + protocol_name: "Equatable", + witness_table: { address: "0x100000e00" }, + decode: { status: "decoded" }, + }, + ]); + expect(result.swift_dispatch_slots[0]).toMatchObject({ + owner: "NSObject: Equatable", + table_kind: "witness_table", + implementation_address: null, + implementation_binding: { + symbol: "_externalWitnessImpl", + requested_address: "0x100000e08", + }, + }); + }); +}); + +describe("Swift partial conformance reference retention", () => { + it("retains type and witness facts when the protocol target is unmapped", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeInt32LE(0x5000 - 0xa10, 0xa10); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + type_reference_target: { address: "0x100000e60", file_offset: 3680 }, + protocol_name: null, + protocol_target: { address: "0x100005000", file_offset: null }, + witness_table: { address: "0x100000e00", file_offset: 3584 }, + decode: { status: "partial" }, + }, + ]); + expect(result.swift_dispatch_slots).toEqual([]); + }); + + it("preserves a typed type binding beside an unmapped protocol without a guessed name", () => { + const bytes = swiftExternalSwiftBindingImage("objc-class"); + bytes.writeInt32LE(0x5000 - 0xa10, 0xa10); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: null, + type_reference_kind: 3, + type_reference_target: { address: null, file_offset: null }, + type_binding: { + symbol: "_OBJC_CLASS_$_NSObject", + requested_address: "0x100000b00", + }, + protocol_name: null, + protocol_target: { address: "0x100005000", file_offset: null }, + decode: { status: "partial" }, + }, + ]); + }); + + it("retains protocol and type targets when the protocol name is unmapped", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeInt32LE(0x5000 - 0xd08, 0xd08); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + protocol_name: null, + protocol_target: { address: "0x100000d00", file_offset: 3328 }, + witness_table: { address: "0x100000e00", file_offset: 3584 }, + decode: { status: "partial" }, + }, + ]); + expect(result.swift_dispatch_slots).toEqual([]); + }); + + it("retains other facts when the protocol descriptor kind is unsupported", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeUInt32LE(4, 0xd00); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + protocol_name: null, + protocol_target: { address: "0x100000d00", file_offset: 3328 }, + witness_table: { address: "0x100000e00", file_offset: 3584 }, + decode: { status: "partial" }, + }, + ]); + }); + + it("retains other facts when a protocol name is invalid UTF-8", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeUInt8(0xff, 0xe50); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + protocol_name: null, + protocol_target: { address: "0x100000d00", file_offset: 3328 }, + witness_table: { address: "0x100000e00", file_offset: 3584 }, + decode: { status: "partial" }, + }, + ]); + }); + + it("retains mixed readable witness slots when a later slot is unmapped", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeUInt32LE(2, 0xd10); + bytes.writeInt32LE(0xff0 - 0xa18, 0xa18); + bytes.writeBigUInt64LE(base + 0xa10n, 0xff0); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: "NSObject", + protocol_name: "Equatable", + witness_table: { address: "0x100000ff0", file_offset: 4080 }, + }, + ]); + expect(result.swift_dispatch_slots).toMatchObject([ + { + slot_index: 1, + location: { address: "0x100000ff8", file_offset: 4088 }, + }, + ]); + expect(result.swift_dispatch_slots).toHaveLength(1); + expect( + result.coverage.find( + ({ facet }) => facet === "swift_conformances_static_witness_slots", + ), + ).toMatchObject({ status: "partial" }); + }); +}); + +describe("Swift unresolved conformance type references", () => { + it.each([3, 4, 7] as const)( + "retains an unresolved type-reference kind %i with conformance and witness locations", + (typeKind) => { + const result = decodeAppleDispatchMetadata( + swiftConformanceTypeReferenceImage(typeKind), + 100, + provenance, + ); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: null, + type_reference_kind: typeKind, + ...(typeKind === 3 + ? { + type_reference_target: { + address: "0x100000e80", + file_offset: 3712, + }, + } + : {}), + protocol_name: "Equatable", + witness_table: { address: "0x100000e00" }, + decode: { status: "partial" }, + }, + ]); + expect(result.swift_dispatch_slots).toEqual([]); + expect( + result.coverage.find( + ({ facet }) => facet === "swift_conformances_static_witness_slots", + ), + ).toMatchObject({ status: "partial" }); + }, + ); + + it("retains a kind-2 source location when its inline class name is empty", () => { + const bytes = swiftConformanceTypeReferenceImage(2); + bytes.writeUInt8(0, 0xe60); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: null, + type_reference_kind: 2, + type_reference_target: { address: "0x100000e60", file_offset: 3680 }, + protocol_name: "Equatable", + witness_table: { address: "0x100000e00" }, + decode: { status: "partial" }, + }, + ]); + expect(result.swift_dispatch_slots).toEqual([]); + }); + + it.each([ + { kind: 0, target: 0x5000, actual: 0x5000 }, + { kind: 1, target: 0x5000, actual: 0x5000 }, + { kind: 2, target: 0x5000, actual: 0x5000 }, + { kind: 3, target: 0x5000, actual: 0x5000 }, + { + kind: 3, + target: 0xe60, + indirectClassTarget: 0x5000, + actual: 0x5000, + }, + { kind: 4, target: 0x5000, actual: 0x5000 }, + ])( + "retains partial conformance evidence when kind $kind targets non-file-backed address $target", + ({ kind, target, indirectClassTarget, actual }) => { + const result = decodeAppleDispatchMetadata( + swiftConformanceTypeReferenceImage(kind, { + typeTargetOffset: target, + ...(indirectClassTarget === undefined + ? {} + : { indirectClassTargetOffset: indirectClassTarget }), + }), + 100, + provenance, + ); + + expect(result.swift_conformances).toMatchObject([ + { + type_name: null, + type_reference_kind: kind, + type_reference_target: { + address: `0x${(base + BigInt(actual)).toString(16)}`, + file_offset: null, + }, + protocol_name: "Equatable", + witness_table: { address: "0x100000e00" }, + decode: { status: "partial" }, + }, + ]); + expect(result.swift_dispatch_slots).toEqual([]); + expect( + result.coverage.find( + ({ facet }) => facet === "swift_conformances_static_witness_slots", + ), + ).toMatchObject({ status: "partial" }); + }, + ); +}); + const withProtocolListSection = (bytes: Buffer): Buffer => { const pointer = (at: number, offset: number) => bytes.writeBigUInt64LE(base + BigInt(offset), at); diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index 007e44f8b5..ced870abda 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -44,6 +44,7 @@ import { createSwiftRelativeReader, decodeSwiftDispatchFacets, type DispatchRecordBudget, + type SwiftRelativeTargetLocation, } from "./AppleSwiftDispatchFacets.js"; import { decodeSwiftClassVtables } from "./AppleSwiftVtables.js"; import { inspectObjcClassImportBindings } from "./AppleObjcClassImportBindings.js"; @@ -97,6 +98,16 @@ export const decodeAppleDispatchMetadata = ( address: `0x${address.toString(16)}`, file_offset: offset(address), }); + const targetLocation = (address: bigint): SwiftRelativeTargetLocation => { + let fileOffset: number | null; + try { + fileOffset = offset(address); + } catch (cause) { + if (!(cause instanceof RangeError)) throw cause; + fileOffset = null; + } + return { address: `0x${address.toString(16)}`, file_offset: fileOffset }; + }; const evidence = (address: bigint, description: string) => [ { kind: "binary_metadata" as const, @@ -164,6 +175,7 @@ export const decodeAppleDispatchMetadata = ( bound, string, location, + targetLocation, evidence, offset, }; diff --git a/src/native/AppleSwiftConformanceTypeReference.test.ts b/src/native/AppleSwiftConformanceTypeReference.test.ts new file mode 100644 index 0000000000..9f8ab40df3 --- /dev/null +++ b/src/native/AppleSwiftConformanceTypeReference.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; + +import { decodeSwiftConformanceTypeReference } from "./AppleSwiftConformanceTypeReference.js"; +import type { SwiftDispatchReaders } from "./AppleSwiftDispatchFacets.js"; + +describe("Swift conformance type-reference read failures", () => { + it("propagates unexpected reader errors instead of reporting malformed evidence", () => { + const failure = new Error("reader invariant failed"); + const read = { + u32: () => { + throw failure; + }, + } as unknown as SwiftDispatchReaders; + + expect(() => + decodeSwiftConformanceTypeReference({ + descriptor: 0x100n, + flags: 0, + read, + relative: () => ({ kind: "target", target: 0x200n }), + unresolvedReason: () => "unresolved", + }), + ).toThrow(failure); + }); +}); diff --git a/src/native/AppleSwiftConformanceTypeReference.ts b/src/native/AppleSwiftConformanceTypeReference.ts new file mode 100644 index 0000000000..a076574374 --- /dev/null +++ b/src/native/AppleSwiftConformanceTypeReference.ts @@ -0,0 +1,265 @@ +import type { PointerResolution } from "./AppleMachoFixups.js"; +import type { + SwiftDispatchReaders, + SwiftRelativeField, + SwiftRelativeValue, +} from "./AppleSwiftDispatchFacets.js"; +import { expectedSwiftMetadataReadFailure } from "./AppleSwiftMetadataReadFailure.js"; + +const hex = (value: bigint) => `0x${value.toString(16)}`; + +export const swiftImportBinding = ( + requestedAddress: bigint, + binding: Extract, +) => ({ + requested_address: hex(requestedAddress), + symbol: binding.symbol, + binding_source: binding.binding_source, + library_ordinal: binding.library_ordinal, + library: binding.library, + weak: binding.weak, + addend: binding.addend.toString(), +}); + +export interface SwiftConformanceTypeReference { + readonly kind: number; + /** Descriptor, inline class-name, local ObjC class pointer, or raw target. */ + readonly target: bigint | null; + readonly name: string | null; + readonly binding: ReturnType | null; + readonly reason: string | null; + readonly staticWitnessIdentity: boolean; + readonly generic: boolean | null; +} + +interface SwiftTypeReferenceInput { + readonly descriptor: bigint; + readonly flags: number; + readonly read: SwiftDispatchReaders; + readonly relative: ( + field: bigint, + kind: SwiftRelativeField, + ) => SwiftRelativeValue; + readonly unresolvedReason: ( + value: Exclude, + field: SwiftRelativeField, + ) => string; +} + +const typeReferenceResult = (input: { + readonly kind: number; + readonly target: bigint | null; + readonly name?: string | null; + readonly binding?: ReturnType | null; + readonly reason?: string | null; + readonly generic?: boolean | null; +}): SwiftConformanceTypeReference => ({ + kind: input.kind, + target: input.target, + name: input.name ?? null, + binding: input.binding ?? null, + reason: input.reason ?? null, + staticWitnessIdentity: + input.name !== null && input.name !== undefined && input.kind <= 2, + generic: input.generic ?? null, +}); + +export const requiredSwiftRelativeTarget = ( + relative: (field: bigint, kind: SwiftRelativeField) => SwiftRelativeValue, + field: bigint, + kind: SwiftRelativeField, + unresolvedReason: SwiftTypeReferenceInput["unresolvedReason"], +): bigint => { + const value = relative(field, kind); + if (value.kind !== "target") + throw new RangeError(unresolvedReason(value, kind)); + return value.target; +}; + +const decodeNominalTypeReference = ( + kind: 0 | 1, + input: SwiftTypeReferenceInput, + value: SwiftRelativeValue, +): SwiftConformanceTypeReference => { + const { read, relative } = input; + let target = value.kind === "target" ? value.target : null; + let binding = + value.kind === "binding" + ? swiftImportBinding(value.requestedAddress, value.binding) + : null; + let reason = + value.kind === "unknown" + ? value.reason + : value.kind === "null" + ? "Null Swift conformance type reference" + : null; + try { + if (kind === 1 && target !== null) { + const pointer = read.resolvePointer(target); + if (pointer.kind === "bind") { + binding = swiftImportBinding(target, pointer); + target = null; + } else if (pointer.kind === "raw" && pointer.target === 0n) { + target = null; + reason = "Null indirect Swift conformance type reference"; + } else if (pointer.kind === "raw" || pointer.kind === "rebase") { + target = pointer.target; + } else { + target = null; + reason = pointer.reason; + } + } + if (target === null) + return typeReferenceResult({ kind, target, binding, reason }); + + const descriptorFlags = read.u32(target); + if (![16, 17, 18].includes(descriptorFlags & 31)) + return typeReferenceResult({ + kind, + target, + binding, + reason: "Unsupported Swift type descriptor kind", + }); + + const name = read.string( + requiredSwiftRelativeTarget( + relative, + target + 8n, + "typeName", + input.unresolvedReason, + ), + ); + return typeReferenceResult({ + kind, + target, + name: name.length === 0 ? null : name, + binding, + reason: name.length === 0 ? "Empty Swift conformance type name" : null, + generic: (descriptorFlags & 0x80) !== 0, + }); + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return typeReferenceResult({ + kind, + target, + binding, + reason, + }); + } +}; + +const decodeDirectObjcClassName = ( + kind: 2, + read: SwiftDispatchReaders, + value: SwiftRelativeValue, +): SwiftConformanceTypeReference => { + if (value.kind !== "target") + return typeReferenceResult({ + kind, + target: null, + reason: + value.kind === "unknown" + ? value.reason + : "Null Swift conformance type reference", + }); + try { + const name = read.string(value.target); + return typeReferenceResult({ + kind, + target: value.target, + name: name.length === 0 ? null : name, + reason: name.length === 0 ? "Empty inline Objective-C class name" : null, + }); + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return typeReferenceResult({ + kind, + target: value.target, + reason, + }); + } +}; + +const decodeIndirectObjcClass = ( + kind: 3, + read: SwiftDispatchReaders, + value: SwiftRelativeValue, +): SwiftConformanceTypeReference => { + if (value.kind !== "target") + return typeReferenceResult({ + kind, + target: null, + reason: + value.kind === "unknown" + ? value.reason + : "Null Swift conformance type reference", + }); + let pointer: PointerResolution; + try { + pointer = read.resolvePointer(value.target); + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return typeReferenceResult({ + kind, + target: value.target, + reason, + }); + } + if (pointer.kind === "bind") + return typeReferenceResult({ + kind, + target: null, + binding: swiftImportBinding(value.target, pointer), + reason: "Indirect Objective-C class metadata requires runtime lookup", + }); + if (pointer.kind === "raw" || pointer.kind === "rebase") + return typeReferenceResult({ + kind, + target: pointer.target === 0n ? null : pointer.target, + reason: "Indirect Objective-C class metadata requires runtime lookup", + }); + return typeReferenceResult({ + kind, + target: null, + reason: pointer.reason, + }); +}; + +/** Decode source-encoded class identity; ObjC runtime lookup remains unavailable. */ +export const decodeSwiftConformanceTypeReference = ( + input: SwiftTypeReferenceInput, +): SwiftConformanceTypeReference => { + const kind = (input.flags >>> 3) & 7; + try { + const value = input.relative(input.descriptor + 4n, "conformanceType"); + switch (kind) { + case 0: + case 1: + return decodeNominalTypeReference(kind, input, value); + case 2: + return decodeDirectObjcClassName(kind, input.read, value); + case 3: + return decodeIndirectObjcClass(kind, input.read, value); + default: + return typeReferenceResult({ + kind, + target: value.kind === "target" ? value.target : null, + reason: + value.kind === "unknown" + ? value.reason + : `Unsupported Swift conformance type-reference kind ${kind}`, + }); + } + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return typeReferenceResult({ + kind, + target: null, + reason, + }); + } +}; diff --git a/src/native/AppleSwiftDispatchFacets.ts b/src/native/AppleSwiftDispatchFacets.ts index 3c41cf7007..433d748ec5 100644 --- a/src/native/AppleSwiftDispatchFacets.ts +++ b/src/native/AppleSwiftDispatchFacets.ts @@ -2,6 +2,12 @@ import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js"; import type { PointerResolution } from "./AppleMachoFixups.js"; import type { Segment, Section } from "./AppleMachoSelection.js"; import { issue, type FacetDecodeFacts } from "./AppleDispatchDecodeFacts.js"; +import { + decodeSwiftConformanceTypeReference, + requiredSwiftRelativeTarget, + swiftImportBinding as importBinding, +} from "./AppleSwiftConformanceTypeReference.js"; +import { expectedSwiftMetadataReadFailure } from "./AppleSwiftMetadataReadFailure.js"; /** Resolved file-backed metadata location from the Mach-O layout. */ export interface ResolvedMetadataLocation { @@ -9,6 +15,12 @@ export interface ResolvedMetadataLocation { file_offset: number; } +/** Address fact for a relative target that may not map to file-backed bytes. */ +export interface SwiftRelativeTargetLocation { + address: string; + file_offset: number | null; +} + /** Binary-metadata evidence produced by the Apple dispatch readers. */ export type BinaryMetadataEvidence = { kind: "binary_metadata"; @@ -84,6 +96,84 @@ export const unresolvedSwiftRelativeReason = ( return `Swift relative pointer for ${field} has an unresolved external binding at ${hex(value.requestedAddress)}`; }; +interface SwiftProtocolFacts { + readonly name: string | null; + readonly readable: boolean; + readonly reason: string | null; +} + +const readSwiftProtocolFacts = (input: { + readonly target: bigint | null; + readonly relative: ( + field: bigint, + kind: SwiftRelativeField, + ) => SwiftRelativeValue; + readonly read: SwiftDispatchReaders; +}): SwiftProtocolFacts => { + const { target, relative, read } = input; + if (target === null) + return { + name: null, + readable: false, + reason: "Protocol descriptor target is unresolved", + }; + let descriptorKind: number; + try { + descriptorKind = read.u32(target) & 31; + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return { name: null, readable: false, reason }; + } + if (descriptorKind !== 3) + return { + name: null, + readable: false, + reason: "Unsupported Swift protocol descriptor kind", + }; + let nameAddress: bigint; + try { + nameAddress = requiredSwiftRelativeTarget( + relative, + target + 8n, + "protocolName", + unresolvedSwiftRelativeReason, + ); + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return { name: null, readable: false, reason }; + } + try { + return { name: read.string(nameAddress), readable: true, reason: null }; + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return { name: null, readable: false, reason }; + } +}; + +const readWitnessConformanceHeader = ( + read: SwiftDispatchReaders, + witness: bigint, + descriptor: bigint, +): string | null => { + try { + const header = read.resolvePointer(witness); + if (header.kind !== "raw" && header.kind !== "rebase") + return header.kind === "unknown" || header.kind === "unsupported" + ? header.reason + : "Witness-table conformance header is an external binding"; + return header.target === descriptor + ? null + : "Witness-table conformance header is unresolved or encoded"; + } catch (cause) { + const reason = expectedSwiftMetadataReadFailure(cause); + if (reason === null) throw cause; + return reason; + } +}; + /** Byte readers for Swift relative-pointer and conformance decode facets. */ export interface SwiftDispatchReaders { u32(address: bigint): number; @@ -91,25 +181,13 @@ export interface SwiftDispatchReaders { resolvePointer(address: bigint): PointerResolution; string(address: bigint): string; location(address: bigint): ResolvedMetadataLocation; + targetLocation(address: bigint): SwiftRelativeTargetLocation; evidence(address: bigint, description: string): BinaryMetadataEvidence; offset(address: bigint, size?: number): number; } const hex = (value: bigint) => `0x${value.toString(16)}`; const decoded = { status: "decoded" as const, reason: null }; -const importBinding = ( - requestedAddress: bigint, - binding: Extract, -) => ({ - requested_address: hex(requestedAddress), - symbol: binding.symbol, - binding_source: binding.binding_source, - library_ordinal: binding.library_ordinal, - library: binding.library, - weak: binding.weak, - addend: binding.addend.toString(), -}); - /** Create a Swift ABI signed-relative pointer decoder that records relative_pointers. */ export const createSwiftRelativeReader = (input: { bytes: Buffer; @@ -239,17 +317,7 @@ export const decodeSwiftDispatchFacets = (input: { result: ObjcSwiftMetadata; }): FacetDecodeFacts => { const { sections, segments, readers: read, relative, budget, result } = input; - const swiftFailures: string[] = []; const issues = [] as ReturnType[]; - const requiredRelative = ( - field: bigint, - kind: SwiftRelativeField, - ): bigint => { - const value = relative(field, kind); - if (value.kind !== "target") - throw new RangeError(unresolvedSwiftRelativeReason(value, kind)); - return value.target; - }; let swiftExamined = 0; let decodedRecords = 0; for (const section of sections.filter( @@ -262,7 +330,12 @@ export const decodeSwiftDispatchFacets = (input: { swiftExamined++; const field = section.address + BigInt(index * 4); try { - const descriptor = requiredRelative(field, "conformanceRecord"); + const descriptor = requiredSwiftRelativeTarget( + relative, + field, + "conformanceRecord", + unresolvedSwiftRelativeReason, + ); const flags = read.u32(descriptor + 12n); const protocolValue = relative(descriptor, "conformanceProtocol"); const protocol = @@ -274,12 +347,12 @@ export const decodeSwiftDispatchFacets = (input: { "conformanceProtocol", ) : null; - if (protocol !== null && (read.u32(protocol) & 31) !== 3) - throw new TypeError("Unsupported Swift protocol descriptor kind"); - const protocolName = - protocol === null - ? null - : read.string(requiredRelative(protocol + 8n, "protocolName")); + const protocolFacts = readSwiftProtocolFacts({ + target: protocol, + relative, + read, + }); + const protocolName = protocolFacts.name; const protocolBinding = protocolValue.kind === "binding" ? importBinding( @@ -287,107 +360,85 @@ export const decodeSwiftDispatchFacets = (input: { protocolValue.binding, ) : null; - const typeKind = (flags >>> 3) & 7; - if (typeKind !== 0 && typeKind !== 1) - throw new TypeError( - "Unsupported Swift conformance type-reference kind", - ); - const typeValue = relative(descriptor + 4n, "conformanceType"); - let type = typeValue.kind === "target" ? typeValue.target : null; - let typeBinding = - typeValue.kind === "binding" - ? importBinding(typeValue.requestedAddress, typeValue.binding) - : null; - let unresolvedTypeReason = - typeValue.kind === "unknown" ? typeValue.reason : null; - if (typeKind === 1 && type !== null) { - const pointer = read.resolvePointer(type); - if (pointer.kind === "bind") { - typeBinding = importBinding(type, pointer); - type = null; - } else if (pointer.kind === "raw" && pointer.target === 0n) { - type = null; - unresolvedTypeReason = - "Null indirect Swift conformance type reference"; - } else if (pointer.kind === "rebase" || pointer.kind === "raw") { - type = pointer.target; - } else { - type = null; - unresolvedTypeReason = pointer.reason; - } - } - if (type !== null && ![16, 17, 18].includes(read.u32(type) & 31)) - throw new TypeError("Unsupported Swift type descriptor kind"); - const typeName = - type === null - ? null - : read.string(requiredRelative(type + 8n, "typeName")); - const witnessValue = - protocol !== null && type !== null - ? relative(descriptor + 8n, "witnessTablePattern") - : { kind: "null" as const }; + const typeReference = decodeSwiftConformanceTypeReference({ + descriptor, + flags, + read, + relative, + unresolvedReason: unresolvedSwiftRelativeReason, + }); + const witnessValue = relative(descriptor + 8n, "witnessTablePattern"); const witness = witnessValue.kind === "target" ? witnessValue.target : null; const unresolvedWitnessReason = witnessValue.kind === "unknown" || witnessValue.kind === "binding" ? unresolvedSwiftRelativeReason(witnessValue, "witnessTablePattern") : null; - const staticWitness = + let staticWitness = witness !== null && protocol !== null && - type !== null && - (flags & ~0x38) === 0 && - (read.u32(type) & 0x80) === 0; + protocolFacts.readable && + typeReference.staticWitnessIdentity && + (flags & ~0x38) === 0; + staticWitness &&= + typeReference.kind === 2 || typeReference.generic === false; let witnessReason: string | null = staticWitness ? null : (unresolvedProtocolReason ?? - unresolvedTypeReason ?? + protocolFacts.reason ?? + typeReference.reason ?? unresolvedWitnessReason ?? - (protocolBinding !== null || typeBinding !== null + (protocolBinding !== null || typeReference.binding !== null ? "Conformance references an external Mach-O binding" : "Conditional, generic, resilient or missing static witness table is not decoded")); - if (staticWitness) { - const header = read.resolvePointer(witness); - if (header.kind !== "raw" && header.kind !== "rebase") - witnessReason = - header.kind === "unknown" || header.kind === "unsupported" - ? header.reason - : "Witness-table conformance header is an external binding"; - else if (header.target !== descriptor) - witnessReason = - "Witness-table conformance header is unresolved or encoded"; + if (staticWitness && witness !== null) { + witnessReason = readWitnessConformanceHeader( + read, + witness, + descriptor, + ); } const conformanceDecode = witnessReason === null ? { status: "decoded" as const, reason: null } : { status: "partial" as const, reason: witnessReason }; result.swift_conformances.push({ - type_name: typeName, - type_binding: typeBinding, + type_name: typeReference.name, + type_reference_kind: typeReference.kind, + type_reference_target: + typeReference.target === null + ? { address: null, file_offset: null } + : read.targetLocation(typeReference.target), + type_binding: typeReference.binding, protocol_name: protocolName, + ...(protocol === null + ? {} + : { protocol_target: read.targetLocation(protocol) }), protocol_binding: protocolBinding, module: null, witness_table: witness === null ? { address: null, file_offset: null } - : read.location(witness), + : read.targetLocation(witness), location: read.location(descriptor), decode: conformanceDecode, evidence: read.evidence( descriptor, - "Swift protocol conformance descriptor and directly encoded type/protocol names", + "Swift protocol conformance descriptor, type reference and protocol name", ), }); if (conformanceDecode.status === "decoded") decodedRecords++; if ( witnessReason !== null || witness === null || - typeName === null || - protocolName === null + typeReference.name === null || + protocolName === null || + !protocolFacts.readable ) { const reason = - witnessReason ?? "Missing static witness table is not decoded"; - swiftFailures.push(`${typeName ?? ""}: ${reason}`); + witnessReason ?? + typeReference.reason ?? + "Missing static witness table is not decoded"; issues.push( issue( "swift_conformances_static_witness_slots", @@ -405,75 +456,87 @@ export const decodeSwiftDispatchFacets = (input: { for (let slot = 0; slot < count; slot++) { if (!budget.admit()) break; const entry = witness + BigInt((slot + 1) * 8); - const implementationResolution = read.resolvePointer(entry); - const implementation = - implementationResolution.kind === "raw" || - implementationResolution.kind === "rebase" - ? implementationResolution.target - : null; - const requirementFlags = read.u32( - protocol! + 24n + BigInt(signatureCount * 12 + slot * 8), - ); - const kind = requirementFlags & 15; - const synchronousFunction = - kind >= 1 && kind <= 4 && (requirementFlags & 0x20) === 0; - const executable = - implementation !== null && - synchronousFunction && - segments.some( - (segment) => - segment.executable && - implementation >= segment.address && - implementation < segment.address + segment.size, + try { + const implementationResolution = read.resolvePointer(entry); + const implementation = + implementationResolution.kind === "raw" || + implementationResolution.kind === "rebase" + ? implementationResolution.target + : null; + const requirementFlags = read.u32( + protocol! + 24n + BigInt(signatureCount * 12 + slot * 8), ); - const slotDecode = executable - ? { status: "decoded" as const, reason: null } - : { - status: "partial" as const, - reason: - implementationResolution.kind === "bind" - ? "Witness implementation is an external Mach-O binding" - : implementationResolution.kind === "unknown" || - implementationResolution.kind === "unsupported" - ? implementationResolution.reason - : "Witness slot is non-function, async, nil, external, authenticated or encoded; implementation target is unresolved", - }; - result.swift_dispatch_slots.push({ - owner: `${typeName}: ${protocolName}`, - table_kind: "witness_table", - slot_index: slot + 1, - requirement: null, - implementation: null, - implementation_address: executable ? hex(implementation) : null, - ...(implementationResolution.kind === "bind" - ? { - implementation_binding: importBinding( - entry, - implementationResolution, - ), - } - : {}), - thunk_address: null, - location: read.location(entry), - decode: slotDecode, - evidence: read.evidence( - entry, - "Static Swift witness slot, indexed after the conformance-descriptor header", - ), - }); - if (slotDecode.status !== "decoded") + const kind = requirementFlags & 15; + const synchronousFunction = + kind >= 1 && kind <= 4 && (requirementFlags & 0x20) === 0; + const executable = + implementation !== null && + synchronousFunction && + segments.some( + (segment) => + segment.executable && + implementation >= segment.address && + implementation < segment.address + segment.size, + ); + const slotDecode = executable + ? { status: "decoded" as const, reason: null } + : { + status: "partial" as const, + reason: + implementationResolution.kind === "bind" + ? "Witness implementation is an external Mach-O binding" + : implementationResolution.kind === "unknown" || + implementationResolution.kind === "unsupported" + ? implementationResolution.reason + : "Witness slot is non-function, async, nil, external, authenticated or encoded; implementation target is unresolved", + }; + result.swift_dispatch_slots.push({ + owner: `${typeReference.name}: ${protocolName}`, + table_kind: "witness_table", + slot_index: slot + 1, + requirement: null, + implementation: null, + implementation_address: executable ? hex(implementation) : null, + ...(implementationResolution.kind === "bind" + ? { + implementation_binding: importBinding( + entry, + implementationResolution, + ), + } + : {}), + thunk_address: null, + location: read.location(entry), + decode: slotDecode, + evidence: read.evidence( + entry, + "Static Swift witness slot, indexed after the conformance-descriptor header", + ), + }); + if (slotDecode.status !== "decoded") + issues.push( + issue( + "swift_conformances_static_witness_slots", + "witness_slot_unresolved", + slotDecode.reason, + hex(entry), + ), + ); + } catch (cause) { + if (!(cause instanceof RangeError)) throw cause; issues.push( issue( "swift_conformances_static_witness_slots", "witness_slot_unresolved", - slotDecode.reason, + cause.message, hex(entry), ), ); + break; + } } } catch (cause) { const message = cause instanceof Error ? cause.message : String(cause); - swiftFailures.push(`${hex(field)}: ${message}`); issues.push( issue( "swift_conformances_static_witness_slots", diff --git a/src/native/AppleSwiftMetadataReadFailure.ts b/src/native/AppleSwiftMetadataReadFailure.ts new file mode 100644 index 0000000000..8eceddf87f --- /dev/null +++ b/src/native/AppleSwiftMetadataReadFailure.ts @@ -0,0 +1,13 @@ +/** Expected format-boundary failures from bounded Swift metadata reads. */ +export const expectedSwiftMetadataReadFailure = ( + cause: unknown, +): string | null => { + if (cause instanceof RangeError) return cause.message; + if ( + cause instanceof TypeError && + "code" in cause && + cause.code === "ERR_ENCODING_INVALID_ENCODED_DATA" + ) + return cause.message; + return null; +}; diff --git a/src/native/AppleSwiftRelativePointer.test.ts b/src/native/AppleSwiftRelativePointer.test.ts index e394a8ddd2..2d7d60e8d0 100644 --- a/src/native/AppleSwiftRelativePointer.test.ts +++ b/src/native/AppleSwiftRelativePointer.test.ts @@ -18,6 +18,10 @@ const readers = (bytes: Buffer) => ({ address: `0x${address.toString(16)}`, file_offset: Number(address), }), + targetLocation: (address: bigint) => ({ + address: `0x${address.toString(16)}`, + file_offset: Number(address), + }), evidence: (address: bigint, description: string) => [ { kind: "binary_metadata" as const, diff --git a/src/native/AppleSwiftVtables.test.ts b/src/native/AppleSwiftVtables.test.ts index c1e5fb4b8f..ddb47fc98a 100644 --- a/src/native/AppleSwiftVtables.test.ts +++ b/src/native/AppleSwiftVtables.test.ts @@ -52,6 +52,10 @@ it("preserves a required conformance pointer's unknown reason", () => { address: `0x${address.toString(16)}`, file_offset: Number(address), }), + targetLocation: (address) => ({ + address: `0x${address.toString(16)}`, + file_offset: Number(address), + }), evidence: () => [], offset: (address) => Number(address), }, @@ -203,6 +207,10 @@ it("returns a located issue for an emitted unresolved witness slot", () => { address: `0x${address.toString(16)}`, file_offset: Number(address), }), + targetLocation: (address) => ({ + address: `0x${address.toString(16)}`, + file_offset: Number(address), + }), evidence: (address, description) => [ { kind: "binary_metadata",