refactor(native): split Apple dispatch metadata decode facets (#855)
Extract Mach-O/FAT selection, Objective-C class/method/ivar decode, and Swift conformance/witness facets so AppleDispatchMetadata stays a thin orchestrator without changing decode semantics. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -9,22 +9,14 @@ import {
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { parseMachoLayout } from "./AppleMachoSelection.js";
|
||||
import { decodeObjcDispatchFacets } from "./AppleObjcDispatchFacets.js";
|
||||
import {
|
||||
createSwiftRelativeReader,
|
||||
decodeSwiftDispatchFacets,
|
||||
type DispatchRecordBudget,
|
||||
} from "./AppleSwiftDispatchFacets.js";
|
||||
import { decodeSwiftClassVtables } from "./AppleSwiftVtables.js";
|
||||
import { createObjcProtocolReader } from "./AppleObjcProtocols.js";
|
||||
|
||||
interface Segment {
|
||||
address: bigint;
|
||||
size: bigint;
|
||||
offset: number;
|
||||
executable: boolean;
|
||||
}
|
||||
interface Section {
|
||||
name: string;
|
||||
address: bigint;
|
||||
size: number;
|
||||
}
|
||||
const hex = (value: bigint) => `0x${value.toString(16)}`;
|
||||
const decoded = { status: "decoded" as const, reason: null };
|
||||
|
||||
/** Decode validated little-endian 64-bit Apple Objective-C metadata without loading target code. */
|
||||
export const decodeAppleDispatchMetadata = (
|
||||
@@ -33,79 +25,12 @@ export const decodeAppleDispatchMetadata = (
|
||||
provenance: { path: string; sha256: string },
|
||||
architecture = "arm64",
|
||||
): ObjcSwiftMetadata => {
|
||||
if (bytes.length < 4) throw new RangeError("Truncated Mach-O header");
|
||||
const { slice, sliceEnd } = selectMachoSlice(bytes, architecture);
|
||||
if (slice === 0 && sliceEnd === bytes.length && bytes.length < 32)
|
||||
throw new RangeError("Truncated Mach-O header");
|
||||
if (slice + 32 > sliceEnd || bytes.readUInt32LE(slice) !== 0xfeedfacf)
|
||||
throw new TypeError(
|
||||
"Only little-endian 64-bit Mach-O metadata is supported",
|
||||
);
|
||||
const commands = bytes.readUInt32LE(slice + 16);
|
||||
const commandEnd = slice + 32 + bytes.readUInt32LE(slice + 20);
|
||||
if (commands > 4096 || commandEnd > sliceEnd)
|
||||
throw new RangeError("Malformed Mach-O command bounds");
|
||||
const segments: Segment[] = [];
|
||||
const sections: Section[] = [];
|
||||
let cursor = slice + 32;
|
||||
for (let index = 0; index < commands; index++) {
|
||||
if (cursor + 8 > commandEnd) throw new RangeError("Truncated load command");
|
||||
const kind = bytes.readUInt32LE(cursor),
|
||||
size = bytes.readUInt32LE(cursor + 4);
|
||||
if (size < 8 || cursor + size > commandEnd)
|
||||
throw new RangeError("Invalid load command size");
|
||||
if (kind === 0x19) {
|
||||
if (size < 72) throw new RangeError("Truncated segment command");
|
||||
const address = bytes.readBigUInt64LE(cursor + 24),
|
||||
fileSize = bytes.readBigUInt64LE(cursor + 48),
|
||||
fileOffset = bytes.readBigUInt64LE(cursor + 40);
|
||||
if (fileOffset + fileSize > BigInt(sliceEnd - slice))
|
||||
throw new RangeError("Segment file range exceeds target bytes");
|
||||
segments.push({
|
||||
address,
|
||||
size: fileSize,
|
||||
offset: slice + Number(fileOffset),
|
||||
executable: (bytes.readUInt32LE(cursor + 60) & 4) !== 0,
|
||||
});
|
||||
const count = bytes.readUInt32LE(cursor + 64);
|
||||
if (72 + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
for (let section = 0; section < count; section++) {
|
||||
const position = cursor + 72 + section * 80;
|
||||
const sectionSize = bytes.readBigUInt64LE(position + 40);
|
||||
if (sectionSize > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new RangeError("Section size exceeds exact numeric range");
|
||||
sections.push({
|
||||
name: bytes
|
||||
.subarray(position, position + 16)
|
||||
.toString("ascii")
|
||||
.replace(/\0.*$/u, ""),
|
||||
address: bytes.readBigUInt64LE(position + 32),
|
||||
size: Number(sectionSize),
|
||||
});
|
||||
}
|
||||
}
|
||||
cursor += size;
|
||||
}
|
||||
const offset = (address: bigint, size = 1): number => {
|
||||
const matches = segments.filter(
|
||||
(segment) =>
|
||||
address >= segment.address &&
|
||||
address + BigInt(size) <= segment.address + segment.size,
|
||||
);
|
||||
if (matches.length !== 1)
|
||||
throw new RangeError(
|
||||
`Unmapped or ambiguous metadata pointer ${hex(address)}`,
|
||||
);
|
||||
const segment = matches[0];
|
||||
if (segment === undefined) throw new RangeError("Missing metadata segment");
|
||||
return segment.offset + Number(address - segment.address);
|
||||
};
|
||||
const { segments, sections, offset } = parseMachoLayout(bytes, architecture);
|
||||
const u32 = (address: bigint) => bytes.readUInt32LE(offset(address, 4));
|
||||
const pointer = (address: bigint) =>
|
||||
bytes.readBigUInt64LE(offset(address, 8));
|
||||
const location = (address: bigint) => ({
|
||||
address: hex(address),
|
||||
address: `0x${address.toString(16)}`,
|
||||
file_offset: offset(address),
|
||||
});
|
||||
const evidence = (address: bigint, description: string) => [
|
||||
@@ -129,420 +54,52 @@ export const decodeAppleDispatchMetadata = (
|
||||
);
|
||||
};
|
||||
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
||||
const failures: string[] = [];
|
||||
let records = 0,
|
||||
examined = 0,
|
||||
truncated = false;
|
||||
const admit = () => {
|
||||
if (records >= maxRecords) {
|
||||
truncated = true;
|
||||
return false;
|
||||
}
|
||||
records++;
|
||||
return true;
|
||||
};
|
||||
const methods = (
|
||||
list: bigint,
|
||||
className: string,
|
||||
methodType: "instance" | "class",
|
||||
) => {
|
||||
if (list === 0n) return;
|
||||
const flags = u32(list),
|
||||
count = u32(list + 4n),
|
||||
relative = (flags & 0x80000000) !== 0;
|
||||
const stride = flags & 0xfffc;
|
||||
if (stride !== (relative ? 12 : 24))
|
||||
throw new TypeError(`Unsupported method stride/flags at ${hex(list)}`);
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!admit()) break;
|
||||
const entry = list + 8n + BigInt(index * stride);
|
||||
const rel = (field: bigint) => {
|
||||
const displacement = bytes.readInt32LE(offset(field, 4));
|
||||
const target = field + BigInt(displacement);
|
||||
let fileOffset: number | null = null;
|
||||
try {
|
||||
fileOffset = offset(target);
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: serialized target remains unresolved.
|
||||
void cause;
|
||||
}
|
||||
if (admit())
|
||||
result.relative_pointers.push({
|
||||
field: location(field),
|
||||
target: { address: hex(target), file_offset: fileOffset },
|
||||
displacement,
|
||||
indirectable: false,
|
||||
indirect: false,
|
||||
decode:
|
||||
fileOffset === null
|
||||
? { status: "partial", reason: "relative_target_unmapped" }
|
||||
: decoded,
|
||||
evidence: evidence(
|
||||
field,
|
||||
"Signed-relative Objective-C method-list pointer",
|
||||
),
|
||||
});
|
||||
return target;
|
||||
};
|
||||
let selectorAddress = relative ? rel(entry) : pointer(entry);
|
||||
if (relative && (flags & 0x40000000) === 0)
|
||||
selectorAddress = pointer(selectorAddress);
|
||||
const implementation = relative ? rel(entry + 8n) : pointer(entry + 16n);
|
||||
let implementationLocation: {
|
||||
address: string;
|
||||
file_offset: number;
|
||||
} | null = null;
|
||||
try {
|
||||
if (
|
||||
!segments.some(
|
||||
(segment) =>
|
||||
segment.executable &&
|
||||
implementation >= segment.address &&
|
||||
implementation < segment.address + segment.size,
|
||||
)
|
||||
)
|
||||
throw new RangeError(
|
||||
"Method implementation is not in an executable segment",
|
||||
);
|
||||
implementationLocation = location(implementation);
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: external/chained pointers remain unresolved.
|
||||
void cause;
|
||||
}
|
||||
result.objc_dispatch_implementations.push({
|
||||
class_name: className,
|
||||
selector: string(selectorAddress),
|
||||
method_type: methodType,
|
||||
implementation_address: implementationLocation?.address ?? null,
|
||||
location: location(entry),
|
||||
decode:
|
||||
implementationLocation === null
|
||||
? {
|
||||
status: "partial",
|
||||
reason: "implementation_pointer_unmapped_or_encoded",
|
||||
}
|
||||
: decoded,
|
||||
evidence: evidence(
|
||||
entry,
|
||||
relative
|
||||
? "Encoded relative Objective-C method entry"
|
||||
: "Encoded absolute Objective-C method entry",
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
const protocolReader = createObjcProtocolReader({
|
||||
readers: {
|
||||
u32,
|
||||
pointer,
|
||||
string,
|
||||
location,
|
||||
evidence,
|
||||
admit,
|
||||
i32: (address) => bytes.readInt32LE(offset(address, 4)),
|
||||
let records = 0;
|
||||
const budget: DispatchRecordBudget = {
|
||||
truncated: false,
|
||||
isFull: () => records >= maxRecords,
|
||||
markTruncated: () => {
|
||||
budget.truncated = true;
|
||||
},
|
||||
admit: () => {
|
||||
if (records >= maxRecords) {
|
||||
budget.truncated = true;
|
||||
return false;
|
||||
}
|
||||
records++;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const readers = {
|
||||
u32,
|
||||
i32: (address: bigint) => bytes.readInt32LE(offset(address, 4)),
|
||||
pointer,
|
||||
string,
|
||||
location,
|
||||
evidence,
|
||||
offset,
|
||||
};
|
||||
const { failures, examined } = decodeObjcDispatchFacets({
|
||||
bytes,
|
||||
sections,
|
||||
segments,
|
||||
readers,
|
||||
budget,
|
||||
result,
|
||||
failures,
|
||||
});
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__objc_protolist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C protocol list");
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (records >= maxRecords) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
protocolReader.record(pointer(section.address + BigInt(index * 8)));
|
||||
}
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
const readClass = (address: bigint, meta = false) => {
|
||||
if (address === 0n || visited.has(hex(address)) || !admit()) return;
|
||||
visited.add(hex(address));
|
||||
const ro = pointer(address + 32n) & ~7n;
|
||||
const name = string(pointer(ro + 24n));
|
||||
const superclass = pointer(address + 8n);
|
||||
const root = (u32(ro) & 2) !== 0;
|
||||
let superclassName: string | null = null;
|
||||
if (superclass !== 0n) {
|
||||
try {
|
||||
superclassName = string(
|
||||
pointer((pointer(superclass + 32n) & ~7n) + 24n),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
// Superclass resolution failure is recorded; the cause adds no identity.
|
||||
void cause;
|
||||
failures.push(
|
||||
`Superclass pointer ${hex(superclass)} of ${name} requires unsupported binding/fixup resolution`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (superclass === 0n && !root)
|
||||
failures.push(
|
||||
`Superclass of ${name} requires external binding resolution`,
|
||||
);
|
||||
const ivarList = pointer(ro + 48n);
|
||||
let ivarCount = 0;
|
||||
if (ivarList !== 0n && !meta) {
|
||||
const stride = u32(ivarList),
|
||||
count = u32(ivarList + 4n);
|
||||
if (stride !== 32)
|
||||
throw new TypeError("Unsupported Objective-C ivar entry size");
|
||||
ivarCount = count;
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!admit()) break;
|
||||
const entry = ivarList + 8n + BigInt(index * stride);
|
||||
const encodedOffset = pointer(entry);
|
||||
let value: number | null = null;
|
||||
try {
|
||||
value = u32(encodedOffset);
|
||||
} catch (cause: unknown) {
|
||||
// Unresolved offsets are recorded; the cause adds no identity.
|
||||
void cause;
|
||||
failures.push(`Unresolved ivar offset pointer ${hex(encodedOffset)}`);
|
||||
}
|
||||
result.objc_ivars.push({
|
||||
class_name: name,
|
||||
name: string(pointer(entry + 8n)),
|
||||
type_encoding: string(pointer(entry + 16n)),
|
||||
offset: value,
|
||||
size: u32(entry + 28n),
|
||||
alignment: u32(entry + 24n) < 31 ? 2 ** u32(entry + 24n) : null,
|
||||
location: location(entry),
|
||||
decode:
|
||||
value === null
|
||||
? { status: "partial", reason: "ivar_offset_pointer_unresolved" }
|
||||
: decoded,
|
||||
evidence: evidence(
|
||||
entry,
|
||||
"Objective-C ivar entry and encoded offset storage",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
methods(pointer(ro + 32n), name, meta ? "class" : "instance");
|
||||
result.objc_classes.push({
|
||||
name,
|
||||
super_class: superclassName,
|
||||
is_meta_class: meta,
|
||||
is_root_class: root,
|
||||
methods: result.objc_dispatch_implementations
|
||||
.filter(
|
||||
(item) =>
|
||||
item.class_name === name &&
|
||||
item.method_type === (meta ? "class" : "instance"),
|
||||
)
|
||||
.map((item) => ({
|
||||
selector: item.selector,
|
||||
method_type: item.method_type,
|
||||
address:
|
||||
item.implementation_address !== null &&
|
||||
BigInt(item.implementation_address) <=
|
||||
BigInt(Number.MAX_SAFE_INTEGER)
|
||||
? Number(BigInt(item.implementation_address))
|
||||
: null,
|
||||
is_required: false,
|
||||
is_optional: false,
|
||||
})),
|
||||
properties: [],
|
||||
protocols: protocolReader.list(pointer(ro + 40n)),
|
||||
ivar_count: ivarCount,
|
||||
instance_size: u32(ro + 8n),
|
||||
location: location(address),
|
||||
superclass_address: superclass === 0n ? null : hex(superclass),
|
||||
metaclass_address: meta ? null : hex(pointer(address)),
|
||||
decode:
|
||||
superclassName === null && !root
|
||||
? { status: "partial", reason: "superclass_binding_unresolved" }
|
||||
: decoded,
|
||||
evidence: evidence(address, "Objective-C class and class_ro_t records"),
|
||||
});
|
||||
if (!meta) {
|
||||
try {
|
||||
readClass(pointer(address), true);
|
||||
} catch (cause) {
|
||||
failures.push(
|
||||
`Metaclass of ${name}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__objc_classlist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C class-list size");
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (records >= maxRecords) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
examined++;
|
||||
const field = section.address + BigInt(index * 8);
|
||||
try {
|
||||
readClass(pointer(field));
|
||||
} catch (cause) {
|
||||
failures.push(
|
||||
`${hex(field)}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const swiftRelative = (field: bigint, indirectable = false): bigint => {
|
||||
const displacement = bytes.readInt32LE(offset(field, 4));
|
||||
const indirect = indirectable && (displacement & 1) !== 0;
|
||||
let target =
|
||||
displacement === 0
|
||||
? 0n
|
||||
: field + BigInt(indirectable ? displacement & ~1 : displacement);
|
||||
let fileOffset: number | null = null;
|
||||
let reason: string | null = null;
|
||||
try {
|
||||
if (indirect && target !== 0n) target = pointer(target);
|
||||
if (target !== 0n) fileOffset = offset(target);
|
||||
} catch (cause) {
|
||||
reason = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
if (admit())
|
||||
result.relative_pointers.push({
|
||||
field: location(field),
|
||||
target: {
|
||||
address: target === 0n ? null : hex(target),
|
||||
file_offset: fileOffset,
|
||||
},
|
||||
displacement,
|
||||
indirectable,
|
||||
indirect,
|
||||
decode: reason === null ? decoded : { status: "partial", reason },
|
||||
evidence: evidence(field, "Swift ABI signed-relative metadata pointer"),
|
||||
});
|
||||
if (reason !== null) throw new RangeError(reason);
|
||||
return target;
|
||||
};
|
||||
const swiftFailures: string[] = [];
|
||||
let swiftExamined = 0;
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__swift5_proto",
|
||||
)) {
|
||||
if (section.size % 4 !== 0)
|
||||
throw new RangeError("Misaligned Swift conformance section");
|
||||
for (let index = 0; index < section.size / 4; index++) {
|
||||
if (!admit()) break;
|
||||
swiftExamined++;
|
||||
const field = section.address + BigInt(index * 4);
|
||||
try {
|
||||
const descriptor = swiftRelative(field);
|
||||
const flags = u32(descriptor + 12n);
|
||||
const protocol = swiftRelative(descriptor, true);
|
||||
if ((u32(protocol) & 31) !== 3)
|
||||
throw new TypeError("Unsupported Swift protocol descriptor kind");
|
||||
const protocolName = string(swiftRelative(protocol + 8n));
|
||||
const typeKind = (flags >>> 3) & 7;
|
||||
if (typeKind !== 0 && typeKind !== 1)
|
||||
throw new TypeError(
|
||||
"Unsupported Swift conformance type-reference kind",
|
||||
);
|
||||
let type = swiftRelative(descriptor + 4n);
|
||||
if (typeKind === 1) type = pointer(type);
|
||||
if (![16, 17, 18].includes(u32(type) & 31))
|
||||
throw new TypeError("Unsupported Swift type descriptor kind");
|
||||
const typeName = string(swiftRelative(type + 8n));
|
||||
const witness = swiftRelative(descriptor + 8n);
|
||||
const staticWitness =
|
||||
witness !== 0n && (flags & ~0x38) === 0 && (u32(type) & 0x80) === 0;
|
||||
let witnessReason: string | null = staticWitness
|
||||
? null
|
||||
: "Conditional, generic, resilient or missing static witness table is not decoded";
|
||||
if (staticWitness && pointer(witness) !== descriptor)
|
||||
witnessReason =
|
||||
"Witness-table conformance header is unresolved or encoded";
|
||||
result.swift_conformances.push({
|
||||
type_name: typeName,
|
||||
protocol_name: protocolName,
|
||||
module: null,
|
||||
witness_table:
|
||||
witness === 0n
|
||||
? { address: null, file_offset: null }
|
||||
: location(witness),
|
||||
location: location(descriptor),
|
||||
decode:
|
||||
witnessReason === null
|
||||
? decoded
|
||||
: {
|
||||
status: "partial",
|
||||
reason: witnessReason,
|
||||
},
|
||||
evidence: evidence(
|
||||
descriptor,
|
||||
"Swift protocol conformance descriptor and directly encoded type/protocol names",
|
||||
),
|
||||
});
|
||||
if (witnessReason !== null) {
|
||||
swiftFailures.push(`${typeName}: ${witnessReason}`);
|
||||
continue;
|
||||
}
|
||||
const count = u32(protocol + 16n);
|
||||
const signatureCount = u32(protocol + 12n);
|
||||
if (count > 20_000 || signatureCount > 20_000)
|
||||
throw new RangeError("Swift witness requirement count exceeds 20000");
|
||||
for (let slot = 0; slot < count; slot++) {
|
||||
if (!admit()) break;
|
||||
const entry = witness + BigInt((slot + 1) * 8);
|
||||
const implementation = pointer(entry);
|
||||
const requirementFlags = u32(
|
||||
protocol + 24n + BigInt(signatureCount * 12 + slot * 8),
|
||||
);
|
||||
const kind = requirementFlags & 15;
|
||||
const synchronousFunction =
|
||||
kind >= 1 && kind <= 4 && (requirementFlags & 0x20) === 0;
|
||||
const executable =
|
||||
synchronousFunction &&
|
||||
segments.some(
|
||||
(segment) =>
|
||||
segment.executable &&
|
||||
implementation >= segment.address &&
|
||||
implementation < segment.address + segment.size,
|
||||
);
|
||||
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,
|
||||
thunk_address: null,
|
||||
location: location(entry),
|
||||
decode: executable
|
||||
? decoded
|
||||
: {
|
||||
status: "partial",
|
||||
reason:
|
||||
"Witness slot is non-function, async, nil, external, authenticated or encoded; implementation target is unresolved",
|
||||
},
|
||||
evidence: evidence(
|
||||
entry,
|
||||
"Static Swift witness slot, indexed after the conformance-descriptor header",
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (cause) {
|
||||
swiftFailures.push(
|
||||
`${hex(field)}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.coverage.push({
|
||||
facet: "swift_conformances_static_witness_slots",
|
||||
status: swiftFailures.length > 0 || truncated ? "partial" : "complete",
|
||||
reason:
|
||||
[...swiftFailures, ...(truncated ? ["max_records_reached"] : [])].join(
|
||||
"; ",
|
||||
) || null,
|
||||
examined: swiftExamined,
|
||||
decoded: result.swift_conformances.length,
|
||||
const swiftRelative = createSwiftRelativeReader({
|
||||
bytes,
|
||||
readers,
|
||||
budget,
|
||||
result,
|
||||
});
|
||||
decodeSwiftDispatchFacets({
|
||||
sections,
|
||||
segments,
|
||||
readers,
|
||||
relative: swiftRelative,
|
||||
budget,
|
||||
result,
|
||||
});
|
||||
const typeEntries: bigint[] = [];
|
||||
for (const section of sections.filter(
|
||||
@@ -552,7 +109,7 @@ export const decodeAppleDispatchMetadata = (
|
||||
throw new RangeError("Misaligned Swift type section");
|
||||
for (let index = 0; index < section.size / 4 && index < maxRecords; index++)
|
||||
typeEntries.push(section.address + BigInt(index * 4));
|
||||
if (section.size / 4 > maxRecords) truncated = true;
|
||||
if (section.size / 4 > maxRecords) budget.truncated = true;
|
||||
}
|
||||
decodeSwiftClassVtables({
|
||||
entries: typeEntries,
|
||||
@@ -563,7 +120,7 @@ export const decodeAppleDispatchMetadata = (
|
||||
string,
|
||||
location,
|
||||
evidence,
|
||||
admit,
|
||||
admit: () => budget.admit(),
|
||||
executable: (address) =>
|
||||
segments.some(
|
||||
(segment) =>
|
||||
@@ -575,21 +132,22 @@ export const decodeAppleDispatchMetadata = (
|
||||
});
|
||||
result.coverage.push({
|
||||
facet: "objc_class_method_ivar_metadata",
|
||||
status: failures.length > 0 || truncated ? "partial" : "complete",
|
||||
status: failures.length > 0 || budget.truncated ? "partial" : "complete",
|
||||
reason:
|
||||
[...failures, ...(truncated ? ["max_records_reached"] : [])].join("; ") ||
|
||||
null,
|
||||
[...failures, ...(budget.truncated ? ["max_records_reached"] : [])].join(
|
||||
"; ",
|
||||
) || null,
|
||||
examined,
|
||||
decoded: result.objc_classes.length,
|
||||
});
|
||||
result.coverage.push({
|
||||
facet: "binary_relative_pointers",
|
||||
status:
|
||||
truncated ||
|
||||
budget.truncated ||
|
||||
result.relative_pointers.some((item) => item.decode.status !== "decoded")
|
||||
? "partial"
|
||||
: "complete",
|
||||
reason: truncated
|
||||
reason: budget.truncated
|
||||
? "max_records_reached"
|
||||
: result.relative_pointers.some(
|
||||
(item) => item.decode.status !== "decoded",
|
||||
@@ -661,64 +219,3 @@ export const inspectAppleDispatchMetadata = async (
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
|
||||
const selectMachoSlice = (bytes: Buffer, architecture: string) => {
|
||||
const magic = bytes.readUInt32BE(0);
|
||||
const header = fatHeaderFormat(magic);
|
||||
if (header === null) return { slice: 0, sliceEnd: bytes.length };
|
||||
const { littleEndian, fat64 } = header;
|
||||
const stride = fat64 ? 32 : 20;
|
||||
const readUInt32 = littleEndian
|
||||
? (offset: number) => bytes.readUInt32LE(offset)
|
||||
: (offset: number) => bytes.readUInt32BE(offset);
|
||||
const readUInt64 = littleEndian
|
||||
? (offset: number) => bytes.readBigUInt64LE(offset)
|
||||
: (offset: number) => bytes.readBigUInt64BE(offset);
|
||||
const byteOrder = littleEndian ? "little-endian" : "big-endian";
|
||||
const malformed = (reason: string) =>
|
||||
new RangeError(`${reason} (FAT header byte order: ${byteOrder})`);
|
||||
const invalid = (reason: string) =>
|
||||
new TypeError(`${reason} (FAT header byte order: ${byteOrder})`);
|
||||
if (bytes.length < 8) throw malformed("Malformed FAT architecture table");
|
||||
const count = readUInt32(4);
|
||||
const headerEnd = 8 + count * stride;
|
||||
if (count > 128 || headerEnd > bytes.length)
|
||||
throw malformed("Malformed FAT architecture table");
|
||||
const cpu = architecture === "arm64" ? 0x0100000c : 0x01000007;
|
||||
let selected: { slice: number; sliceEnd: number } | undefined;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const offset = 8 + index * stride;
|
||||
if (readUInt32(offset) !== cpu) continue;
|
||||
if (selected !== undefined)
|
||||
throw invalid("Ambiguous FAT architecture slice");
|
||||
const start = fat64
|
||||
? readUInt64(offset + 8)
|
||||
: BigInt(readUInt32(offset + 8));
|
||||
const size = fat64
|
||||
? readUInt64(offset + 16)
|
||||
: BigInt(readUInt32(offset + 12));
|
||||
if (start < BigInt(headerEnd) || start + size > BigInt(bytes.length))
|
||||
throw malformed("FAT slice exceeds file");
|
||||
selected = { slice: Number(start), sliceEnd: Number(start + size) };
|
||||
}
|
||||
if (selected === undefined)
|
||||
throw invalid("Requested FAT architecture is absent");
|
||||
return selected;
|
||||
};
|
||||
|
||||
const fatHeaderFormat = (
|
||||
magic: number,
|
||||
): { readonly littleEndian: boolean; readonly fat64: boolean } | null => {
|
||||
switch (magic) {
|
||||
case 0xcafebabe:
|
||||
return { littleEndian: false, fat64: false };
|
||||
case 0xcafebabf:
|
||||
return { littleEndian: false, fat64: true };
|
||||
case 0xbebafeca:
|
||||
return { littleEndian: true, fat64: false };
|
||||
case 0xbfbafeca:
|
||||
return { littleEndian: true, fat64: true };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/** One mapped Mach-O segment with file-backed VA range. */
|
||||
export interface Segment {
|
||||
address: bigint;
|
||||
size: bigint;
|
||||
offset: number;
|
||||
executable: boolean;
|
||||
}
|
||||
|
||||
/** Named Mach-O section within a loaded segment. */
|
||||
export interface Section {
|
||||
name: string;
|
||||
address: bigint;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** Selected thin slice bounds inside a Mach-O or FAT container. */
|
||||
export interface MachoSlice {
|
||||
slice: number;
|
||||
sliceEnd: number;
|
||||
}
|
||||
|
||||
/** Parsed little-endian 64-bit Mach-O layout for the selected architecture slice. */
|
||||
export interface MachoLayout {
|
||||
segments: Segment[];
|
||||
sections: Section[];
|
||||
/** Map a virtual address range onto a file offset within the selected slice. */
|
||||
offset(address: bigint, size?: number): number;
|
||||
}
|
||||
|
||||
const hex = (value: bigint) => `0x${value.toString(16)}`;
|
||||
|
||||
/** Classify a FAT magic word; thin Mach-O returns null. */
|
||||
export const fatHeaderFormat = (
|
||||
magic: number,
|
||||
): { readonly littleEndian: boolean; readonly fat64: boolean } | null => {
|
||||
switch (magic) {
|
||||
case 0xcafebabe:
|
||||
return { littleEndian: false, fat64: false };
|
||||
case 0xcafebabf:
|
||||
return { littleEndian: false, fat64: true };
|
||||
case 0xbebafeca:
|
||||
return { littleEndian: true, fat64: false };
|
||||
case 0xbfbafeca:
|
||||
return { littleEndian: true, fat64: true };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Select the thin Mach-O slice for `architecture` inside a FAT or thin container. */
|
||||
export const selectMachoSlice = (
|
||||
bytes: Buffer,
|
||||
architecture: string,
|
||||
): MachoSlice => {
|
||||
const magic = bytes.readUInt32BE(0);
|
||||
const header = fatHeaderFormat(magic);
|
||||
if (header === null) return { slice: 0, sliceEnd: bytes.length };
|
||||
const { littleEndian, fat64 } = header;
|
||||
const stride = fat64 ? 32 : 20;
|
||||
const readUInt32 = littleEndian
|
||||
? (offset: number) => bytes.readUInt32LE(offset)
|
||||
: (offset: number) => bytes.readUInt32BE(offset);
|
||||
const readUInt64 = littleEndian
|
||||
? (offset: number) => bytes.readBigUInt64LE(offset)
|
||||
: (offset: number) => bytes.readBigUInt64BE(offset);
|
||||
const byteOrder = littleEndian ? "little-endian" : "big-endian";
|
||||
const malformed = (reason: string) =>
|
||||
new RangeError(`${reason} (FAT header byte order: ${byteOrder})`);
|
||||
const invalid = (reason: string) =>
|
||||
new TypeError(`${reason} (FAT header byte order: ${byteOrder})`);
|
||||
if (bytes.length < 8) throw malformed("Malformed FAT architecture table");
|
||||
const count = readUInt32(4);
|
||||
const headerEnd = 8 + count * stride;
|
||||
if (count > 128 || headerEnd > bytes.length)
|
||||
throw malformed("Malformed FAT architecture table");
|
||||
const cpu = architecture === "arm64" ? 0x0100000c : 0x01000007;
|
||||
let selected: MachoSlice | undefined;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const offset = 8 + index * stride;
|
||||
if (readUInt32(offset) !== cpu) continue;
|
||||
if (selected !== undefined)
|
||||
throw invalid("Ambiguous FAT architecture slice");
|
||||
const start = fat64
|
||||
? readUInt64(offset + 8)
|
||||
: BigInt(readUInt32(offset + 8));
|
||||
const size = fat64
|
||||
? readUInt64(offset + 16)
|
||||
: BigInt(readUInt32(offset + 12));
|
||||
if (start < BigInt(headerEnd) || start + size > BigInt(bytes.length))
|
||||
throw malformed("FAT slice exceeds file");
|
||||
selected = { slice: Number(start), sliceEnd: Number(start + size) };
|
||||
}
|
||||
if (selected === undefined)
|
||||
throw invalid("Requested FAT architecture is absent");
|
||||
return selected;
|
||||
};
|
||||
|
||||
/** Parse segments/sections for a little-endian 64-bit Mach-O architecture slice. */
|
||||
export const parseMachoLayout = (
|
||||
bytes: Buffer,
|
||||
architecture: string,
|
||||
): MachoLayout => {
|
||||
if (bytes.length < 4) throw new RangeError("Truncated Mach-O header");
|
||||
const { slice, sliceEnd } = selectMachoSlice(bytes, architecture);
|
||||
if (slice === 0 && sliceEnd === bytes.length && bytes.length < 32)
|
||||
throw new RangeError("Truncated Mach-O header");
|
||||
if (slice + 32 > sliceEnd || bytes.readUInt32LE(slice) !== 0xfeedfacf)
|
||||
throw new TypeError(
|
||||
"Only little-endian 64-bit Mach-O metadata is supported",
|
||||
);
|
||||
const commands = bytes.readUInt32LE(slice + 16);
|
||||
const commandEnd = slice + 32 + bytes.readUInt32LE(slice + 20);
|
||||
if (commands > 4096 || commandEnd > sliceEnd)
|
||||
throw new RangeError("Malformed Mach-O command bounds");
|
||||
const segments: Segment[] = [];
|
||||
const sections: Section[] = [];
|
||||
let cursor = slice + 32;
|
||||
for (let index = 0; index < commands; index++) {
|
||||
if (cursor + 8 > commandEnd) throw new RangeError("Truncated load command");
|
||||
const kind = bytes.readUInt32LE(cursor),
|
||||
size = bytes.readUInt32LE(cursor + 4);
|
||||
if (size < 8 || cursor + size > commandEnd)
|
||||
throw new RangeError("Invalid load command size");
|
||||
if (kind === 0x19) {
|
||||
if (size < 72) throw new RangeError("Truncated segment command");
|
||||
const address = bytes.readBigUInt64LE(cursor + 24),
|
||||
fileSize = bytes.readBigUInt64LE(cursor + 48),
|
||||
fileOffset = bytes.readBigUInt64LE(cursor + 40);
|
||||
if (fileOffset + fileSize > BigInt(sliceEnd - slice))
|
||||
throw new RangeError("Segment file range exceeds target bytes");
|
||||
segments.push({
|
||||
address,
|
||||
size: fileSize,
|
||||
offset: slice + Number(fileOffset),
|
||||
executable: (bytes.readUInt32LE(cursor + 60) & 4) !== 0,
|
||||
});
|
||||
const count = bytes.readUInt32LE(cursor + 64);
|
||||
if (72 + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
for (let section = 0; section < count; section++) {
|
||||
const position = cursor + 72 + section * 80;
|
||||
const sectionSize = bytes.readBigUInt64LE(position + 40);
|
||||
if (sectionSize > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new RangeError("Section size exceeds exact numeric range");
|
||||
sections.push({
|
||||
name: bytes
|
||||
.subarray(position, position + 16)
|
||||
.toString("ascii")
|
||||
.replace(/\0.*$/u, ""),
|
||||
address: bytes.readBigUInt64LE(position + 32),
|
||||
size: Number(sectionSize),
|
||||
});
|
||||
}
|
||||
}
|
||||
cursor += size;
|
||||
}
|
||||
const offset = (address: bigint, size = 1): number => {
|
||||
const matches = segments.filter(
|
||||
(segment) =>
|
||||
address >= segment.address &&
|
||||
address + BigInt(size) <= segment.address + segment.size,
|
||||
);
|
||||
if (matches.length !== 1)
|
||||
throw new RangeError(
|
||||
`Unmapped or ambiguous metadata pointer ${hex(address)}`,
|
||||
);
|
||||
const segment = matches[0];
|
||||
if (segment === undefined) throw new RangeError("Missing metadata segment");
|
||||
return segment.offset + Number(address - segment.address);
|
||||
};
|
||||
return { segments, sections, offset };
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
import type { ObjcSwiftMetadata } from "../domain/objcSwiftMetadata.js";
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
import { createObjcProtocolReader } from "./AppleObjcProtocols.js";
|
||||
import type {
|
||||
BinaryMetadataEvidence,
|
||||
DispatchRecordBudget,
|
||||
ResolvedMetadataLocation,
|
||||
} from "./AppleSwiftDispatchFacets.js";
|
||||
|
||||
/** Byte readers shared by Objective-C class/method/ivar decode facets. */
|
||||
export interface ObjcDispatchReaders {
|
||||
u32(address: bigint): number;
|
||||
i32(address: bigint): number;
|
||||
pointer(address: bigint): bigint;
|
||||
string(address: bigint): string;
|
||||
location(address: bigint): ResolvedMetadataLocation;
|
||||
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 };
|
||||
|
||||
/** Decode Objective-C protocol lists, classlists, methods, and ivars into `result`. */
|
||||
export const decodeObjcDispatchFacets = (input: {
|
||||
bytes: Buffer;
|
||||
sections: readonly Section[];
|
||||
segments: readonly Segment[];
|
||||
readers: ObjcDispatchReaders;
|
||||
budget: DispatchRecordBudget;
|
||||
result: ObjcSwiftMetadata;
|
||||
}): { failures: string[]; examined: number } => {
|
||||
const { bytes, sections, segments, readers: read, budget, result } = input;
|
||||
const failures: string[] = [];
|
||||
let examined = 0;
|
||||
const methods = (
|
||||
list: bigint,
|
||||
className: string,
|
||||
methodType: "instance" | "class",
|
||||
) => {
|
||||
if (list === 0n) return;
|
||||
const flags = read.u32(list),
|
||||
count = read.u32(list + 4n),
|
||||
relative = (flags & 0x80000000) !== 0;
|
||||
const stride = flags & 0xfffc;
|
||||
if (stride !== (relative ? 12 : 24))
|
||||
throw new TypeError(`Unsupported method stride/flags at ${hex(list)}`);
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!budget.admit()) break;
|
||||
const entry = list + 8n + BigInt(index * stride);
|
||||
const rel = (field: bigint) => {
|
||||
const displacement = bytes.readInt32LE(read.offset(field, 4));
|
||||
const target = field + BigInt(displacement);
|
||||
let fileOffset: number | null = null;
|
||||
try {
|
||||
fileOffset = read.offset(target);
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: serialized target remains unresolved.
|
||||
void cause;
|
||||
}
|
||||
if (budget.admit())
|
||||
result.relative_pointers.push({
|
||||
field: read.location(field),
|
||||
target: { address: hex(target), file_offset: fileOffset },
|
||||
displacement,
|
||||
indirectable: false,
|
||||
indirect: false,
|
||||
decode:
|
||||
fileOffset === null
|
||||
? { status: "partial", reason: "relative_target_unmapped" }
|
||||
: decoded,
|
||||
evidence: read.evidence(
|
||||
field,
|
||||
"Signed-relative Objective-C method-list pointer",
|
||||
),
|
||||
});
|
||||
return target;
|
||||
};
|
||||
let selectorAddress = relative ? rel(entry) : read.pointer(entry);
|
||||
if (relative && (flags & 0x40000000) === 0)
|
||||
selectorAddress = read.pointer(selectorAddress);
|
||||
const implementation = relative
|
||||
? rel(entry + 8n)
|
||||
: read.pointer(entry + 16n);
|
||||
let implementationLocation: {
|
||||
address: string;
|
||||
file_offset: number;
|
||||
} | null = null;
|
||||
try {
|
||||
if (
|
||||
!segments.some(
|
||||
(segment) =>
|
||||
segment.executable &&
|
||||
implementation >= segment.address &&
|
||||
implementation < segment.address + segment.size,
|
||||
)
|
||||
)
|
||||
throw new RangeError(
|
||||
"Method implementation is not in an executable segment",
|
||||
);
|
||||
implementationLocation = read.location(implementation);
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: external/chained pointers remain unresolved.
|
||||
void cause;
|
||||
}
|
||||
result.objc_dispatch_implementations.push({
|
||||
class_name: className,
|
||||
selector: read.string(selectorAddress),
|
||||
method_type: methodType,
|
||||
implementation_address: implementationLocation?.address ?? null,
|
||||
location: read.location(entry),
|
||||
decode:
|
||||
implementationLocation === null
|
||||
? {
|
||||
status: "partial",
|
||||
reason: "implementation_pointer_unmapped_or_encoded",
|
||||
}
|
||||
: decoded,
|
||||
evidence: read.evidence(
|
||||
entry,
|
||||
relative
|
||||
? "Encoded relative Objective-C method entry"
|
||||
: "Encoded absolute Objective-C method entry",
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
const protocolReader = createObjcProtocolReader({
|
||||
readers: {
|
||||
u32: read.u32,
|
||||
pointer: read.pointer,
|
||||
string: read.string,
|
||||
location: read.location,
|
||||
evidence: read.evidence,
|
||||
admit: () => budget.admit(),
|
||||
i32: read.i32,
|
||||
},
|
||||
result,
|
||||
failures,
|
||||
});
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__objc_protolist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C protocol list");
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (budget.isFull()) {
|
||||
budget.markTruncated();
|
||||
break;
|
||||
}
|
||||
protocolReader.record(read.pointer(section.address + BigInt(index * 8)));
|
||||
}
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
const readClass = (address: bigint, meta = false) => {
|
||||
if (address === 0n || visited.has(hex(address)) || !budget.admit()) return;
|
||||
visited.add(hex(address));
|
||||
const ro = read.pointer(address + 32n) & ~7n;
|
||||
const name = read.string(read.pointer(ro + 24n));
|
||||
const superclass = read.pointer(address + 8n);
|
||||
const root = (read.u32(ro) & 2) !== 0;
|
||||
let superclassName: string | null = null;
|
||||
if (superclass !== 0n) {
|
||||
try {
|
||||
superclassName = read.string(
|
||||
read.pointer((read.pointer(superclass + 32n) & ~7n) + 24n),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
// Superclass resolution failure is recorded; the cause adds no identity.
|
||||
void cause;
|
||||
failures.push(
|
||||
`Superclass pointer ${hex(superclass)} of ${name} requires unsupported binding/fixup resolution`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (superclass === 0n && !root)
|
||||
failures.push(
|
||||
`Superclass of ${name} requires external binding resolution`,
|
||||
);
|
||||
const ivarList = read.pointer(ro + 48n);
|
||||
let ivarCount = 0;
|
||||
if (ivarList !== 0n && !meta) {
|
||||
const stride = read.u32(ivarList),
|
||||
count = read.u32(ivarList + 4n);
|
||||
if (stride !== 32)
|
||||
throw new TypeError("Unsupported Objective-C ivar entry size");
|
||||
ivarCount = count;
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!budget.admit()) break;
|
||||
const entry = ivarList + 8n + BigInt(index * stride);
|
||||
const encodedOffset = read.pointer(entry);
|
||||
let value: number | null = null;
|
||||
try {
|
||||
value = read.u32(encodedOffset);
|
||||
} catch (cause: unknown) {
|
||||
// Unresolved offsets are recorded; the cause adds no identity.
|
||||
void cause;
|
||||
failures.push(`Unresolved ivar offset pointer ${hex(encodedOffset)}`);
|
||||
}
|
||||
result.objc_ivars.push({
|
||||
class_name: name,
|
||||
name: read.string(read.pointer(entry + 8n)),
|
||||
type_encoding: read.string(read.pointer(entry + 16n)),
|
||||
offset: value,
|
||||
size: read.u32(entry + 28n),
|
||||
alignment:
|
||||
read.u32(entry + 24n) < 31 ? 2 ** read.u32(entry + 24n) : null,
|
||||
location: read.location(entry),
|
||||
decode:
|
||||
value === null
|
||||
? { status: "partial", reason: "ivar_offset_pointer_unresolved" }
|
||||
: decoded,
|
||||
evidence: read.evidence(
|
||||
entry,
|
||||
"Objective-C ivar entry and encoded offset storage",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
methods(read.pointer(ro + 32n), name, meta ? "class" : "instance");
|
||||
result.objc_classes.push({
|
||||
name,
|
||||
super_class: superclassName,
|
||||
is_meta_class: meta,
|
||||
is_root_class: root,
|
||||
methods: result.objc_dispatch_implementations
|
||||
.filter(
|
||||
(item) =>
|
||||
item.class_name === name &&
|
||||
item.method_type === (meta ? "class" : "instance"),
|
||||
)
|
||||
.map((item) => ({
|
||||
selector: item.selector,
|
||||
method_type: item.method_type,
|
||||
address:
|
||||
item.implementation_address !== null &&
|
||||
BigInt(item.implementation_address) <=
|
||||
BigInt(Number.MAX_SAFE_INTEGER)
|
||||
? Number(BigInt(item.implementation_address))
|
||||
: null,
|
||||
is_required: false,
|
||||
is_optional: false,
|
||||
})),
|
||||
properties: [],
|
||||
protocols: protocolReader.list(read.pointer(ro + 40n)),
|
||||
ivar_count: ivarCount,
|
||||
instance_size: read.u32(ro + 8n),
|
||||
location: read.location(address),
|
||||
superclass_address: superclass === 0n ? null : hex(superclass),
|
||||
metaclass_address: meta ? null : hex(read.pointer(address)),
|
||||
decode:
|
||||
superclassName === null && !root
|
||||
? { status: "partial", reason: "superclass_binding_unresolved" }
|
||||
: decoded,
|
||||
evidence: read.evidence(
|
||||
address,
|
||||
"Objective-C class and class_ro_t records",
|
||||
),
|
||||
});
|
||||
if (!meta) {
|
||||
try {
|
||||
readClass(read.pointer(address), true);
|
||||
} catch (cause) {
|
||||
failures.push(
|
||||
`Metaclass of ${name}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__objc_classlist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C class-list size");
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (budget.isFull()) {
|
||||
budget.markTruncated();
|
||||
break;
|
||||
}
|
||||
examined++;
|
||||
const field = section.address + BigInt(index * 8);
|
||||
try {
|
||||
readClass(read.pointer(field));
|
||||
} catch (cause) {
|
||||
failures.push(
|
||||
`${hex(field)}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { failures, examined };
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import type { ObjcSwiftMetadata } from "../domain/objcSwiftMetadata.js";
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
|
||||
/** Resolved file-backed metadata location from the Mach-O layout. */
|
||||
export interface ResolvedMetadataLocation {
|
||||
address: string;
|
||||
file_offset: number;
|
||||
}
|
||||
|
||||
/** Binary-metadata evidence produced by the Apple dispatch readers. */
|
||||
export type BinaryMetadataEvidence = {
|
||||
kind: "binary_metadata";
|
||||
description: string;
|
||||
location: ResolvedMetadataLocation;
|
||||
artifact_path: string;
|
||||
artifact_sha256: string;
|
||||
}[];
|
||||
|
||||
/** Shared admission budget for max_records truncation across decode facets. */
|
||||
export interface DispatchRecordBudget {
|
||||
truncated: boolean;
|
||||
isFull(): boolean;
|
||||
markTruncated(): void;
|
||||
admit(): boolean;
|
||||
}
|
||||
|
||||
/** Byte readers for Swift relative-pointer and conformance decode facets. */
|
||||
export interface SwiftDispatchReaders {
|
||||
u32(address: bigint): number;
|
||||
pointer(address: bigint): bigint;
|
||||
string(address: bigint): string;
|
||||
location(address: bigint): ResolvedMetadataLocation;
|
||||
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 };
|
||||
|
||||
/** Create a Swift ABI signed-relative pointer decoder that records relative_pointers. */
|
||||
export const createSwiftRelativeReader = (input: {
|
||||
bytes: Buffer;
|
||||
readers: SwiftDispatchReaders;
|
||||
budget: DispatchRecordBudget;
|
||||
result: ObjcSwiftMetadata;
|
||||
}) => {
|
||||
const { bytes, readers: read, budget, result } = input;
|
||||
return (field: bigint, indirectable = false): bigint => {
|
||||
const displacement = bytes.readInt32LE(read.offset(field, 4));
|
||||
const indirect = indirectable && (displacement & 1) !== 0;
|
||||
let target =
|
||||
displacement === 0
|
||||
? 0n
|
||||
: field + BigInt(indirectable ? displacement & ~1 : displacement);
|
||||
let fileOffset: number | null = null;
|
||||
let reason: string | null = null;
|
||||
try {
|
||||
if (indirect && target !== 0n) target = read.pointer(target);
|
||||
if (target !== 0n) fileOffset = read.offset(target);
|
||||
} catch (cause) {
|
||||
reason = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
if (budget.admit())
|
||||
result.relative_pointers.push({
|
||||
field: read.location(field),
|
||||
target: {
|
||||
address: target === 0n ? null : hex(target),
|
||||
file_offset: fileOffset,
|
||||
},
|
||||
displacement,
|
||||
indirectable,
|
||||
indirect,
|
||||
decode: reason === null ? decoded : { status: "partial", reason },
|
||||
evidence: read.evidence(
|
||||
field,
|
||||
"Swift ABI signed-relative metadata pointer",
|
||||
),
|
||||
});
|
||||
if (reason !== null) throw new RangeError(reason);
|
||||
return target;
|
||||
};
|
||||
};
|
||||
|
||||
/** Decode `__swift5_proto` conformances and static witness slots into `result`. */
|
||||
export const decodeSwiftDispatchFacets = (input: {
|
||||
sections: readonly Section[];
|
||||
segments: readonly Segment[];
|
||||
readers: SwiftDispatchReaders;
|
||||
relative: (field: bigint, indirectable?: boolean) => bigint;
|
||||
budget: DispatchRecordBudget;
|
||||
result: ObjcSwiftMetadata;
|
||||
}): void => {
|
||||
const { sections, segments, readers: read, relative, budget, result } = input;
|
||||
const swiftFailures: string[] = [];
|
||||
let swiftExamined = 0;
|
||||
for (const section of sections.filter(
|
||||
({ name }) => name === "__swift5_proto",
|
||||
)) {
|
||||
if (section.size % 4 !== 0)
|
||||
throw new RangeError("Misaligned Swift conformance section");
|
||||
for (let index = 0; index < section.size / 4; index++) {
|
||||
if (!budget.admit()) break;
|
||||
swiftExamined++;
|
||||
const field = section.address + BigInt(index * 4);
|
||||
try {
|
||||
const descriptor = relative(field);
|
||||
const flags = read.u32(descriptor + 12n);
|
||||
const protocol = relative(descriptor, true);
|
||||
if ((read.u32(protocol) & 31) !== 3)
|
||||
throw new TypeError("Unsupported Swift protocol descriptor kind");
|
||||
const protocolName = read.string(relative(protocol + 8n));
|
||||
const typeKind = (flags >>> 3) & 7;
|
||||
if (typeKind !== 0 && typeKind !== 1)
|
||||
throw new TypeError(
|
||||
"Unsupported Swift conformance type-reference kind",
|
||||
);
|
||||
let type = relative(descriptor + 4n);
|
||||
if (typeKind === 1) type = read.pointer(type);
|
||||
if (![16, 17, 18].includes(read.u32(type) & 31))
|
||||
throw new TypeError("Unsupported Swift type descriptor kind");
|
||||
const typeName = read.string(relative(type + 8n));
|
||||
const witness = relative(descriptor + 8n);
|
||||
const staticWitness =
|
||||
witness !== 0n &&
|
||||
(flags & ~0x38) === 0 &&
|
||||
(read.u32(type) & 0x80) === 0;
|
||||
let witnessReason: string | null = staticWitness
|
||||
? null
|
||||
: "Conditional, generic, resilient or missing static witness table is not decoded";
|
||||
if (staticWitness && read.pointer(witness) !== descriptor)
|
||||
witnessReason =
|
||||
"Witness-table conformance header is unresolved or encoded";
|
||||
result.swift_conformances.push({
|
||||
type_name: typeName,
|
||||
protocol_name: protocolName,
|
||||
module: null,
|
||||
witness_table:
|
||||
witness === 0n
|
||||
? { address: null, file_offset: null }
|
||||
: read.location(witness),
|
||||
location: read.location(descriptor),
|
||||
decode:
|
||||
witnessReason === null
|
||||
? decoded
|
||||
: {
|
||||
status: "partial",
|
||||
reason: witnessReason,
|
||||
},
|
||||
evidence: read.evidence(
|
||||
descriptor,
|
||||
"Swift protocol conformance descriptor and directly encoded type/protocol names",
|
||||
),
|
||||
});
|
||||
if (witnessReason !== null) {
|
||||
swiftFailures.push(`${typeName}: ${witnessReason}`);
|
||||
continue;
|
||||
}
|
||||
const count = read.u32(protocol + 16n);
|
||||
const signatureCount = read.u32(protocol + 12n);
|
||||
if (count > 20_000 || signatureCount > 20_000)
|
||||
throw new RangeError("Swift witness requirement count exceeds 20000");
|
||||
for (let slot = 0; slot < count; slot++) {
|
||||
if (!budget.admit()) break;
|
||||
const entry = witness + BigInt((slot + 1) * 8);
|
||||
const implementation = read.pointer(entry);
|
||||
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 =
|
||||
synchronousFunction &&
|
||||
segments.some(
|
||||
(segment) =>
|
||||
segment.executable &&
|
||||
implementation >= segment.address &&
|
||||
implementation < segment.address + segment.size,
|
||||
);
|
||||
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,
|
||||
thunk_address: null,
|
||||
location: read.location(entry),
|
||||
decode: executable
|
||||
? decoded
|
||||
: {
|
||||
status: "partial",
|
||||
reason:
|
||||
"Witness slot is non-function, async, nil, external, authenticated or encoded; implementation target is unresolved",
|
||||
},
|
||||
evidence: read.evidence(
|
||||
entry,
|
||||
"Static Swift witness slot, indexed after the conformance-descriptor header",
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (cause) {
|
||||
swiftFailures.push(
|
||||
`${hex(field)}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.coverage.push({
|
||||
facet: "swift_conformances_static_witness_slots",
|
||||
status:
|
||||
swiftFailures.length > 0 || budget.truncated ? "partial" : "complete",
|
||||
reason:
|
||||
[
|
||||
...swiftFailures,
|
||||
...(budget.truncated ? ["max_records_reached"] : []),
|
||||
].join("; ") || null,
|
||||
examined: swiftExamined,
|
||||
decoded: result.swift_conformances.length,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user