Files
reverse-engineering-anything/tests/boundary/filesystem/nativeExports.test.ts
T
morluto 60b55be7e7 fix(native): bound PE export names before intermediate retention
Charge PE alias names when storing them, before target interpretation can suppress output rows. Validate unused-slot names as bytes without retaining or decoding strings. Keep row and forwarder accounting separate so names are charged once.

Preserve alias ordering, malformed unused-name issues and ordinal-only suppression. Add production service/provider retention regressions using compact shared-pointer fixtures.

Validation: 43 focused parser, filesystem, CLI and stdio MCP tests; fresh build, typecheck, lint and formatting passed. Three bounded Node 24.22.1 measurement pairs showed median process RSS high-water 154000 to 93168 KiB; timing and sampled family memory have cache/startup/sampling limits. No Windows loader acceptance or OOM reproduction claim.
2026-10-27 00:40:35 +08:00

203 lines
7.2 KiB
TypeScript

import { createHash } from "node:crypto";
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createNativeExportsService } from "../../../src/composition/binaryDiagnostics.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { nativeExportsSchema } from "../../../src/domain/native/nativeExports.js";
import {
elfExportImage,
peExportImage,
peRepeatedExportNamesImage,
} from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("native export stable filesystem boundary", () => {
it("bounds retained PE aliases before interpreting their target", async () => {
const root = await createTestTempDirectory("rea-native-export-aliases-");
const service = createNativeExportsService();
try {
// A malformed forwarder suppresses all rows, but its names still occupy
// the intermediate inventory and must be bounded before EAT traversal.
for (const target of [0x3000, 0x1080, 0]) {
const path = join(root, `aliases-${String(target)}.dll`);
const bytes = peRepeatedExportNamesImage(target);
expect(bytes.length).toBeLessThan(32_000);
await writeFile(path, bytes);
const result = await service.inspect({ path });
if (target === 0) {
if (!result.ok) throw new Error(JSON.stringify(result.error));
expect(result.value.normalized_result).toMatchObject({
exports: [],
issues: [],
coverage: { status: "complete", examined_entries: 1, exports: 0 },
});
} else {
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError" },
});
}
}
} finally {
await service.close();
}
});
it("binds PE and ELF export inventories to the selected file", async () => {
const root = await createTestTempDirectory("rea-native-exports-");
const service = createNativeExportsService();
for (const [name, bytes, format] of [
[
"fixture.dll",
peExportImage({
functions: [{ rva: 0x2000 }],
names: [{ name: "Exported", index: 0 }],
}),
"pe",
],
[
"libfixture.so",
elfExportImage({ symbols: [{ name: "exported" }] }),
"elf",
],
] as const) {
const path = join(root, name);
await writeFile(path, bytes);
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).toMatchObject({
operation: "inspect_native_exports",
provider: { id: "rea-native-exports" },
confidence: "observed",
subject: { local_path: path, format, digest: { sha256 } },
});
expect(
nativeExportsSchema.parse(evidence.normalized_result),
).toMatchObject({
artifact: { path, sha256, bytes: bytes.length },
format,
coverage: { status: "complete", exports: 1 },
});
}
await service.close();
});
});
describe("native export encoding and failures", () => {
it("keeps ELF symbol bytes while exposing valid UTF-8 text", async () => {
const root = await createTestTempDirectory("rea-native-export-unicode-");
const path = join(root, "libfixture.so");
const bytes = elfExportImage({
symbols: [{ name: "café", version: 2 }, { name: "\u00ff" }],
versionDefinitions: ["libfixture.so", "libcafé.so"],
});
await writeFile(path, bytes);
const service = createNativeExportsService();
const result = await service.inspect({ path });
if (!result.ok) throw new Error(JSON.stringify(result.error));
expect(result.value.normalized_result).toMatchObject({
format: "elf",
exports: [
{
name: "café",
name_bytes_base64: "Y2Fmw6k=",
name_utf8_decodable: true,
version: {
index: 2,
name: "libcafé.so",
name_bytes_base64: "bGliY2Fmw6kuc28=",
name_utf8_decodable: true,
},
},
{
name: "ÿ",
name_bytes_base64: "/w==",
name_utf8_decodable: false,
},
],
});
const olderRows = JSON.parse(
JSON.stringify(result.value.normalized_result),
) as { exports: Record<string, unknown>[] };
for (const row of olderRows.exports) {
delete row.name_bytes_base64;
delete row.name_utf8_decodable;
const version = row.version;
if (version !== null && typeof version === "object") {
Reflect.deleteProperty(version, "name_bytes_base64");
Reflect.deleteProperty(version, "name_utf8_decodable");
}
}
expect(nativeExportsSchema.parse(olderRows)).toMatchObject({
exports: [
{
name: "café",
name_bytes_base64: null,
name_utf8_decodable: null,
version: {
index: 2,
name: "libcafé.so",
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
},
{ name: "ÿ", name_bytes_base64: null, name_utf8_decodable: null },
],
});
await service.close();
});
it("returns typed input, unsupported, budget and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-native-export-failures-");
const path = join(root, "fixture.dll");
const alias = join(root, "alias.dll");
const macho = join(root, "fixture.dylib");
const malformed = join(root, "malformed.dll");
await writeFile(path, peExportImage({ functions: [] }));
const machoBytes = Buffer.alloc(64);
machoBytes.writeUInt32LE(0xfeedfacf, 0);
await writeFile(macho, machoBytes);
await writeFile(malformed, Buffer.from("MZ not a PE image"));
await symlink(path, alias);
const service = createNativeExportsService();
const failure = async (
input: Record<string, unknown>,
signal?: AbortSignal,
) => {
const result = await service.inspect(
input,
signal === undefined ? undefined : { signal },
);
if (result.ok) throw new Error("Expected a failure");
return result.error;
};
for (const [candidate, reason] of [
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing.dll"), "invalid_value"],
] as const)
expect(await failure({ path: candidate })).toMatchObject({
_tag: "AnalysisInputError",
operation: "inspect_native_exports",
issues: [{ path: ["path"], reason }],
});
expect(await failure({ path: macho })).toMatchObject({
_tag: "AnalysisUnsupportedTargetError",
reason: expect.stringContaining("inspect_macho"),
});
expect(await failure({ path, max_file_bytes: 64 })).toMatchObject({
_tag: "AnalysisResourceConstraintError",
});
expect(
await failure({ path }, AbortSignal.abort("caller stop")),
).toMatchObject({ _tag: "AnalysisCancelledError" });
await service.close();
});
});