Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23ec9931ba |
No files matched your search
@@ -1193,3 +1193,44 @@ describe("malformed Objective-C protocol method lists", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Objective-C method lists cut by the record budget", () => {
|
||||||
|
it("never reports a declaration decoded while the budget omitted its methods", () => {
|
||||||
|
const complete = decodeAppleDispatchMetadata(fixture(), 100, provenance);
|
||||||
|
const classKey = (item: { name: string; is_meta_class: boolean | null }) =>
|
||||||
|
`${item.name}:${String(item.is_meta_class)}`;
|
||||||
|
const completeProtocols = new Map(
|
||||||
|
complete.objc_protocol_records.map((item) => [item.name, item]),
|
||||||
|
);
|
||||||
|
const completeClasses = new Map(
|
||||||
|
complete.objc_classes.map((item) => [classKey(item), item]),
|
||||||
|
);
|
||||||
|
const cutReasons = new Set<string>();
|
||||||
|
for (let budget = 1; budget < 40; budget++) {
|
||||||
|
const result = decodeAppleDispatchMetadata(fixture(), budget, provenance);
|
||||||
|
for (const record of result.objc_protocol_records) {
|
||||||
|
const full = completeProtocols.get(record.name);
|
||||||
|
if (record.decode.status === "decoded") {
|
||||||
|
expect(record.methods).toEqual(full?.methods);
|
||||||
|
expect(record.optional_methods).toEqual(full?.optional_methods);
|
||||||
|
} else if (record.decode.reason !== null)
|
||||||
|
cutReasons.add(`protocol:${record.decode.reason}`);
|
||||||
|
}
|
||||||
|
for (const record of result.objc_classes) {
|
||||||
|
const full = completeClasses.get(classKey(record));
|
||||||
|
if (record.decode?.status === "decoded")
|
||||||
|
expect(record.methods).toEqual(full?.methods);
|
||||||
|
else if (record.decode?.reason != null)
|
||||||
|
cutReasons.add(`class:${record.decode.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The fixture has methods on both declaration kinds, so some budget must
|
||||||
|
// admit each declaration but stop inside its method list.
|
||||||
|
expect([...cutReasons]).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"protocol:method_list_record_budget_exhausted",
|
||||||
|
"class:method_list_record_budget_exhausted",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@ export const decodeObjcCategories = (input: {
|
|||||||
className: string,
|
className: string,
|
||||||
methodType: "instance" | "class",
|
methodType: "instance" | "class",
|
||||||
category: string,
|
category: string,
|
||||||
) => string[];
|
) => { selectors: string[]; budgetExhausted: boolean };
|
||||||
readonly protocols: (list: bigint) => string[];
|
readonly protocols: (list: bigint) => string[];
|
||||||
/** Name of a class defined in this image, from its class_ro_t. */
|
/** Name of a class defined in this image, from its class_ro_t. */
|
||||||
readonly localClassName: (address: bigint) => string;
|
readonly localClassName: (address: bigint) => string;
|
||||||
@@ -71,6 +71,18 @@ export const decodeObjcCategories = (input: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const owner = className ?? `(${name})`;
|
const owner = className ?? `(${name})`;
|
||||||
|
const instanceMethods = input.methods(
|
||||||
|
read.pointer(category + 16n),
|
||||||
|
owner,
|
||||||
|
"instance",
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
const classMethods = input.methods(
|
||||||
|
read.pointer(category + 24n),
|
||||||
|
owner,
|
||||||
|
"class",
|
||||||
|
name,
|
||||||
|
);
|
||||||
result.objc_categories.push({
|
result.objc_categories.push({
|
||||||
name,
|
name,
|
||||||
class_name: className,
|
class_name: className,
|
||||||
@@ -80,18 +92,8 @@ export const decodeObjcCategories = (input: {
|
|||||||
: className === null
|
: className === null
|
||||||
? "unresolved"
|
? "unresolved"
|
||||||
: "local",
|
: "local",
|
||||||
instance_methods: input.methods(
|
instance_methods: instanceMethods.selectors,
|
||||||
read.pointer(category + 16n),
|
class_methods: classMethods.selectors,
|
||||||
owner,
|
|
||||||
"instance",
|
|
||||||
name,
|
|
||||||
),
|
|
||||||
class_methods: input.methods(
|
|
||||||
read.pointer(category + 24n),
|
|
||||||
owner,
|
|
||||||
"class",
|
|
||||||
name,
|
|
||||||
),
|
|
||||||
protocols: input.protocols(read.pointer(category + 32n)),
|
protocols: input.protocols(read.pointer(category + 32n)),
|
||||||
properties: readObjcPropertiesOf(read, read.pointer(category + 40n), {
|
properties: readObjcPropertiesOf(read, read.pointer(category + 40n), {
|
||||||
owner: `category ${name}`,
|
owner: `category ${name}`,
|
||||||
@@ -101,9 +103,14 @@ export const decodeObjcCategories = (input: {
|
|||||||
}),
|
}),
|
||||||
location: read.location(category),
|
location: read.location(category),
|
||||||
decode:
|
decode:
|
||||||
className === null
|
instanceMethods.budgetExhausted || classMethods.budgetExhausted
|
||||||
? { status: "partial", reason: "category_class_unresolved" }
|
? {
|
||||||
: { status: "decoded", reason: null },
|
status: "partial",
|
||||||
|
reason: "method_list_record_budget_exhausted",
|
||||||
|
}
|
||||||
|
: className === null
|
||||||
|
? { status: "partial", reason: "category_class_unresolved" }
|
||||||
|
: { status: "decoded", reason: null },
|
||||||
evidence: read.evidence(category, "Objective-C category_t record"),
|
evidence: read.evidence(category, "Objective-C category_t record"),
|
||||||
});
|
});
|
||||||
} catch (cause: unknown) {
|
} catch (cause: unknown) {
|
||||||
|
|||||||
@@ -103,9 +103,9 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
className: string,
|
className: string,
|
||||||
methodType: "instance" | "class",
|
methodType: "instance" | "class",
|
||||||
category?: string,
|
category?: string,
|
||||||
): string[] => {
|
): { selectors: string[]; budgetExhausted: boolean } => {
|
||||||
const selectors: string[] = [];
|
const selectors: string[] = [];
|
||||||
if (list === 0n) return selectors;
|
if (list === 0n) return { selectors, budgetExhausted: false };
|
||||||
const flags = read.u32(list),
|
const flags = read.u32(list),
|
||||||
count = read.u32(list + 4n),
|
count = read.u32(list + 4n),
|
||||||
relative = (flags & 0x80000000) !== 0;
|
relative = (flags & 0x80000000) !== 0;
|
||||||
@@ -113,7 +113,7 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
if (stride !== (relative ? 12 : 24))
|
if (stride !== (relative ? 12 : 24))
|
||||||
throw new TypeError(`Unsupported method stride/flags at ${hex(list)}`);
|
throw new TypeError(`Unsupported method stride/flags at ${hex(list)}`);
|
||||||
for (let index = 0; index < count; index++) {
|
for (let index = 0; index < count; index++) {
|
||||||
if (!budget.admit()) break;
|
if (!budget.admit()) return { selectors, budgetExhausted: true };
|
||||||
const entry = list + 8n + BigInt(index * stride);
|
const entry = list + 8n + BigInt(index * stride);
|
||||||
const rel = (field: bigint) => {
|
const rel = (field: bigint) => {
|
||||||
const displacement = bytes.readInt32LE(read.offset(field, 4));
|
const displacement = bytes.readInt32LE(read.offset(field, 4));
|
||||||
@@ -196,7 +196,7 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return selectors;
|
return { selectors, budgetExhausted: false };
|
||||||
};
|
};
|
||||||
const protocolReader = createObjcProtocolReader({
|
const protocolReader = createObjcProtocolReader({
|
||||||
readers: {
|
readers: {
|
||||||
@@ -326,7 +326,11 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
methods(read.pointer(ro + 32n), name, meta ? "class" : "instance");
|
const { budgetExhausted } = methods(
|
||||||
|
read.pointer(ro + 32n),
|
||||||
|
name,
|
||||||
|
meta ? "class" : "instance",
|
||||||
|
);
|
||||||
result.objc_classes.push({
|
result.objc_classes.push({
|
||||||
name,
|
name,
|
||||||
super_class: superclass.name,
|
super_class: superclass.name,
|
||||||
@@ -362,8 +366,9 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
location: read.location(address),
|
location: read.location(address),
|
||||||
superclass_address: superclass.address,
|
superclass_address: superclass.address,
|
||||||
metaclass_address: meta ? null : hex(read.pointer(address)),
|
metaclass_address: meta ? null : hex(read.pointer(address)),
|
||||||
decode:
|
decode: budgetExhausted
|
||||||
superclass.name === null && !root
|
? { status: "partial", reason: "method_list_record_budget_exhausted" }
|
||||||
|
: superclass.name === null && !root
|
||||||
? { status: "partial", reason: "superclass_binding_unresolved" }
|
? { status: "partial", reason: "superclass_binding_unresolved" }
|
||||||
: decoded,
|
: decoded,
|
||||||
evidence: read.evidence(
|
evidence: read.evidence(
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const createObjcProtocolReader = (input: {
|
|||||||
instance: boolean,
|
instance: boolean,
|
||||||
/** Entries skipped for an empty selector, reported by the declaring protocol. */
|
/** Entries skipped for an empty selector, reported by the declaring protocol. */
|
||||||
emptySelectors: string[],
|
emptySelectors: string[],
|
||||||
|
/** Set when the record budget stops the list before its last entry. */
|
||||||
|
budget: { exhausted: boolean },
|
||||||
) => {
|
) => {
|
||||||
const output: ObjcSwiftMetadata["objc_protocols"][number]["methods"] = [];
|
const output: ObjcSwiftMetadata["objc_protocols"][number]["methods"] = [];
|
||||||
if (address === 0n) return output;
|
if (address === 0n) return output;
|
||||||
@@ -49,7 +51,10 @@ export const createObjcProtocolReader = (input: {
|
|||||||
if (stride !== (relative ? 12 : 24))
|
if (stride !== (relative ? 12 : 24))
|
||||||
throw new TypeError("Unsupported Objective-C protocol method entry size");
|
throw new TypeError("Unsupported Objective-C protocol method entry size");
|
||||||
for (let index = 0; index < count; index++) {
|
for (let index = 0; index < count; index++) {
|
||||||
if (!read.admit()) break;
|
if (!read.admit()) {
|
||||||
|
budget.exhausted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const entry = address + 8n + BigInt(index * stride);
|
const entry = address + 8n + BigInt(index * stride);
|
||||||
let selector = relative
|
let selector = relative
|
||||||
? entry + BigInt(read.i32(entry))
|
? entry + BigInt(read.i32(entry))
|
||||||
@@ -88,13 +93,14 @@ export const createObjcProtocolReader = (input: {
|
|||||||
if (protocolName.length === 0)
|
if (protocolName.length === 0)
|
||||||
throw new TypeError("Objective-C protocol has an empty name");
|
throw new TypeError("Objective-C protocol has an empty name");
|
||||||
const skipped: string[] = [];
|
const skipped: string[] = [];
|
||||||
|
const budget = { exhausted: false };
|
||||||
const required = [
|
const required = [
|
||||||
...methods(read.pointer(address + 24n), true, true, skipped),
|
...methods(read.pointer(address + 24n), true, true, skipped, budget),
|
||||||
...methods(read.pointer(address + 32n), true, false, skipped),
|
...methods(read.pointer(address + 32n), true, false, skipped, budget),
|
||||||
];
|
];
|
||||||
const optional = [
|
const optional = [
|
||||||
...methods(read.pointer(address + 40n), false, true, skipped),
|
...methods(read.pointer(address + 40n), false, true, skipped, budget),
|
||||||
...methods(read.pointer(address + 48n), false, false, skipped),
|
...methods(read.pointer(address + 48n), false, false, skipped, budget),
|
||||||
];
|
];
|
||||||
for (const message of skipped)
|
for (const message of skipped)
|
||||||
failures.push(
|
failures.push(
|
||||||
@@ -112,8 +118,11 @@ export const createObjcProtocolReader = (input: {
|
|||||||
methods: required,
|
methods: required,
|
||||||
optional_methods: optional,
|
optional_methods: optional,
|
||||||
location: read.location(address),
|
location: read.location(address),
|
||||||
decode:
|
// A budget-cut list is recoverable with a larger budget, so that
|
||||||
skipped.length === 0
|
// cause is reported first; an empty list is otherwise complete.
|
||||||
|
decode: budget.exhausted
|
||||||
|
? { status: "partial", reason: "method_list_record_budget_exhausted" }
|
||||||
|
: skipped.length === 0
|
||||||
? { status: "decoded", reason: null }
|
? { status: "decoded", reason: null }
|
||||||
: { status: "partial", reason: "protocol_method_selector_empty" },
|
: { status: "partial", reason: "protocol_method_selector_empty" },
|
||||||
evidence: read.evidence(
|
evidence: read.evidence(
|
||||||
|
|||||||
Reference in new issue
Block a user