mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(nativeaot): charge only examined candidate table entries
This commit is contained in:
1 parent
bbf7943ba6
commit
6b0c4b43a4
4 files changed
+166
-60
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user