Files
reverse-engineering-anything/src/native/AppleObjcProtocols.ts
T
d5be402fb3 fix(objc): isolate empty protocol selectors and category class names (#4549)
* fix(objc): isolate empty Objective-C protocol method selectors

A protocol method_list_t entry whose selector reached an empty string was
appended to objc_protocols and objc_protocol_records unchecked. The final
metadata schema then rejected the whole result, discarding every valid
class, protocol and category.

The protocol method decoder now skips only that entry and reports it as
a protocol failure. The protocol keeps its other method declarations, and
its record is marked partial with protocol_method_selector_empty.

Fixes #4540

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

* fix(objc): treat empty local category class names as unresolved

A category whose class reference pointed at a mapped local class with an
empty class_ro_t name took that empty string as class_name. The final
metadata schema then rejected the whole result, discarding every valid
class, protocol and category.

An empty local class name now fails the class reference the same way an
unmapped one does: the category is kept with class_name null,
class_source unresolved and a category_class_unresolved issue, and its
name, methods and protocols survive.

Fixes #4541

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-17 06:43:12 +08:00

132 lines
4.9 KiB
TypeScript

import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
import type { SwiftMetadataReaders } from "./AppleSwiftVtables.js";
/** Read protocol lists and absolute protocol method declarations with cycle-safe identities. */
export const createObjcProtocolReader = (input: {
readers: Pick<
SwiftMetadataReaders,
"u32" | "string" | "location" | "evidence" | "admit"
> & {
pointer(address: bigint): bigint;
/** Plain 64-bit data, never a fixup. */
u64(address: bigint): bigint;
i32(address: bigint): number;
};
result: ObjcSwiftMetadata;
failures: string[];
}) => {
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[] => {
if (address === 0n) return [];
// `count` is plain uintptr_t data, not a pointer fixup.
const count = read.u64(address);
if (count > 20000n)
throw new RangeError("Protocol list exceeds 20000 entries");
const names: string[] = [];
for (let index = 0n; index < count; index++) {
if (!read.admit()) break;
const protocol = read.pointer(address + 8n + index * 8n);
names.push(name(protocol));
record(protocol);
}
return names;
};
const methods = (
address: bigint,
required: boolean,
instance: boolean,
/** Entries skipped for an empty selector, reported by the declaring protocol. */
emptySelectors: string[],
) => {
const output: ObjcSwiftMetadata["objc_protocols"][number]["methods"] = [];
if (address === 0n) return output;
const flags = read.u32(address),
count = read.u32(address + 4n);
const relative = (flags & 0x80000000) !== 0;
const stride = flags & 0xfffc;
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;
const entry = address + 8n + BigInt(index * stride);
let selector = relative
? entry + BigInt(read.i32(entry))
: read.pointer(entry);
if (relative && (flags & 0x40000000) === 0)
selector = read.pointer(selector);
// One empty selector is reported on its own, so the protocol and its
// other method declarations keep their decoded records.
const selectorName = read.string(selector);
if (selectorName.length === 0) {
emptySelectors.push(
`${required ? "required" : "optional"} ${instance ? "instance" : "class"} method ${String(index)} at 0x${entry.toString(16)} has an empty selector`,
);
continue;
}
output.push({
selector: selectorName,
method_type: instance ? "instance" : "class",
address: null,
is_required: required,
is_optional: !required,
});
}
return output;
};
const record = (address: bigint) => {
if (address === 0n || visited.has(address) || !read.admit()) return;
visited.add(address);
try {
if (read.u32(address + 64n) < 72)
throw new RangeError("Truncated Objective-C protocol declaration");
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));
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),
];
const optional = [
...methods(read.pointer(address + 40n), false, true, skipped),
...methods(read.pointer(address + 48n), false, false, skipped),
];
for (const message of skipped)
failures.push(
`Protocol 0x${address.toString(16)}: Objective-C protocol ${protocolName} ${message}`,
);
result.objc_protocols.push({
name: protocolName,
methods: required,
optional_methods: optional,
properties: [],
});
result.objc_protocol_records.push({
name: protocolName,
adopted_protocols: adopted,
methods: required,
optional_methods: optional,
location: read.location(address),
decode:
skipped.length === 0
? { status: "decoded", reason: null }
: { status: "partial", reason: "protocol_method_selector_empty" },
evidence: read.evidence(
address,
"Objective-C protocol declaration and encoded method lists; IMP fields are not handler targets",
),
});
} catch (cause) {
failures.push(
`Protocol 0x${address.toString(16)}: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
};
return { list, record };
};