* fix(managed): report malformed PE layouts as invalid input readManagedPeLayout raises ManagedReaderFailure for DOS, COFF and section header failures such as section data past EOF. Those escaped the managed inspectors, so ManagedStaticProvider wrapped them as ProviderAdapterError and the CLI and MCP reported execution_failure with advice to run rea doctor. Map format failures to an invalid_format input issue carrying the reader's detail, as inspect-pe-resources and inspect-go-binary do. Fixes #2191 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(managed): route unmappable PE layouts through one input error inspect-managed-native-boundaries caught a layout-level ManagedReaderFailure itself and returned an empty malformed inspection with exit 0, while the other managed operations now report the same bytes as invalid input. Let the layout failure reach the provider's invalid_format mapping; metadata failures after a readable layout still return malformed coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect } from "vitest";
|
|
|
|
import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
|
|
cliTest(
|
|
"managed CLI retains metadata and CIL after repeated local constructor owner lookups",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-row-lookup-");
|
|
const path = join(root, "local-attributes.dll");
|
|
await writeFile(
|
|
path,
|
|
buildManagedPeFixture({
|
|
additionalEmptyTypes: 512,
|
|
customAttributeRowCount: 512,
|
|
customAttributeTypeRaw: (1 << 3) | 2,
|
|
typeName: "LookupAttribute",
|
|
extendsRaw: 0,
|
|
methodName: ".ctor",
|
|
methodFlags: 0x1886,
|
|
methodSignature: Buffer.from([0x20, 0x01, 0x01, 0x0e]),
|
|
ilBody: Buffer.from([0x06, 0x2a]),
|
|
}),
|
|
);
|
|
const artifact = await cli.run({
|
|
arguments: ["inspect-managed-artifact", path, "--json"],
|
|
});
|
|
expect(artifact.exitCode).toBe(0);
|
|
expect(artifact.json).toMatchObject({
|
|
normalized_result: {
|
|
coverage: { state: "complete", issues: [] },
|
|
attributes: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type_name: "Fixture.LookupAttribute",
|
|
constructor_token: "0x06000001",
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
expect(artifact.json).toHaveProperty(
|
|
"normalized_result.attributes.length",
|
|
512,
|
|
);
|
|
const members = await cli.run({
|
|
arguments: ["inspect-managed-members", path, "--json"],
|
|
});
|
|
expect(members.exitCode).toBe(0);
|
|
expect(members.json).toMatchObject({
|
|
normalized_result: {
|
|
coverage: { state: "complete", issues: [] },
|
|
types: expect.arrayContaining([
|
|
expect.objectContaining({ full_name: "Fixture.Empty0" }),
|
|
expect.objectContaining({ full_name: "Fixture.Empty511" }),
|
|
expect.objectContaining({ full_name: "Fixture.LookupAttribute" }),
|
|
]),
|
|
methods: [
|
|
expect.objectContaining({
|
|
name: ".ctor",
|
|
declaring_type: "Fixture.LookupAttribute",
|
|
body: expect.objectContaining({
|
|
status: "present",
|
|
instruction_count: 1,
|
|
}),
|
|
}),
|
|
],
|
|
},
|
|
});
|
|
expect(members.json).toHaveProperty("normalized_result.types.length", 513);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI separates .NET Standard API targets from runtime evidence",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-runtime-family-");
|
|
for (const [name, targetFramework, readyToRun, runtimeFamily] of [
|
|
["standard", ".NETStandard,Version=v2.0", false, "unknown"],
|
|
["core", ".NETCoreApp,Version=v8.0", false, "modern-dotnet"],
|
|
["standard-r2r", ".NETStandard,Version=v2.0", true, "modern-dotnet"],
|
|
] as const) {
|
|
const path = join(root, `${name}.dll`);
|
|
await writeFile(
|
|
path,
|
|
buildManagedPeFixture({ targetFramework, readyToRun }),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-managed-artifact", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
operation: "inspect_managed_artifact",
|
|
subject: { local_path: path },
|
|
normalized_result: {
|
|
classification: {
|
|
status: "managed",
|
|
runtime_family: runtimeFamily,
|
|
implementation: readyToRun ? "cil-and-ready-to-run" : "cil",
|
|
evidence: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: readyToRun
|
|
? "ready-to-run-signature-observed"
|
|
: "target-framework-observed",
|
|
detail: expect.stringContaining(
|
|
readyToRun
|
|
? "ReadyToRun signature"
|
|
: name === "standard"
|
|
? "does not identify a runtime family"
|
|
: "names modern .NET",
|
|
),
|
|
file_offset: expect.any(Number),
|
|
}),
|
|
]),
|
|
},
|
|
target_frameworks: [targetFramework],
|
|
coverage: { state: "complete", issues: [] },
|
|
},
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI preserves evidence, filters, envelopes, and bounded token counts",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-cli-output-");
|
|
const path = join(root, "fixture.dll");
|
|
await writeFile(path, buildManagedPeFixture());
|
|
const argv = ["inspect-managed-members", path, "--json"];
|
|
const result = await cli.run({ arguments: argv });
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
operation: "inspect_managed_members",
|
|
provider: { id: "rea-dotnet-static" },
|
|
subject: { local_path: path },
|
|
normalized_result: {
|
|
methods: [expect.objectContaining({ token: expect.any(String) })],
|
|
},
|
|
});
|
|
const filtered = await cli.run({
|
|
arguments: [...argv, "--filter-output", "operation", "--full-output"],
|
|
});
|
|
expect(filtered.exitCode).toBe(0);
|
|
expect(filtered.json).toMatchObject({
|
|
ok: true,
|
|
data: "inspect_managed_members",
|
|
meta: {
|
|
command: "inspect-managed-members",
|
|
duration: expect.stringMatching(/^\d+ms$/u),
|
|
},
|
|
});
|
|
const count = await cli.run({
|
|
arguments: [...argv, "--filter-output", "operation", "--token-count"],
|
|
});
|
|
expect(count.exitCode).toBe(0);
|
|
expect(count.stdout).toMatch(/^\d+\n$/u);
|
|
const omittedCount = await cli.run({
|
|
arguments: [...argv, "--filter-output", "toString", "--token-count"],
|
|
});
|
|
expect(omittedCount.exitCode).toBe(0);
|
|
expect(omittedCount.stdout).toBe("0\n");
|
|
const jsonl = await cli.run({
|
|
arguments: [
|
|
"inspect-managed-members",
|
|
path,
|
|
"--format",
|
|
"jsonl",
|
|
"--filter-output",
|
|
"operation",
|
|
],
|
|
});
|
|
expect(jsonl.exitCode).toBe(0);
|
|
expect(jsonl.json).toBe("inspect_managed_members");
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI rejects repeated or empty type selections as invalid input",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-cli-types-");
|
|
const path = join(root, "fixture.dll");
|
|
await writeFile(path, buildManagedPeFixture());
|
|
for (const [types, message] of [
|
|
[["Fixture.Empty0", "Fixture.Empty0"], "must not repeat a name"],
|
|
[[""], ">=1 characters"],
|
|
] as const) {
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-managed-members",
|
|
path,
|
|
...types.flatMap((type) => ["--type", type]),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "VALIDATION_ERROR",
|
|
fieldErrors: [
|
|
expect.objectContaining({
|
|
path: expect.stringMatching(/^type/u),
|
|
message: expect.stringContaining(message),
|
|
}),
|
|
],
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI bounds CIL claims by section virtual ranges",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-virtual-size-");
|
|
for (const virtualSectionSize of [0, 0x800, 0x801, 0x802]) {
|
|
// The two-byte tiny method starts at RVA 0x2800. Its header alone fits
|
|
// at size 0x801, and its full CIL fits exactly at size 0x802.
|
|
const bytes = buildManagedPeFixture({
|
|
virtualSectionSize,
|
|
ilBody: Buffer.from([0x06, 0x2a]),
|
|
});
|
|
const path = join(root, `virtual-${virtualSectionSize}.dll`);
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-managed-members", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
normalized_result:
|
|
virtualSectionSize === 0
|
|
? {
|
|
methods: [],
|
|
metadata: { status: "malformed" },
|
|
coverage: { state: "unavailable" },
|
|
}
|
|
: {
|
|
methods: [
|
|
{
|
|
rva: 0x2800,
|
|
body:
|
|
virtualSectionSize === 0x802
|
|
? {
|
|
status: "present",
|
|
normalized_il_sha256: expect.any(String),
|
|
}
|
|
: {
|
|
status: "malformed",
|
|
normalized_il_sha256: null,
|
|
},
|
|
},
|
|
],
|
|
coverage: {
|
|
state: virtualSectionSize === 0x802 ? "complete" : "partial",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI distinguishes raw padding from overlapping virtual sections",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-section-identity-");
|
|
for (const virtualSectionSize of [0x800, 0x900]) {
|
|
const bytes = buildManagedPeFixture({ virtualSectionSize });
|
|
const coff = bytes.readUInt32LE(0x3c) + 4;
|
|
const firstSection = coff + 20 + bytes.readUInt16LE(coff + 16);
|
|
const secondSection = firstSection + 40;
|
|
bytes.writeUInt16LE(2, coff + 2);
|
|
bytes.write(".il", secondSection, "ascii");
|
|
bytes.writeUInt32LE(0x100, secondSection + 8);
|
|
bytes.writeUInt32LE(0x2800, secondSection + 12);
|
|
bytes.writeUInt32LE(0x100, secondSection + 16);
|
|
bytes.writeUInt32LE(0x0a00, secondSection + 20);
|
|
const path = join(root, `sections-${virtualSectionSize}.dll`);
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-managed-members", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
normalized_result: {
|
|
methods: [
|
|
{
|
|
rva: 0x2800,
|
|
body:
|
|
virtualSectionSize === 0x800
|
|
? {
|
|
status: "present",
|
|
file_offset: 0x0a00,
|
|
normalized_il_sha256: expect.any(String),
|
|
}
|
|
: {
|
|
status: "malformed",
|
|
normalized_il_sha256: null,
|
|
issue: expect.stringContaining("ambiguous overlapping"),
|
|
},
|
|
},
|
|
],
|
|
coverage: {
|
|
state: virtualSectionSize === 0x800 ? "complete" : "partial",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"managed CLI reports a section table past EOF as invalid input",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-managed-cli-sections-");
|
|
const path = join(root, "fixture.dll");
|
|
const bytes = buildManagedPeFixture();
|
|
const pe = bytes.readUInt32LE(0x3c);
|
|
const sections = pe + 24 + bytes.readUInt16LE(pe + 20);
|
|
bytes.writeUInt32LE(bytes.length + 0x1000, sections + 20);
|
|
await writeFile(path, bytes);
|
|
for (const command of [
|
|
"inspect-managed-artifact",
|
|
"inspect-managed-members",
|
|
"inspect-managed-native-boundaries",
|
|
]) {
|
|
const result = await cli.run({ arguments: [command, path, "--json"] });
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: expect.stringContaining("pe.section-data"),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
},
|
|
);
|