fix(il2cpp): report the exceeded native-link maximum in the public refusal (#10741)

* fix(il2cpp): report the exceeded native-link maximum in the public refusal

* fix(il2cpp): report each native-link budget's maximum in the unit its producer counts

* refactor(elf): report pointer relocation failures through ParseFailure (#10748)
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 13:44:54 +08:00
1 parent ad6e27e256
commit a090fa7098
13 files changed
+124 -29

No files matched your search

+1 -1
View File
@@ -453,7 +453,7 @@ The builds checked are listed after the module search rules above.
| `generic-method-registration-not-found` | input error | No CodeRegistration layout bounds every genericMethodTable row, or the generic tables are unreadable. |
| `ambiguous-generic-method-registration` | input error | More than one CodeRegistration layout bounds every genericMethodTable row. |
Format failures name `binary_path` and slice-selection failures name `binary_architecture`; capacity refusals remain typed resource errors. Failed linking preserves the metadata source identity, validated counts, selected binary path/architecture and its SHA-256 when observed, plus prior producer observations. It never publishes an incomplete native table as complete. The binary input is bounded to 1 GiB. The virtual-pointer index has the same 1 GiB estimated representation allowance, reserving 2 KiB per distinct virtual destination before insertion for retained Maps and finish-time lookup storage. Repeated declarations of one destination share that reservation. This conservative allocation accounting is separate from actual process RSS and the report allowance; exhaustion returns a typed `native-index-capacity` refusal before another destination is retained.
Format failures name `binary_path` and slice-selection failures name `binary_architecture`; capacity refusals remain typed resource errors whose reported limits name the exceeded budget (`boundary`, the capacity reason) and its maximum in the unit the budget counts (`maximum_entries`, `maximum_bytes` or `maximum_frames`). Failed linking preserves the metadata source identity, validated counts, selected binary path/architecture and its SHA-256 when observed, plus prior producer observations. It never publishes an incomplete native table as complete. The binary input is bounded to 1 GiB. The virtual-pointer index has the same 1 GiB estimated representation allowance, reserving 2 KiB per distinct virtual destination before insertion for retained Maps and finish-time lookup storage. Repeated declarations of one destination share that reservation. This conservative allocation accounting is separate from actual process RSS and the report allowance; exhaustion returns a typed `native-index-capacity` refusal before another destination is retained.
Scans yield cooperatively in bounded batches. Name-address retention and reverse-trie construction are each limited to 65,536 entries/nodes and fallback module-array candidates to 64 (`native-index-capacity`); exceeding capacity does not prove absence. Rendered type text has a 64 MiB UTF-8 cache-workspace guard, separate from the 64 MiB encoded-report admission, and 64 nested rendering frames (`native-type-text-capacity` / `type-nesting-capacity`). Genuine cycles are malformed; unsupported type kinds are explicit coverage failures. Native row/text admission runs before allocation or retention. Unused generic parameter names are read on demand from the same stable metadata snapshot. There is no cross-inspection file cache; validated immutable Evidence is reused for session views.
+8
View File
@@ -39,6 +39,9 @@ type Il2cppBinaryFormatReason = Exclude<
Il2cppBinaryCapacityReason
>;
/** What a native-link budget counts, so its maximum is reported with its unit. */
export type NativeBudgetUnit = "entries" | "bytes" | "frames";
/** The ParseFailure format of a native image linked against IL2CPP metadata. */
export const IL2CPP_BINARY_FORMAT = "il2cpp-binary";
@@ -55,16 +58,20 @@ const isUnsupported = (reason: Il2cppBinaryFormatReason) =>
export class Il2cppBinaryFormatFailure extends ParseFailureError<
"malformed" | "unsupported" | "limit"
> {
/** What the exceeded maximum counts; set only for a capacity reason. */
readonly unit: NativeBudgetUnit | undefined;
constructor(reason: Il2cppBinaryFormatReason, message: string);
constructor(
reason: Il2cppBinaryCapacityReason,
message: string,
maximum: number,
unit: NativeBudgetUnit,
);
constructor(
readonly reason: Il2cppBinaryFailureReason,
message: string,
maximum?: number,
unit?: NativeBudgetUnit,
) {
const detail = `${reason}: ${message}`;
super(
@@ -84,6 +91,7 @@ export class Il2cppBinaryFormatFailure extends ParseFailureError<
}
: { kind: "malformed", format: IL2CPP_BINARY_FORMAT, detail },
);
this.unit = unit;
this.name = "Il2cppBinaryFormatFailure";
}
}
+8 -6
View File
@@ -94,6 +94,7 @@ export function* openImageSteps(image: Buffer): Generator<void, NativeImage> {
"native-index-capacity",
cause.message,
cause.failure.limit.maximum,
"bytes",
);
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure(
@@ -126,14 +127,15 @@ export function* openImageSteps(image: Buffer): Generator<void, NativeImage> {
);
} catch (cause) {
if (cause instanceof ElfPointerRelocationFailure) {
if (cause.failure.kind === "limit" && cause.unit !== undefined)
throw new Il2cppBinaryFormatFailure(
"native-index-capacity",
cause.message,
cause.failure.limit.maximum,
cause.unit,
);
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);
+4
View File
@@ -113,6 +113,7 @@ function* nameAddresses(
"native-index-capacity",
`module-name trie exceeds its ${TOTAL_NAME_OCCURRENCES}-node workspace`,
TOTAL_NAME_OCCURRENCES,
"entries",
);
let node = root;
const bytes = Buffer.from(name, "utf8");
@@ -125,6 +126,7 @@ function* nameAddresses(
"native-index-capacity",
`module-name trie exceeds its ${TOTAL_NAME_OCCURRENCES}-node workspace`,
TOTAL_NAME_OCCURRENCES,
"entries",
);
next = { next: new Map(), owners: [] };
node.next.set(bytes[at]!, next);
@@ -165,6 +167,7 @@ function* nameAddresses(
"native-index-capacity",
`native name-address index exceeds ${TOTAL_NAME_OCCURRENCES} entries`,
TOTAL_NAME_OCCURRENCES,
"entries",
);
found.set(address, node.owners);
}
@@ -535,6 +538,7 @@ function* findModuleArrayByCode(
"native-index-capacity",
`module-array index exceeds ${MODULE_ARRAY_CANDIDATES} entries`,
MODULE_ARRAY_CANDIDATES,
"entries",
);
}
}
+2
View File
@@ -107,6 +107,7 @@ const createTypeRenderer = (
"native-type-text-capacity",
`rendered type text workspace exceeds ${TYPE_TEXT_BYTES} UTF-8 bytes`,
TYPE_TEXT_BYTES,
"bytes",
);
beforeText?.(bytes);
};
@@ -133,6 +134,7 @@ const createTypeRenderer = (
"type-nesting-capacity",
`type rendering exceeds ${TYPE_DEPTH} nested frames`,
TYPE_DEPTH,
"frames",
);
active.add(address);
try {
+11 -2
View File
@@ -83,12 +83,21 @@ const failureForPrimary = (
const { cause, origin } = facts;
if (origin === "owned-or-classified" && cause instanceof AnalysisError)
return cause;
if (cause instanceof Il2cppBinaryFormatFailure && cause.kind === "limit")
if (
cause instanceof Il2cppBinaryFormatFailure &&
cause.failure.kind === "limit"
)
return new AnalysisResourceConstraintError(
OPERATION,
cause.reason === "native-type-text-capacity" ? "memory" : "capacity",
cause.message,
{},
// The producing site states what its maximum counts.
{
boundary: cause.reason,
...(cause.unit === undefined
? {}
: { [`maximum_${cause.unit}`]: cause.failure.limit.maximum }),
},
{
cause,
...fixedAllowanceOptions(
+2 -4
View File
@@ -501,8 +501,7 @@ export const inspectAndroidJniLibrary = async (
});
if (
cause instanceof BinaryResourceFailure ||
(cause instanceof ElfPointerRelocationFailure &&
cause.reason === "native-index-capacity")
(cause instanceof ElfPointerRelocationFailure && cause.kind === "limit")
)
return err({ kind: "capacity", message: cause.message });
if (cause instanceof BinaryFormatFailure)
@@ -512,8 +511,7 @@ export const inspectAndroidJniLibrary = async (
});
if (cause instanceof ElfPointerRelocationFailure)
return err({
kind:
cause.reason === "malformed-container" ? "malformed" : "unsupported",
kind: cause.kind === "malformed" ? "malformed" : "unsupported",
message: cause.message,
});
throw cause;
+7 -7
View File
@@ -645,13 +645,13 @@ const loadWrites = async (
}
return { writes, clobbered, trustRaw: true };
} catch (cause) {
if (!(cause instanceof ElfPointerRelocationFailure)) throw cause;
if (cause.reason === "native-index-capacity")
throw new BinaryResourceFailure(
"structure",
16 * 1024 * 1024,
cause.message,
);
// A budget stop is not a malformed table; the provider reports it with
// the maximum the failing index declared.
if (
!(cause instanceof ElfPointerRelocationFailure) ||
cause.kind === "limit"
)
throw cause;
note(scan, cause.message);
return { writes: new Map(), clobbered: new Set(), trustRaw: false };
}
@@ -199,13 +199,13 @@ const failurePrimary = (
if (identityFailure !== undefined) return identityFailure;
if (
cause instanceof ElfPointerRelocationFailure &&
cause.reason === "native-index-capacity"
cause.failure.kind === "limit"
)
return new AnalysisResourceConstraintError(
OPERATION,
"capacity",
`Selected artifact ${input.path}: ${cause.message}`,
{ boundary: "structure" },
{ boundary: "structure", maximum: cause.failure.limit.maximum },
{
cause,
...fixedAllowanceOptions(
@@ -397,7 +397,12 @@ it("refuses pointer workspace growth before inserting the next virtual destinati
expect(() => pointerValues(relativeValues(), 4096)).toThrow(
expect.objectContaining({
reason: "native-index-capacity",
maximum: 4096,
failure: expect.objectContaining({
kind: "limit",
format: "elf-pointer-relocations",
limit: { name: "native-index-capacity", maximum: 4096 },
}),
unit: "bytes",
message: expect.stringContaining("4096 estimated representation bytes"),
}),
);
+35 -6
View File
@@ -11,11 +11,21 @@ import {
isZeroFilledSpan,
type ElfDynamicSegment,
} from "./BinaryContainer.js";
import { ParseFailureError } from "../../domain/parseFailure.js";
/** 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;
/** The ParseFailure format of ELF loader pointer interpretation. */
export const ELF_POINTER_RELOCATION_FORMAT = "elf-pointer-relocations";
/**
* Shared loader interpretation; language adapters choose their public errors.
* The stable `reason` names the loader rule; `native-index-capacity` is a
* ParseFailure `limit` carrying the exceeded maximum.
*/
export class ElfPointerRelocationFailure extends ParseFailureError<
"malformed" | "unsupported" | "limit"
> {
/** What the limit's maximum counts, set only for `native-index-capacity`. */
readonly unit: "entries" | "bytes" | undefined;
constructor(
reason:
| "malformed-container"
@@ -27,6 +37,7 @@ export class ElfPointerRelocationFailure extends Error {
reason: "native-index-capacity",
message: string,
maximum: number,
unit: "entries" | "bytes",
);
constructor(
readonly reason:
@@ -36,9 +47,23 @@ export class ElfPointerRelocationFailure extends Error {
| "native-index-capacity",
message: string,
maximum?: number,
unit?: "entries" | "bytes",
) {
super(message);
this.maximum = maximum;
const facts = { format: ELF_POINTER_RELOCATION_FORMAT, detail: message };
super(
maximum !== undefined
? { ...facts, kind: "limit", limit: { name: reason, maximum } }
: reason === "malformed-container"
? { ...facts, kind: "malformed" }
: {
...facts,
kind: "unsupported",
// A pointer the loader encodes in an unread form still ships.
undecodedContent: reason === "unsupported-pointer-encoding",
},
);
this.unit = unit;
this.name = "ElfPointerRelocationFailure";
}
}
export interface ElfPointerRelocationSource {
@@ -269,6 +294,7 @@ function* dynamicTags(
"native-index-capacity",
"ELF dynamic directory exceeds the 16 MiB structural allowance",
16 * 1024 * 1024,
"bytes",
);
if (
declaration.size % BigInt(width) !== 0n ||
@@ -677,6 +703,7 @@ class ElfVirtualWriteIndex {
"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,
"bytes",
);
this.#reservedBytes += ELF_POINTER_INDEX_ENTRY_BYTES;
}
@@ -828,6 +855,7 @@ export function* readElfPointerRelocationsSteps(
"native-index-capacity",
`ELF sparse relocation index exceeds ${MAX_RELOCATION_RECORDS} retained writes`,
MAX_RELOCATION_RECORDS,
"entries",
);
writes.push(write);
index.add(write);
@@ -895,6 +923,7 @@ export function* readElfPointerValuesSteps(
"native-index-capacity",
`ELF unresolved and zero-fill overlay exceeds ${MAX_RELOCATION_RECORDS} entries`,
MAX_RELOCATION_RECORDS,
"entries",
);
}
next = steps.next();
@@ -1977,6 +1977,7 @@ it("keeps a capacity refusal while naming a generic row a refusal", () => {
"native-type-text-capacity",
"budget",
1,
"bytes",
);
tableChecked = bytes === 3 * minimumGenericRow;
},
@@ -1053,3 +1053,40 @@ it("refuses generic rows that exceed the report budget and keeps the requested p
await provider.close();
}
});
it("reports the exceeded native-link maximum in the public refusal", async () => {
const root = await createTestTempDirectory("rea-il2cpp-link-limit-");
const path = join(root, "global-metadata.dat");
const binaryPath = join(root, "GameAssembly.dylib");
await writeFile(path, createLinkedIl2cppMetadataFixture().bytes);
// 80 copies of the two-slot module array exceed the 64 candidate arrays
// the fallback search compares; x86-64 makes that fallback run.
const base = createIl2cppBinaryFixture({
codeRegistration: false,
codeReference: true,
});
const crafted = Buffer.concat([
base,
...Array.from({ length: 80 }, () => base.subarray(0x500, 0x510)),
]);
crafted.writeBigUInt64LE(BigInt(crafted.length), 64);
crafted.writeBigUInt64LE(BigInt(crafted.length), 80);
crafted.writeUInt32LE(0x01000007, 4);
crafted.writeUInt32LE(3, 8);
await writeFile(binaryPath, crafted);
const provider = new Il2cppMetadataProvider();
try {
const result = await provider.inspect({ path, binary_path: binaryPath });
if (result.ok) throw new Error("Expected the link budget refusal");
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
expect(result.error).toMatchObject({
resource: "capacity",
reportedLimits: {
boundary: "native-index-capacity",
maximum_entries: 64,
},
});
} finally {
await provider.close();
}
});