From d36ba5897150eb58bc8b158247f32a7747f135f2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:34:39 +0800 Subject: [PATCH] fix(native): retain NativeAOT frozen scan budget stops (#10094) --- src/domain/native/nativeAotPe.budget.test.ts | 27 ++++++++++++++++++++ src/domain/native/nativeAotPe.ts | 25 +++++++++++++++--- src/ghidra/GhidraProvider.behavior.test.ts | 2 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/domain/native/nativeAotPe.budget.test.ts b/src/domain/native/nativeAotPe.budget.test.ts index 9e1b52d4f5..5358ae3d77 100644 --- a/src/domain/native/nativeAotPe.budget.test.ts +++ b/src/domain/native/nativeAotPe.budget.test.ts @@ -371,3 +371,30 @@ it("retains the index memory refusal after successful hydration", () => { expect(report.reason).toMatchObject({ code: "system_object_root_unproven" }); expect(report.coverage.truncation_reason).toBe("working-memory-budget"); }); + +it("reports a frozen-string scan stopped during a budgeted image read", () => { + const bytes = nativeAotPeFixture(); + const sha = nativeAotPeDigest(bytes); + const limits = { + sourceBytes: bytes.length, + workUnits: 3260, + workingMemoryBytes: 64 * 1024, + }; + const complete = parseNativeAotPe(bytes, sha, REPORT_BUDGET).summary; + expect(complete.status).toBe("complete"); + expect(complete.frozen_strings).not.toHaveLength(0); + const partial = parseNativeAotPe( + bytes, + sha, + REPORT_BUDGET, + undefined, + limits, + ).summary; + expect(partial).toMatchObject({ + status: "partial", + truncated: true, + reason: { code: "truncated", truncation_reason: "work-unit-budget" }, + coverage: { truncation_reason: "work-unit-budget" }, + }); + expect(partial.frozen_strings).toEqual(complete.frozen_strings); +}); diff --git a/src/domain/native/nativeAotPe.ts b/src/domain/native/nativeAotPe.ts index f27cdd6cc6..ab89dd33e7 100644 --- a/src/domain/native/nativeAotPe.ts +++ b/src/domain/native/nativeAotPe.ts @@ -30,7 +30,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-v9"; + "rtr-9.1-x64-pe-read-only-v10"; export type NativeAotParserLimits = { readonly sourceBytes: number; @@ -1706,14 +1706,27 @@ export const parseNativeAotPe = ( frozenScanTruncated = true; break; } - frozenScanBytesExamined += 8; const table = readU64(image.read(address, 8, budget)); - if (table === null || !isRecoveredStringTable(table)) { + if (budget.truncationReason !== undefined) { + frozenScanTruncated = true; + break; + } + frozenScanBytesExamined += 8; + const recovered = table !== null && isRecoveredStringTable(table); + if (budget.truncationReason !== undefined) { + frozenScanTruncated = true; + break; + } + if (!recovered) { address += 8n; continue; } frozenCandidates++; const length = readU32(image.read(address + 8n, 4, budget)); + if (budget.truncationReason !== undefined) { + frozenScanTruncated = true; + break; + } if (length === null || address + 14n + BigInt(length) * 2n > frozen.end) { address += 8n; continue; @@ -1723,12 +1736,16 @@ export const parseNativeAotPe = ( frozenScanTruncated = true; break; } - frozenStringDataBytesExamined += stringBytes; // NativeAOT's System.String layout is the object header, 32-bit length, // UTF-16 payload, then the terminating code unit. The payload starts at // +12, not +16 (the latter silently shifts every decoded string by two // characters and includes adjacent object padding). const raw = image.read(address + 12n, stringBytes, budget); + if (budget.truncationReason !== undefined) { + frozenScanTruncated = true; + break; + } + frozenStringDataBytesExamined += stringBytes; if (raw === null || raw.readUInt16LE(length * 2) !== 0) { address += 8n; continue; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 0d7fe9c4fa..de27cfbd19 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -742,7 +742,7 @@ describe("Ghidra platform support", () => { executable_role: target.executableRole, managed: target.managed, native_aot_metadata: { - contract_revision: "rtr-9.1-x64-pe-read-only-v9", + contract_revision: "rtr-9.1-x64-pe-read-only-v10", source_bytes: 128 * 1024 * 1024, work_units: 64 * 1024 * 1024, working_memory_bytes: 64 * 1024 * 1024,