fix(native): keep a category whose class reference cannot be read (#4093)

An Objective-C category whose nonzero class reference pointed outside
mapped metadata failed as a whole, discarding its readable name, method
selectors and implementations. The category decoder now resolves the
local class name in its own boundary: on failure the category keeps its
facts with class_name null and class_source unresolved, and a
category_class_unresolved issue records the unreadable reference in the
category coverage.

Fixes #4012

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-15 05:21:04 -07:00
1 parent 2c90082a69
commit fc41b24dc4
2 files changed
+54 -5

No files matched your search

+38 -3
View File
@@ -5,7 +5,7 @@ import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
const base = 0x100000000n;
const fixture = (
relative = false,
category?: "partial-method" | "unmapped-pointer",
category?: "partial-method" | "unmapped-pointer" | "unmapped-class",
) => {
const bytes = Buffer.alloc(4096);
const u32 = (offset: number, value: number) =>
@@ -103,13 +103,15 @@ const fixture = (
} else {
ptr(0xb00, 0xb40);
ptr(0xb40, 0xbc0);
ptr(0xb48, 0x200);
if (category === "unmapped-class") ptr(0xb48, 0x5000);
else ptr(0xb48, 0x200);
ptr(0xb50, 0xb80);
u32(0xb80, 24);
u32(0xb84, 1);
ptr(0xb88, 0x460);
ptr(0xb90, 0x5c0);
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0xb98);
if (category === "unmapped-class") ptr(0xb98, 0x700);
else bytes.writeBigUInt64LE(0xffffffffffffffffn, 0xb98);
bytes.write("FixtureCategory\0", 0xbc0);
}
}
@@ -398,6 +400,39 @@ describe("Apple dispatch coverage ownership", () => {
reason: expect.stringContaining("category method FixtureCategory"),
});
});
it("keeps a category whose class reference is unmapped", () => {
const result = decodeAppleDispatchMetadata(
fixture(false, "unmapped-class"),
100,
provenance,
);
expect(result.objc_categories).toMatchObject([
{
name: "FixtureCategory",
class_name: null,
class_source: "unresolved",
decode: { status: "partial", reason: "category_class_unresolved" },
},
]);
expect(result.objc_categories[0]?.instance_methods).toHaveLength(1);
expect(result.objc_dispatch_implementations).toContainEqual(
expect.objectContaining({
category: "FixtureCategory",
class_name: "(FixtureCategory)",
implementation_address: "0x100000700",
}),
);
expect(
result.coverage.find(
({ facet }) => facet === "objc_properties_categories",
),
).toMatchObject({
status: "partial",
reason: expect.stringContaining(
"category_class_unresolved at 0x100000b48: Category FixtureCategory class reference 0x100005000",
),
});
});
it("keeps malformed category pointers out of class failure coverage", () => {
const baseline = decodeAppleDispatchMetadata(fixture(), 100, provenance);
const result = decodeAppleDispatchMetadata(
+16 -2
View File
@@ -52,8 +52,22 @@ export const decodeObjcCategories = (input: {
const name = read.string(read.pointer(category));
const external = boundClassName(read, category + 8n);
const local = read.pointer(category + 8n);
const className =
external ?? (local === 0n ? null : input.localClassName(local));
let className = external ?? null;
if (external === undefined && local !== 0n)
try {
className = input.localClassName(local);
} catch (cause: unknown) {
// The category's own name and methods do not depend on its owner;
// keep them and report the unreadable class reference.
input.issues.push(
issue(
"objc_properties_categories",
"category_class_unresolved",
`Category ${name} class reference 0x${local.toString(16)}: ${cause instanceof Error ? cause.message : String(cause)}`,
`0x${(category + 8n).toString(16)}`,
),
);
}
const owner = className ?? `(${name})`;
result.objc_categories.push({
name,