Compare commits

...
Author SHA1 Message Date
Kaoru0822 4ff226e98e Merge remote-tracking branch 'origin/main' into fix/nativeaot-pe-virtual-extent 2026-10-11 03:10:16 +00:00
Kaoru0822andClaude Opus 5.5 9f30b01387 style(native): format NativeAOT diagnostics after lint fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:50:20 +00:00
Kaoru0822andClaude Opus 5.5 707797d80f fix(native): bound NativeAOT PE reads by section virtual size
The NativeAOT PE parser from 76d8f68cb treated a section as
`max(VirtualSize, SizeOfRawData)` bytes when reading, and as
SizeOfRawData bytes when classifying executable code and choosing scan
ranges. These are the extents #1596 removed from managed RVA reads.
Raw alignment padding past VirtualSize could supply frozen strings with
complete status, and vtable slots pointing into it counted as executable.

Reads, executable checks, hydration extents and the image-extent check now
use the loader's virtual extent (VirtualSize, or SizeOfRawData when it is
zero). Pointer scans cover only bytes that are both file-backed and mapped.

Refs #1596

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:47:37 +00:00
2 changed files with 56 additions and 24 deletions
+36
View File
@@ -103,3 +103,39 @@ describe("read-only NativeAOT PE parser", () => {
});
});
});
describe("NativeAOT PE section virtual extent", () => {
const TEXT_VIRTUAL_SIZE = 0x188 + 8;
const RDATA_VIRTUAL_SIZE = 0x188 + 40 + 8;
const parseWithVirtualSize = (at: number, virtualSize: number) => {
const bytes = nativeAotPeFixture();
bytes.writeUInt32LE(virtualSize, at);
return parseNativeAotPe(
bytes,
nativeAotPeDigest(bytes),
SUMMARY_BUDGET,
"0x140000000",
).summary;
};
it("does not treat raw padding past VirtualSize as executable code", () => {
// The 0x2280 MethodTable's only slot targets 0x1040, past .text's 0x40 bytes.
expect(parseWithVirtualSize(TEXT_VIRTUAL_SIZE, 0x40)).toMatchObject({
method_tables: 2,
});
});
it("does not read a frozen string from raw padding past VirtualSize", () => {
expect(parseWithVirtualSize(RDATA_VIRTUAL_SIZE, 0x300)).toMatchObject({
status: "partial",
frozen_strings: [],
});
});
it("maps raw bytes when VirtualSize is zero, as the image loader does", () => {
expect(parseWithVirtualSize(TEXT_VIRTUAL_SIZE, 0)).toMatchObject({
status: "complete",
method_tables: 3,
});
});
});
+20 -24
View File
@@ -366,6 +366,13 @@ const parsedSummary = (
readTypeDetail: () => undefined,
});
// The image loader maps VirtualSize bytes, using SizeOfRawData only when
// VirtualSize is zero. Raw bytes past that extent are file-alignment padding.
const virtualExtent = (section: Section): number =>
section.virtualSize === 0 ? section.rawSize : section.virtualSize;
const fileBackedExtent = (section: Section): number =>
Math.min(section.rawSize, virtualExtent(section));
class PeImage {
readonly imageBase: bigint;
readonly sizeOfImage: number;
@@ -417,10 +424,7 @@ class PeImage {
});
for (const section of this.sections) {
this.#range(section.rawOffset, section.rawSize);
if (
section.rva + Math.max(section.virtualSize, section.rawSize) >
this.sizeOfImage
)
if (section.rva + virtualExtent(section) > this.sizeOfImage)
throw new Error("PE section exceeds the declared image extent");
}
}
@@ -456,11 +460,11 @@ class PeImage {
for (const section of this.sections) {
if (this.#budget !== undefined && !this.#budget.consumeWork(1))
return null;
const extent = Math.max(section.virtualSize, section.rawSize);
const extent = virtualExtent(section);
if (relative < section.rva || relative - section.rva > extent - size)
continue;
const delta = relative - section.rva;
if (delta > section.rawSize - size) return null;
if (delta > fileBackedExtent(section) - size) return null;
const offset = section.rawOffset + delta;
return this.#bytes.subarray(offset, offset + size);
}
@@ -475,7 +479,7 @@ class PeImage {
if (
(section.characteristics & 0x20000000) !== 0 &&
rva >= BigInt(section.rva) &&
rva < BigInt(section.rva + section.rawSize)
rva < BigInt(section.rva + virtualExtent(section))
)
return true;
}
@@ -492,10 +496,7 @@ class PeImage {
if (this.#budget !== undefined && !this.#budget.consumeWork(1)) return 0;
if (
rva >= BigInt(candidate.rva) &&
rva <
BigInt(
candidate.rva + Math.max(candidate.virtualSize, candidate.rawSize),
)
rva < BigInt(candidate.rva + virtualExtent(candidate))
) {
section = candidate;
break;
@@ -503,9 +504,7 @@ class PeImage {
}
return section === undefined
? 0
: section.rva +
Math.max(section.virtualSize, section.rawSize) -
Number(rva);
: section.rva + virtualExtent(section) - Number(rva);
}
#range(offset: number, size: number): void {
if (
@@ -602,7 +601,7 @@ const findDirectory = (
)
continue;
const begin = section.rawOffset;
const end = begin + section.rawSize;
const end = begin + fileBackedExtent(section);
const first = (begin + 7) & ~7;
const candidateBytes =
first <= end - 16 ? (Math.floor((end - 16 - first) / 8) + 1) * 8 : 0;
@@ -970,10 +969,10 @@ const discoverTables = (
(section.characteristics & 0x20000000) !== 0
)
continue;
if (section.rawSize > 0) {
if (fileBackedExtent(section) > 0) {
ranges.push({
start: image.addressForRva(section.rva),
end: image.addressForRva(section.rva + section.rawSize),
end: image.addressForRva(section.rva + fileBackedExtent(section)),
});
}
}
@@ -984,9 +983,7 @@ const discoverTables = (
(candidate.characteristics & 0x20000000) === 0 &&
hydrated.base >= image.addressForRva(candidate.rva) &&
hydrated.base + BigInt(hydrated.sizeBytes) <=
image.addressForRva(
candidate.rva + Math.max(candidate.virtualSize, candidate.rawSize),
),
image.addressForRva(candidate.rva + virtualExtent(candidate)),
);
if (section !== undefined)
ranges.push({
@@ -1592,11 +1589,10 @@ export const parseNativeAotPe = (
? "raw-image-pointer-scan"
: "raw-image-and-rehydrated-pointer-scan",
},
diagnostics: [
...(hydrationError === null
diagnostics:
hydrationError === null
? []
: [`Read-only hydration stopped: ${hydrationError}`]),
],
: [`Read-only hydration stopped: ${hydrationError}`],
limitations: reportLimitations,
};
if (Buffer.byteLength(JSON.stringify(summary)) > maximumSummaryBytes)