fix(pe): bound forwarders to export directory

This commit is contained in:
morluto committed 2026-10-27 14:04:44 +08:00
1 parent 50822e1eff
commit 1fe92bff8e
3 files changed
+90 -3

No files matched your search

@@ -108,6 +108,47 @@ describe("PE export name coverage", () => {
});
});
describe("PE forwarder directory bounds", () => {
it.each([32, 64] as const)(
"requires the complete PE%d forwarder string to remain inside the export directory",
(bits) => {
const bytes = peExportImage({
bits,
functions: [{ rva: 0x11fc }, { rva: 0x2000 }],
names: [{ name: "Kept", index: 1 }],
});
// The string starts in .edata's declared export range but its NUL is
// outside it. The adjacent section bytes remain mapped and readable.
bytes.write("DLL.Target\0", 0x5fc, "latin1");
const result = parse(bytes);
expect(result.exports).toMatchObject([{ name: "Kept", ordinal: 2 }]);
expect(result.issues).toMatchObject([
{ code: "malformed_entry", location: "0x11fc" },
]);
expect(result.coverage).toMatchObject({
status: "partial",
examined_entries: 2,
exports: 1,
});
},
);
it.each([32, 64] as const)(
"accepts a PE%d forwarder whose NUL is the final export-directory byte",
(bits) => {
const bytes = peExportImage({ bits, functions: [{ rva: 0x11f5 }] });
bytes.write("DLL.Target\0", 0x5f5, "latin1");
expect(parse(bytes)).toMatchObject({
exports: [{ name: null, ordinal: 1, forwarder: "DLL.Target" }],
coverage: { status: "complete", exports: 1 },
issues: [],
});
},
);
});
describe("PE export directories", () => {
it.each([32, 64] as const)(
"reports named, aliased, ordinal-only and forwarded PE%d exports",
+11 -3
View File
@@ -132,12 +132,15 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const mappings = imageFileMappings(image);
const map = (rva: number, size: number): number =>
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
const cStringBytes = (rva: number): Buffer => {
const cStringBytes = (
rva: number,
maximumBytes = MAXIMUM_NAME_BYTES,
): Buffer => {
const value = readMappedCString(
bytes,
image,
base + BigInt(rva),
MAXIMUM_NAME_BYTES,
maximumBytes,
);
if (value.length === 0)
throw new BinaryFormatFailure(
@@ -216,7 +219,12 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
try {
target = {
address: null,
forwarder: cStringBytes(rva).toString("latin1"),
// PE forwarder strings, including their NUL, must fit within the
// export data directory. Ordinary export names have no such bound.
forwarder: cStringBytes(
rva,
Math.min(MAXIMUM_NAME_BYTES, forwarderEnd - rva - 1),
).toString("latin1"),
};
} catch (cause) {
issues.push({
@@ -13,6 +13,44 @@ import {
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("native export stable filesystem boundary", () => {
it("retains source identity and usable exports when a forwarder leaves its declared directory", async () => {
const root = await createTestTempDirectory("rea-native-export-forwarder-");
const path = join(root, "truncated-forwarder.dll");
const bytes = peExportImage({
functions: [{ rva: 0x11fc }, { rva: 0x2000 }],
names: [{ name: "Kept", index: 1 }],
});
bytes.write("DLL.Target\0", 0x5fc, "latin1");
await writeFile(path, bytes);
const service = createNativeExportsService();
try {
const result = await service.inspect({ path });
if (!result.ok) throw new Error(JSON.stringify(result.error));
const evidence = parseEvidence(result.value);
const sha256 = createHash("sha256").update(bytes).digest("hex");
expect(evidence.subject).toMatchObject({
local_path: path,
digest: { sha256 },
});
expect(
nativeExportsSchema.parse(evidence.normalized_result),
).toMatchObject({
artifact: { path, sha256, bytes: bytes.length },
exports: [{ name: "Kept", ordinal: 2 }],
issues: [
{
code: "malformed_entry",
location: "0x11fc",
message: expect.stringContaining("Forwarder of ordinal 1"),
},
],
coverage: { status: "partial", examined_entries: 2, exports: 1 },
});
} finally {
await service.close();
}
});
it("bounds retained PE aliases before interpreting their target", async () => {
const root = await createTestTempDirectory("rea-native-export-aliases-");
const service = createNativeExportsService();