From 3a29b6b8eac4d958ade8cffce3f7d53fcf7052b1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 10:11:29 +0800 Subject: [PATCH] refactor(il2cpp): report format failures through ParseFailure (#10701) --- .../verify/architecture-guard-baselines.json | 2 - src/il2cpp/Il2cppBinaryFormat.ts | 63 +++++++++++++++---- src/il2cpp/Il2cppBinaryGenerics.ts | 2 +- src/il2cpp/Il2cppBinaryImage.ts | 13 +++- src/il2cpp/Il2cppBinaryRegistration.ts | 8 ++- src/il2cpp/Il2cppBinaryTypes.ts | 2 + src/il2cpp/Il2cppMetadataFormat.ts | 49 +++++++++++---- src/il2cpp/Il2cppMetadataProvider.ts | 2 +- .../metadata/ElfPointerRelocations.test.ts | 1 + src/native/metadata/ElfPointerRelocations.ts | 22 ++++++- .../boundary/il2cpp/il2cppBinaryFile.test.ts | 20 ++++-- .../il2cpp/il2cppMetadataFile.test.ts | 8 ++- 12 files changed, 150 insertions(+), 42 deletions(-) diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index c6ad7dd075..84cb88d11a 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -188,8 +188,6 @@ "src/browser/assets/WebArtifactReads.ts": 1, "src/browser/history/CaptureFormatError.ts": 1, "src/domain/xmlDocument.ts": 1, - "src/il2cpp/Il2cppBinaryFormat.ts": 1, - "src/il2cpp/Il2cppMetadataFormat.ts": 1, "src/native/AppleMachoSelection.ts": 1, "src/native/MachoCommandNames.ts": 1, "src/python/PythonPyc.errors.ts": 1 diff --git a/src/il2cpp/Il2cppBinaryFormat.ts b/src/il2cpp/Il2cppBinaryFormat.ts index 07d70f5176..da242590f4 100644 --- a/src/il2cpp/Il2cppBinaryFormat.ts +++ b/src/il2cpp/Il2cppBinaryFormat.ts @@ -1,3 +1,5 @@ +import { ParseFailureError } from "../domain/parseFailure.js"; + /** Existing selected-binary allowance also bounds its retained pointer workspace. */ export const IL2CPP_BINARY_BYTES = 1024 * 1024 * 1024; @@ -29,24 +31,59 @@ export const IL2CPP_BINARY_FAILURE_REASONS = { export type Il2cppBinaryFailureReason = (typeof IL2CPP_BINARY_FAILURE_REASONS)[keyof typeof IL2CPP_BINARY_FAILURE_REASONS][number]; +/** A named workspace budget; the parse stopped without showing the input invalid. */ +export type Il2cppBinaryCapacityReason = + (typeof IL2CPP_BINARY_FAILURE_REASONS.capacity)[number]; +type Il2cppBinaryFormatReason = Exclude< + Il2cppBinaryFailureReason, + Il2cppBinaryCapacityReason +>; -/** A native image REA cannot link, either outside coverage or inconsistent with the metadata. */ -export class Il2cppBinaryFormatFailure extends Error { - readonly kind: "unsupported" | "malformed" | "capacity"; +/** The ParseFailure format of a native image linked against IL2CPP metadata. */ +export const IL2CPP_BINARY_FORMAT = "il2cpp-binary"; + +const isUnsupported = (reason: Il2cppBinaryFormatReason) => + (IL2CPP_BINARY_FAILURE_REASONS.unsupported as readonly string[]).includes( + reason, + ); + +/** + * A native image REA cannot link, either outside coverage or inconsistent + * with the metadata. The stable `reason` names the IL2CPP rule; a capacity + * reason is a ParseFailure `limit` carrying the exceeded maximum. + */ +export class Il2cppBinaryFormatFailure extends ParseFailureError< + "malformed" | "unsupported" | "limit" +> { + constructor(reason: Il2cppBinaryFormatReason, message: string); + constructor( + reason: Il2cppBinaryCapacityReason, + message: string, + maximum: number, + ); constructor( readonly reason: Il2cppBinaryFailureReason, message: string, + maximum?: number, ) { - super(`${reason}: ${message}`); + const detail = `${reason}: ${message}`; + super( + maximum !== undefined + ? { + kind: "limit", + format: IL2CPP_BINARY_FORMAT, + detail, + limit: { name: reason, maximum }, + } + : isUnsupported(reason as Il2cppBinaryFormatReason) + ? { + kind: "unsupported", + format: IL2CPP_BINARY_FORMAT, + detail, + undecodedContent: false, + } + : { kind: "malformed", format: IL2CPP_BINARY_FORMAT, detail }, + ); this.name = "Il2cppBinaryFormatFailure"; - this.kind = ( - IL2CPP_BINARY_FAILURE_REASONS.unsupported as readonly string[] - ).includes(reason) - ? "unsupported" - : (IL2CPP_BINARY_FAILURE_REASONS.capacity as readonly string[]).includes( - reason, - ) - ? "capacity" - : "malformed"; } } diff --git a/src/il2cpp/Il2cppBinaryGenerics.ts b/src/il2cpp/Il2cppBinaryGenerics.ts index bf3b2582da..ef3c31e243 100644 --- a/src/il2cpp/Il2cppBinaryGenerics.ts +++ b/src/il2cpp/Il2cppBinaryGenerics.ts @@ -366,7 +366,7 @@ export function* linkGenericMethodsSteps( // Capacity is a budget refusal, never a fact about this row. if ( !(cause instanceof Il2cppBinaryFormatFailure) || - cause.kind === "capacity" + cause.kind === "limit" ) throw cause; issue(row, specAt, cause.message); diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts index bf7a46e61e..d834e3c2a9 100644 --- a/src/il2cpp/Il2cppBinaryImage.ts +++ b/src/il2cpp/Il2cppBinaryImage.ts @@ -93,6 +93,7 @@ export function* openImageSteps(image: Buffer): Generator { throw new Il2cppBinaryFormatFailure( "native-index-capacity", cause.message, + cause.failure.limit.maximum, ); if (cause instanceof BinaryFormatFailure) throw new Il2cppBinaryFormatFailure( @@ -124,8 +125,16 @@ export function* openImageSteps(image: Buffer): Generator { IL2CPP_BINARY_BYTES, ); } catch (cause) { - if (cause instanceof ElfPointerRelocationFailure) - throw new Il2cppBinaryFormatFailure(cause.reason, cause.message); + if (cause instanceof ElfPointerRelocationFailure) { + if (cause.reason !== "native-index-capacity") + throw new Il2cppBinaryFormatFailure(cause.reason, cause.message); + if (cause.maximum !== undefined) + throw new Il2cppBinaryFormatFailure( + cause.reason, + cause.message, + cause.maximum, + ); + } if (cause instanceof BinaryFormatFailure) throw new Il2cppBinaryFormatFailure("malformed-container", cause.message); throw cause; diff --git a/src/il2cpp/Il2cppBinaryRegistration.ts b/src/il2cpp/Il2cppBinaryRegistration.ts index cacec8df0f..ea82038c3e 100644 --- a/src/il2cpp/Il2cppBinaryRegistration.ts +++ b/src/il2cpp/Il2cppBinaryRegistration.ts @@ -111,7 +111,8 @@ function* nameAddresses( if (Buffer.byteLength(name) > TOTAL_NAME_OCCURRENCES) throw new Il2cppBinaryFormatFailure( "native-index-capacity", - "module-name trie exceeds its65536-node workspace", + `module-name trie exceeds its ${TOTAL_NAME_OCCURRENCES}-node workspace`, + TOTAL_NAME_OCCURRENCES, ); let node = root; const bytes = Buffer.from(name, "utf8"); @@ -122,7 +123,8 @@ function* nameAddresses( if (nodes >= TOTAL_NAME_OCCURRENCES) throw new Il2cppBinaryFormatFailure( "native-index-capacity", - "module-name trie exceeds its65536-node workspace", + `module-name trie exceeds its ${TOTAL_NAME_OCCURRENCES}-node workspace`, + TOTAL_NAME_OCCURRENCES, ); next = { next: new Map(), owners: [] }; node.next.set(bytes[at]!, next); @@ -162,6 +164,7 @@ function* nameAddresses( throw new Il2cppBinaryFormatFailure( "native-index-capacity", `native name-address index exceeds ${TOTAL_NAME_OCCURRENCES} entries`, + TOTAL_NAME_OCCURRENCES, ); found.set(address, node.owners); } @@ -531,6 +534,7 @@ function* findModuleArrayByCode( throw new Il2cppBinaryFormatFailure( "native-index-capacity", `module-array index exceeds ${MODULE_ARRAY_CANDIDATES} entries`, + MODULE_ARRAY_CANDIDATES, ); } } diff --git a/src/il2cpp/Il2cppBinaryTypes.ts b/src/il2cpp/Il2cppBinaryTypes.ts index 57af891a6f..63826fb2b3 100644 --- a/src/il2cpp/Il2cppBinaryTypes.ts +++ b/src/il2cpp/Il2cppBinaryTypes.ts @@ -106,6 +106,7 @@ const createTypeRenderer = ( throw new Il2cppBinaryFormatFailure( "native-type-text-capacity", `rendered type text workspace exceeds ${TYPE_TEXT_BYTES} UTF-8 bytes`, + TYPE_TEXT_BYTES, ); beforeText?.(bytes); }; @@ -131,6 +132,7 @@ const createTypeRenderer = ( throw new Il2cppBinaryFormatFailure( "type-nesting-capacity", `type rendering exceeds ${TYPE_DEPTH} nested frames`, + TYPE_DEPTH, ); active.add(address); try { diff --git a/src/il2cpp/Il2cppMetadataFormat.ts b/src/il2cpp/Il2cppMetadataFormat.ts index 290e9c4eeb..897c6fcb58 100644 --- a/src/il2cpp/Il2cppMetadataFormat.ts +++ b/src/il2cpp/Il2cppMetadataFormat.ts @@ -15,26 +15,47 @@ export const IL2CPP_METADATA_FAILURE_REASONS = { "invalid-string", ], } as const; -export type Il2cppMetadataFailureKind = - keyof typeof IL2CPP_METADATA_FAILURE_REASONS; export type Il2cppMetadataFailureReason = - (typeof IL2CPP_METADATA_FAILURE_REASONS)[Il2cppMetadataFailureKind][number]; + (typeof IL2CPP_METADATA_FAILURE_REASONS)[keyof typeof IL2CPP_METADATA_FAILURE_REASONS][number]; -/** Distinguish inputs outside decoder coverage from damaged metadata. */ -export class Il2cppMetadataFormatFailure extends Error { - readonly kind: Il2cppMetadataFailureKind; +/** The ParseFailure format of an IL2CPP `global-metadata.dat`. */ +export const IL2CPP_METADATA_FORMAT = "il2cpp-metadata"; + +const metadataFailure = ( + reason: Il2cppMetadataFailureReason, + detail: string, +): ParseFailure & { + readonly kind: "malformed" | "truncated" | "unsupported"; +} => + (IL2CPP_METADATA_FAILURE_REASONS.unsupported as readonly string[]).includes( + reason, + ) + ? { + kind: "unsupported", + format: IL2CPP_METADATA_FORMAT, + detail, + undecodedContent: false, + } + : { + kind: reason === "truncated-header" ? "truncated" : "malformed", + format: IL2CPP_METADATA_FORMAT, + detail, + }; + +/** + * Distinguish inputs outside decoder coverage from damaged metadata. The + * stable `reason` names the IL2CPP rule the bytes broke. + */ +export class Il2cppMetadataFormatFailure extends ParseFailureError< + "malformed" | "truncated" | "unsupported" +> { constructor( readonly reason: Il2cppMetadataFailureReason, message: string, options?: ErrorOptions, ) { - super(`${reason}: ${message}`, options); + super(metadataFailure(reason, `${reason}: ${message}`), options); this.name = "Il2cppMetadataFormatFailure"; - this.kind = ( - IL2CPP_METADATA_FAILURE_REASONS.unsupported as readonly string[] - ).includes(reason) - ? "unsupported" - : "corrupt"; } } @@ -84,3 +105,7 @@ export const readIndex = (bytes: Buffer, offset: number, width: 1 | 2 | 4) => { return bytes.readInt32LE(offset); }; import type { Il2cppMetadataSection } from "../domain/il2cpp/il2cppMetadata.js"; +import { + ParseFailureError, + type ParseFailure, +} from "../domain/parseFailure.js"; diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index 3933611e54..17984e092a 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -83,7 +83,7 @@ const failureForPrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if (cause instanceof Il2cppBinaryFormatFailure && cause.kind === "capacity") + if (cause instanceof Il2cppBinaryFormatFailure && cause.kind === "limit") return new AnalysisResourceConstraintError( OPERATION, cause.reason === "native-type-text-capacity" ? "memory" : "capacity", diff --git a/src/native/metadata/ElfPointerRelocations.test.ts b/src/native/metadata/ElfPointerRelocations.test.ts index e88bc10035..fcf2692e8d 100644 --- a/src/native/metadata/ElfPointerRelocations.test.ts +++ b/src/native/metadata/ElfPointerRelocations.test.ts @@ -397,6 +397,7 @@ it("refuses pointer workspace growth before inserting the next virtual destinati expect(() => pointerValues(relativeValues(), 4096)).toThrow( expect.objectContaining({ reason: "native-index-capacity", + maximum: 4096, message: expect.stringContaining("4096 estimated representation bytes"), }), ); diff --git a/src/native/metadata/ElfPointerRelocations.ts b/src/native/metadata/ElfPointerRelocations.ts index b0e557c5a0..ff351a3556 100644 --- a/src/native/metadata/ElfPointerRelocations.ts +++ b/src/native/metadata/ElfPointerRelocations.ts @@ -14,6 +14,20 @@ import { /** Shared loader interpretation; language adapters choose their public errors. */ export class ElfPointerRelocationFailure extends Error { + /** The exceeded workspace bound, set only for `native-index-capacity`. */ + readonly maximum: number | undefined; + constructor( + reason: + | "malformed-container" + | "unsupported-container" + | "unsupported-pointer-encoding", + message: string, + ); + constructor( + reason: "native-index-capacity", + message: string, + maximum: number, + ); constructor( readonly reason: | "malformed-container" @@ -21,8 +35,10 @@ export class ElfPointerRelocationFailure extends Error { | "unsupported-pointer-encoding" | "native-index-capacity", message: string, + maximum?: number, ) { super(message); + this.maximum = maximum; } } export interface ElfPointerRelocationSource { @@ -252,6 +268,7 @@ function* dynamicTags( throw new ElfPointerRelocationFailure( "native-index-capacity", "ELF dynamic directory exceeds the 16 MiB structural allowance", + 16 * 1024 * 1024, ); if ( declaration.size % BigInt(width) !== 0n || @@ -659,6 +676,7 @@ class ElfVirtualWriteIndex { throw new ElfPointerRelocationFailure( "native-index-capacity", `ELF virtual pointer index exceeds ${this.maximumBytes} estimated representation bytes (${ELF_POINTER_INDEX_ENTRY_BYTES} reserved per virtual address, including lookup workspace)`, + this.maximumBytes, ); this.#reservedBytes += ELF_POINTER_INDEX_ENTRY_BYTES; } @@ -809,6 +827,7 @@ export function* readElfPointerRelocationsSteps( throw new ElfPointerRelocationFailure( "native-index-capacity", `ELF sparse relocation index exceeds ${MAX_RELOCATION_RECORDS} retained writes`, + MAX_RELOCATION_RECORDS, ); writes.push(write); index.add(write); @@ -874,7 +893,8 @@ export function* readElfPointerValuesSteps( if (index.unresolvedSize + zeroFill.size > MAX_RELOCATION_RECORDS) throw new ElfPointerRelocationFailure( "native-index-capacity", - "ELF unresolved and zero-fill overlay exceeds 65536 entries", + `ELF unresolved and zero-fill overlay exceeds ${MAX_RELOCATION_RECORDS} entries`, + MAX_RELOCATION_RECORDS, ); } next = steps.next(); diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index e51521b883..9b9ec5f578 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -447,10 +447,16 @@ it("refuses an image name repeated past the search bound", () => { // The Mach-O segment covers the original size; widen it to the new tail. flooded.writeBigUInt64LE(BigInt(flooded.length), 64); flooded.writeBigUInt64LE(BigInt(flooded.length), 80); - expect(failure(flooded)).toEqual({ - reason: "native-index-capacity", - kind: "capacity", - }); + expect(() => link(flooded)).toThrow( + expect.objectContaining({ + reason: "native-index-capacity", + failure: expect.objectContaining({ + kind: "limit", + format: "il2cpp-binary", + limit: { name: "native-index-capacity", maximum: 65536 }, + }), + }), + ); }); it.each([ @@ -1970,6 +1976,7 @@ it("keeps a capacity refusal while naming a generic row a refusal", () => { throw new Il2cppBinaryFormatFailure( "native-type-text-capacity", "budget", + 1, ); tableChecked = bytes === 3 * minimumGenericRow; }, @@ -1982,7 +1989,10 @@ it("keeps a capacity refusal while naming a generic row a refusal", () => { })(); expect(tableChecked).toBe(true); expect(failure).toBeInstanceOf(Il2cppBinaryFormatFailure); - expect((failure as Il2cppBinaryFormatFailure).kind).toBe("capacity"); + expect((failure as Il2cppBinaryFormatFailure).failure).toMatchObject({ + kind: "limit", + limit: { name: "native-type-text-capacity", maximum: 1 }, + }); }); it.each([0, 8, 16])( diff --git a/tests/boundary/il2cpp/il2cppMetadataFile.test.ts b/tests/boundary/il2cpp/il2cppMetadataFile.test.ts index 768125347b..b7c04ac854 100644 --- a/tests/boundary/il2cpp/il2cppMetadataFile.test.ts +++ b/tests/boundary/il2cpp/il2cppMetadataFile.test.ts @@ -45,7 +45,7 @@ const failure = ( const rejects = ( bytes: Buffer, reason: Il2cppMetadataFailureReason, - kind: "unsupported" | "corrupt" = "corrupt", + kind: "unsupported" | "truncated" | "malformed" = "malformed", ) => { const rejected = failure(bytes); expect(rejected.reason).toBe(reason); @@ -195,7 +195,7 @@ describe("rejected IL2CPP global-metadata", () => { bytes.writeUInt32LE(0xfab11baf, 0); const rejected = failure(bytes); expect(rejected.reason).toBe("truncated-header"); - expect(rejected.kind).toBe("corrupt"); + expect(rejected.kind).toBe("truncated"); expect(rejected.message).toContain( "fewer than the 8-byte magic/version prefix", ); @@ -208,7 +208,7 @@ describe("rejected IL2CPP global-metadata", () => { bytes.writeUInt32LE(0xfab11baf, 0); const rejected = failure(bytes, readIl2cppMetadataForLinking); expect(rejected.reason).toBe("truncated-header"); - expect(rejected.kind).toBe("corrupt"); + expect(rejected.kind).toBe("truncated"); expect(rejected.message).toContain("metadata version is unknown"); }); @@ -216,6 +216,7 @@ describe("rejected IL2CPP global-metadata", () => { rejects( createIl2cppMetadataFixture().bytes.subarray(0, 200), "truncated-header", + "truncated", ); rejects( mutated((fixture, bytes) => @@ -586,6 +587,7 @@ describe("IL2CPP global-metadata versions 29 and 31", () => { rejects( createIl2cppMetadataFixture({ version: 29 }).bytes.subarray(0, 200), "truncated-header", + "truncated", ); rejects( mutated(