fix(native): scale the NativeAOT work budget with the admitted image

The parser's work budget was a fixed 64 Mi units, so a real 26 MB .NET 8
NativeAOT image (bflat 8.0.2) stopped frozen-string recovery at 4,433 of
6,685 strings and reported partial. Give each image a base budget plus 8
units per admitted source byte, bounded by the source-byte ceiling, and
record the scaling in the analysis profile (contract revision v11). With it,
that image completes with all 6,685 frozen strings using 71.6 M units.

Only the budget scaling of #4559 is taken: main already resolves the
base-type closure in linear time.

Fixes #4445
Supersedes #4559

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Nitin13-gitandgensokyo-dev authored and Ysopwnable committed 2026-11-14 16:51:40 +01:00
1 parent 79097f52b2
commit c8d5413d0f
4 files changed
+78 -23

No files matched your search

@@ -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();
+23 -6
View File
@@ -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, [
+5 -2
View File
@@ -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,
},
+18 -15
View File
@@ -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", () => {