fix(nativeaot): charge only examined candidate table entries

This commit is contained in:
morluto committed 2026-10-24 04:23:56 +08:00
1 parent bbf7943ba6
commit 6b0c4b43a4
4 files changed
+166 -60

No files matched your search

+7 -3
View File
@@ -125,11 +125,15 @@ budget is spent, including later sections that were never visited. MethodTable
closure indexes each validated table's related pointer once and visits a work
queue for inheritance and interface reachability. Its temporary index and queue
consume the existing working-memory budget; the parser does not increase its
work limit. Discovery still must finish before claiming a unique System.Object
work limit. Candidate headers/counts are parsed once; unmapped table bodies are
rejected before charging table entries, and validation charges only each slot or
interface pointer actually examined. Declared counts and unvisited tails do not
become examined coverage. Discovery still must finish before claiming a unique System.Object
root, so a discovery budget stop can return zero recovered tables without
establishing their absence. Performance on the large bflat report has not been
measured; this change removes repeated closure scans rather than resolving
that separate discovery limitation.
measured. Indexed closure and candidate validation avoid separate repeated or
unperformed work, but do not establish that the fixed discovery budget admits
that artifact. Budget stops remain partial results with unknown type absence.
If captured snapshot bytes no longer match their admitted digest, the parser
retains both digests and the Ghidra owner reports `artifact_changed`, with the
@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import {
NATIVE_AOT_MAX_REPORT_BYTES,
parseNativeAotPe,
} from "./nativeAotPe.js";
import {
nativeAotPeDigest,
nativeAotPeFixture,
} from "../../../tests/fixtures/nativeaotPe.js";
const extendedFixture = (length: number) => {
const bytes = Buffer.alloc(length);
nativeAotPeFixture().copy(bytes);
const data = 0x98 + 0xf0 + 40;
bytes.writeUInt32LE(length - 0x600, data + 8);
bytes.writeUInt32LE(length - 0x600, data + 16);
bytes.writeUInt32LE(0xb000, 0x98 + 56);
return bytes;
};
const candidate = (
bytes: Buffer,
offset: number,
slots: number,
interfaces: number,
related = 0n,
) => {
bytes.writeUInt32LE(0x50000000, offset);
bytes.writeUInt32LE(0x20, offset + 4);
bytes.writeBigUInt64LE(related, offset + 8);
bytes.writeUInt16LE(slots, offset + 16);
bytes.writeUInt16LE(interfaces, offset + 18);
};
describe("NativeAOT candidate work accounting", () => {
it("rejects an unmapped declared body without spending its unexamined item count", () => {
const bytes = nativeAotPeFixture();
// Gap between authored System.Object and its existing derived table.
candidate(bytes, 0x850, 0xffff, 0xffff);
const before = Buffer.from(bytes);
const summary = parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
NATIVE_AOT_MAX_REPORT_BYTES,
undefined,
{
sourceBytes: bytes.length,
workUnits: 64 * 1024,
workingMemoryBytes: 64 * 1024,
},
).summary;
expect(summary).toMatchObject({
status: "complete",
method_tables: 3,
coverage: {
method_table_validation_items_examined: 4,
scan_bytes_omitted: 0,
},
});
expect(summary.coverage.work_units_examined).toBeLessThan(64 * 1024);
expect(bytes).toEqual(before);
});
it.each(["slots", "interfaces"] as const)(
"charges the first invalid %s pointer but no unvisited declared tail",
(kind) => {
const bytes = extendedFixture(0x9000);
candidate(
bytes,
0x850,
kind === "slots" ? 4096 : 0,
kind === "interfaces" ? 4096 : 0,
);
bytes.writeBigUInt64LE(0xdeadbeefn, 0x850 + 24);
const before = Buffer.from(bytes);
const summary = parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
NATIVE_AOT_MAX_REPORT_BYTES,
).summary;
expect(summary).toMatchObject({
status: "complete",
method_tables: 3,
coverage: {
method_table_validation_items_examined: 5,
scan_bytes_omitted: 0,
},
});
expect(bytes).toEqual(before);
},
);
it("still validates and charges every entry of a valid mapped table", () => {
const bytes = extendedFixture(0xc18);
candidate(bytes, 0xa00, 64, 0, 0x140002200n);
const before = Buffer.from(bytes);
const summary = parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
NATIVE_AOT_MAX_REPORT_BYTES,
).summary;
expect(summary).toMatchObject({
status: "complete",
method_tables: 4,
coverage: {
method_table_validation_items_examined: 68,
scan_bytes_omitted: 0,
},
});
expect(summary.types.map(({ address }) => address)).toEqual([
"0x140002200",
"0x140002280",
"0x1400022c0",
"0x140002400",
]);
expect(bytes).toEqual(before);
});
});
+41 -56
View File
@@ -13,7 +13,7 @@ export const NATIVE_AOT_MAX_WORK_UNITS = 64 * 1024 * 1024;
export const NATIVE_AOT_MAX_WORKING_MEMORY_BYTES = 64 * 1024 * 1024;
/** Version of the built-in read-only PE metadata interpretation in profiles. */
export const NATIVE_AOT_PROFILE_CONTRACT_REVISION =
"rtr-9.1-x64-pe-read-only-v6";
"rtr-9.1-x64-pe-read-only-v7";
export type NativeAotParserLimits = {
readonly sourceBytes: number;
@@ -832,31 +832,40 @@ const findDirectory = (
};
};
/** One discovery owner counts only entries the validator actually examines. */
const parseMethodTable = (
image: PeImage,
address: bigint,
remainingValidationItems: number,
budget: NativeAotParserBudget,
validation: { itemsExamined: number },
): MethodTable | null => {
const header = image.read(address, 16);
const header = image.read(address, 24);
if (header === null) return null;
const flags = header.readUInt32LE(0),
baseSize = header.readUInt32LE(4),
related = header.readBigUInt64LE(8);
const counts = image.read(address + 16n, 4);
if (counts === null) return null;
const slotsCount = counts.readUInt16LE(0),
interfacesCount = counts.readUInt16LE(2);
if (baseSize > 0x10000000 || (baseSize !== 0 && baseSize < 16)) return null;
const itemCount = slotsCount + interfacesCount;
if (itemCount > remainingValidationItems) return null;
const tableBytes = 24 + itemCount * 8;
const elementType = (flags & 0x7c000000) >>> 26;
if (
(elementType !== 0x14 && elementType !== 0x15) ||
baseSize < 16 ||
baseSize > 0x10000000 ||
(related !== 0n && image.read(related, 24) === null)
)
return null;
const slotsCount = header.readUInt16LE(16),
interfacesCount = header.readUInt16LE(18);
const tableBytes = 24 + (slotsCount + interfacesCount) * 8;
const table = image.read(address, tableBytes);
if (table === null) return null;
for (let slot = 0; slot < slotsCount; slot++) {
if (!budget.consumeWork(8)) return null;
validation.itemsExamined++;
const target = table.readBigUInt64LE(24 + slot * 8);
if (target !== 0n && !image.isExecutable(target)) return null;
}
for (let index = 0; index < interfacesCount; index++) {
if (!budget.consumeWork(8)) return null;
validation.itemsExamined++;
const iface = table.readBigUInt64LE(24 + (slotsCount + index) * 8);
if (iface !== 0n && image.read(iface, 20) === null) return null;
}
@@ -1097,7 +1106,6 @@ const discoverTables = (
scanBytesExamined: number;
scanBytesOmitted: number;
methodTableValidationItemsExamined: number;
workBytesExamined: number;
truncated: boolean;
} => {
const ranges: { start: bigint; end: bigint }[] = [];
@@ -1147,9 +1155,8 @@ const discoverTables = (
let pointerCount = 0,
pointerSlotsExamined = 0,
scanBytesExamined = 0,
validationItems = 0,
workBytesExamined = 0,
truncated = false;
const validation = { itemsExamined: 0 };
const candidates = new MethodTableIndex(budget);
scan: for (const range of merged) {
let address = (range.start + 7n) & ~7n;
@@ -1159,48 +1166,27 @@ const discoverTables = (
break scan;
}
const value = readU64(image.read(address, 8));
if (budget.truncationReason !== undefined) {
truncated = true;
break scan;
}
pointerSlotsExamined++;
scanBytesExamined += 8;
workBytesExamined += 8;
if (value !== null && value !== 0n) pointerCount++;
const candidateAddress = address - 8n;
const header =
candidateAddress >= image.imageBase
? image.read(candidateAddress, 24)
: null;
if (header !== null) {
const flags = header.readUInt32LE(0);
const elementType = (flags & 0x7c000000) >>> 26;
const baseSize = header.readUInt32LE(4);
const related = header.readBigUInt64LE(8);
const plausibleHeader =
(elementType === 0x14 || elementType === 0x15) &&
baseSize >= 16 &&
baseSize <= 0x10000000 &&
(related === 0n || image.read(related, 24) !== null);
if (!plausibleHeader) {
address += 8n;
continue;
}
const counts = image.read(candidateAddress + 16n, 4);
if (counts === null) {
address += 8n;
continue;
}
const itemCount = counts.readUInt16LE(0) + counts.readUInt16LE(2);
if (!budget.consumeWork(itemCount * 8)) {
truncated = true;
break scan;
}
validationItems += itemCount;
workBytesExamined += itemCount * 8;
const candidate = parseMethodTable(image, candidateAddress, itemCount);
if (candidate !== null) {
if (!candidates.append(candidate)) {
truncated = true;
break scan;
}
}
const candidate = parseMethodTable(
image,
candidateAddress,
budget,
validation,
);
if (budget.truncationReason !== undefined) {
truncated = true;
break scan;
}
if (candidate !== null && !candidates.append(candidate)) {
truncated = true;
break scan;
}
address += 8n;
}
@@ -1232,6 +1218,7 @@ const discoverTables = (
objectCount++;
}
});
truncated ||= budget.truncationReason !== undefined;
if (objectCount !== 1 || objectRoot < 0 || truncated)
return {
tables: candidates,
@@ -1239,8 +1226,7 @@ const discoverTables = (
pointerSlotsExamined,
scanBytesExamined,
scanBytesOmitted: Math.max(0, totalScanBytes - scanBytesExamined),
methodTableValidationItemsExamined: validationItems,
workBytesExamined,
methodTableValidationItemsExamined: validation.itemsExamined,
truncated,
};
truncated = !candidates.includeReachable(objectRoot, image);
@@ -1250,8 +1236,7 @@ const discoverTables = (
pointerSlotsExamined,
scanBytesExamined,
scanBytesOmitted: Math.max(0, totalScanBytes - scanBytesExamined),
methodTableValidationItemsExamined: validationItems,
workBytesExamined,
methodTableValidationItemsExamined: validation.itemsExamined,
truncated,
};
};
+1 -1
View File
@@ -666,7 +666,7 @@ describe("Ghidra platform support", () => {
executable_role: target.executableRole,
managed: target.managed,
native_aot_metadata: {
contract_revision: "rtr-9.1-x64-pe-read-only-v6",
contract_revision: "rtr-9.1-x64-pe-read-only-v7",
source_bytes: 128 * 1024 * 1024,
work_units: 64 * 1024 * 1024,
working_memory_bytes: 64 * 1024 * 1024,