Files
reverse-engineering-anything/src/dotnet/ManagedMemberInspector.ts
T
morluto cb4e5cfbff feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
2026-11-10 05:11:20 -05:00

507 lines
17 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,
readManagedValue,
uniqueManagedParseIssues,
managedMetadataFailureStatus,
type ManagedMetadataHeaderFacts,
} from "./ManagedReaderFailure.js";
import {
parseTypes,
readTypeDefinitions,
type DecodedTypeDefinition,
} from "./ManagedMemberInspectorCore.js";
import { edgeTargets, methodSpecMethod } from "./ManagedMethodSpecs.js";
import { managedSignatureTypeNames } from "./ManagedSignatureTypeNames.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,
readReferencedMemberCores,
type SelectedTypeTokens,
} from "./ManagedMemberRows.js";
import type { MetadataTokenFact } from "./ManagedMetadataHeaps.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-v2",
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,
maxRepresentationBytes?: number,
) => {
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,
maxRepresentationBytes,
);
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,
maxRepresentationBytes,
}: {
readonly typeNames?: readonly string[];
/** Zero-based offset into MethodDef rows. */
readonly methodOffset?: number;
readonly methodLimit?: number;
readonly userStrings?: boolean;
readonly maxRepresentationBytes?: number;
} = {},
): 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,
maxRepresentationBytes,
);
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,
displayName: displayTypeName,
} = managedSignatureTypeNames(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,
displayTypeName,
declarationTypeName,
selected,
});
const resolveTypeName = typeNameLookup(
bytes,
layout,
ranges,
issues,
typeReferences,
);
const memberRefs = parseMemberRefs(bytes, layout, {
typeName: resolveTypeName,
signatureTypeName,
displayTypeName,
declarationTypeName,
});
appendManagedParseIssues(issues, memberRefs.issues);
const methods = parseMethods({
bytes,
layout,
pe,
ranges,
signatureTypeName,
displayTypeName,
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);
// Names for edge targets outside the selection come only from the rows
// selected method bodies actually reference. Unselected rows were
// skipped without reading them, so those names are recovered here
// instead of spending the budget on the whole assembly.
const referenced = new Map<
string,
Pick<MetadataTokenFact, "table" | "row" | "token">
>();
const needsCore = (
reference: Pick<MetadataTokenFact, "table" | "token">,
): boolean =>
reference.table === 4
? !fields.core.has(reference.token)
: reference.table === 6 && !methods.core.has(reference.token);
// Unscoped inspection already read every core. Selected members also
// retain their cores; only missing edge targets need the recovery pass.
if (selected !== null)
for (const { tokenInstructions } of methods.methods)
for (const { reference } of tokenInstructions) {
if (needsCore(reference)) referenced.set(reference.token, reference);
else if (
reference.table === 0x2b &&
reference.row >= 1 &&
reference.row <= (layout.table(0x2b)?.rowCount ?? 0)
) {
const spec = readManagedValue(
() => methodSpecMethod(bytes, layout, reference.row),
issues,
);
const generic = spec?.method;
if (
generic !== undefined &&
generic.kind === "valid" &&
needsCore(generic)
)
referenced.set(generic.token, generic);
}
}
const referencedCores = readReferencedMemberCores(
bytes,
layout,
ranges,
memberLists,
referenced.values(),
);
appendManagedParseIssues(issues, referencedCores.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,
[referencedCores.core, 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-v2",
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.",
"Field constants are file-backed Constant-table declarations, not runtime field values. Parameter and property defaults and enum-name mappings are not decoded.",
"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; type operands keep their encoded TypeDefOrRefOrSpec values and append readable TypeDef/TypeRef names when resolved.",
...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;
}
};