mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
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:
1 parent
ad6e27e256
commit
a090fa7098
13 files changed
+124
-29
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user