fix(pe): ignore unused export ordinal slots

Fixes #6577
This commit is contained in:
morluto committed 2026-10-23 18:07:45 +08:00
1 parent ce48f43550
commit 379f37307a
4 files changed
+72 -6

No files matched your search

+2 -1
View File
@@ -323,7 +323,8 @@ count and SHA-256.
- PE rows reproduce the export directory: each used export-address-table slot
with its `ordinal`, one row per name (aliases share an ordinal), `address`
as the preferred-base virtual address, or `forwarder` text when the slot
forwards to another DLL. Ordinal-only exports have a null `name`. The
forwards to another DLL. Unused zero-address slots are omitted;
ordinal-only exports have a null `name`. The
directory's DLL name, ordinal base and timestamp are reported, and an image
without an export directory is a complete empty inventory.
- ELF rows list defined `.dynsym` symbols with global, weak or GNU-unique
@@ -127,6 +127,22 @@ describe("PE export directories", () => {
).toThrow("16-bit ordinals");
});
it("ignores a trailing unused slot after the last 16-bit ordinal", () => {
const result = parse(
peExportImage({
ordinalBase: 0xffff,
functions: [{ rva: 0x2000 }, null],
names: [{ name: "Kept", index: 0 }],
}),
);
expect(result).toMatchObject({
coverage: { status: "complete", examined_entries: 2, exports: 1 },
issues: [],
exports: [{ name: "Kept", ordinal: 0xffff }],
});
});
it("refuses an export table above the inventory allowance", () => {
expect(() =>
parseNativeExports(
+5 -5
View File
@@ -134,11 +134,6 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const functionCount = reader.u32(header + 20);
const nameCount = reader.u32(header + 24);
rowBudget(functionCount, "PE export address table");
if (ordinalBase + functionCount > 0x10000)
throw new BinaryFormatFailure(
"malformed",
"PE export ordinal base and address-table size exceed 16-bit ordinals",
);
rowBudget(nameCount, "PE export name table");
const functions =
functionCount === 0 ? 0 : map(reader.u32(header + 28), functionCount * 4);
@@ -193,6 +188,11 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
// A zero slot is an unused ordinal, not an export.
if (rva === 0) continue;
const ordinal = ordinalBase + index;
if (ordinal > 0xffff)
throw new BinaryFormatFailure(
"malformed",
"PE exported ordinal exceeds 16-bit ordinals",
);
let target: Pick<PeExport, "address" | "forwarder">;
if (rva >= directory.virtual_address && rva < forwarderEnd) {
try {
@@ -0,0 +1,49 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { nativeExportsSchema } from "../../../src/domain/native/nativeExports.js";
import { peExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"reports the last PE ordinal when an unused EAT slot follows it",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-export-last-ordinal-");
const path = join(root, "exports.dll");
await writeFile(
path,
peExportImage({
ordinalBase: 0xffff,
functions: [{ rva: 0x1300 }, null],
names: [{ name: "Kept", index: 0 }],
}),
);
const result = await cli.run({
arguments: ["inspect-native-exports", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_native_exports").outputSchema.parse(
result.json,
);
expect(nativeExportsSchema.parse(evidence.normalized_result)).toMatchObject(
{
format: "pe",
coverage: { status: "complete", examined_entries: 2, exports: 1 },
issues: [],
exports: [
{
name: "Kept",
ordinal: 0xffff,
address: "0x180001300",
forwarder: null,
},
],
},
);
},
30_000,
);