diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index 84c4abe3b0..f2b1da0fd1 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -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 diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts index 1237dc4382..3ac9b67615 100644 --- a/src/native/exports/NativeExportParser.test.ts +++ b/src/native/exports/NativeExportParser.test.ts @@ -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( diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts index d2d3eb9c55..87870b1c3e 100644 --- a/src/native/exports/NativeExportParser.ts +++ b/src/native/exports/NativeExportParser.ts @@ -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; if (rva >= directory.virtual_address && rva < forwarderEnd) { try { diff --git a/tests/acceptance/applications/nativeExportsOrdinalSlots.test.ts b/tests/acceptance/applications/nativeExportsOrdinalSlots.test.ts new file mode 100644 index 0000000000..fd6b6b085f --- /dev/null +++ b/tests/acceptance/applications/nativeExportsOrdinalSlots.test.ts @@ -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, +);