fix(managed): retain facts without inventing delta identities

Recognize baseline-dependent #JTD metadata at the shared layout owner before table sizing and decoding. Report unsupported partial metadata across all inspection workflows, preserve admitted PE/CLI and metadata header facts, and keep member identities and inventories unknown without the baseline.

A generated PE carrier reproduced complete coverage and a wrong standalone MethodDef token before the fix. Producer and actual CLI/MCP checks now cover all three inspections, normal metadata, retained comparison after source deletion, exact caller selection, source digests, unchanged bytes, and known header facts. Released .NET writer/reader sources establish framing applicability; natural PE occurrence and real CLR/Edit-and-Continue execution were not verified.
This commit is contained in:
morluto committed 2026-10-24 07:31:55 +08:00
1 parent 36f528efcb
commit fc2dddc1d9
10 files changed
+419 -28

No files matched your search

+19
View File
@@ -206,6 +206,25 @@ reports `resource-limit` with unavailable or partial coverage; omitted facts do
not become empty observations. Ambiguous PE section mappings are rejected
rather than interpreted according to section order.
Minimal Edit-and-Continue metadata marked by `#JTD` is not a standalone
assembly inventory. The pinned [.NET 9 reader](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/MetadataReader.cs)
uses different reference widths for this mode, and delta handles require the
baseline and prior generations. The released
[metadata serializer](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/MetadataBuilder.cs)
emits this marker with `#-`; the
[PE writer](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Reflection.Metadata/src/System/Reflection/PortableExecutable/ManagedPEBuilder.cs)
can embed that metadata root. REA reports `unsupported-metadata` at the marker
header before interpreting table rows, preserving the selected path, artifact
digest, parsed metadata version and stream names, and admitted PE/CLI facts.
Metadata status is partial; member and native
declaration inventories are unavailable. Empty collections do not establish
absence, and a requested type name is not declared unmatched. Ordinary
standalone `#~` and `#-` metadata keep their existing interpretation. This does
not add raw `.dmeta` input, baseline attachment or delta reconstruction.
Regression coverage uses authored metadata in a generated PE shell; it does
not establish naturally emitted application PEs, CLR loadability or a real
Edit-and-Continue execution.
### Selected member inspection
Member inspection admits type names, tokens, list ranges/counts, nesting facts,
+1
View File
@@ -400,6 +400,7 @@ const managedParseIssueSchema = z.object({
"invalid-stream",
"missing-stream",
"invalid-tables",
"unsupported-metadata",
"invalid-row",
"invalid-heap-index",
"invalid-string",
+27 -9
View File
@@ -26,7 +26,11 @@ import {
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import {
ManagedReaderFailure,
managedMetadataFailureStatus,
type ManagedMetadataHeaderFacts,
} from "./ManagedReaderFailure.js";
const flagNames = (flags: number): string[] => {
const names: string[] = [];
@@ -313,6 +317,7 @@ interface PartialMetadataContext {
readonly bytes: Buffer;
readonly layout: ManagedPeLayout;
readonly issue: ManagedParseIssue;
readonly metadataHeader?: ManagedMetadataHeaderFacts | null;
}
const partialMetadataResult = ({
@@ -320,10 +325,14 @@ const partialMetadataResult = ({
bytes,
layout,
issue,
metadataHeader,
}: PartialMetadataContext): ManagedArtifactInspection => ({
...base(target, bytes, layout),
classification: {
status: issue.code === "resource-limit" ? "managed" : "malformed",
status:
managedMetadataFailureStatus(issue) === "partial"
? "managed"
: "malformed",
container: "pe",
runtime_family: runtimeFamilyFromCliHeader(layout.cli),
implementation: "unknown",
@@ -337,9 +346,9 @@ const partialMetadataResult = ({
],
},
metadata: {
status: issue.code === "resource-limit" ? "partial" : "malformed",
version: null,
stream_names: [],
status: managedMetadataFailureStatus(issue),
version: metadataHeader?.version ?? null,
stream_names: [...(metadataHeader?.streamNames ?? [])],
table_row_counts: {},
},
module: null,
@@ -350,9 +359,11 @@ const partialMetadataResult = ({
attributes: [],
coverage: { state: "partial", issues: [issue] },
limitations: [
issue.code === "resource-limit"
? "CLI metadata inspection stopped at its representation budget; omitted facts are unknown."
: "CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.",
issue.code === "unsupported-metadata"
? issue.detail
: issue.code === "resource-limit"
? "CLI metadata inspection stopped at its representation budget; omitted facts are unknown."
: "CLI metadata could not be read because the PE/CLI structure is malformed or unsupported.",
],
});
@@ -385,6 +396,7 @@ export const inspectManagedArtifactBytes = (
bytes,
layout,
issue: cause.issue,
metadataHeader: cause.metadataHeader,
}),
),
);
@@ -445,7 +457,13 @@ export const inspectManagedArtifactBytes = (
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return managedArtifactInspectionSchema.parse(
admitManagedProjection(
partialMetadataResult({ target, bytes, layout, issue: cause.issue }),
partialMetadataResult({
target,
bytes,
layout,
issue: cause.issue,
metadataHeader: cause.metadataHeader,
}),
),
);
}
+13 -10
View File
@@ -25,6 +25,8 @@ import {
appendManagedParseIssues,
ManagedReaderFailure,
uniqueManagedParseIssues,
managedMetadataFailureStatus,
type ManagedMetadataHeaderFacts,
} from "./ManagedReaderFailure.js";
import {
parseTypes,
@@ -50,8 +52,14 @@ import {
const unavailable = (
target: BinaryTarget,
bytes: Buffer,
issue: ManagedParseIssue | null,
typeNames: readonly string[] | undefined,
{
issue,
metadataHeader,
}: {
readonly issue: ManagedParseIssue | null;
readonly metadataHeader?: ManagedMetadataHeaderFacts | null;
},
): ManagedMemberInspection =>
managedMemberInspectionSchema.parse(
admitManagedProjection({
@@ -63,13 +71,8 @@ const unavailable = (
},
module: null,
metadata: {
status:
issue === null
? "absent"
: issue.code === "resource-limit"
? "partial"
: "malformed",
version: null,
status: issue === null ? "absent" : managedMetadataFailureStatus(issue),
version: metadataHeader?.version ?? null,
table_row_counts: {},
},
identity_scope: {
@@ -185,7 +188,7 @@ export const inspectManagedMembersBytes = (
);
const pe = readManagedPeLayout(bytes);
if (pe.cli === null)
return unavailable(target, bytes, pe.cliIssue, typeNames);
return unavailable(target, bytes, typeNames, { issue: pe.cliIssue });
const issues: ManagedParseIssue[] = [];
let recover: ReturnType<typeof prepareManagedMemberRecovery> | undefined;
try {
@@ -429,7 +432,7 @@ export const inspectManagedMembersBytes = (
cause instanceof ManagedRepresentationFailure
)
return recover(cause.issue.offset);
return unavailable(target, bytes, cause.issue, typeNames);
return unavailable(target, bytes, typeNames, cause);
}
throw cause;
}
+73
View File
@@ -0,0 +1,73 @@
import { expect, it } from "vitest";
import { managedMetadataDelta } from "../../tests/fixtures/managedMetadataDelta.js";
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
import { managedPeFixtureTarget } from "./ManagedPe.fixture.js";
import {
managedFailure,
managedInspectionInputFailure,
} from "./ManagedReaderFailure.js";
it("keeps minimal EnC delta metadata unsupported instead of assigning standalone identities", () => {
const { bytes, markerOffset } = managedMetadataDelta();
const target = managedPeFixtureTarget(bytes);
const issue = {
code: "unsupported-metadata",
scope: "metadata.#JTD",
offset: markerOffset,
detail: expect.stringContaining("baseline"),
};
const artifact = inspectManagedArtifactBytes(bytes, target);
const members = inspectManagedMembersBytes(bytes, target, {
typeNames: ["Fixture.Program"],
});
const boundaries = inspectManagedNativeBoundariesBytes(bytes, target);
expect(artifact).toMatchObject({
classification: { status: "managed", implementation: "unknown" },
metadata: {
status: "partial",
version: "v4.0.30319",
stream_names: ["#-", "#Strings", "#US", "#GUID", "#Blob", "#JTD"],
table_row_counts: {},
},
coverage: { state: "partial", issues: [issue] },
module: null,
assembly: null,
});
for (const report of [members, boundaries]) {
expect(report).toMatchObject({
artifact: { sha256: target.sha256 },
metadata: {
status: "partial",
version: "v4.0.30319",
table_row_counts: {},
},
coverage: { state: "unavailable", issues: [issue] },
module: null,
identity_scope: { requires_mvid: null },
});
}
expect(members.selection).toEqual({
type_names: ["Fixture.Program"],
unmatched_type_names: [],
});
expect(members.methods).toEqual([]);
expect(members.types).toEqual([]);
expect(members.fields).toEqual([]);
expect(members.user_strings).toBeNull();
expect(boundaries.pinvoke_imports).toEqual([]);
expect(
managedInspectionInputFailure(
"inspect_managed_members",
target.path,
managedFailure(
"unsupported-metadata",
"metadata.#JTD",
"Missing baseline",
markerOffset,
),
),
).toBeUndefined();
});
+27 -3
View File
@@ -2,7 +2,10 @@ import {
attachManagedDecodeBudget,
ManagedDecodeBudget,
} from "./ManagedDecodeBudget.js";
import { managedFailure } from "./ManagedReaderFailure.js";
import {
managedFailure,
ManagedReaderFailure,
} from "./ManagedReaderFailure.js";
import {
computeRowSize,
type RowSizeContext,
@@ -214,6 +217,7 @@ const tableSize = (index: number, rows: readonly number[]): 2 | 4 =>
interface ParsedStreams {
readonly streams: ReadonlyMap<string, MetadataStream>;
readonly version: string;
readonly minimalDeltaOffset: number | null;
}
const readMetadataStreams = (
@@ -249,6 +253,7 @@ const readMetadataStreams = (
);
cursor += 4;
const streams = new Map<string, MetadataStream>();
let minimalDeltaOffset: number | null = null;
for (let index = 0; index < streamCount; index += 1) {
requireRange(bytes, rootOffset, cursor, 8, "metadata.stream-header");
const relativeOffset = bytes.readUInt32LE(cursor);
@@ -273,9 +278,10 @@ const readMetadataStreams = (
offset: rootOffset + relativeOffset,
size: streamSize,
});
if (named.name === "#JTD") minimalDeltaOffset = rootOffset + cursor;
cursor = named.next;
}
return { streams, version };
return { streams, version, minimalDeltaOffset };
};
interface HeapSizes {
@@ -442,7 +448,25 @@ export const readManagedMetadataLayout = (
);
const bytes = artifact.subarray(rootOffset, rootOffset + size);
const budget = new ManagedDecodeBudget();
const { streams, version } = readMetadataStreams(bytes, rootOffset, budget);
const { streams, version, minimalDeltaOffset } = readMetadataStreams(
bytes,
rootOffset,
budget,
);
// Runtime v9.0.0 MetadataReader recognizes #JTD before sizing references.
// Minimal deltas also use baseline coordinates and cumulative heap handles;
// widening local rows alone cannot establish standalone member identities.
if (minimalDeltaOffset !== null)
throw new ManagedReaderFailure(
{
code: "unsupported-metadata",
scope: "metadata.#JTD",
detail:
"Minimal Edit-and-Continue metadata (#JTD) requires its baseline and prior generations; standalone metadata identities and inventories are unknown.",
offset: minimalDeltaOffset,
},
{ metadataHeader: { version, streamNames: [...streams.keys()] } },
);
const { tableBytes, tablesOffset, heaps, valid } = readTablesHeader(
artifact,
streams,
+10 -2
View File
@@ -22,6 +22,8 @@ import {
import {
appendManagedParseIssues,
ManagedReaderFailure,
managedMetadataFailureStatus,
type ManagedMetadataHeaderFacts,
} from "./ManagedReaderFailure.js";
import {
buildNativeBoundaryInspection,
@@ -48,9 +50,11 @@ const emptyInspection = (
{
cli = null,
issues = [],
metadataHeader = null,
}: {
readonly cli?: ManagedCliHeader | null;
readonly issues?: readonly ManagedParseIssue[];
readonly metadataHeader?: ManagedMetadataHeaderFacts | null;
} = {},
): ManagedNativeBoundaryInspection => {
return managedNativeBoundaryInspectionSchema.parse(
@@ -63,12 +67,14 @@ const emptyInspection = (
},
module: null,
metadata: {
status: issues.some((issue) => issue.code === "resource-limit")
status: issues.some(
(issue) => managedMetadataFailureStatus(issue) === "partial",
)
? "partial"
: classification === "malformed"
? "malformed"
: "absent",
version: null,
version: metadataHeader?.version ?? null,
table_row_counts: {},
},
identity_scope: {
@@ -147,6 +153,7 @@ export const inspectManagedNativeBoundariesBytes = (
return emptyInspection(target, bytes, "malformed", {
cli: pe.cli,
issues: [cause.issue],
metadataHeader: cause.metadataHeader,
});
}
try {
@@ -190,6 +197,7 @@ export const inspectManagedNativeBoundariesBytes = (
return emptyInspection(target, bytes, "malformed", {
cli: pe.cli,
issues: [...issues, cause.issue],
metadataHeader: cause.metadataHeader,
});
}
};
+24 -4
View File
@@ -1,23 +1,43 @@
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
/** Bounded format failure retained as caller-visible managed coverage. */
/** Header facts admitted before an unsupported table layout was identified. */
export interface ManagedMetadataHeaderFacts {
readonly version: string;
readonly streamNames: readonly string[];
}
/** Bounded format or applicability failure retained as managed coverage. */
export class ManagedReaderFailure extends Error {
readonly metadataHeader: ManagedMetadataHeaderFacts | null;
constructor(
readonly issue: ManagedParseIssue,
options?: ErrorOptions,
options?: ErrorOptions & {
readonly metadataHeader?: ManagedMetadataHeaderFacts;
},
) {
super(issue.detail, options);
this.metadataHeader = options?.metadataHeader ?? null;
}
}
/** A resource or unsupported layout leaves metadata unknown, not malformed. */
export const managedMetadataFailureStatus = (
issue: ManagedParseIssue,
): "partial" | "malformed" =>
issue.code === "resource-limit" || issue.code === "unsupported-metadata"
? "partial"
: "malformed";
/** Malformed selected metadata is input failure for every inspection workflow. */
export const managedInspectionInputFailure = (
operation: string,
path: string,
cause: unknown,
): AnalysisInputError | undefined =>
cause instanceof ManagedReaderFailure && cause.issue.code !== "resource-limit"
cause instanceof ManagedReaderFailure &&
managedMetadataFailureStatus(cause.issue) === "malformed"
? new AnalysisInputError(operation, { cause }, [
{
path: ["path"],
@@ -46,7 +66,7 @@ export const readManagedValue = <Value>(
} catch (cause: unknown) {
if (
!(cause instanceof ManagedReaderFailure) ||
cause.issue.code === "resource-limit"
managedMetadataFailureStatus(cause.issue) === "partial"
)
throw cause;
issues.push(cause.issue);
@@ -0,0 +1,147 @@
import { createHash } from "node:crypto";
import { readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { parseEvidence } from "../../src/domain/evidence.js";
import {
managedArtifactInspectionSchema,
managedMemberInspectionSchema,
managedNativeBoundaryInspectionSchema,
} from "../../src/domain/managed/managedArtifact.js";
import { managedMemberComparisonResultSchema } from "../../src/domain/managed/managedMemberComparison.js";
import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
import { managedMetadataDelta } from "../fixtures/managedMetadataDelta.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"keeps EnC baseline-dependent metadata unknown through CLI and stdio MCP, then compares retained sources after deletion",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-managed-delta-");
const call = await cli.sharedMcpCall!();
const sources: ReturnType<typeof parseEvidence>[] = [];
for (const [name, bytes] of [
["delta", managedMetadataDelta().bytes],
["complete", buildManagedPeFixture()],
] as const) {
const path = join(root, `${name}.dll`);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(path, bytes);
for (const [operation, command] of [
["inspect_managed_artifact", "inspect-managed-artifact"],
["inspect_managed_members", "inspect-managed-members"],
[
"inspect_managed_native_boundaries",
"inspect-managed-native-boundaries",
],
] as const) {
const selected = operation === "inspect_managed_members";
const output = await cli.sharedCliRun!({
arguments: [
command,
path,
...(selected ? ["--type", "Fixture.Program"] : []),
"--json",
],
});
expect(output.exitCode, output.stderr).toBe(0);
const evidence = parseEvidence(output.json);
const schema =
operation === "inspect_managed_artifact"
? managedArtifactInspectionSchema
: selected
? managedMemberInspectionSchema
: managedNativeBoundaryInspectionSchema;
const report = schema.parse(evidence.normalized_result);
expect(report.artifact).toMatchObject({ path, sha256 });
expect(report.metadata.status).toBe(
name === "delta" ? "partial" : "complete",
);
expect(report.coverage.state).toBe(
name === "complete"
? "complete"
: selected || operation === "inspect_managed_native_boundaries"
? "unavailable"
: "partial",
);
if (name === "delta") {
expect(report.module).toBeNull();
expect(report.metadata.version).toBe("v4.0.30319");
expect(report.metadata.table_row_counts).toEqual({});
if ("identity_scope" in report)
expect(report.identity_scope.requires_mvid).toBeNull();
expect(report.coverage.issues).toEqual([
expect.objectContaining({
code: "unsupported-metadata",
scope: "metadata.#JTD",
detail: expect.stringContaining("baseline"),
}),
]);
if (operation === "inspect_managed_artifact") {
const artifact = managedArtifactInspectionSchema.parse(report);
expect(artifact.classification).toMatchObject({
status: "managed",
implementation: "unknown",
});
expect(artifact.pe.cli).toMatchObject({
flags: 1,
metadata_rva: expect.any(Number),
});
expect(artifact.metadata.stream_names).toEqual([
"#-",
"#Strings",
"#US",
"#GUID",
"#Blob",
"#JTD",
]);
}
} else expect(report.coverage.issues).toEqual([]);
if (selected) {
const members = managedMemberInspectionSchema.parse(report);
expect(members.selection).toEqual({
type_names: ["Fixture.Program"],
unmatched_type_names: [],
});
if (name === "delta") {
expect(members.method_rows).toBeNull();
expect(members.methods).toEqual([]);
expect(members.types).toEqual([]);
expect(members.fields).toEqual([]);
expect(members.user_strings).toBeNull();
} else
expect(members.method_rows).toEqual({
offset: 0,
count: 1,
total: 1,
});
sources.push(evidence);
}
const response = await call(operation, {
path,
...(selected ? { type_names: ["Fixture.Program"] } : {}),
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(
parseEvidence(response.structuredContent).normalized_result,
).toEqual(evidence.normalized_result);
}
expect(await readFile(path)).toEqual(bytes);
await rm(path);
}
const compared = await call("compare_managed_members", {
left: sources[0]!,
right: sources[1]!,
});
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
const report = managedMemberComparisonResultSchema.parse(
parseEvidence(compared.structuredContent).normalized_result,
);
expect(report.coverage.status).toBe("partial");
expect(report.summary).toMatchObject({ added: 0, removed: 0, unknown: 2 });
expect(report.evidence_links).toEqual(
sources.map(({ evidence_id }) => evidence_id),
);
},
);
+78
View File
@@ -0,0 +1,78 @@
import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
import { readManagedPeLayout } from "../../src/dotnet/ManagedPeReader.js";
const align4 = (size: number): number => Math.ceil(size / 4) * 4;
/**
* Authored minimal EnC metadata carried by an ordinary PE fixture shell.
* Matches runtime v9.0.0 MetadataBuilder/MetadataSizes: #- and empty #JTD,
* wide references despite small local tables, Module and EncMap rows.
* MethodDef physical row 1 maps to baseline token 0x06000002. This does not
* establish CLR loadability or execute an Edit-and-Continue operation.
*/
export const managedMetadataDelta = () => {
const bytes = buildManagedPeFixture({ targetFramework: null });
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("Fixture omitted CLI metadata");
const rootOffset = pe.rvaToOffset(
pe.cli.metadata.rva,
pe.cli.metadata.size,
"fixture metadata",
);
const strings = Buffer.from("\0Delta.dll\0Updated\0");
const tables = Buffer.alloc(align4(24 + 3 * 4 + 18 + 20 + 2 * 4 + 1));
tables[4] = 2;
tables[6] = 7;
tables[7] = 1;
tables.writeBigUInt64LE(1n | (1n << 6n) | (1n << 31n), 8);
tables.writeUInt32LE(1, 24); // Module
tables.writeUInt32LE(1, 28); // MethodDef
tables.writeUInt32LE(2, 32); // EncMap
tables.writeUInt16LE(1, 36); // Module.Generation
tables.writeUInt32LE(1, 38); // Module.Name
tables.writeUInt32LE(1, 42); // MVID
tables.writeUInt32LE(2, 46); // EncId
tables.writeUInt32LE(3, 50); // EncBaseId
tables.writeUInt16LE(0x0086, 54 + 6); // MethodDef.Flags
tables.writeUInt32LE(strings.indexOf("Updated"), 54 + 8);
tables.writeUInt32LE(1, 54 + 12); // Signature
tables.writeUInt32LE(1, 54 + 16); // ParamList, four bytes even with no Param rows
tables.writeUInt32LE(0x0000_0001, 74); // EncMap: Module
tables.writeUInt32LE(0x0600_0002, 78); // EncMap: MethodDef in baseline coordinates
const streams = [
{ name: "#-", data: tables },
{ name: "#Strings", data: strings },
{ name: "#US", data: Buffer.from([0]) },
{ name: "#GUID", data: Buffer.alloc(48, 0x41) },
{ name: "#Blob", data: Buffer.from([0, 3, 0, 0, 1]) },
{ name: "#JTD", data: Buffer.alloc(0) },
];
const headerSize =
32 +
streams.reduce((sum, { name }) => sum + 8 + align4(name.length + 1), 0);
const metadata = Buffer.alloc(
headerSize +
streams.reduce((sum, { data }) => sum + align4(data.length), 0),
);
metadata.writeUInt32LE(0x424a_5342);
metadata.writeUInt16LE(1, 4);
metadata.writeUInt16LE(1, 6);
metadata.writeUInt32LE(12, 12);
metadata.write("v4.0.30319", 16);
metadata.writeUInt16LE(streams.length, 30);
let header = 32;
let content = headerSize;
let markerOffset = 0;
for (const { name, data } of streams) {
metadata.writeUInt32LE(content, header);
metadata.writeUInt32LE(align4(data.length), header + 4);
metadata.write(name, header + 8);
metadata.set(data, content);
if (name === "#JTD") markerOffset = rootOffset + header;
header += 8 + align4(name.length + 1);
content += align4(data.length);
}
bytes.set(metadata, rootOffset);
bytes.writeUInt32LE(metadata.length, pe.cli.headerOffset + 12);
return { bytes, markerOffset };
};