Merge remote-tracking branch 'origin/main' into fix/nativeaot-pe-virtual-extent

This commit is contained in:
Kaoru0822
2026-10-11 03:10:16 +00:00
4 changed files with 77 additions and 5 deletions
@@ -176,6 +176,9 @@ const startStorageSite = async () => {
origin: `http://127.0.0.1:${port}`,
close: async () => {
server.close();
// Chrome can leave speculative TCP connections without an HTTP request;
// server.close() alone waits indefinitely for those owned sockets.
server.closeAllConnections();
await once(server, "close");
},
};
+4
View File
@@ -277,11 +277,13 @@ try {
throw new Error("Scenario attachment terminated its external browser");
const profilesBefore = await scenarioProfiles();
process.stderr.write("Browser verifier: scenario environment\n");
const scenarioEnvironment = await verifyScenarioEnvironment(
endpoint,
target,
site.origin,
);
process.stderr.write("Browser verifier: scenario storage\n");
const scenarioStorage = await verifyScenarioStorage({
executable,
endpoint,
@@ -339,7 +341,9 @@ try {
`Browser scenario included unselected structured value: ${unselectedValue}`,
);
process.stderr.write("Browser verifier: large screenshot\n");
const largeScreenshot = await verifyLargeScreenshotE2e(endpoint, site.origin);
process.stderr.write("Browser verifier: popup coverage\n");
const popupEvents = await verifyPopupEventCoverage(executable);
const networkEvidence = await verifyBrowserNetworkEvidence(
executable,
+11 -5
View File
@@ -28,6 +28,11 @@ interface Section {
readonly size: number;
}
// 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.size : section.virtualSize;
/** Header admission and unambiguous file-backed RVA mapping, independent of section names. */
export const readPeResourceLayout = (bytes: Buffer) => {
requirePeRange(bytes, 0, 64);
@@ -64,7 +69,7 @@ export const readPeResourceLayout = (bytes: Buffer) => {
offset: bytes.readUInt32LE(at + 20),
size: bytes.readUInt32LE(at + 16),
};
if (section.rva + Math.max(section.virtualSize, section.size) > 0x100000000)
if (section.rva + virtualExtent(section) > 0x100000000)
peFailure("PE section RVA range overflows.");
if (section.size > 0) {
requirePeRange(bytes, section.offset, section.size);
@@ -90,15 +95,14 @@ export const readPeResourceLayout = (bytes: Buffer) => {
if (rva + size > 0x100000000) peFailure("Resource RVA range overflows.");
const candidates = sections.filter(
(section) =>
rva >= section.rva &&
rva - section.rva < Math.max(section.size, section.virtualSize),
rva >= section.rva && rva - section.rva < virtualExtent(section),
);
if (rva < headerSize) {
if (
sections.some(
(section) =>
section.rva < rva + size &&
section.rva + Math.max(section.size, section.virtualSize) > rva,
section.rva + virtualExtent(section) > rva,
) ||
size > headerSize - rva
)
@@ -111,6 +115,8 @@ export const readPeResourceLayout = (bytes: Buffer) => {
const section = candidates[0];
if (section === undefined) return peFailure("Missing resource section.");
const within = rva - section.rva;
if (size > virtualExtent(section) - within)
peFailure("Resource range leaves its section's virtual size.");
if (within > section.size || size > section.size - within)
peFailure("Resource range is not entirely file-backed in one section.");
// A second section starting inside the requested interval is ambiguous too.
@@ -119,7 +125,7 @@ export const readPeResourceLayout = (bytes: Buffer) => {
(other) =>
other !== section &&
other.rva < rva + size &&
other.rva + Math.max(other.size, other.virtualSize) > rva,
other.rva + virtualExtent(other) > rva,
)
)
peFailure("Resource interval intersects overlapping virtual sections.");
+59
View File
@@ -259,3 +259,62 @@ describe("PE header admission and bounded traversal", () => {
await expect(parsing).rejects.toThrow();
});
});
describe("PE resource section virtual extent", () => {
const sectionSizes = (fixture: ReturnType<typeof peResourceFixture>) => {
const virtualSize = fixture.bytes.readUInt32LE(fixture.sectionAt + 8);
const rawSize = fixture.bytes.readUInt32LE(fixture.sectionAt + 16);
expect(rawSize - virtualSize).toBeGreaterThanOrEqual(8);
return { virtualSize, rawSize };
};
it.each([
[0, "Resource RVA has missing or overlapping section mappings."],
[-2, "Resource range leaves its section's virtual size."],
])(
"rejects a payload reaching raw alignment padding past VirtualSize (start %i)",
async (start, message) => {
const fixture = peResourceFixture();
const { virtualSize } = sectionSizes(fixture);
const rva = 0x2000 + virtualSize + start;
fixture.bytes.writeUInt32LE(rva, fixture.dataOffsets[0]);
fixture.bytes.writeUInt32LE(4, (fixture.dataOffsets[0] ?? 0) + 4);
await expect(parse(fixture.bytes)).rejects.toMatchObject({
reason: "format",
message,
});
},
);
it("maps raw bytes when VirtualSize is zero, as the image loader does", async () => {
const fixture = peResourceFixture();
fixture.bytes.writeUInt32LE(0, fixture.sectionAt + 8);
expect(await parse(fixture.bytes)).toMatchObject({
coverage: { status: "complete", resources: 5 },
});
});
it("selects a later section that starts inside earlier raw padding", async () => {
const fixture = peResourceFixture();
const { virtualSize } = sectionSizes(fixture);
const laterRva = 0x2000 + Math.ceil(virtualSize / 4) * 4;
const rawOffset = fixture.bytes.length;
const payload = Buffer.from("LATE");
const bytes = Buffer.concat([fixture.bytes, payload]);
bytes.writeUInt16LE(2, 0x86);
const later = fixture.sectionAt + 40;
bytes.writeUInt32LE(payload.length, later + 8);
bytes.writeUInt32LE(laterRva, later + 12);
bytes.writeUInt32LE(payload.length, later + 16);
bytes.writeUInt32LE(rawOffset, later + 20);
bytes.writeUInt32LE(laterRva, fixture.dataOffsets[0]);
bytes.writeUInt32LE(payload.length, (fixture.dataOffsets[0] ?? 0) + 4);
const result = peResourcesSchema.parse(await parse(bytes));
expect(result.resources[0]?.payload).toMatchObject({
rva: laterRva,
location: { offset: rawOffset, bytes: payload.length },
sha256: createHash("sha256").update(payload).digest("hex"),
});
});
});