Files
reverse-engineering-anything/src/dotnet/ManagedMemberInspector.ts
T
morluto fc2dddc1d9 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.
2026-10-24 07:31:55 +08:00

440 lines
15 KiB
TypeScript

import {
admitManagedProjection,
managedDecodeBudget,
ManagedRepresentationFailure,
} from "./ManagedDecodeBudget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
type ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import {
readManagedPeLayout,
type ManagedPeLayout,
} from "./ManagedPeReader.js";
import {
readManagedResourceDirectory,
readManagedMetadataInventory,
readManagedModuleIdentity,
} from "./ManagedMetadataInventory.js";
import { managedMetadataTypeNames } from "./ManagedMetadataInventoryRows.js";
import { managedPeInspectionEnvelope } from "./ManagedPeInspectionEnvelope.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
appendManagedParseIssues,
ManagedReaderFailure,
uniqueManagedParseIssues,
managedMetadataFailureStatus,
type ManagedMetadataHeaderFacts,
} from "./ManagedReaderFailure.js";
import {
parseTypes,
readTypeDefinitions,
type DecodedTypeDefinition,
} from "./ManagedMemberInspectorCore.js";
import { edgeTargets } from "./ManagedMethodSpecs.js";
import { managedTypeNameDigests } from "./ManagedTypeNameDigests.js";
import { typeNameLookup } from "./ManagedTypeNames.js";
import { managedTypeReferences } from "./ManagedTypeReferences.js";
import { parseUserStrings } from "./ManagedUserStrings.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import { prepareManagedMemberRecovery } from "./ManagedMemberRecovery.js";
import {
edges,
parseExportedTypes,
parseFields,
parseMemberRefs,
parseMethods,
type SelectedTypeTokens,
} from "./ManagedMemberRows.js";
const unavailable = (
target: BinaryTarget,
bytes: Buffer,
typeNames: readonly string[] | undefined,
{
issue,
metadataHeader,
}: {
readonly issue: ManagedParseIssue | null;
readonly metadataHeader?: ManagedMetadataHeaderFacts | null;
},
): ManagedMemberInspection =>
managedMemberInspectionSchema.parse(
admitManagedProjection({
artifact: {
path: target.path,
sha256: target.sha256,
byte_length: bytes.length,
format: "pe",
},
module: null,
metadata: {
status: issue === null ? "absent" : managedMetadataFailureStatus(issue),
version: metadataHeader?.version ?? null,
table_row_counts: {},
},
identity_scope: {
token_identity: "build-local",
requires_artifact_sha256: target.sha256,
requires_mvid: null,
},
types: [],
signature_resolution: "type-names-v1",
nesting_read: false,
exported_types: null,
user_strings: null,
fields: [],
methods: [],
member_refs: [],
call_edges: [],
field_accesses: [],
// Nothing was examined, so no requested name is known to be unmatched.
selection:
typeNames === undefined
? null
: { type_names: [...typeNames], unmatched_type_names: [] },
method_rows: null,
coverage: {
state: "unavailable",
issues: issue === null ? [] : [issue],
},
limitations: [
issue === null
? "The PE has no admitted CLI metadata; managed member inspection is unavailable."
: "The CLI metadata could not be admitted; managed member inspection is unavailable.",
],
}),
);
const readMemberLayout = (bytes: Buffer, pe: ManagedPeLayout) => {
const cli = pe.cli;
if (cli === null)
throw new TypeError("Managed inventory requires CLI metadata");
const rootOffset = pe.rvaToOffset(
cli.metadata.rva,
cli.metadata.size,
"cli.metadata",
);
const layout = readManagedMetadataLayout(
bytes,
rootOffset,
cli.metadata.size,
);
const memberLists = readTypeMemberListFacts(bytes, layout);
return { layout, memberLists };
};
const selectTypes = (
types: readonly DecodedTypeDefinition[],
typeNames: readonly string[] | undefined,
): {
readonly tokens: SelectedTypeTokens;
readonly selection: ManagedMemberInspection["selection"];
} => {
if (typeNames === undefined) return { tokens: null, selection: null };
const requested = new Set(typeNames);
const tokens = new Set<string>();
const matched = new Set<string>();
for (const { type } of types) {
const nested = (type.flags & 0x7) >= 2;
const spellings = nested ? [type.full_name, type.name] : [type.full_name];
for (const name of spellings) {
if (!requested.has(name)) continue;
tokens.add(type.token);
matched.add(name);
}
}
return {
tokens,
selection: {
type_names: [...typeNames],
unmatched_type_names: typeNames.filter((name) => !matched.has(name)),
},
};
};
/**
* Inspect metadata members and method bodies without loading target code.
* Selected type names restrict full decoding to members of those exact types.
* A caller that never reads literals, such as member comparison, can skip
* user strings; they are then reported as not examined.
*/
export const inspectManagedMembersBytes = (
bytes: Buffer,
target: BinaryTarget,
{
typeNames,
methodOffset = 0,
methodLimit = Number.MAX_SAFE_INTEGER,
userStrings: readUserStrings = true,
}: {
readonly typeNames?: readonly string[];
/** Zero-based offset into MethodDef rows. */
readonly methodOffset?: number;
readonly methodLimit?: number;
readonly userStrings?: boolean;
} = {},
): ManagedMemberInspection => {
if (
!Number.isSafeInteger(methodOffset) ||
methodOffset < 0 ||
!Number.isSafeInteger(methodLimit) ||
methodLimit < 0
)
throw new RangeError(
"methodOffset and methodLimit must be non-negative safe integers",
);
const pe = readManagedPeLayout(bytes);
if (pe.cli === null)
return unavailable(target, bytes, typeNames, { issue: pe.cliIssue });
const issues: ManagedParseIssue[] = [];
let recover: ReturnType<typeof prepareManagedMemberRecovery> | undefined;
try {
const { layout, memberLists } = readMemberLayout(bytes, pe);
const methodTotal = layout.table(6)?.rowCount ?? 0;
const methodRows = {
offset: Math.min(methodOffset, methodTotal),
count: Math.min(methodLimit, Math.max(0, methodTotal - methodOffset)),
total: methodTotal,
};
const identity = readManagedModuleIdentity(bytes, layout);
appendManagedParseIssues(issues, identity.issues);
const ranges = readTypeDefinitions(bytes, layout, issues, memberLists);
const parsedTypes = parseTypes(bytes, layout, ranges);
const { tokens: selected, selection } = selectTypes(
parsedTypes.types,
typeNames,
);
const unreadableNames =
typeNames === undefined ? 0 : ranges.length - parsedTypes.types.length;
appendManagedParseIssues(issues, parsedTypes.issues);
const typeReferences = managedTypeReferences(bytes, layout, issues);
const { operand: signatureTypeName, declaration: declarationTypeName } =
managedTypeNameDigests(layout, issues, parsedTypes, typeReferences);
const types = parsedTypes.types
.filter(({ type }) => selected === null || selected.has(type.token))
.map(({ row, type }) => {
managedDecodeBudget(layout).reserve(512, type.row_offset);
return {
...type,
name_sha256: declarationTypeName(2, row)?.toString("hex") ?? null,
};
});
const envelope = managedPeInspectionEnvelope(
target,
bytes.length,
layout,
identity.module,
issues,
);
const unnamedTypes = types.reduce(
(count, type) => count + Number(type.name_sha256 === null),
0,
);
const typeIdentityLimitations =
unnamedTypes === 0
? []
: [
`${String(unnamedTypes)} returned TypeDef name identity(s) remain unresolved; incomplete, missing, contradictory, cyclic or over-deep enclosing facts cannot be repaired by bare display names.`,
];
const selectionLimitations = [
...(selection === null
? []
: [
"Types, fields, methods, call edges, field accesses, and user strings cover only the selected declaring types; coverage describes that selection. Edge targets outside it keep their names but are not described.",
]),
...(unreadableNames === 0
? []
: [
`${String(unreadableNames)} TypeDef name(s) could not be read; a name in selection.unmatched_type_names may belong to one of them, so its absence is not observed.`,
]),
];
recover = prepareManagedMemberRecovery(
{
...envelope,
types,
nesting_read: parsedTypes.nesting.complete,
selection,
method_rows: methodRows,
limitations: [...typeIdentityLimitations, ...selectionLimitations],
},
uniqueManagedParseIssues(issues),
managedDecodeBudget(layout),
);
const inventory = readManagedMetadataInventory(
bytes,
layout,
readManagedResourceDirectory(pe, issues),
memberLists,
identity,
managedMetadataTypeNames(
bytes,
layout,
issues,
parsedTypes,
typeReferences,
),
);
appendManagedParseIssues(issues, inventory.issues);
const exportedTypes = parseExportedTypes(bytes, layout);
appendManagedParseIssues(issues, exportedTypes.issues);
const fields = parseFields(bytes, layout, ranges, memberLists, {
signatureTypeName,
declarationTypeName,
selected,
});
const resolveTypeName = typeNameLookup(
bytes,
layout,
ranges,
issues,
typeReferences,
);
const memberRefs = parseMemberRefs(bytes, layout, {
typeName: resolveTypeName,
signatureTypeName,
declarationTypeName,
});
appendManagedParseIssues(issues, memberRefs.issues);
const methods = parseMethods({
bytes,
layout,
pe,
ranges,
signatureTypeName,
declarationTypeName,
memberRefKinds: new Map(
memberRefs.refs.flatMap((memberRef) =>
memberRef.signature.kind === "field" ||
memberRef.signature.kind === "method"
? [[memberRef.token, memberRef.signature.kind] as const]
: [],
),
),
memberLists,
selected,
methodOffset,
methodLimit,
});
appendManagedParseIssues(issues, fields.issues, methods.issues);
const resolvedMethods = methods.methods.map(({ method, catchTokens }) => ({
...method,
body: {
...method.body,
exception_regions: method.body.exception_regions.map(
(region, ordinal) => {
const reference = catchTokens.get(ordinal) ?? null;
const classType = resolveTypeName(reference);
const identityResolved =
reference === null
? null
: classType !== null && reference.table === 2
? declarationTypeName(2, reference.row) !== null
: false;
return {
...region,
class_type: classType,
class_type_name_sha256:
reference !== null &&
(reference.table === 1 || reference.table === 2)
? (declarationTypeName(
reference.table,
reference.row,
)?.toString("hex") ?? null)
: null,
class_type_identity_resolved: identityResolved,
};
},
),
},
}));
// #US literals sit beside the member inventory and carry their own
// coverage, so their issues never make member facts partial.
const userStrings = readUserStrings
? parseUserStrings(bytes, layout, {
...methods,
unreadableSelectedTypes: unreadableNames,
})
: null;
const targets = edgeTargets(
bytes,
layout,
methods.methods,
[fields.core, memberRefs.core, methods.core],
signatureTypeName,
);
appendManagedParseIssues(issues, targets.issues);
const incompleteMembers =
[...fields.fields, ...memberRefs.refs].some(
(member) => member.signature.parse_status !== "decoded",
) ||
resolvedMethods.some(
(method) => method.signature.parse_status !== "decoded",
) ||
resolvedMethods.some(
(method) =>
method.body.status === "partial" ||
method.body.status === "malformed",
);
const related = edges(
methods.methods,
targets.members,
managedDecodeBudget(layout),
);
const coverageIssues = uniqueManagedParseIssues(issues);
return managedMemberInspectionSchema.parse(
admitManagedProjection(
{
...envelope,
metadata: {
...envelope.metadata,
status: issues.length === 0 ? "complete" : "partial",
},
assembly: inventory.assembly,
types,
signature_resolution: "type-names-v1",
nesting_read: parsedTypes.nesting.complete,
exported_types: exportedTypes.exportedTypes,
fields: fields.fields,
methods: resolvedMethods,
member_refs: memberRefs.refs,
call_edges: related.callEdges,
field_accesses: related.fieldAccesses,
user_strings: userStrings,
selection,
method_rows: methodRows,
coverage: {
state:
issues.length === 0 && !incompleteMembers
? "complete"
: "partial",
issues: coverageIssues,
},
limitations: [
...typeIdentityLimitations,
"Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
"Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.",
"User strings are the #US literals loaded by ldstr in the inspected method bodies, with their own coverage; instruction anchors and normalized CIL digests keep their build-local tokens.",
"Member signatures decode ECMA-335 primitive, class, valuetype, generic instantiation, generic variable, pointer, byref, single-dimension and general array, function pointer including VARARG sentinels, custom modifier, and typed reference forms; class and valuetype operands keep their encoded TypeDefOrRefOrSpec values.",
...selectionLimitations,
],
},
managedDecodeBudget(layout),
),
);
} catch (cause: unknown) {
if (cause instanceof ManagedReaderFailure) {
if (
recover !== undefined &&
cause instanceof ManagedRepresentationFailure
)
return recover(cause.issue.offset);
return unavailable(target, bytes, typeNames, cause);
}
throw cause;
}
};