Files
reverse-engineering-anything/src/dotnet/ManagedMethodSpecs.ts
T
91830f1e0e feat(managed): name the declaring type of call and field targets (#2163)
* feat(managed): name the declaring type of call and field targets

Call edges named external targets by member name alone (Join, ToJson,
WriteAllText), and method-spec edges had no name at all, so an analyst could
not tell which library a method calls or find every use of a type without a
decompiler. Field accesses through MemberRefs had no name either.

Name each TypeDef, TypeRef, and generic-instantiation TypeSpec once, in the
namespace-qualified form TypeDef full_name uses, and add member_refs
parent_name, call_edges target_declaring_type, and field_accesses
field_declaring_type. A method-spec target reports the generic method it
instantiates. Fields, member references, methods, and method specs now
resolve through one token-keyed MemberCore lookup instead of three maps.
Evidence recorded before these fields reads them as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate spec blobs and member kinds before naming targets

Address review findings on declaring-type naming:

- A generic instantiation must name a TypeDef or TypeRef, so a TypeSpec
  base is reported as malformed instead of followed. Naming no longer
  recurses.
- TypeSpec and MethodSpec instantiation blobs are decoded with the
  member-signature grammar. Malformed blobs and out-of-range TypeDef rows
  are reported against their owning row and make coverage partial.
- A MethodSpec reads both columns. An invalid Method coded index is
  reported; a readable Method keeps its name beside a malformed
  instantiation.
- MemberCore carries its signature kind, so a field operand is named only
  by a field or field-signature MemberRef. This replaces the token-table
  guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate MethodSpec arity and report unsupported spec blobs

- A MethodSpec now checks that its method signature is generic and that
  the instantiation supplies as many type arguments as it declares. A
  non-generic target, a field reference, or a count mismatch is reported
  as invalid-row; the readable method keeps its name.
- Unsupported TypeSpec and MethodSpec forms raise an owner-scoped
  unsupported-signature issue instead of reading as an intentional
  unknown.
- MethodSpec reading moves to ManagedMethodSpecs.ts, and its tests get
  their own describe block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): reject non-method signature headers and invalid TypeRef scopes

The method signature header reader now owns the calling-convention rule
that only FNPTR applied: FIELD, LOCAL_SIG, PROPERTY, and GENERICINST
headers, the reserved bit, and EXPLICITTHIS without HASTHIS are malformed.
A MethodSpec whose method has a PROPERTY header is no longer accepted as a
generic method, and member signatures with such headers no longer decode
as methods.

The type-name lookup validates a TypeRef's ResolutionScope coded index and
reports an out-of-range scope as invalid-row, keeping the readable name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): hold member rows to MethodDefSig and FieldSig headers

Method signature headers are now checked in context. MethodDef and
MemberRef rows accept only the MethodDefSig/MethodRefSig headers: DEFAULT,
VARARG, or GENERIC with DEFAULT. FNPTR keeps the unmanaged conventions
compilers emit for function pointers. A field signature must be exactly
FIELD, so a flagged FIELD header is malformed and never names a field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): accept MethodSpec arity only from a fully decoded signature

The arity check read only the target's signature header, so a truncated
generic method signature passed. Under type selection a MethodDef target
outside the selection is never decoded elsewhere, so coverage stayed
complete. The arity now comes from a transient full decode, and only a
decoded method signature supplies it. signature() keeps its raw length
and digest around the shared decodeSignature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(managed): decode each MethodSpec target signature once

edgeTargets caches the generic arity, including a failed decode, by
target method token, so many instantiations of one generic method share
a single signature decode. Unity.InputSystem has 472 distinct MethodSpecs
over about 133 target methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): validate spec type operands and transitive reference scopes

* fix(managed): retain readable names when a scope row is unreadable

* test(managed): pin owner-scoped spec operand and scope-chain checks

e59217c83 validates spec type operands inside specSignature, so a
malformed generic TypeSpec base is now invalid-row under its owning
TypeSpec row; the three stale expectations follow that. New cases cover
a TypeSpec argument and a MethodSpec argument that name a missing TypeRef,
and a TypeRef whose ResolutionScope names itself. The scope cases move to
their own describe block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): keep field targets from naming MethodSpecs

A MethodSpec over a field-signature MemberRef inherited kind "field", so
an ldsfld on that MethodSpec published a field name and declaring type
for malformed IL. A MethodSpec now borrows its target's name only when
the target is not a field; the arity check still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(managed): reuse owner-decoded signatures for MethodSpec arity

MemberRef signatures, and MethodDef signatures in the type selection, are
already fully decoded, but the MethodSpec arity check decoded them again
and admitted a second transient allocation, which could exhaust the
budget. MemberCore now carries the owner's genericArity, and only a target
nothing decoded, such as an unselected MethodDef, falls back to the raw
signature. signatureGenericArity is the one rule for both paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-13 21:12:15 +08:00

