Files
reverse-engineering-anything/src/native/AppleDispatchMetadata.test.ts
T
Kaoru0822andClaude Opus 5.5 c02945bc44 feat(native): decode Swift nominal type declarations from __swift5_types (#6257)
* feat(native): decode Swift nominal type declarations from __swift5_types

Fill swift_decls from the nominal type context descriptors that survive
stripping: kind (class, struct, enum, actor), own and qualified name,
module, generic flag, anonymous-context flag, metadata accessor and
descriptor location, with evidence citing the type record. Unresolved
parent contexts keep qualified_name null and the entry partial.

One shared reader walks each context once; class vtable recovery now
consumes its descriptors instead of re-reading the records, so a type
record that cannot be read keeps both facets partial. Remove the
never-populated swift_decls fields that descriptors do not carry.

Fixes #5341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): bound Swift context walks and decode vtables in record order

Address review of the nominal-type reader:

- Cache intrinsic context depth (distance to the module) so a cached
  parent cannot bypass the 32-context bound, and never cache a stop that
  depends on where the walk began. Bound qualified names to 4096
  characters so nesting cannot multiply name storage.
- Visit each descriptor as soon as its kind and name are read, so a
  class vtable spends the record budget in record order instead of after
  every later declaration.
- Report an accessor outside executable mapped code as null and keep the
  declaration partial.
- Forward a name failure to vtable coverage only when the record is a
  class or its kind is unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:19:15 -07:00

2059 lines
69 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { machoWithPropertyAttributes } from "../../tests/fixtures/objcPropertyMacho.js";
import { objcRecordBudgetMacho } from "../../tests/fixtures/objcRecordBudgetMacho.js";
import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
const base = 0x100000000n;
const fixture = (
relative = false,
category?: "partial-method" | "unmapped-pointer" | "unmapped-class",
) => {
const bytes = Buffer.alloc(4096);
const u32 = (offset: number, value: number) =>
bytes.writeUInt32LE(value, offset);
const ptr = (offset: number, target: number) =>
bytes.writeBigUInt64LE(base + BigInt(target), offset);
u32(0, 0xfeedfacf);
u32(4, 0x0100000c);
u32(16, 1);
u32(20, category === undefined ? 152 : 232);
u32(32, 0x19);
u32(36, category === undefined ? 152 : 232);
bytes.write("__DATA", 40);
ptr(56, 0);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
u32(92, 5);
u32(96, category === undefined ? 1 : 2);
bytes.write("__objc_classlist", 104);
bytes.write("__DATA", 120);
ptr(136, 0x180);
bytes.writeBigUInt64LE(8n, 144);
if (category !== undefined) {
bytes.write("__objc_catlist", 184);
bytes.write("__DATA", 200);
ptr(216, 0xb00);
bytes.writeBigUInt64LE(8n, 224);
u32(232, 0xb00);
}
u32(152, 0x180);
ptr(0x180, 0x200);
ptr(0x200, 0x280);
ptr(0x220, 0x300);
ptr(0x2a0, 0x380);
u32(0x308, 16);
ptr(0x318, 0x450);
ptr(0x320, 0x480);
ptr(0x330, 0x500);
ptr(0x328, 0x700);
ptr(0x340, 0x600);
u32(0x380, 1);
ptr(0x398, 0x450);
bytes.write("Fixture\0", 0x450);
bytes.write("performAction:\0", 0x460);
u32(0x480, relative ? 0xc000000c : 24);
u32(0x484, 1);
if (relative) {
bytes.writeInt32LE(0x460 - 0x488, 0x488);
bytes.writeInt32LE(0x5c0 - 0x48c, 0x48c);
bytes.writeInt32LE(0x700 - 0x490, 0x490);
} else {
ptr(0x488, 0x460);
ptr(0x490, 0x5c0);
ptr(0x498, 0x700);
}
u32(0x500, 32);
u32(0x504, 1);
ptr(0x508, 0x580);
ptr(0x510, 0x5a0);
ptr(0x518, 0x5c0);
u32(0x580, 8);
bytes.write("state\0", 0x5a0);
bytes.write("i\0", 0x5c0);
u32(0x600, 16);
u32(0x604, 3);
ptr(0x608, 0x640);
ptr(0x610, 0x650);
ptr(0x618, 0x660);
ptr(0x620, 0x670);
ptr(0x628, 0x680);
ptr(0x630, 0x690);
bytes.write("atomicValue\0", 0x640);
bytes.write("Tq\0", 0x650);
bytes.write("nonatomicValue\0", 0x660);
bytes.write("Tq,N\0", 0x670);
bytes.write("unknownValue\0", 0x680);
bytes.writeBigUInt64LE(1n, 0x700);
ptr(0x708, 0x740);
ptr(0x748, 0x900);
ptr(0x758, 0x800);
ptr(0x768, 0x840);
u32(0x780, 72);
u32(0x800, 24);
u32(0x804, 1);
ptr(0x808, 0x920);
u32(0x840, 24);
u32(0x844, 1);
ptr(0x848, 0x940);
bytes.write("FixtureProtocol\0", 0x900);
bytes.write("requiredAction:\0", 0x920);
bytes.write("optionalAction:\0", 0x940);
if (category !== undefined) {
if (category === "unmapped-pointer") {
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0xb00);
} else {
ptr(0xb00, 0xb40);
ptr(0xb40, 0xbc0);
if (category === "unmapped-class") ptr(0xb48, 0x5000);
else ptr(0xb48, 0x200);
ptr(0xb50, 0xb80);
u32(0xb80, 24);
u32(0xb84, 1);
ptr(0xb88, 0x460);
ptr(0xb90, 0x5c0);
if (category === "unmapped-class") ptr(0xb98, 0x700);
else bytes.writeBigUInt64LE(0xffffffffffffffffn, 0xb98);
bytes.write("FixtureCategory\0", 0xbc0);
}
}
return bytes;
};
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
const swiftExternalSwiftBindingImage = (
boundField: "protocol" | "type" | "objc-class" = "protocol",
) => {
const bytes = fixture();
const sectionCommand = 32;
bytes.writeUInt32LE(232, sectionCommand + 4);
bytes.writeUInt32LE(2, sectionCommand + 64);
bytes.write("__swift5_proto", 184);
bytes.write("__DATA", 200);
bytes.writeBigUInt64LE(base + 0xa00n, 216);
bytes.writeBigUInt64LE(4n, 224);
bytes.writeUInt32LE(0xa00, 232);
bytes.writeUInt32LE(2, 236);
const dyldInfo = sectionCommand + 232;
bytes.writeUInt32LE(0x80000022, dyldInfo);
bytes.writeUInt32LE(48, dyldInfo + 4);
bytes.writeUInt32LE(0xf00, dyldInfo + 16);
const bind = Buffer.from([
0x11, // SET_DYLIB_ORDINAL_IMM 1
0x40,
...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
0x90,
0x00,
]);
bytes.writeUInt32LE(bind.length, dyldInfo + 20);
const dylibCommand = dyldInfo + 48;
bytes.writeUInt32LE(0xc, dylibCommand); // LC_LOAD_DYLIB
bytes.writeUInt32LE(56, dylibCommand + 4);
bytes.writeUInt32LE(24, dylibCommand + 8);
bytes.write("/usr/lib/libswiftCore.dylib\0", dylibCommand + 24);
bytes.writeUInt32LE(3, 16); // segment, dyld info, and dependent dylib
bytes.writeUInt32LE(336, 20);
bind.copy(bytes, 0xf00);
// Conformance at 0xa10: one reference is an indirect relative pointer to
// the import slot at 0xb00; the sibling reference points to local metadata.
bytes.writeInt32LE(0x10, 0xa00);
bytes.writeInt32LE(boundField === "protocol" ? 0x1ec : 0xec, 0xa14);
bytes.writeInt32LE(0, 0xa18); // Nullable witness-table pattern.
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 !== "protocol") {
bytes.writeUInt32LE(3, 0xd00);
bytes.writeInt32LE(0xf8, 0xd08);
bytes.write("Equatable\0", 0xe00);
} else {
bytes.write("Widget\0", 0xe00);
}
return bytes;
};
const swiftBoundWitnessSlotImage = () => {
const bytes = fixture();
const sectionCommand = 32;
bytes.writeUInt32LE(232, sectionCommand + 4);
bytes.writeUInt32LE(2, sectionCommand + 64);
bytes.write("__swift5_proto", 184);
bytes.write("__DATA", 200);
bytes.writeBigUInt64LE(base + 0xa00n, 216);
bytes.writeBigUInt64LE(4n, 224);
bytes.writeUInt32LE(0xa00, 232);
bytes.writeUInt32LE(2, 236);
const dyldInfo = sectionCommand + 232;
bytes.writeUInt32LE(0x80000022, dyldInfo);
bytes.writeUInt32LE(48, dyldInfo + 4);
bytes.writeUInt32LE(0xf20, dyldInfo + 8);
bytes.writeUInt32LE(6, dyldInfo + 12);
bytes.writeUInt32LE(0xf00, dyldInfo + 16);
const bind = Buffer.from([
0x11,
0x40,
...Buffer.from("_externalWitnessImpl\0"),
0x70,
0x88,
0x1c, // SET_SEGMENT_AND_OFFSET_ULEB segment 0 + witness slot 0xe08
0x90,
0x00,
]);
bytes.writeUInt32LE(bind.length, dyldInfo + 20);
const dylibCommand = dyldInfo + 48;
bytes.writeUInt32LE(0xc, dylibCommand);
bytes.writeUInt32LE(56, dylibCommand + 4);
bytes.writeUInt32LE(24, dylibCommand + 8);
bytes.write("/usr/lib/libswiftCore.dylib\0", dylibCommand + 24);
bytes.writeUInt32LE(3, 16);
bytes.writeUInt32LE(336, 20);
bind.copy(bytes, 0xf00);
// One unrelated, valid local rebase proves unmatched structural pointers
// stay known-raw after the complete legacy stream is walked.
Buffer.from([0x11, 0x20, 0xa0, 0x1c, 0x51, 0]).copy(bytes, 0xf20);
bytes.writeInt32LE(0x10, 0xa00);
bytes.writeInt32LE(0x1ec, 0xa14);
bytes.writeInt32LE(0x3e8, 0xa18);
bytes.writeUInt32LE(0, 0xa1c);
bytes.writeInt32LE(0x2f0, 0xa10);
bytes.writeUInt32LE(16, 0xc00);
bytes.writeInt32LE(0x238, 0xc08);
bytes.writeUInt32LE(3, 0xd00);
bytes.writeInt32LE(0x148, 0xd08);
bytes.writeUInt32LE(0, 0xd0c);
bytes.writeUInt32LE(1, 0xd10);
bytes.writeUInt32LE(1, 0xd18);
bytes.writeBigUInt64LE(base + 0xa10n, 0xe00);
bytes.write("Widget\0", 0xe40);
bytes.write("Equatable\0", 0xe50);
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(
swiftExternalSwiftBindingImage(),
100,
provenance,
);
expect(result.swift_conformances).toMatchObject([
{
type_name: "Widget",
protocol_name: null,
protocol_binding: {
symbol: "_$sSQMp",
requested_address: "0x100000b00",
},
decode: { status: "partial" },
},
]);
expect(
result.relative_pointers.find(
({ field }) => field.address === "0x100000a10",
),
).toMatchObject({
target: { address: null, file_offset: null },
binding: { symbol: "_$sSQMp" },
decode: { status: "partial" },
});
expect(
result.coverage.find(
({ facet }) => facet === "swift_conformances_static_witness_slots",
),
).toMatchObject({ status: "partial" });
});
it("preserves an indirect imported type reference as a typed binding", () => {
const result = decodeAppleDispatchMetadata(
swiftExternalSwiftBindingImage("type"),
100,
provenance,
);
expect(result.swift_conformances).toMatchObject([
{
type_name: null,
type_reference_kind: 1,
type_binding: {
symbol: "_$sSiMn",
requested_address: "0x100000b00",
},
protocol_name: "Equatable",
protocol_binding: null,
decode: { status: "partial" },
},
]);
expect(
result.coverage.find(
({ facet }) => facet === "swift_conformances_static_witness_slots",
),
).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;
bytes.writeUInt32LE(0, dyldInfo + 16);
bytes.writeUInt32LE(0, dyldInfo + 20);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(
result.coverage.find(({ facet }) => facet === "pointer_fixups"),
).toEqual({
facet: "pointer_fixups",
status: "complete",
reason: "LC_DYLD_INFO rebase opcodes",
examined: 1,
decoded: 1,
});
});
it("retains an unmatched local witness header beside a legacy rebase and preserves a bound implementation", () => {
const result = decodeAppleDispatchMetadata(
swiftBoundWitnessSlotImage(),
100,
provenance,
);
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" },
},
]);
expect(result.swift_dispatch_slots).toMatchObject([
{
table_kind: "witness_table",
slot_index: 1,
implementation_address: null,
implementation_binding: {
symbol: "_externalWitnessImpl",
requested_address: "0x100000e08",
},
decode: { status: "partial" },
},
]);
expect(
result.coverage.find(
({ facet }) => facet === "swift_conformances_static_witness_slots",
),
).toMatchObject({ status: "partial" });
});
});
describe("Swift conformance type-reference kinds", () => {
it.each([
{
label: "zero rebase",
target: 0n,
rebase: true,
address: "0x0",
fileOffset: 0,
},
{
label: "nonzero rebase",
target: 0x100n,
rebase: true,
address: "0x100",
fileOffset: 256,
},
{
label: "unmapped rebase",
target: 0x800n,
rebase: true,
address: "0x800",
fileOffset: null,
},
{
label: "raw zero",
target: 0n,
rebase: false,
address: null,
fileOffset: null,
},
{
label: "raw nonzero",
target: 0x100n,
rebase: false,
address: "0x100",
fileOffset: 256,
},
])(
"preserves kind-3 target identity for $label",
({ target, rebase, address, fileOffset }) => {
const bytes = Buffer.alloc(1024);
bytes.writeUInt32LE(0xfeedfacf, 0);
bytes.writeUInt32LE(0x0100000c, 4);
bytes.writeUInt32LE(6, 12); // MH_DYLIB; read statically, never executed.
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(200, 20);
bytes.writeUInt32LE(0x19, 32); // LC_SEGMENT_64, mapping address zero.
bytes.writeUInt32LE(152, 36);
bytes.write("__TEXT", 40);
bytes.writeBigUInt64LE(1024n, 64);
bytes.writeBigUInt64LE(1024n, 80);
bytes.writeUInt32LE(5, 88);
bytes.writeUInt32LE(5, 92);
bytes.writeUInt32LE(1, 96);
bytes.write("__swift5_proto", 104);
bytes.write("__TEXT", 120);
bytes.writeBigUInt64LE(0x200n, 136);
bytes.writeBigUInt64LE(4n, 144);
bytes.writeUInt32LE(0x200, 152);
bytes.writeUInt32LE(2, 156);
bytes.writeUInt32LE(0x80000022, 184); // LC_DYLD_INFO_ONLY.
bytes.writeUInt32LE(48, 188);
if (rebase) {
bytes.writeUInt32LE(0x380, 192);
bytes.writeUInt32LE(6, 196);
// Pointer rebase at segment 0 + 0x300, then end the stream.
Buffer.from([0x11, 0x20, 0x80, 0x06, 0x51, 0x00]).copy(bytes, 0x380);
}
bytes.writeInt32LE(0x20, 0x200); // Conformance descriptor at 0x220.
bytes.writeInt32LE(0x40, 0x220); // Protocol descriptor at 0x260.
bytes.writeInt32LE(0xdc, 0x224); // Indirect class cell at 0x300.
bytes.writeUInt32LE(3 << 3, 0x22c);
bytes.writeUInt32LE(3, 0x260); // Protocol context descriptor.
bytes.writeInt32LE(0x18, 0x268);
bytes.write("FixtureProtocol\0", 0x280);
bytes.writeBigUInt64LE(target, 0x300);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.swift_conformances).toMatchObject([
{
type_reference_kind: 3,
type_reference_target: { address, file_offset: fileOffset },
type_name: null,
type_binding: null,
protocol_name: "FixtureProtocol",
protocol_target: { address: "0x260", file_offset: 608 },
witness_table: { address: null, file_offset: null },
decode: {
status: "partial",
reason:
"Indirect Objective-C class metadata requires runtime lookup",
},
},
]);
expect(result.swift_conformances).toHaveLength(1);
expect(result.swift_dispatch_slots).toEqual([]);
expect(result.relative_pointers).toContainEqual(
expect.objectContaining({
field: { address: "0x224", file_offset: 548 },
target: { address: "0x300", file_offset: 768 },
}),
);
},
);
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);
bytes.writeUInt32LE(232, 20);
bytes.writeUInt32LE(232, 36);
bytes.writeUInt32LE(2, 96);
bytes.write("__objc_protolist", 184);
bytes.write("__DATA", 200);
pointer(216, 0x6c0);
bytes.writeBigUInt64LE(8n, 224);
pointer(0x6c0, 0x740);
return bytes;
};
const partialSiblingsFixture = (
options: {
readonly emptyMiddleClass?: boolean;
} = {},
) => {
const bytes = Buffer.alloc(4096);
const imageBase = 0x100000000n;
bytes.writeUInt32LE(0xfeedfacf, 0);
bytes.writeUInt32LE(0x0100000c, 4);
bytes.writeUInt32LE(2, 12);
bytes.writeUInt32LE(1, 16);
bytes.writeUInt32LE(232, 20);
bytes.writeUInt32LE(0x19, 32);
bytes.writeUInt32LE(232, 36);
bytes.write("__DATA", 40);
bytes.writeBigUInt64LE(imageBase, 56);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
bytes.writeUInt32LE(5, 92);
bytes.writeUInt32LE(2, 96);
bytes.write("__objc_classlist", 104);
bytes.writeBigUInt64LE(imageBase + 0x200n, 136);
bytes.writeBigUInt64LE(24n, 144);
bytes.write("__objc_protolist", 184);
bytes.writeBigUInt64LE(imageBase + 0x300n, 216);
bytes.writeBigUInt64LE(8n, 224);
for (const [slot, record, ro, name] of [
[0x200, 0x400, 0x500, 0x600],
[0x208, 0x440, 0x540, 0x620],
[0x210, 0x480, 0x580, 0x640],
] as const) {
bytes.writeBigUInt64LE(imageBase + BigInt(record), slot);
bytes.writeBigUInt64LE(imageBase + BigInt(ro), record + 32);
bytes.writeUInt32LE(2, ro);
bytes.writeUInt32LE(16, ro + 8);
bytes.writeBigUInt64LE(imageBase + BigInt(name), ro + 24);
}
bytes.write("KeptClass\0", 0x600);
if (!options.emptyMiddleClass) bytes.write("OtherClass\0", 0x620);
bytes.write("LaterClass\0", 0x640);
return bytes;
};
describe("Apple dispatch pointer fixup metadata", () => {
it.each([
{ name: "unsupported opcode", operand: [0xd0] },
{
name: "oversized addend whose tail resembles DO_BIND",
operand: [0x60, ...Array<number>(10).fill(0x80), 0x90, 0x00],
},
])(
"keeps decoded bindings when an independent stream has $name",
({ operand }) => {
const bytes = fixture();
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(200, 20);
bytes.writeUInt32LE(0x80000022, 184);
bytes.writeUInt32LE(48, 188);
const bindings = Buffer.from([
0x40,
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
0x70,
0x88,
0x04,
0x90,
0x00,
]);
bindings.copy(bytes, 0xc00);
const invalid = Buffer.from([...bindings.subarray(0, -2), ...operand]);
invalid.copy(bytes, 0xc80);
bytes.writeUInt32LE(0xc00, 200);
bytes.writeUInt32LE(bindings.length, 204);
bytes.writeUInt32LE(0xc80, 208);
bytes.writeUInt32LE(invalid.length, 212);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes[0]).toMatchObject({
name: "Fixture",
super_class: "NSObject",
});
expect(
result.coverage.find(({ facet }) => facet === "pointer_fixups"),
).toMatchObject({ status: "partial", examined: 2, decoded: 1 });
},
);
it("keeps absolute metadata pointers between declared chained fixups", () => {
const bytes = fixture();
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(168, 20);
bytes.writeUInt32LE(0x80000034, 184);
bytes.writeUInt32LE(16, 188);
bytes.writeUInt32LE(0xa00, 192);
bytes.writeUInt32LE(64, 196);
// Only the class-list pointer needs rebasing. All remaining metadata
// pointers carry their stored absolute values, outside the single chain.
bytes.writeBigUInt64LE(0x200n, 0x180);
bytes.writeUInt32LE(28, 0xa04);
bytes.writeUInt32LE(60, 0xa08);
bytes.writeUInt32LE(60, 0xa0c);
bytes.writeUInt32LE(1, 0xa14);
bytes.writeUInt32LE(1, 0xa1c);
bytes.writeUInt32LE(8, 0xa20);
bytes.writeUInt32LE(24, 0xa24);
bytes.writeUInt16LE(0x1000, 0xa28);
bytes.writeUInt16LE(6, 0xa2a);
bytes.writeUInt16LE(1, 0xa38);
bytes.writeUInt16LE(0x180, 0xa3a);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes[0]).toMatchObject({ name: "Fixture" });
expect(result.objc_dispatch_implementations[0]).toMatchObject({
selector: "performAction:",
implementation_address: "0x100000700",
});
expect(
result.coverage.find(({ facet }) => facet === "pointer_fixups"),
).toMatchObject({ status: "complete", examined: 1, decoded: 1 });
});
});
describe("Apple dispatch binary metadata", () => {
it("retains structural Objective-C records when legacy rebase coverage is unknown", () => {
const bytes = fixture();
// Add LC_DYLD_INFO_ONLY with a nonempty rebase stream. Legacy rebase
// stream is malformed, so provenance is unknown while readable
// nonzero structural pointer words must remain useful.
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(200, 20);
bytes.writeUInt32LE(0x80000022, 184);
bytes.writeUInt32LE(48, 188);
bytes.writeUInt32LE(4000, 192);
bytes.writeUInt32LE(1, 196);
bytes.writeUInt8(0xf0, 4000);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes[0]?.name).toBe("Fixture");
expect(result.objc_classes[0]?.methods).toMatchObject([
{ selector: "performAction:" },
]);
expect(result.objc_classes[0]?.decode).toMatchObject({
status: "partial",
});
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({ status: "partial" });
expect(
result.coverage.find(({ facet }) => facet === "pointer_fixups"),
).toMatchObject({ status: "partial" });
});
it.each([false, true])(
"decodes stripped absolute/relative method lists (relative=%s) with exact offsets",
(relative) => {
const result = decodeAppleDispatchMetadata(
fixture(relative),
100,
provenance,
);
expect(result.objc_classes).toMatchObject([
{
name: "Fixture",
instance_size: 16,
location: { address: "0x100000200", file_offset: 512 },
},
{ name: "Fixture", is_meta_class: true },
]);
expect(result.objc_dispatch_implementations).toMatchObject([
{
selector: "performAction:",
implementation_address: "0x100000700",
location: { file_offset: 0x488 },
decode: { status: "decoded" },
},
]);
expect(result.objc_ivars).toMatchObject([
{
name: "state",
type_encoding: "i",
offset: 8,
location: { file_offset: 0x508 },
},
]);
expect(result.objc_classes[0]?.properties).toEqual([
{
name: "atomicValue",
type_encoding: "q",
attributes: [{ name: "T", value: "q" }],
atomicity: "atomic",
is_readonly: false,
getter: null,
setter: null,
},
{
name: "nonatomicValue",
type_encoding: "q",
attributes: [
{ name: "T", value: "q" },
{ name: "N", value: "" },
],
atomicity: "nonatomic",
is_readonly: false,
getter: null,
setter: null,
},
{
name: "unknownValue",
type_encoding: null,
attributes: [],
atomicity: null,
is_readonly: null,
getter: null,
setter: null,
},
]);
expect(result.objc_classes[0]).toMatchObject({
is_meta_class: false,
is_root_class: false,
ivar_count: 1,
});
expect(result.objc_classes[1]).toMatchObject({
is_meta_class: true,
is_root_class: false,
ivar_count: null,
});
expect(result.objc_classes[0]?.methods[0]).toMatchObject({
is_required: null,
is_optional: null,
});
expect(result.objc_protocols[0]).toMatchObject({
name: "FixtureProtocol",
methods: [
{
selector: "requiredAction:",
is_required: true,
is_optional: false,
},
],
optional_methods: [
{
selector: "optionalAction:",
is_required: false,
is_optional: true,
},
],
});
},
);
it.each([
[0xffffffff, 8],
[3, 8],
[0xfffffffe, null],
])("decodes ivar alignment raw value %i as %s", (raw, alignment) => {
const bytes = fixture();
bytes.writeUInt32LE(raw, 0x520);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_ivars).toMatchObject([
{ name: "state", alignment, decode: { status: "decoded" } },
]);
});
});
describe("Apple dispatch coverage ownership", () => {
it("keeps unsupported pointers and record truncation explicit", () => {
const bytes = fixture();
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x498);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_dispatch_implementations[0]).toMatchObject({
implementation_address: null,
decode: { status: "partial" },
});
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("method Fixture instance performAction:"),
});
const truncated = decodeAppleDispatchMetadata(fixture(), 1, provenance);
expect(truncated.coverage[0]?.reason).toContain("max_records_reached");
expect(
truncated.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({ status: "partial", reason: "max_records_reached" });
});
it("keeps partial category methods in category coverage only", () => {
const baseline = decodeAppleDispatchMetadata(fixture(), 100, provenance);
const result = decodeAppleDispatchMetadata(
fixture(false, "partial-method"),
100,
provenance,
);
expect(result.objc_dispatch_implementations).toContainEqual(
expect.objectContaining({
category: "FixtureCategory",
decode: {
status: "partial",
reason: "implementation_pointer_unmapped_or_encoded",
},
}),
);
const baselineClassCoverage = baseline.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
const classCoverage = result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
expect(baselineClassCoverage).toBeDefined();
expect(classCoverage).toEqual(baselineClassCoverage);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("category method FixtureCategory"),
});
});
it("keeps a category whose class reference is unmapped", () => {
const result = decodeAppleDispatchMetadata(
fixture(false, "unmapped-class"),
100,
provenance,
);
expect(result.objc_categories).toMatchObject([
{
name: "FixtureCategory",
class_name: null,
class_source: "unresolved",
decode: { status: "partial", reason: "category_class_unresolved" },
},
]);
expect(result.objc_categories[0]?.instance_methods).toHaveLength(1);
expect(result.objc_dispatch_implementations).toContainEqual(
expect.objectContaining({
category: "FixtureCategory",
class_name: "(FixtureCategory)",
implementation_address: "0x100000700",
}),
);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining(
"category_class_unresolved at 0x100000b48: Category FixtureCategory class reference 0x100005000",
),
});
});
it("keeps malformed category pointers out of class failure coverage", () => {
const baseline = decodeAppleDispatchMetadata(fixture(), 100, provenance);
const result = decodeAppleDispatchMetadata(
fixture(false, "unmapped-pointer"),
100,
provenance,
);
const baselineClassCoverage = baseline.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
const classCoverage = result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
expect(baselineClassCoverage).toBeDefined();
expect(classCoverage).toEqual(baselineClassCoverage);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("category_decode_failed"),
});
});
});
describe("Objective-C declaration-local record-budget coverage", () => {
it.each(["protocol", "class"] as const)(
"preserves the decoded $0 method at a sufficient budget",
(owner) => {
const result = decodeAppleDispatchMetadata(
objcRecordBudgetMacho(owner),
100,
provenance,
);
if (owner === "protocol")
expect(result.objc_protocol_records).toMatchObject([
{
name: "FixtureProtocol",
methods: [{ selector: "protocolMethod" }],
decode: { status: "decoded", reason: null },
},
]);
else
expect(result.objc_classes).toMatchObject([
{
name: "FixtureClass",
methods: [{ selector: "classMethod", method_type: "instance" }],
decode: { status: "decoded", reason: null },
},
]);
},
);
it("marks an omitted protocol method list partial while preserving known emptiness", () => {
const truncated = decodeAppleDispatchMetadata(
withProtocolListSection(fixture()),
1,
provenance,
);
expect(truncated.objc_protocol_records).toMatchObject([
{
name: "FixtureProtocol",
methods: [],
decode: { status: "partial", reason: "max_records_reached" },
},
]);
const emptyBytes = withProtocolListSection(fixture());
emptyBytes.writeBigUInt64LE(0n, 0x758);
emptyBytes.writeBigUInt64LE(0n, 0x768);
const empty = decodeAppleDispatchMetadata(emptyBytes, 1, provenance);
expect(empty.objc_protocol_records).toMatchObject([
{
name: "FixtureProtocol",
methods: [],
optional_methods: [],
decode: { status: "decoded", reason: null },
},
]);
});
it("marks omitted class methods, ivars, properties, and adopted protocols partial", () => {
const methodBytes = fixture();
methodBytes.writeUInt32LE(2, 0x300);
methodBytes.writeBigUInt64LE(0n, 0x330);
methodBytes.writeBigUInt64LE(0n, 0x340);
const methodLimited = decodeAppleDispatchMetadata(
methodBytes,
1,
provenance,
);
expect(methodLimited.objc_classes[0]).toMatchObject({
name: "Fixture",
methods: [],
decode: { status: "partial", reason: "max_records_reached" },
});
const ivarBytes = fixture();
ivarBytes.writeBigUInt64LE(0n, 0x320);
ivarBytes.writeBigUInt64LE(0n, 0x328);
ivarBytes.writeBigUInt64LE(0n, 0x340);
ivarBytes.writeUInt32LE(2, 0x300);
const ivarsLimited = decodeAppleDispatchMetadata(ivarBytes, 1, provenance);
expect(ivarsLimited.objc_classes[0]).toMatchObject({
name: "Fixture",
ivar_count: 1,
decode: { status: "partial", reason: "max_records_reached" },
});
expect(ivarsLimited.objc_ivars).toEqual([]);
const propertyBytes = fixture();
propertyBytes.writeBigUInt64LE(0n, 0x320);
propertyBytes.writeBigUInt64LE(0n, 0x328);
propertyBytes.writeBigUInt64LE(0n, 0x330);
propertyBytes.writeUInt32LE(2, 0x300);
const propertiesLimited = decodeAppleDispatchMetadata(
propertyBytes,
1,
provenance,
);
expect(propertiesLimited.objc_classes[0]).toMatchObject({
name: "Fixture",
properties: [],
decode: { status: "partial", reason: "max_records_reached" },
});
const adoptedBytes = fixture();
adoptedBytes.writeBigUInt64LE(0n, 0x320);
adoptedBytes.writeBigUInt64LE(0n, 0x330);
adoptedBytes.writeBigUInt64LE(0n, 0x340);
adoptedBytes.writeUInt32LE(2, 0x300);
const adoptedLimited = decodeAppleDispatchMetadata(
adoptedBytes,
1,
provenance,
);
expect(adoptedLimited.objc_classes[0]).toMatchObject({
name: "Fixture",
protocols: [],
decode: { status: "partial", reason: "max_records_reached" },
});
});
it("keeps genuinely empty class method lists decoded at the record limit", () => {
const bytes = fixture();
bytes.writeBigUInt64LE(0n, 0x320);
bytes.writeBigUInt64LE(0n, 0x328);
bytes.writeBigUInt64LE(0n, 0x330);
bytes.writeBigUInt64LE(0n, 0x340);
bytes.writeUInt32LE(2, 0x300);
const result = decodeAppleDispatchMetadata(bytes, 1, provenance);
expect(result.objc_classes[0]).toMatchObject({
name: "Fixture",
methods: [],
decode: { status: "decoded", reason: null },
});
});
});
describe("Objective-C category-local record-budget coverage", () => {
it.each(["instance", "class"] as const)(
"marks category %s methods partial only when a declared entry is omitted",
(methodType) => {
const bytes = fixture(false, "partial-method");
const listField = methodType === "instance" ? 0xb50 : 0xb58;
const methodList = bytes.readBigUInt64LE(0xb50);
bytes.writeBigUInt64LE(0n, 0xb50);
bytes.writeBigUInt64LE(methodList, listField);
bytes.writeUInt32LE(2, 0x300);
bytes.writeBigUInt64LE(0n, 0x320);
const truncated = decodeAppleDispatchMetadata(bytes, 11, provenance);
expect(truncated.objc_categories).toMatchObject([
{
name: "FixtureCategory",
[`${methodType}_methods`]: [],
decode: { status: "partial", reason: "max_records_reached" },
},
]);
bytes.writeBigUInt64LE(0n, listField);
const empty = decodeAppleDispatchMetadata(bytes, 11, provenance);
expect(empty.objc_categories).toMatchObject([
{
name: "FixtureCategory",
[`${methodType}_methods`]: [],
decode: { status: "decoded", reason: null },
},
]);
},
);
});
describe("malformed Objective-C property records", () => {
it.each([
["an unmapped name", { secondNameOffset: 0x2000 }, "first", "0x100000518"],
["an empty name", { firstName: "" }, "second", "0x100000508"],
] as const)(
"retains the valid sibling of a property with %s",
(_, layout, kept, location) => {
const result = decodeAppleDispatchMetadata(
machoWithPropertyAttributes("Tq,N", layout),
100,
provenance,
);
expect(
result.objc_classes.map(({ name, properties }) => ({
name,
properties: properties.map((property) => property.name),
})),
).toEqual([{ name: "Fixture", properties: [kept] }]);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining(
`property_decode_failed at ${location}: Fixture: property`,
),
});
},
);
});
describe("Objective-C malformed identity recovery", () => {
it.each(["class selector", "category name"] as const)(
"isolates an empty %s before result validation",
(malformed) => {
const bytes = fixture(false, "unmapped-class");
bytes[malformed === "class selector" ? 0x460 : 0xbc0] = 0;
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_categories).toEqual([]);
if (malformed === "category name") {
expect(result.objc_classes).toHaveLength(2);
expect(result.objc_protocols).toMatchObject([
{ name: "FixtureProtocol" },
]);
}
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining(
malformed === "class selector" ? "empty selector" : "empty name",
),
});
},
);
it("keeps an unresolved superclass address when its class name is empty", () => {
const bytes = fixture();
bytes.writeBigUInt64LE(base + 0xc00n, 0x208);
bytes.writeBigUInt64LE(base + 0xc40n, 0xc20);
bytes.writeBigUInt64LE(base + 0xc80n, 0xc58);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes[0]).toMatchObject({
name: "Fixture",
super_class: null,
});
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("Superclass pointer 0x100000c00"),
});
});
it("keeps a category whose local owner has an empty name", () => {
const bytes = fixture(false, "unmapped-class");
bytes.writeBigUInt64LE(base + 0xc00n, 0xb48);
bytes.writeBigUInt64LE(base + 0xc40n, 0xc20);
bytes.writeBigUInt64LE(base + 0xc80n, 0xc58);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"Fixture",
"Fixture",
]);
expect(result.objc_categories).toMatchObject([
{
name: "FixtureCategory",
class_name: null,
class_source: "unresolved",
instance_methods: ["performAction:"],
decode: { status: "partial", reason: "category_class_unresolved" },
},
]);
expect(result.objc_dispatch_implementations).toContainEqual(
expect.objectContaining({
category: "FixtureCategory",
class_name: "(FixtureCategory)",
selector: "performAction:",
implementation_address: "0x100000700",
}),
);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("empty name"),
});
});
it.each([0x920, 0x940])(
"isolates an empty protocol selector at %i while keeping adopted declarations and classes",
(selector) => {
const bytes = fixture();
bytes[selector] = 0;
// Decode the independent adopted declaration before the malformed parent.
bytes.writeBigUInt64LE(base + 0xa00n, 0x750);
bytes.writeBigUInt64LE(1n, 0xa00);
bytes.writeBigUInt64LE(base + 0xa40n, 0xa08);
bytes.writeBigUInt64LE(base + 0xac0n, 0xa48);
bytes.writeUInt32LE(72, 0xa80);
bytes.write("AdoptedProtocol\0", 0xac0);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"Fixture",
"Fixture",
]);
expect(result.objc_protocols.map(({ name }) => name)).toEqual([
"AdoptedProtocol",
"FixtureProtocol",
]);
expect(result.objc_protocol_records.map(({ name }) => name)).toEqual([
"AdoptedProtocol",
"FixtureProtocol",
]);
expect(result.objc_protocol_records[1]).toMatchObject({
adopted_protocols: ["AdoptedProtocol"],
methods: selector === 0x920 ? [] : [{ selector: "requiredAction:" }],
optional_methods:
selector === 0x940 ? [] : [{ selector: "optionalAction:" }],
decode: { status: "partial", reason: "protocol_method_selector_empty" },
});
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("empty selector"),
});
},
);
});
describe("malformed Apple dispatch metadata", () => {
it("retains valid class siblings when one class has an empty ivar name", () => {
const bytes = partialSiblingsFixture();
bytes.writeBigUInt64LE(base + 0x700n, 0x570);
bytes.writeUInt32LE(32, 0x700);
bytes.writeUInt32LE(1, 0x704);
bytes.writeBigUInt64LE(base + 0x800n, 0x708);
bytes.writeBigUInt64LE(base + 0x820n, 0x710);
bytes.writeBigUInt64LE(base + 0x840n, 0x718);
bytes.write("i\0", 0x840);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"KeptClass",
"LaterClass",
]);
expect(result.objc_ivars).toEqual([]);
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("empty name"),
});
});
it("retains valid class siblings when one class has an empty name", () => {
const result = decodeAppleDispatchMetadata(
partialSiblingsFixture({ emptyMiddleClass: true }),
100,
provenance,
);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"KeptClass",
"LaterClass",
]);
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({ status: "partial", examined: 3 });
});
it("retains valid Swift class vtable siblings when one descriptor has an empty class name", () => {
const bytes = Buffer.alloc(4096);
const base = 0x100000000n;
const u32 = (at: number, n: number) => bytes.writeUInt32LE(n, at);
const ptr = (at: number, n: number) =>
bytes.writeBigUInt64LE(base + BigInt(n), at);
const rel = (at: number, n: number) => bytes.writeInt32LE(n - at, at);
u32(0, 0xfeedfacf);
u32(4, 0x0100000c);
u32(12, 2);
u32(16, 1);
u32(20, 152);
u32(32, 0x19);
u32(36, 152);
bytes.write("__TEXT", 40);
ptr(56, 0);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
u32(88, 5);
u32(92, 5);
u32(96, 1);
bytes.write("__swift5_types", 104);
bytes.write("__TEXT", 120);
ptr(136, 0x200);
bytes.writeBigUInt64LE(12n, 144);
u32(152, 0x200);
const entries: readonly (readonly [number, number, number, number])[] = [
[0x200, 0x300, 0x500, 0x800],
[0x204, 0x380, 0x520, 0x810],
[0x208, 0x400, 0x540, 0x820],
];
for (const [field, descriptor, name, implementation] of entries) {
rel(field, descriptor);
u32(descriptor, 0x80000010);
rel(descriptor + 8, name);
u32(descriptor + 28, 1);
u32(descriptor + 44, 0);
u32(descriptor + 48, 1);
u32(descriptor + 52, 0);
rel(descriptor + 56, implementation);
}
bytes.write("EarlierClass\0", 0x500);
bytes.write("LaterClass\0", 0x540);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.swift_dispatch_slots.map(({ owner }) => owner)).toEqual([
"EarlierClass",
"LaterClass",
]);
expect(result.swift_decls.map(({ name }) => name)).toEqual([
"EarlierClass",
"LaterClass",
]);
const coverage = result.coverage.find(
({ facet }) => facet === "swift_class_vtable_descriptors",
);
expect(coverage).toMatchObject({
status: "partial",
examined: 2,
decoded: 2,
reason: expect.stringContaining("type descriptor name is empty"),
});
});
it("rejects malformed command and section boundaries", () => {
const bytes = fixture();
bytes.writeUInt32LE(7, 36);
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
"command size",
);
expect(() =>
decodeAppleDispatchMetadata(Buffer.alloc(10), 100, provenance),
).toThrow("Truncated");
});
});
describe("universal Apple dispatch metadata", () => {
it.each([false, true])("decodes selected FAT slices (fat64=%s)", (fat64) => {
const thin = fixture();
const offset = 256;
const bytes = Buffer.alloc(offset + thin.length);
bytes.writeUInt32BE(fat64 ? 0xcafebabf : 0xcafebabe, 0);
bytes.writeUInt32BE(1, 4);
bytes.writeUInt32BE(0x0100000c, 8);
if (fat64) {
bytes.writeBigUInt64BE(BigInt(offset), 16);
bytes.writeBigUInt64BE(BigInt(thin.length), 24);
} else {
bytes.writeUInt32BE(offset, 16);
bytes.writeUInt32BE(thin.length, 20);
}
thin.copy(bytes, offset);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes[0]).toMatchObject({
name: "Fixture",
location: { address: "0x100000200", file_offset: offset + 512 },
});
expect(result.objc_dispatch_implementations[0]).toMatchObject({
implementation_address: "0x100000700",
location: { file_offset: offset + 0x488 },
});
});
});
describe("FAT64 dispatch slice validation", () => {
const wrapped = () => {
const thin = fixture();
const bytes = Buffer.alloc(256 + thin.length);
bytes.writeUInt32BE(0xcafebabf, 0);
bytes.writeUInt32BE(1, 4);
bytes.writeUInt32BE(0x0100000c, 8);
bytes.writeBigUInt64BE(256n, 16);
bytes.writeBigUInt64BE(BigInt(thin.length), 24);
thin.copy(bytes, 256);
return bytes;
};
it("rejects a truncated architecture table", () => {
expect(() =>
decodeAppleDispatchMetadata(wrapped().subarray(0, 32), 100, provenance),
).toThrow("Malformed FAT architecture table");
});
it.each([16, 24])(
"checks the full 64-bit slice extent at field %i",
(field) => {
const bytes = wrapped();
bytes.writeBigUInt64BE(0x100000000n, field);
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
"FAT architecture 0 slice range extends beyond the file",
);
},
);
it("rejects ambiguous matching architectures", () => {
const bytes = wrapped();
bytes.writeUInt32BE(2, 4);
bytes.copy(bytes, 40, 8, 40);
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
"Ambiguous FAT architecture slice",
);
});
it("rejects a missing selected architecture", () => {
expect(() =>
decodeAppleDispatchMetadata(wrapped(), 100, provenance, "x86_64"),
).toThrow("Requested FAT architecture is absent");
});
});
describe("byte-swapped universal Apple dispatch metadata", () => {
const wrapped = (fat64: boolean) => {
const thin = fixture();
const offset = 256;
const bytes = Buffer.alloc(offset + thin.length);
bytes.writeUInt32LE(fat64 ? 0xcafebabf : 0xcafebabe, 0);
bytes.writeUInt32LE(1, 4);
bytes.writeUInt32LE(0x0100000c, 8);
if (fat64) {
bytes.writeBigUInt64LE(BigInt(offset), 16);
bytes.writeBigUInt64LE(BigInt(thin.length), 24);
} else {
bytes.writeUInt32LE(offset, 16);
bytes.writeUInt32LE(thin.length, 20);
}
thin.copy(bytes, offset);
return bytes;
};
it.each([false, true])(
"decodes selected little-endian FAT slices (fat64=%s)",
(fat64) => {
const result = decodeAppleDispatchMetadata(
wrapped(fat64),
100,
provenance,
);
expect(result.objc_classes[0]).toMatchObject({
name: "Fixture",
location: { address: "0x100000200", file_offset: 256 + 512 },
});
expect(result.objc_dispatch_implementations[0]).toMatchObject({
implementation_address: "0x100000700",
location: { file_offset: 256 + 0x488 },
});
},
);
it.each([6, 16])(
"states the header byte order when its architecture table is truncated at %i bytes",
(length) => {
expect(() =>
decodeAppleDispatchMetadata(
wrapped(false).subarray(0, length),
100,
provenance,
),
).toThrow(
"Malformed FAT architecture table (FAT header byte order: little-endian)",
);
},
);
it("reads the selected thin slice's CPU identity in its own byte order", () => {
const bytes = wrapped(false);
bytes.writeUInt32BE(0xfeedfacf, 256);
bytes.writeUInt32BE(0x0100000c, 260);
bytes.writeUInt32BE(0, 264);
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
"Unsupported big-endian Mach-O CPU type",
);
});
it("rejects a 32-bit slice payload inside a little-endian container", () => {
const bytes = wrapped(false);
bytes.writeUInt32LE(12, 8);
bytes.writeUInt32LE(0, 12);
bytes.writeUInt32LE(0xfeedface, 256);
bytes.writeUInt32LE(12, 260);
bytes.writeUInt32LE(0, 264);
expect(() =>
decodeAppleDispatchMetadata(bytes, 100, provenance, "arm"),
).toThrow("Unsupported 32-bit Mach-O metadata for requested CPU type arm");
});
it("checks the full little-endian FAT64 slice extent", () => {
const bytes = wrapped(true);
bytes.writeBigUInt64LE(0x100000000n, 16);
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
"FAT architecture 0 slice range extends beyond the file (FAT header byte order: little-endian)",
);
});
});
const unmappedProtocolSectionBase = 0x100000000n;
const unmappedProtocolImage = (bad: "protocol" | "class" | "neither") => {
const bytes = Buffer.alloc(4096);
bytes.writeUInt32LE(0xfeedfacf, 0);
bytes.writeUInt32LE(0x0100000c, 4);
bytes.writeUInt32LE(2, 12);
bytes.writeUInt32LE(1, 16);
bytes.writeUInt32LE(232, 20);
bytes.writeUInt32LE(0x19, 32);
bytes.writeUInt32LE(232, 36);
bytes.write("__DATA", 40);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase, 56);
bytes.writeBigUInt64LE(4096n, 64);
bytes.writeBigUInt64LE(4096n, 80);
bytes.writeUInt32LE(5, 92);
bytes.writeUInt32LE(2, 96);
bytes.write("__objc_classlist", 104);
bytes.writeBigUInt64LE(
unmappedProtocolSectionBase + (bad === "class" ? 0x9000n : 0x200n),
136,
);
bytes.writeBigUInt64LE(8n, 144);
bytes.write("__objc_protolist", 184);
bytes.writeBigUInt64LE(
unmappedProtocolSectionBase + (bad === "protocol" ? 0x9000n : 0x300n),
216,
);
bytes.writeBigUInt64LE(8n, 224);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x400n, 0x200);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x500n, 0x420);
bytes.writeUInt32LE(2, 0x500);
bytes.writeUInt32LE(16, 0x508);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x600n, 0x518);
bytes.write("RetainedClass\0", 0x600);
return bytes;
};
const unmappedProtocolClassCoverage = (result: {
coverage: { facet: string; status: string; reason: string | null }[];
}) =>
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
describe("Apple dispatch unmapped protocol-list slot", () => {
it("returns the class when every list slot is mapped", () => {
const result = decodeAppleDispatchMetadata(
unmappedProtocolImage("neither"),
100,
provenance,
);
expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]);
expect(unmappedProtocolClassCoverage(result)).toMatchObject({
status: "complete",
});
});
it("records partial coverage when the class-list slot is unmapped", () => {
const result = decodeAppleDispatchMetadata(
unmappedProtocolImage("class"),
100,
provenance,
);
expect(unmappedProtocolClassCoverage(result)).toMatchObject({
status: "partial",
reason: expect.stringContaining("0x100009000"),
});
});
it("retains the class and partial coverage when the protocol-list slot is unmapped", () => {
const result = decodeAppleDispatchMetadata(
unmappedProtocolImage("protocol"),
100,
provenance,
);
expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]);
expect(unmappedProtocolClassCoverage(result)).toMatchObject({
status: "partial",
reason: expect.stringContaining("0x100009000"),
});
});
});
describe("Apple dispatch list scan budget", () => {
it("bounds failed protocol-list slots by the record budget", () => {
// The section size is file-declared, so a crafted image can claim an
// arbitrarily large unmapped protocol list. Failed slots must spend the
// record budget like decoded ones, otherwise the loop runs unbounded.
const bytes = unmappedProtocolImage("protocol");
bytes.writeBigUInt64LE(80000n, 224);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
const coverage = unmappedProtocolClassCoverage(result);
expect(coverage).toMatchObject({ status: "partial" });
expect(coverage?.reason).toContain("max_records_reached");
const failures =
coverage?.reason?.match(/Unmapped or ambiguous metadata pointer/g) ?? [];
expect(failures.length).toBeLessThanOrEqual(100);
});
it("keeps independent facets complete when only zero-pointer list scanning is capped", () => {
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(0n, 144);
bytes.writeBigUInt64LE(16n, 224);
const result = decodeAppleDispatchMetadata(bytes, 1, provenance);
expect(unmappedProtocolClassCoverage(result)).toMatchObject({
status: "partial",
reason: expect.stringContaining("max_records_reached"),
});
for (const facet of [
"objc_properties_categories",
"binary_relative_pointers",
])
expect(
result.coverage.find((item) => item.facet === facet),
).toMatchObject({
status: "complete",
reason: null,
});
});
it("marks class-dependent coverage partial when zero slots hide a later class", () => {
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(0n, 224);
bytes.writeBigUInt64LE(16n, 144);
bytes.writeBigUInt64LE(0n, 0x200);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x400n, 0x208);
const result = decodeAppleDispatchMetadata(bytes, 1, provenance);
expect(result.objc_classes).toEqual([]);
for (const facet of [
"objc_class_method_ivar_metadata",
"objc_properties_categories",
"binary_relative_pointers",
])
expect(
result.coverage.find((item) => item.facet === facet),
).toMatchObject({
status: "partial",
reason: expect.stringContaining("max_records_reached"),
});
for (const facet of [
"swift_class_vtable_descriptors",
"swift_conformances_static_witness_slots",
])
expect(
result.coverage.find((item) => item.facet === facet),
).toMatchObject({ status: "complete" });
});
it("bounds failed class-list slots by the record budget", () => {
// Same file-declared-size hazard as the protocol list: a crafted image
// can declare an arbitrarily large unmapped class list. Failed class
// slots must spend the record budget like decoded ones, otherwise the
// loop runs unbounded (each failed read produced an unbounded failure
// string with no budget spend).
const bytes = unmappedProtocolImage("class");
bytes.writeBigUInt64LE(80000n, 144);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
const coverage = unmappedProtocolClassCoverage(result);
expect(coverage).toMatchObject({ status: "partial" });
expect(coverage?.reason).toContain("max_records_reached");
const failures =
coverage?.reason?.match(/Unmapped or ambiguous metadata pointer/g) ?? [];
expect(failures.length).toBeLessThanOrEqual(100);
});
it("reports the real failure for a mapped-but-malformed class at the budget boundary", () => {
// The class-list slot itself is mapped, so readClass admits the record
// budget and only then throws on the unmapped class_ro pointer. The
// failure must be recorded as-is: it already spent one budget unit, so
// admitting again would break before the reason is appended and report
// only max_records_reached.
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x700n, 0x200);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x9000n, 0x720);
const result = decodeAppleDispatchMetadata(bytes, 1, provenance);
const coverage = unmappedProtocolClassCoverage(result);
expect(coverage).toMatchObject({ status: "partial" });
expect(coverage?.reason).toContain(
"Unmapped or ambiguous metadata pointer 0x100009018",
);
expect(coverage?.reason).not.toContain("max_records_reached");
});
it("does not skip a valid class after a mapped-but-malformed class", () => {
// With two slots and a budget of two, the corrupt first slot must spend
// exactly one unit: admitting twice would exhaust the budget and skip
// the valid class behind it.
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(16n, 144);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x700n, 0x200);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x9000n, 0x720);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x400n, 0x208);
const result = decodeAppleDispatchMetadata(bytes, 2, provenance);
expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]);
expect(unmappedProtocolClassCoverage(result)?.reason).toContain(
"Unmapped or ambiguous metadata pointer 0x100009018",
);
});
it("reports the real failure for a mapped-but-truncated protocol slot", () => {
// The protocol slot is mapped, so protocolReader.record admits the
// budget itself before throwing on the truncated declaration. The loop
// must not admit a second time; the recorded reason stays the
// declaration failure, not max_records_reached.
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x700n, 0x300);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
const coverage = unmappedProtocolClassCoverage(result);
expect(coverage).toMatchObject({ status: "partial" });
expect(coverage?.reason).toContain(
"Truncated Objective-C protocol declaration",
);
expect(coverage?.reason).not.toContain("max_records_reached");
});
it.each([
["RetainedProtocol", ["RetainedProtocol"], "complete"],
["", [], "partial"],
])(
"keeps the class when the protocol name is %j",
(protocolName, protocols, status) => {
const bytes = unmappedProtocolImage("neither");
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x700n, 0x300);
bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x800n, 0x708);
bytes.writeUInt32LE(72, 0x740);
bytes.write(`${protocolName}\0`, 0x800);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]);
expect(result.objc_protocols.map(({ name }) => name)).toEqual(protocols);
const coverage = unmappedProtocolClassCoverage(result);
expect(coverage).toMatchObject({ status });
if (status === "partial")
expect(coverage?.reason).toBe(
"Protocol 0x100000700: Objective-C protocol has an empty name",
);
},
);
it("keeps a protocol adopted only by an empty-named protocol", () => {
const bytes = unmappedProtocolImage("neither");
const at = (offset: bigint) => unmappedProtocolSectionBase + offset;
bytes.writeBigUInt64LE(at(0x700n), 0x300);
bytes.writeBigUInt64LE(at(0x800n), 0x708); // empty parent name
bytes.writeBigUInt64LE(at(0x900n), 0x710); // adopted protocol list
bytes.writeUInt32LE(72, 0x740);
bytes.writeBigUInt64LE(1n, 0x900);
bytes.writeBigUInt64LE(at(0xa00n), 0x908);
bytes.writeBigUInt64LE(at(0xb00n), 0xa08);
bytes.writeUInt32LE(72, 0xa40);
bytes.write("ChildProtocol\0", 0xb00);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]);
expect(result.objc_protocols.map(({ name }) => name)).toEqual([
"ChildProtocol",
]);
expect(unmappedProtocolClassCoverage(result)).toMatchObject({
status: "partial",
reason: "Protocol 0x100000700: Objective-C protocol has an empty name",
});
});
});
describe("malformed Objective-C protocol method lists", () => {
it("reports an empty protocol method selector without discarding the protocol or its other methods", () => {
const bytes = fixture();
bytes.writeUInt32LE(2, 0x804);
// The first required method names an empty string; the second is valid.
bytes.writeBigUInt64LE(base + 0x930n, 0x808);
bytes.writeBigUInt64LE(base + 0x920n, 0x820);
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
expect(result.objc_protocols).toMatchObject([
{
name: "FixtureProtocol",
methods: [{ selector: "requiredAction:", is_required: true }],
optional_methods: [{ selector: "optionalAction:", is_optional: true }],
},
]);
expect(result.objc_protocol_records).toMatchObject([
{
name: "FixtureProtocol",
methods: [{ selector: "requiredAction:" }],
decode: { status: "partial", reason: "protocol_method_selector_empty" },
},
]);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"Fixture",
"Fixture",
]);
expect(result.objc_dispatch_implementations).toMatchObject([
{ selector: "performAction:" },
]);
expect(
result.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining(
"Protocol 0x100000740: Objective-C protocol FixtureProtocol required instance method 0 at 0x100000808 has an empty selector",
),
});
});
});