diff --git a/src/domain/native/nativeAotPe.budget.test.ts b/src/domain/native/nativeAotPe.budget.test.ts index 5358ae3d77..a5835353a0 100644 --- a/src/domain/native/nativeAotPe.budget.test.ts +++ b/src/domain/native/nativeAotPe.budget.test.ts @@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest"; import type { NativeAotParserLimits } from "./nativeAotPe.js"; import { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; import { + DEFAULT_NATIVE_AOT_PARSER_LIMITS, + NATIVE_AOT_BASE_WORK_UNITS, NATIVE_AOT_MAX_REPORT_BYTES, + NATIVE_AOT_MAX_SOURCE_BYTES, + nativeAotParserLimitsFor, parseNativeAotPe, } from "./nativeAotPe.js"; import { @@ -12,6 +16,34 @@ import { const REPORT_BUDGET = NATIVE_AOT_MAX_REPORT_BYTES; +describe("NativeAOT work budget scaling", () => { + it("grows with the admitted image and stops at the source-byte ceiling", () => { + const small = nativeAotParserLimitsFor(1024); + const large = nativeAotParserLimitsFor(26_180_096); + expect(small.workUnits).toBeGreaterThan(NATIVE_AOT_BASE_WORK_UNITS); + // bflat 8.0.2's 26,180,096-byte image needs 71,645,819 units to complete. + expect(large.workUnits).toBeGreaterThan(71_645_819); + expect(nativeAotParserLimitsFor(Number.MAX_SAFE_INTEGER)).toEqual( + DEFAULT_NATIVE_AOT_PARSER_LIMITS, + ); + expect(DEFAULT_NATIVE_AOT_PARSER_LIMITS.sourceBytes).toBe( + NATIVE_AOT_MAX_SOURCE_BYTES, + ); + }); + + it("charges an image against the budget for its own size by default", () => { + const bytes = nativeAotPeFixture(); + const report = parseNativeAotPe( + bytes, + nativeAotPeDigest(bytes), + REPORT_BUDGET, + ).summary; + expect(report.coverage.work_budget_units).toBe( + nativeAotParserLimitsFor(bytes.length).workUnits, + ); + }); +}); + describe("NativeAOT parser resource budgets", () => { it("reports source-cap rejection without hashing or inventing omitted-byte coverage", () => { const bytes = nativeAotPeFixture(); diff --git a/src/domain/native/nativeAotPe.ts b/src/domain/native/nativeAotPe.ts index ab89dd33e7..066b1fa8a2 100644 --- a/src/domain/native/nativeAotPe.ts +++ b/src/domain/native/nativeAotPe.ts @@ -26,22 +26,39 @@ const FROZEN_OBJECT_REGION = 206; export const NATIVE_AOT_MAX_SOURCE_BYTES = 128 * 1024 * 1024; /** Bound one parser-produced JSON report independently of any transport. */ export const NATIVE_AOT_MAX_REPORT_BYTES = 4 * 1024 * 1024; -export const NATIVE_AOT_MAX_WORK_UNITS = 64 * 1024 * 1024; +/** Work every image may use, covering headers, directories and sections. */ +export const NATIVE_AOT_BASE_WORK_UNITS = 64 * 1024 * 1024; +/** + * Work added per admitted source byte, so discovery is not starved on larger + * images. bflat 8.0.2's 26,180,096-byte win-x64 NativeAOT image completes with + * 71,645,819 units (about 2.7 per byte), past the former fixed 64 Mi budget. + */ +export const NATIVE_AOT_WORK_UNITS_PER_SOURCE_BYTE = 8; 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-v10"; + "rtr-9.1-x64-pe-read-only-v11"; export type NativeAotParserLimits = { readonly sourceBytes: number; readonly workUnits: number; readonly workingMemoryBytes: number; }; -export const DEFAULT_NATIVE_AOT_PARSER_LIMITS: NativeAotParserLimits = { +/** Limits for one admitted image; the work budget follows its size. */ +export const nativeAotParserLimitsFor = ( + sourceBytes: number, +): NativeAotParserLimits => ({ sourceBytes: NATIVE_AOT_MAX_SOURCE_BYTES, - workUnits: NATIVE_AOT_MAX_WORK_UNITS, + workUnits: + NATIVE_AOT_BASE_WORK_UNITS + + Math.min(Math.max(sourceBytes, 0), NATIVE_AOT_MAX_SOURCE_BYTES) * + NATIVE_AOT_WORK_UNITS_PER_SOURCE_BYTE, workingMemoryBytes: NATIVE_AOT_MAX_WORKING_MEMORY_BYTES, -}; +}); + +/** The scaled limits at the source-byte ceiling. */ +export const DEFAULT_NATIVE_AOT_PARSER_LIMITS: NativeAotParserLimits = + nativeAotParserLimitsFor(NATIVE_AOT_MAX_SOURCE_BYTES); class NativeAotParserBudget { workUnits = 0; @@ -1395,7 +1412,7 @@ export const parseNativeAotPe = ( expectedSha256: string, maximumSummaryBytes: number, loadedImageBase?: string, - limits = DEFAULT_NATIVE_AOT_PARSER_LIMITS, + limits = nativeAotParserLimitsFor(bytes.byteLength), ): NativeAotPeResult => { if (!Number.isSafeInteger(maximumSummaryBytes) || maximumSummaryBytes < 0) throw new AnalysisInputError("parse_native_aot_pe", undefined, [ diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index 16ac39dd53..d49d0a8d5e 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -48,7 +48,8 @@ import { NATIVE_AOT_MAX_REPORT_BYTES, NATIVE_AOT_MAX_SOURCE_BYTES, NATIVE_AOT_MAX_WORKING_MEMORY_BYTES, - NATIVE_AOT_MAX_WORK_UNITS, + NATIVE_AOT_BASE_WORK_UNITS, + NATIVE_AOT_WORK_UNITS_PER_SOURCE_BYTE, NATIVE_AOT_PROFILE_CONTRACT_REVISION, } from "../domain/native/nativeAotPe.js"; @@ -301,7 +302,9 @@ export const resolveGhidraAnalysisProfile = async ( native_aot_metadata: { contract_revision: NATIVE_AOT_PROFILE_CONTRACT_REVISION, source_bytes: NATIVE_AOT_MAX_SOURCE_BYTES, - work_units: NATIVE_AOT_MAX_WORK_UNITS, + base_work_units: NATIVE_AOT_BASE_WORK_UNITS, + work_units_per_source_byte: + NATIVE_AOT_WORK_UNITS_PER_SOURCE_BYTE, working_memory_bytes: NATIVE_AOT_MAX_WORKING_MEMORY_BYTES, report_bytes: NATIVE_AOT_MAX_REPORT_BYTES, }, diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 7dde125816..2dc00c4062 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -883,6 +883,21 @@ describe("Ghidra platform support", () => { ).toBe("read-only-relocation-limitations-v1"); }); + it("keeps Windows annotation mutation unavailable independently of native controls", () => { + const ghidra = provider(installationHost("win32")); + expect( + ghidra + .capabilities() + .find(({ operation }) => operation === "annotate_native_function"), + ).toMatchObject({ + available: false, + reason: "Windows Ghidra P0 does not admit database mutation.", + availabilityCode: "unsupported_host", + }); + }); +}); + +describe("Ghidra PE analysis profile identity", () => { it("commits the PE role and managed classification to profile identity", async () => { const ghidra = provider(installationHost("win32")); const application = peTarget("x86_64"); @@ -904,9 +919,10 @@ describe("Ghidra platform support", () => { executable_role: target.executableRole, managed: target.managed, native_aot_metadata: { - contract_revision: "rtr-9.1-x64-pe-read-only-v10", + contract_revision: "rtr-9.1-x64-pe-read-only-v11", source_bytes: 128 * 1024 * 1024, - work_units: 64 * 1024 * 1024, + base_work_units: 64 * 1024 * 1024, + work_units_per_source_byte: 8, working_memory_bytes: 64 * 1024 * 1024, report_bytes: 4 * 1024 * 1024, }, @@ -923,19 +939,6 @@ describe("Ghidra platform support", () => { "native_aot_metadata", ); }); - - it("keeps Windows annotation mutation unavailable independently of native controls", () => { - const ghidra = provider(installationHost("win32")); - expect( - ghidra - .capabilities() - .find(({ operation }) => operation === "annotate_native_function"), - ).toMatchObject({ - available: false, - reason: "Windows Ghidra P0 does not admit database mutation.", - availabilityCode: "unsupported_host", - }); - }); }); describe("Ghidra Windows DOS admission", () => {