155 lines
5.0 KiB
TypeScript

import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
import {
methodGenericArity,
specSignature,
type ManagedMethod,
type MemberCore,
} from "./ManagedMemberInspectorCore.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
metadataCodedToken,
metadataCodedTokenInvalidReason,
metadataRowCursor,
readMetadataBlob,
} from "./ManagedMetadataHeaps.js";
import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
/**
* Index fields, member references, and methods by token, then name each
* MethodSpec an inspected body calls after the generic method it instantiates,
* so every edge target resolves through one lookup.
*/
export const edgeTargets = (
bytes: Buffer,
layout: ManagedMetadataLayout,
methods: readonly ManagedMethod[],
cores: readonly ReadonlyMap<string, MemberCore>[],
): {
readonly members: ReadonlyMap<string, MemberCore>;
readonly issues: readonly ManagedParseIssue[];
} => {
const members = new Map(cores.flatMap((core) => [...core]));
const issues: ManagedParseIssue[] = [];
const seen = new Set<string>();
// Generic arity by method token, so instantiations of one method decode it once.
const arities = new Map<string, number | null>();
for (const method of methods)
for (const { operand } of method.body.anchors) {
if (operand?.startsWith("0x2b") !== true || seen.has(operand)) continue;
seen.add(operand);
readManagedValue(
() =>
readMethodSpec({ bytes, layout, members, issues, arities }, operand),
issues,
);
}
return { members, issues };
};
/**
* Map one MethodSpec to its generic method, then check its instantiation
* decodes and matches that method's generic arity. A readable Method column
* keeps its name when the rest is malformed; the failure makes coverage partial.
*/
const readMethodSpec = (
{
bytes,
layout,
members,
issues,
arities,
}: {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly members: Map<string, MemberCore>;
readonly issues: ManagedParseIssue[];
readonly arities: Map<string, number | null>;
},
token: string,
): void => {
const scope = `metadata.MethodSpec:${token}`;
const cursor = metadataRowCursor(
bytes,
layout,
0x2b,
Number.parseInt(token.slice(4), 16),
);
const methodRaw = cursor.readIndex(layout.codedIndexSize("MethodDefOrRef"));
const instantiation = cursor.readIndex(layout.blobIndexSize);
const methodReason = metadataCodedTokenInvalidReason(
methodRaw,
1,
[6, 10],
layout.rowCounts,
);
if (methodReason !== null || methodRaw === 0)
issues.push({
code: "invalid-row",
scope,
offset: cursor.start,
detail:
methodReason === null
? "MethodSpec method coded index 0x0 is null, but the Method column must reference a row"
: `MethodSpec method coded index 0x${methodRaw.toString(16)} is invalid: ${methodReason}`,
});
const generic = metadataCodedToken(methodRaw, 1, [6, 10], layout.rowCounts);
const target = generic === null ? undefined : members.get(generic);
// A MethodSpec instantiates a method, so a field target never lends it a
// name, even to a field operand.
if (target !== undefined && target.kind !== "field")
members.set(token, { ...target, token });
const types = specSignature(
layout,
readMetadataBlob(bytes, layout, instantiation, layout.blob.size),
{ form: "instantiation", scope, offset: cursor.start },
);
if (generic === null) return;
// Prefer the arity the target's owner decoded; read the raw signature only
// for a target nothing decoded, such as a MethodDef outside the selection.
if (target?.genericArity === undefined && !arities.has(generic))
arities.set(
generic,
methodGenericArity(layout, methodSignatureBlob(bytes, layout, generic)),
);
const arity =
target?.genericArity === undefined
? (arities.get(generic) ?? null)
: target.genericArity;
if (arity !== types.length)
throw managedFailure(
"invalid-row",
scope,
arity === null
? "MethodSpec method does not have a decodable method signature"
: `MethodSpec supplies ${String(types.length)} type argument(s), but its method declares ${String(arity)} generic parameter(s)`,
cursor.start,
);
};
/** The Signature column of a MethodDef or MemberRef row. */
const methodSignatureBlob = (
bytes: Buffer,
layout: ManagedMetadataLayout,
token: string,
): Buffer => {
const table = Number.parseInt(token.slice(2, 4), 16);
const cursor = metadataRowCursor(
bytes,
layout,
table,
Number.parseInt(token.slice(4), 16),
);
if (table === 6) {
cursor.readUInt32();
cursor.readUInt16();
cursor.readUInt16();
} else cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
cursor.readIndex(layout.stringIndexSize);
return readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
layout.blob.size,
);
};