From 9eec57b469087fe9bff3d29dcc02f47074159e91 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:15:05 +0800 Subject: [PATCH] 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. --- src/native/AppleDispatchMetadata.test.ts | 182 ++++++++++++++++++ src/native/AppleObjcCategories.ts | 69 ++++--- src/native/AppleObjcDispatchFacets.ts | 54 ++++-- src/native/AppleObjcProperties.ts | 8 +- src/native/AppleObjcProtocols.ts | 59 +++++- .../cli/nativeDispatchRecordBudget.test.ts | 64 ++++++ tests/fixtures/objcRecordBudgetMacho.ts | 72 +++++++ 7 files changed, 460 insertions(+), 48 deletions(-) create mode 100644 tests/boundary/cli/nativeDispatchRecordBudget.test.ts create mode 100644 tests/fixtures/objcRecordBudgetMacho.ts diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index b267b89638..3a51c8a1f2 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -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"], diff --git a/src/native/AppleObjcCategories.ts b/src/native/AppleObjcCategories.ts index f81369d4dc..994f34b5d4 100644 --- a/src/native/AppleObjcCategories.ts +++ b/src/native/AppleObjcCategories.ts @@ -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) { diff --git a/src/native/AppleObjcDispatchFacets.ts b/src/native/AppleObjcDispatchFacets.ts index 089d1906b1..5f507336e8 100644 --- a/src/native/AppleObjcDispatchFacets.ts +++ b/src/native/AppleObjcDispatchFacets.ts @@ -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 { diff --git a/src/native/AppleObjcProperties.ts b/src/native/AppleObjcProperties.ts index 266d5ead62..4c820c72f0 100644 --- a/src/native/AppleObjcProperties.ts +++ b/src/native/AppleObjcProperties.ts @@ -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( diff --git a/src/native/AppleObjcProtocols.ts b/src/native/AppleObjcProtocols.ts index 1d130b8685..13bc76f8d2 100644 --- a/src/native/AppleObjcProtocols.ts +++ b/src/native/AppleObjcProtocols.ts @@ -18,7 +18,7 @@ export const createObjcProtocolReader = (input: { const { readers: read, result, failures } = input; const visited = new Set(); 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", diff --git a/tests/boundary/cli/nativeDispatchRecordBudget.test.ts b/tests/boundary/cli/nativeDispatchRecordBudget.test.ts new file mode 100644 index 0000000000..2d8b521226 --- /dev/null +++ b/tests/boundary/cli/nativeDispatchRecordBudget.test.ts @@ -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"), + }), + ]), + ); + }, + ); + }, +); diff --git a/tests/fixtures/objcRecordBudgetMacho.ts b/tests/fixtures/objcRecordBudgetMacho.ts new file mode 100644 index 0000000000..53329226ea --- /dev/null +++ b/tests/fixtures/objcRecordBudgetMacho.ts @@ -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; +};