mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(pe): preserve biased export ordinals
Accept the format-derived ordinal range for PE export slots.
This commit is contained in:
@@ -321,10 +321,12 @@ The complete Evidence identifies the stable artifact snapshot by path, byte
|
||||
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`
|
||||
with its biased `ordinal` (`OrdinalBase` plus the slot index), 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. Unused zero-address slots are omitted;
|
||||
ordinal-only exports have a null `name`. The
|
||||
ordinal-only exports have a null `name`. The name-ordinal table stores an
|
||||
unbiased 16-bit EAT index; reported biased ordinals may exceed 16 bits. 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
|
||||
|
||||
@@ -4,6 +4,9 @@ import { absoluteLocalPathStringSchema } from "../localPath.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const address = z.string().regex(/^0x[0-9a-f]+$/u);
|
||||
// PE ordinals are the DWORD OrdinalBase plus an index into the DWORD-sized
|
||||
// export address table. The last valid index is one less than its entry count.
|
||||
const MAX_PE_EXPORT_ORDINAL = 0xffffffff + 0xfffffffe;
|
||||
|
||||
/** Hard ceiling on the caller-selected input read budget. */
|
||||
export const MAX_INSPECT_NATIVE_EXPORTS_FILE_BYTES = 536870912;
|
||||
@@ -33,7 +36,7 @@ export type InspectNativeExportsInput = z.infer<
|
||||
*/
|
||||
const peExportSchema = z.strictObject({
|
||||
name: z.string().min(1).nullable(),
|
||||
ordinal: z.number().int().min(0).max(0xffff),
|
||||
ordinal: z.number().int().min(0).max(MAX_PE_EXPORT_ORDINAL),
|
||||
address: address.nullable(),
|
||||
forwarder: z.string().min(1).nullable(),
|
||||
});
|
||||
|
||||
@@ -115,16 +115,29 @@ describe("PE export directories", () => {
|
||||
expect(result.limitations[0]).toContain("declares no export directory");
|
||||
});
|
||||
|
||||
it("refuses ordinals beyond the 16-bit ordinal space", () => {
|
||||
expect(() =>
|
||||
parseNativeExports(
|
||||
it("preserves biased ordinals beyond WORD and DWORD", () => {
|
||||
const cases = [
|
||||
{ ordinalBase: 0x10000, index: 0, ordinal: 0x10000 },
|
||||
{ ordinalBase: 0xffffffff, index: 0, ordinal: 0xffffffff },
|
||||
{
|
||||
ordinalBase: 0xffffffff,
|
||||
index: 1,
|
||||
ordinal: 0x1_0000_0000,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { ordinalBase, index, ordinal } of cases) {
|
||||
const result = parse(
|
||||
peExportImage({
|
||||
functions: [{ rva: 0x2000 }],
|
||||
ordinalBase: 0xffff + 1,
|
||||
ordinalBase,
|
||||
functions: Array.from({ length: index + 1 }, (_, row) =>
|
||||
row === index ? { rva: 0x2000 } : null,
|
||||
),
|
||||
names: [{ name: "Named", index }],
|
||||
}),
|
||||
artifact,
|
||||
),
|
||||
).toThrow("16-bit ordinals");
|
||||
);
|
||||
expect(result.exports).toMatchObject([{ name: "Named", ordinal }]);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores a trailing unused slot after the last 16-bit ordinal", () => {
|
||||
|
||||
@@ -188,11 +188,6 @@ 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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { peExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
@@ -45,5 +46,73 @@ cliTest(
|
||||
},
|
||||
);
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"preserves biased PE ordinals above the name-table index width through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-pe-biased-ordinal-");
|
||||
const cases = [
|
||||
{ name: "word-base", base: 0x10000, index: 0, ordinal: 0x10000 },
|
||||
{
|
||||
name: "maximum-dword-base",
|
||||
base: 0xffffffff,
|
||||
index: 0,
|
||||
ordinal: 0xffffffff,
|
||||
},
|
||||
{
|
||||
name: "sum-above-dword",
|
||||
base: 0xffffffff,
|
||||
index: 1,
|
||||
ordinal: 0x1_0000_0000,
|
||||
},
|
||||
];
|
||||
const mcp =
|
||||
surface === "stdio MCP" ? await connectLocalToolsMcp() : undefined;
|
||||
|
||||
for (const fixture of cases) {
|
||||
const path = join(root, `${fixture.name}.dll`);
|
||||
const functions = Array.from({ length: fixture.index + 1 }, (_, row) =>
|
||||
row === fixture.index ? { rva: 0x1300 } : null,
|
||||
);
|
||||
await writeFile(
|
||||
path,
|
||||
peExportImage({
|
||||
ordinalBase: fixture.base,
|
||||
functions,
|
||||
names: [{ name: "Named", index: fixture.index }],
|
||||
}),
|
||||
);
|
||||
|
||||
let document: unknown;
|
||||
if (surface === "CLI") {
|
||||
const response = await cli.run({
|
||||
arguments: ["inspect-native-exports", path, "--json"],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
document = toolContract("inspect_native_exports").outputSchema.parse(
|
||||
response.json,
|
||||
);
|
||||
} else {
|
||||
const response = await mcp!.call("inspect_native_exports", { path });
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
document = response.structuredContent;
|
||||
}
|
||||
|
||||
const evidence = toolContract(
|
||||
"inspect_native_exports",
|
||||
).outputSchema.parse(document);
|
||||
expect(
|
||||
nativeExportsSchema.parse(evidence.normalized_result),
|
||||
).toMatchObject({
|
||||
format: "pe",
|
||||
coverage: {
|
||||
status: "complete",
|
||||
examined_entries: fixture.index + 1,
|
||||
exports: 1,
|
||||
},
|
||||
exports: [{ name: "Named", ordinal: fixture.ordinal }],
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user