mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(native): recover malformed Objective-C names at decode boundaries
Validate protocol and implementation selectors, ivar names and category names before publishing records. Share validated local class identity between class, superclass and category consumers, retaining unresolved addresses and the decoder's failure reason. Byte fixtures reproduced the reported validation failures. Final focused 4-file run: 118 passed; npm run check passed. Six existing CLI property controls remain blocked before decoding by deep-provider auto-selection ambiguity, as exposed by improved assertion diagnostics (#4555). No Windows runtime or real malformed-application verification performed. Refs #4540, #4541, #4533, #4534, #4115, #4555
This commit is contained in:
@@ -501,7 +501,151 @@ describe("malformed Objective-C property records", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Objective-C malformed identity recovery", () => {
|
||||
it.each(["class selector", "category name"] as const)(
|
||||
"isolates an empty %s before result validation",
|
||||
(malformed) => {
|
||||
const bytes = fixture(false, "unmapped-class");
|
||||
bytes[malformed === "class selector" ? 0x460 : 0xbc0] = 0;
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_categories).toEqual([]);
|
||||
if (malformed === "category name") {
|
||||
expect(result.objc_classes).toHaveLength(2);
|
||||
expect(result.objc_protocols).toMatchObject([
|
||||
{ name: "FixtureProtocol" },
|
||||
]);
|
||||
}
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_properties_categories",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining(
|
||||
malformed === "class selector" ? "empty selector" : "empty name",
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an unresolved superclass address when its class name is empty", () => {
|
||||
const bytes = fixture();
|
||||
bytes.writeBigUInt64LE(base + 0xc00n, 0x208);
|
||||
bytes.writeBigUInt64LE(base + 0xc40n, 0xc20);
|
||||
bytes.writeBigUInt64LE(base + 0xc80n, 0xc58);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_classes[0]).toMatchObject({
|
||||
name: "Fixture",
|
||||
super_class: null,
|
||||
});
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("Superclass pointer 0x100000c00"),
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a category whose local owner has an empty name", () => {
|
||||
const bytes = fixture(false, "unmapped-class");
|
||||
bytes.writeBigUInt64LE(base + 0xc00n, 0xb48);
|
||||
bytes.writeBigUInt64LE(base + 0xc40n, 0xc20);
|
||||
bytes.writeBigUInt64LE(base + 0xc80n, 0xc58);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_classes.map(({ name }) => name)).toEqual([
|
||||
"Fixture",
|
||||
"Fixture",
|
||||
]);
|
||||
expect(result.objc_categories).toMatchObject([
|
||||
{
|
||||
name: "FixtureCategory",
|
||||
class_name: null,
|
||||
class_source: "unresolved",
|
||||
instance_methods: ["performAction:"],
|
||||
decode: { status: "partial", reason: "category_class_unresolved" },
|
||||
},
|
||||
]);
|
||||
expect(result.objc_dispatch_implementations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
category: "FixtureCategory",
|
||||
class_name: "(FixtureCategory)",
|
||||
selector: "performAction:",
|
||||
implementation_address: "0x100000700",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_properties_categories",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("empty name"),
|
||||
});
|
||||
});
|
||||
|
||||
it.each([0x920, 0x940])(
|
||||
"isolates an empty protocol selector at %i while keeping adopted declarations and classes",
|
||||
(selector) => {
|
||||
const bytes = fixture();
|
||||
bytes[selector] = 0;
|
||||
// Decode the independent adopted declaration before the malformed parent.
|
||||
bytes.writeBigUInt64LE(base + 0xa00n, 0x750);
|
||||
bytes.writeBigUInt64LE(1n, 0xa00);
|
||||
bytes.writeBigUInt64LE(base + 0xa40n, 0xa08);
|
||||
bytes.writeBigUInt64LE(base + 0xac0n, 0xa48);
|
||||
bytes.writeUInt32LE(72, 0xa80);
|
||||
bytes.write("AdoptedProtocol\0", 0xac0);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_classes.map(({ name }) => name)).toEqual([
|
||||
"Fixture",
|
||||
"Fixture",
|
||||
]);
|
||||
expect(result.objc_protocols.map(({ name }) => name)).toEqual([
|
||||
"AdoptedProtocol",
|
||||
]);
|
||||
expect(result.objc_protocol_records.map(({ name }) => name)).toEqual([
|
||||
"AdoptedProtocol",
|
||||
]);
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("empty selector"),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("malformed Apple dispatch metadata", () => {
|
||||
it("retains valid class siblings when one class has an empty ivar name", () => {
|
||||
const bytes = partialSiblingsFixture();
|
||||
bytes.writeBigUInt64LE(base + 0x700n, 0x570);
|
||||
bytes.writeUInt32LE(32, 0x700);
|
||||
bytes.writeUInt32LE(1, 0x704);
|
||||
bytes.writeBigUInt64LE(base + 0x800n, 0x708);
|
||||
bytes.writeBigUInt64LE(base + 0x820n, 0x710);
|
||||
bytes.writeBigUInt64LE(base + 0x840n, 0x718);
|
||||
bytes.write("i\0", 0x840);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_classes.map(({ name }) => name)).toEqual([
|
||||
"KeptClass",
|
||||
"LaterClass",
|
||||
]);
|
||||
expect(result.objc_ivars).toEqual([]);
|
||||
expect(
|
||||
result.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining("empty name"),
|
||||
});
|
||||
});
|
||||
|
||||
it("retains valid class siblings when one class has an empty name", () => {
|
||||
const result = decodeAppleDispatchMetadata(
|
||||
partialSiblingsFixture({ emptyMiddleClass: true }),
|
||||
|
||||
@@ -50,6 +50,8 @@ export const decodeObjcCategories = (input: {
|
||||
try {
|
||||
const category = read.pointer(field);
|
||||
const name = read.string(read.pointer(category));
|
||||
if (name.length === 0)
|
||||
throw new TypeError("Objective-C category has an empty name");
|
||||
const external = boundClassName(read, category + 8n);
|
||||
const local = read.pointer(category + 8n);
|
||||
let className = external ?? null;
|
||||
|
||||
@@ -31,9 +31,16 @@ export interface ObjcDispatchReaders {
|
||||
const hex = (value: bigint) => `0x${value.toString(16)}`;
|
||||
const decoded = { status: "decoded" as const, reason: null };
|
||||
|
||||
/** Name of a class defined in this image, from its class_ro_t. */
|
||||
const localClassName = (read: ObjcDispatchReaders, address: bigint): string =>
|
||||
read.string(read.pointer((read.pointer(address + 32n) & ~7n) + 24n));
|
||||
/** Admit the local class identity once for classes, superclasses and categories. */
|
||||
const readLocalClass = (read: ObjcDispatchReaders, address: bigint) => {
|
||||
const ro = read.pointer(address + 32n) & ~7n;
|
||||
const name = read.string(read.pointer(ro + 24n));
|
||||
if (name.length === 0)
|
||||
throw new TypeError(
|
||||
`Objective-C class at ${hex(address)} has an empty name`,
|
||||
);
|
||||
return { ro, name };
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a class's superclass: a bind to `_OBJC_CLASS_$_Name` names a class
|
||||
@@ -50,12 +57,13 @@ const resolveSuperclass = (
|
||||
const superclass = read.pointer(address + 8n);
|
||||
if (superclass === 0n) return { name: null, address: null };
|
||||
try {
|
||||
return { name: localClassName(read, superclass), address: hex(superclass) };
|
||||
return {
|
||||
name: readLocalClass(read, superclass).name,
|
||||
address: hex(superclass),
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
// Superclass resolution failure is recorded; the cause adds no identity.
|
||||
void cause;
|
||||
failures.push(
|
||||
`Superclass pointer ${hex(superclass)} of ${className} requires unsupported binding/fixup resolution`,
|
||||
`Superclass pointer ${hex(superclass)} of ${className} could not be decoded: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
return { name: null, address: hex(superclass) };
|
||||
}
|
||||
@@ -163,6 +171,8 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
void cause;
|
||||
}
|
||||
const selector = read.string(selectorAddress);
|
||||
if (selector.length === 0)
|
||||
throw new TypeError("Objective-C method has an empty selector");
|
||||
selectors.push(selector);
|
||||
result.objc_dispatch_implementations.push({
|
||||
class_name: className,
|
||||
@@ -260,12 +270,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
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));
|
||||
if (name.length === 0)
|
||||
throw new TypeError(
|
||||
`Objective-C class at ${hex(address)} has an empty name`,
|
||||
);
|
||||
const { ro, name } = readLocalClass(read, address);
|
||||
const root = (read.u32(ro) & 2) !== 0;
|
||||
const superclass = resolveSuperclass(read, address, name, failures);
|
||||
if (superclass.name === null && superclass.address === null && !root)
|
||||
@@ -292,10 +297,13 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
void cause;
|
||||
failures.push(`Unresolved ivar offset pointer ${hex(encodedOffset)}`);
|
||||
}
|
||||
const ivarName = read.string(read.pointer(entry + 8n));
|
||||
if (ivarName.length === 0)
|
||||
throw new TypeError("Objective-C ivar has an empty name");
|
||||
const alignment = read.u32(entry + 24n);
|
||||
result.objc_ivars.push({
|
||||
class_name: name,
|
||||
name: read.string(read.pointer(entry + 8n)),
|
||||
name: ivarName,
|
||||
type_encoding: read.string(read.pointer(entry + 16n)),
|
||||
offset: value,
|
||||
size: read.u32(entry + 28n),
|
||||
@@ -415,7 +423,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
admit: () => budget.admit(),
|
||||
methods,
|
||||
protocols: (list) => protocolReader.list(list),
|
||||
localClassName: (address) => localClassName(read, address),
|
||||
localClassName: (address) => readLocalClass(read, address).name,
|
||||
});
|
||||
return {
|
||||
failures,
|
||||
|
||||
@@ -50,8 +50,13 @@ export const createObjcProtocolReader = (input: {
|
||||
: read.pointer(entry);
|
||||
if (relative && (flags & 0x40000000) === 0)
|
||||
selector = read.pointer(selector);
|
||||
const selectorName = read.string(selector);
|
||||
if (selectorName.length === 0)
|
||||
throw new TypeError(
|
||||
"Objective-C protocol method has an empty selector",
|
||||
);
|
||||
output.push({
|
||||
selector: read.string(selector),
|
||||
selector: selectorName,
|
||||
method_type: instance ? "instance" : "class",
|
||||
address: null,
|
||||
is_required: required,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe.skipIf(process.platform !== "darwin")(
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(output.exitCode, `${output.stdout}\n${output.stderr}`).toBe(0);
|
||||
const property = {
|
||||
atomicity,
|
||||
is_readonly: readonly,
|
||||
@@ -86,7 +86,7 @@ describe.skipIf(process.platform !== "darwin")(
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(output.exitCode, `${output.stdout}\n${output.stderr}`).toBe(0);
|
||||
expect(output.json).toMatchObject({
|
||||
normalized_result: {
|
||||
result: {
|
||||
|
||||
Reference in New Issue
Block a user