mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
c02945bc44
* 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>
279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { objcSwiftMetadataSchema } from "../domain/native/objcSwiftMetadata.js";
|
|
import {
|
|
createSwiftRelativeReader,
|
|
type DispatchRecordBudget,
|
|
} from "./AppleSwiftDispatchFacets.js";
|
|
|
|
const readers = (bytes: Buffer) => ({
|
|
u32: (address: bigint) => bytes.readUInt32LE(Number(address)),
|
|
pointer: (address: bigint) => bytes.readBigUInt64LE(Number(address)),
|
|
resolvePointer: (address: bigint) => ({
|
|
kind: "raw" as const,
|
|
target: bytes.readBigUInt64LE(Number(address)),
|
|
}),
|
|
string: () => "fixture",
|
|
location: (address: bigint) => ({
|
|
address: `0x${address.toString(16)}`,
|
|
file_offset: Number(address),
|
|
}),
|
|
targetLocation: (address: bigint) => ({
|
|
address: `0x${address.toString(16)}`,
|
|
file_offset: Number(address),
|
|
}),
|
|
evidence: (address: bigint, description: string) => [
|
|
{
|
|
kind: "binary_metadata" as const,
|
|
description,
|
|
location: {
|
|
address: `0x${address.toString(16)}`,
|
|
file_offset: Number(address),
|
|
},
|
|
artifact_path: "/fixture",
|
|
artifact_sha256: "a".repeat(64),
|
|
},
|
|
],
|
|
offset: (address: bigint, size = 1) => {
|
|
if (address < 0n || address + BigInt(size) > BigInt(bytes.length))
|
|
throw new RangeError("outside fixture bytes");
|
|
return Number(address);
|
|
},
|
|
});
|
|
|
|
const budget = (): DispatchRecordBudget => ({
|
|
truncated: false,
|
|
isFull: () => false,
|
|
markTruncated: () => undefined,
|
|
admit: () => true,
|
|
});
|
|
|
|
it("keeps an encoded null distinct from a nonzero relative offset to address zero", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(0, 0x20);
|
|
bytes.writeInt32LE(-0x40, 0x40);
|
|
bytes.writeInt32LE(0x25, 0x24);
|
|
bytes.writeBigUInt64LE(0n, 0x48);
|
|
bytes.writeInt32LE(0x25, 0x2c);
|
|
bytes.writeBigUInt64LE(0x60n, 0x50);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x20n, "methodImplementation")).toEqual({ kind: "null" });
|
|
expect(relative(0x40n, "methodImplementation")).toEqual({
|
|
kind: "target",
|
|
target: 0n,
|
|
});
|
|
expect(relative(0x24n, "conformanceProtocol")).toEqual({ kind: "null" });
|
|
expect(relative(0x2cn, "conformanceProtocol")).toEqual({
|
|
kind: "target",
|
|
target: 0x60n,
|
|
});
|
|
expect(result.relative_pointers).toMatchObject([
|
|
{ displacement: 0, target: { address: null, file_offset: null } },
|
|
{ displacement: -0x40, target: { address: "0x0", file_offset: 0 } },
|
|
{ displacement: 0x25, indirect: true, target: { address: null } },
|
|
{ displacement: 0x25, indirect: true, target: { address: "0x60" } },
|
|
]);
|
|
});
|
|
|
|
it("resolves zero offsets for nonnullable fields to their own field address", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(0, 0x20);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
for (const kind of [
|
|
"conformanceRecord",
|
|
"protocolName",
|
|
"typeName",
|
|
"typeMetadataRecord",
|
|
"moduleName",
|
|
] as const) {
|
|
expect(relative(0x20n, kind)).toEqual({ kind: "target", target: 0x20n });
|
|
}
|
|
expect(result.relative_pointers).toHaveLength(5);
|
|
expect(result.relative_pointers).toMatchObject(
|
|
Array.from({ length: 5 }, () => ({
|
|
target: { address: "0x20", file_offset: 0x20 },
|
|
})),
|
|
);
|
|
});
|
|
|
|
it("accepts encoded zero only for nullable direct Swift fields", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
for (const kind of [
|
|
"conformanceType",
|
|
"witnessTablePattern",
|
|
"methodImplementation",
|
|
] as const) {
|
|
expect(relative(0x20n, kind)).toEqual({ kind: "null" });
|
|
}
|
|
});
|
|
|
|
it("masks only the verified direct type metadata tag and keeps other tags unsupported", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(0x24, 0x20);
|
|
bytes.writeInt32LE(0x25, 0x28);
|
|
bytes.writeInt32LE(0x26, 0x2c);
|
|
bytes.writeInt32LE(0x27, 0x30);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x20n, "typeMetadataRecord")).toEqual({
|
|
kind: "target",
|
|
target: 0x44n,
|
|
});
|
|
for (const [field, tag] of [
|
|
[0x28n, 1],
|
|
[0x2cn, 2],
|
|
[0x30n, 3],
|
|
] as const) {
|
|
expect(relative(field, "typeMetadataRecord")).toEqual({
|
|
kind: "unknown",
|
|
reason: `Unsupported Swift type metadata record tag ${tag}`,
|
|
});
|
|
}
|
|
expect(result.relative_pointers.slice(-4)).toMatchObject([
|
|
{ displacement: 0x24, encoding_tag: 0, target: { address: "0x44" } },
|
|
{
|
|
displacement: 0x25,
|
|
encoding_tag: 1,
|
|
decode: { status: "unsupported" },
|
|
},
|
|
{
|
|
displacement: 0x26,
|
|
encoding_tag: 2,
|
|
decode: { status: "unsupported" },
|
|
},
|
|
{
|
|
displacement: 0x27,
|
|
encoding_tag: 3,
|
|
decode: { status: "unsupported" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("treats a zero pointer loaded through a nullable indirect field as null", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(-0x23, 0x24); // odd-tagged displacement to cell address 0
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x24n, "conformanceProtocol")).toEqual({ kind: "null" });
|
|
expect(result.relative_pointers[0]).toMatchObject({
|
|
indirect: true,
|
|
target: { address: null, file_offset: null },
|
|
});
|
|
});
|
|
|
|
it("keeps a fixup rebase to VM address zero as an address, not null", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(-0x23, 0x24);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: {
|
|
...readers(bytes),
|
|
resolvePointer: () => ({ kind: "rebase", target: 0n }),
|
|
offset: (address, size = 1) => {
|
|
if (address === 0n) throw new RangeError("unmapped target address");
|
|
return readers(bytes).offset(address, size);
|
|
},
|
|
},
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x24n, "conformanceProtocol")).toEqual({
|
|
kind: "target",
|
|
target: 0n,
|
|
});
|
|
expect(result.relative_pointers[0]).toMatchObject({
|
|
target: { address: "0x0", file_offset: null },
|
|
decode: { status: "partial", reason: "unmapped target address" },
|
|
});
|
|
});
|
|
|
|
it("preserves an indirect import binding instead of reading VM address zero", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(-0x23, 0x24);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: {
|
|
...readers(bytes),
|
|
resolvePointer: () => ({
|
|
kind: "bind",
|
|
symbol: "_$sSQMp",
|
|
binding_source: "chained",
|
|
library_ordinal: 1,
|
|
library: "/System/Library/libswiftCore.dylib",
|
|
weak: false,
|
|
addend: 0n,
|
|
}),
|
|
},
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x24n, "conformanceProtocol")).toMatchObject({
|
|
kind: "binding",
|
|
binding: { symbol: "_$sSQMp" },
|
|
});
|
|
expect(result.relative_pointers[0]).toMatchObject({
|
|
target: { address: null, file_offset: null },
|
|
binding: { symbol: "_$sSQMp", requested_address: "0x0" },
|
|
decode: { status: "partial" },
|
|
});
|
|
});
|
|
|
|
it("retains unsafe target addresses as partial decode facts", () => {
|
|
const bytes = Buffer.alloc(0x80);
|
|
bytes.writeInt32LE(0x100, 0x20);
|
|
const result = objcSwiftMetadataSchema.parse({ db_save_result: null });
|
|
const relative = createSwiftRelativeReader({
|
|
bytes,
|
|
readers: readers(bytes),
|
|
budget: budget(),
|
|
result,
|
|
});
|
|
|
|
expect(relative(0x20n, "protocolName")).toEqual({
|
|
kind: "target",
|
|
target: 0x120n,
|
|
});
|
|
expect(result.relative_pointers[0]).toMatchObject({
|
|
target: { address: "0x120", file_offset: null },
|
|
decode: { status: "partial", reason: "outside fixture bytes" },
|
|
});
|
|
});
|