fix(native): report record-local Objective-C budget omissions
Have method, property, ivar, and adopted-protocol list owners flag real budget omissions on returned class, protocol, and category records. Preserve known empty lists and existing decode reasons. Validated 59 source tests, two built CLI controls, five static inspections of an Apple-clang fixture, and typecheck/lint. The target was never executed. Fixes #5543.
This commit is contained in:
1 parent
8d198d6ec7
commit
9eec57b469
7 files changed
+460
-48
No files matched your search
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -119,6 +120,20 @@ const fixture = (
|
||||
};
|
||||
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
|
||||
|
||||
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;
|
||||
@@ -469,6 +484,173 @@ describe("Apple dispatch coverage ownership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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"],
|
||||
|
||||
@@ -31,8 +31,9 @@ export const decodeObjcCategories = (input: {
|
||||
className: string,
|
||||
methodType: "instance" | "class",
|
||||
category: string,
|
||||
onTruncated?: () => void,
|
||||
) => string[];
|
||||
readonly protocols: (list: bigint) => string[];
|
||||
readonly protocols: (list: bigint, onTruncated?: () => void) => string[];
|
||||
/** Name of a class defined in this image, from its class_ro_t. */
|
||||
readonly localClassName: (address: bigint) => string;
|
||||
}): number => {
|
||||
@@ -71,6 +72,43 @@ export const decodeObjcCategories = (input: {
|
||||
);
|
||||
}
|
||||
const owner = className ?? `(${name})`;
|
||||
let budgetTruncated = false;
|
||||
const onTruncated = () => {
|
||||
budgetTruncated = true;
|
||||
};
|
||||
const instanceMethods = input.methods(
|
||||
read.pointer(category + 16n),
|
||||
owner,
|
||||
"instance",
|
||||
name,
|
||||
onTruncated,
|
||||
);
|
||||
const classMethods = input.methods(
|
||||
read.pointer(category + 24n),
|
||||
owner,
|
||||
"class",
|
||||
name,
|
||||
onTruncated,
|
||||
);
|
||||
const protocols = input.protocols(
|
||||
read.pointer(category + 32n),
|
||||
onTruncated,
|
||||
);
|
||||
const properties = readObjcPropertiesOf(
|
||||
read,
|
||||
read.pointer(category + 40n),
|
||||
{
|
||||
owner: `category ${name}`,
|
||||
admit: input.admit,
|
||||
issues: input.issues,
|
||||
location: `0x${read.pointer(category + 40n).toString(16)}`,
|
||||
onTruncated,
|
||||
},
|
||||
);
|
||||
const decodeReasons = [
|
||||
...(className === null ? ["category_class_unresolved"] : []),
|
||||
...(budgetTruncated ? ["max_records_reached"] : []),
|
||||
];
|
||||
result.objc_categories.push({
|
||||
name,
|
||||
class_name: className,
|
||||
@@ -80,30 +118,15 @@ export const decodeObjcCategories = (input: {
|
||||
: className === null
|
||||
? "unresolved"
|
||||
: "local",
|
||||
instance_methods: input.methods(
|
||||
read.pointer(category + 16n),
|
||||
owner,
|
||||
"instance",
|
||||
name,
|
||||
),
|
||||
class_methods: input.methods(
|
||||
read.pointer(category + 24n),
|
||||
owner,
|
||||
"class",
|
||||
name,
|
||||
),
|
||||
protocols: input.protocols(read.pointer(category + 32n)),
|
||||
properties: readObjcPropertiesOf(read, read.pointer(category + 40n), {
|
||||
owner: `category ${name}`,
|
||||
admit: input.admit,
|
||||
issues: input.issues,
|
||||
location: `0x${read.pointer(category + 40n).toString(16)}`,
|
||||
}),
|
||||
instance_methods: instanceMethods,
|
||||
class_methods: classMethods,
|
||||
protocols,
|
||||
properties,
|
||||
location: read.location(category),
|
||||
decode:
|
||||
className === null
|
||||
? { status: "partial", reason: "category_class_unresolved" }
|
||||
: { status: "decoded", reason: null },
|
||||
decodeReasons.length === 0
|
||||
? { status: "decoded", reason: null }
|
||||
: { status: "partial", reason: decodeReasons.join("; ") },
|
||||
evidence: read.evidence(category, "Objective-C category_t record"),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
|
||||
@@ -103,6 +103,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
className: string,
|
||||
methodType: "instance" | "class",
|
||||
category?: string,
|
||||
onTruncated?: () => void,
|
||||
): string[] => {
|
||||
const selectors: string[] = [];
|
||||
if (list === 0n) return selectors;
|
||||
@@ -113,7 +114,10 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
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;
|
||||
if (!budget.admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
}
|
||||
const entry = list + 8n + BigInt(index * stride);
|
||||
const rel = (field: bigint) => {
|
||||
const displacement = bytes.readInt32LE(read.offset(field, 4));
|
||||
@@ -279,6 +283,10 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
);
|
||||
const ivarList = read.pointer(ro + 48n);
|
||||
let ivarCount: number | null = meta ? null : 0;
|
||||
let classListTruncated = false;
|
||||
const onTruncated = () => {
|
||||
classListTruncated = true;
|
||||
};
|
||||
if (ivarList !== 0n && !meta) {
|
||||
const stride = read.u32(ivarList),
|
||||
count = read.u32(ivarList + 4n);
|
||||
@@ -286,7 +294,10 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
throw new TypeError("Unsupported Objective-C ivar entry size");
|
||||
ivarCount = count;
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!budget.admit()) break;
|
||||
if (!budget.admit()) {
|
||||
classListTruncated = true;
|
||||
break;
|
||||
}
|
||||
const entry = ivarList + 8n + BigInt(index * stride);
|
||||
const encodedOffset = read.pointer(entry);
|
||||
let value: number | null = null;
|
||||
@@ -326,7 +337,27 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
});
|
||||
}
|
||||
}
|
||||
methods(read.pointer(ro + 32n), name, meta ? "class" : "instance");
|
||||
methods(
|
||||
read.pointer(ro + 32n),
|
||||
name,
|
||||
meta ? "class" : "instance",
|
||||
undefined,
|
||||
onTruncated,
|
||||
);
|
||||
const properties = readObjcPropertiesOf(read, read.pointer(ro + 64n), {
|
||||
owner: name,
|
||||
admit: () => budget.admit(),
|
||||
issues: categoryIssues,
|
||||
location: hex(read.pointer(ro + 64n)),
|
||||
onTruncated,
|
||||
});
|
||||
const protocols = protocolReader.list(read.pointer(ro + 40n), onTruncated);
|
||||
const classDecodeReasons = [
|
||||
...(superclass.name === null && !root
|
||||
? ["superclass_binding_unresolved"]
|
||||
: []),
|
||||
...(classListTruncated ? ["max_records_reached"] : []),
|
||||
];
|
||||
result.objc_classes.push({
|
||||
name,
|
||||
super_class: superclass.name,
|
||||
@@ -350,22 +381,17 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
})),
|
||||
properties: readObjcPropertiesOf(read, read.pointer(ro + 64n), {
|
||||
owner: name,
|
||||
admit: () => budget.admit(),
|
||||
issues: categoryIssues,
|
||||
location: hex(read.pointer(ro + 64n)),
|
||||
}),
|
||||
protocols: protocolReader.list(read.pointer(ro + 40n)),
|
||||
properties,
|
||||
protocols,
|
||||
ivar_count: ivarCount,
|
||||
instance_size: read.u32(ro + 8n),
|
||||
location: read.location(address),
|
||||
superclass_address: superclass.address,
|
||||
metaclass_address: meta ? null : hex(read.pointer(address)),
|
||||
decode:
|
||||
superclass.name === null && !root
|
||||
? { status: "partial", reason: "superclass_binding_unresolved" }
|
||||
: decoded,
|
||||
classDecodeReasons.length === 0
|
||||
? decoded
|
||||
: { status: "partial", reason: classDecodeReasons.join("; ") },
|
||||
evidence: read.evidence(
|
||||
address,
|
||||
"Objective-C class and class_ro_t records",
|
||||
@@ -422,7 +448,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
issues: categoryIssues,
|
||||
admit: () => budget.admit(),
|
||||
methods,
|
||||
protocols: (list) => protocolReader.list(list),
|
||||
protocols: (list, onTruncated) => protocolReader.list(list, onTruncated),
|
||||
localClassName: (address) => readLocalClass(read, address).name,
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -107,6 +107,7 @@ export const readObjcPropertyList = (
|
||||
read: ObjcPropertyReaders,
|
||||
list: bigint,
|
||||
admit: () => boolean,
|
||||
onTruncated?: () => void,
|
||||
): {
|
||||
readonly properties: ObjcProperty[];
|
||||
readonly issues: PropertyListIssue[];
|
||||
@@ -121,7 +122,10 @@ export const readObjcPropertyList = (
|
||||
`Unsupported Objective-C property entry size ${entrySize}`,
|
||||
);
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!admit()) break;
|
||||
if (!admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
}
|
||||
const entry = list + 8n + BigInt(index * entrySize);
|
||||
const location = `0x${entry.toString(16)}`;
|
||||
try {
|
||||
@@ -158,6 +162,7 @@ export const readObjcPropertiesOf = (
|
||||
readonly admit: () => boolean;
|
||||
readonly issues: DecodeIssue[];
|
||||
readonly location?: string;
|
||||
readonly onTruncated?: () => void;
|
||||
},
|
||||
): ObjcProperty[] => {
|
||||
try {
|
||||
@@ -165,6 +170,7 @@ export const readObjcPropertiesOf = (
|
||||
read,
|
||||
list,
|
||||
context.admit,
|
||||
context.onTruncated,
|
||||
);
|
||||
for (const item of issues)
|
||||
context.issues.push(
|
||||
|
||||
@@ -18,7 +18,7 @@ export const createObjcProtocolReader = (input: {
|
||||
const { readers: read, result, failures } = input;
|
||||
const visited = new Set<bigint>();
|
||||
const name = (address: bigint) => read.string(read.pointer(address + 8n));
|
||||
const list = (address: bigint): string[] => {
|
||||
const list = (address: bigint, onTruncated?: () => void): string[] => {
|
||||
if (address === 0n) return [];
|
||||
// `count` is plain uintptr_t data, not a pointer fixup.
|
||||
const count = read.u64(address);
|
||||
@@ -26,7 +26,10 @@ export const createObjcProtocolReader = (input: {
|
||||
throw new RangeError("Protocol list exceeds 20000 entries");
|
||||
const names: string[] = [];
|
||||
for (let index = 0n; index < count; index++) {
|
||||
if (!read.admit()) break;
|
||||
if (!read.admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
}
|
||||
const protocol = read.pointer(address + 8n + index * 8n);
|
||||
names.push(name(protocol));
|
||||
record(protocol);
|
||||
@@ -39,6 +42,7 @@ export const createObjcProtocolReader = (input: {
|
||||
instance: boolean,
|
||||
/** Entries skipped for an empty selector, reported by the declaring protocol. */
|
||||
emptySelectors: string[],
|
||||
onTruncated?: () => void,
|
||||
) => {
|
||||
const output: ObjcSwiftMetadata["objc_protocols"][number]["methods"] = [];
|
||||
if (address === 0n) return output;
|
||||
@@ -49,7 +53,10 @@ export const createObjcProtocolReader = (input: {
|
||||
if (stride !== (relative ? 12 : 24))
|
||||
throw new TypeError("Unsupported Objective-C protocol method entry size");
|
||||
for (let index = 0; index < count; index++) {
|
||||
if (!read.admit()) break;
|
||||
if (!read.admit()) {
|
||||
onTruncated?.();
|
||||
break;
|
||||
}
|
||||
const entry = address + 8n + BigInt(index * stride);
|
||||
let selector = relative
|
||||
? entry + BigInt(read.i32(entry))
|
||||
@@ -84,22 +91,54 @@ export const createObjcProtocolReader = (input: {
|
||||
const protocolName = name(address);
|
||||
// Adopted protocols are independent declarations; decode them before
|
||||
// rejecting a parent whose own name is empty.
|
||||
const adopted = list(read.pointer(address + 16n));
|
||||
let budgetTruncated = false;
|
||||
const onTruncated = () => {
|
||||
budgetTruncated = true;
|
||||
};
|
||||
const adopted = list(read.pointer(address + 16n), onTruncated);
|
||||
if (protocolName.length === 0)
|
||||
throw new TypeError("Objective-C protocol has an empty name");
|
||||
const skipped: string[] = [];
|
||||
const required = [
|
||||
...methods(read.pointer(address + 24n), true, true, skipped),
|
||||
...methods(read.pointer(address + 32n), true, false, skipped),
|
||||
...methods(
|
||||
read.pointer(address + 24n),
|
||||
true,
|
||||
true,
|
||||
skipped,
|
||||
onTruncated,
|
||||
),
|
||||
...methods(
|
||||
read.pointer(address + 32n),
|
||||
true,
|
||||
false,
|
||||
skipped,
|
||||
onTruncated,
|
||||
),
|
||||
];
|
||||
const optional = [
|
||||
...methods(read.pointer(address + 40n), false, true, skipped),
|
||||
...methods(read.pointer(address + 48n), false, false, skipped),
|
||||
...methods(
|
||||
read.pointer(address + 40n),
|
||||
false,
|
||||
true,
|
||||
skipped,
|
||||
onTruncated,
|
||||
),
|
||||
...methods(
|
||||
read.pointer(address + 48n),
|
||||
false,
|
||||
false,
|
||||
skipped,
|
||||
onTruncated,
|
||||
),
|
||||
];
|
||||
for (const message of skipped)
|
||||
failures.push(
|
||||
`Protocol 0x${address.toString(16)}: Objective-C protocol ${protocolName} ${message}`,
|
||||
);
|
||||
const decodeReasons = [
|
||||
...(skipped.length === 0 ? [] : ["protocol_method_selector_empty"]),
|
||||
...(budgetTruncated ? ["max_records_reached"] : []),
|
||||
];
|
||||
result.objc_protocols.push({
|
||||
name: protocolName,
|
||||
methods: required,
|
||||
@@ -113,9 +152,9 @@ export const createObjcProtocolReader = (input: {
|
||||
optional_methods: optional,
|
||||
location: read.location(address),
|
||||
decode:
|
||||
skipped.length === 0
|
||||
decodeReasons.length === 0
|
||||
? { status: "decoded", reason: null }
|
||||
: { status: "partial", reason: "protocol_method_selector_empty" },
|
||||
: { status: "partial", reason: decodeReasons.join("; ") },
|
||||
evidence: read.evidence(
|
||||
address,
|
||||
"Objective-C protocol declaration and encoded method lists; IMP fields are not handler targets",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js";
|
||||
|
||||
import { objcRecordBudgetMacho } from "../../fixtures/objcRecordBudgetMacho.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native dispatch declaration-local record budget",
|
||||
() => {
|
||||
cliTest.for([["protocol"], ["class"]] as const)(
|
||||
"marks an omitted $0 method list partial in public CLI output",
|
||||
async ([owner], { cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-objc-budget-");
|
||||
const path = join(directory, "fixture.macho");
|
||||
await writeFile(path, objcRecordBudgetMacho(owner));
|
||||
const output = await cli.run({
|
||||
arguments: [
|
||||
"inspect-native-dispatch-metadata",
|
||||
path,
|
||||
"--max-records",
|
||||
"1",
|
||||
"--json",
|
||||
],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(output.exitCode, `${output.stdout}\n${output.stderr}`).toBe(0);
|
||||
const result = nativeDispatchMetadataResultSchema.parse(
|
||||
parseEvidence(output.json).normalized_result,
|
||||
).result;
|
||||
if (owner === "protocol") {
|
||||
expect(result?.objc_protocol_records).toMatchObject([
|
||||
{
|
||||
name: "FixtureProtocol",
|
||||
methods: [],
|
||||
decode: { status: "partial", reason: "max_records_reached" },
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(result?.objc_classes).toMatchObject([
|
||||
{
|
||||
name: "FixtureClass",
|
||||
methods: [],
|
||||
decode: { status: "partial", reason: "max_records_reached" },
|
||||
},
|
||||
]);
|
||||
}
|
||||
expect(result?.coverage).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
facet: "objc_class_method_ivar_metadata",
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("max_records_reached"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
const base = 0x100000000n;
|
||||
|
||||
/** Minimal inert Mach-O with one class instance method and protocol method. */
|
||||
export const objcRecordBudgetMacho = (owner: "class" | "protocol"): Buffer => {
|
||||
const bytes = Buffer.alloc(4096);
|
||||
const u32 = (offset: number, value: number) =>
|
||||
bytes.writeUInt32LE(value, offset);
|
||||
const pointer = (offset: number, target: number) =>
|
||||
bytes.writeBigUInt64LE(base + BigInt(target), offset);
|
||||
const nullPointer = (offset: number) => bytes.writeBigUInt64LE(0n, offset);
|
||||
|
||||
u32(0, 0xfeedfacf);
|
||||
u32(4, 0x0100000c);
|
||||
u32(12, 2);
|
||||
u32(16, 1);
|
||||
u32(20, owner === "protocol" ? 232 : 152);
|
||||
u32(32, 0x19);
|
||||
u32(36, owner === "protocol" ? 232 : 152);
|
||||
bytes.write("__DATA", 40);
|
||||
pointer(56, 0);
|
||||
bytes.writeBigUInt64LE(4096n, 64);
|
||||
bytes.writeBigUInt64LE(4096n, 80);
|
||||
u32(88, 7);
|
||||
u32(92, 5);
|
||||
u32(96, owner === "protocol" ? 2 : 1);
|
||||
bytes.write("__objc_classlist", 104);
|
||||
bytes.write("__DATA", 120);
|
||||
pointer(136, 0x180);
|
||||
bytes.writeBigUInt64LE(8n, 144);
|
||||
u32(152, 0x180);
|
||||
if (owner === "protocol") {
|
||||
bytes.write("__objc_protolist", 184);
|
||||
bytes.write("__DATA", 200);
|
||||
pointer(216, 0x6c0);
|
||||
bytes.writeBigUInt64LE(8n, 224);
|
||||
u32(232, 0x6c0);
|
||||
pointer(0x6c0, 0x740);
|
||||
}
|
||||
|
||||
pointer(0x180, 0x200);
|
||||
// class_t: zero isa/superclass; class_ro_t is root, named and has one method.
|
||||
nullPointer(0x200);
|
||||
nullPointer(0x208);
|
||||
pointer(0x220, 0x300);
|
||||
u32(0x300, 2);
|
||||
u32(0x308, 8);
|
||||
pointer(0x318, 0x900);
|
||||
pointer(0x320, 0x500);
|
||||
nullPointer(0x328);
|
||||
nullPointer(0x330);
|
||||
nullPointer(0x340);
|
||||
bytes.write("FixtureClass\0", 0x900);
|
||||
u32(0x500, 24);
|
||||
u32(0x504, 1);
|
||||
pointer(0x508, 0x920);
|
||||
pointer(0x510, 0x940);
|
||||
pointer(0x518, 0x700);
|
||||
bytes.write("classMethod\0", 0x920);
|
||||
bytes.write("v@:\0", 0x940);
|
||||
|
||||
// Protocol method lists use the same declaration boundary and budget.
|
||||
pointer(0x6c0, 0x740);
|
||||
pointer(0x748, 0x980);
|
||||
pointer(0x758, 0x800);
|
||||
u32(0x780, 72);
|
||||
u32(0x800, 24);
|
||||
u32(0x804, 1);
|
||||
pointer(0x808, 0x9a0);
|
||||
bytes.write("FixtureProtocol\0", 0x980);
|
||||
bytes.write("protocolMethod\0", 0x9a0);
|
||||
return bytes;
|
||||
};
|
||||
Reference in new issue
Block a user