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>
This commit is contained in:
NickandClaude Opus 5.5 committed 2026-10-12 13:24:07 +00:00
1 parent 02bad95bee
commit 6fef3eaabe
5 files changed
+77 -25

No files matched your search

+3 -3
View File
@@ -105,9 +105,9 @@ generic method it instantiates. A field access is named only by a field or a
field-signature MemberRef. TypeSpec and MethodSpec blobs are decoded with the
member-signature grammar, and a MethodSpec must instantiate a generic method
with as many type arguments as it declares. Malformed or unsupported rows and
blobs, and arity mismatches, make coverage partial; a readable MethodSpec
method column keeps its name. Older member Evidence without these fields reads
them as `null`: unknown.
blobs, and arity mismatches, make coverage partial. A readable MethodSpec
method column, or a TypeRef whose resolution scope is invalid, keeps its name.
Older member Evidence without these fields reads them as `null`: unknown.
## Analysis objective
+29
View File
@@ -263,6 +263,18 @@ describe("managed edge target declaring types", () => {
});
});
it("keeps a TypeRef name beside an invalid resolution scope", () => {
// AssemblyRef 9 does not exist.
const result = inspect({ typeRefScopeRaw: (9 << 2) | 2 });
expect(result.member_refs[0]?.parent_name).toBe(attribute);
expect(result.call_edges[0]?.target_declaring_type).toBe(attribute);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [{ code: "invalid-row", scope: "metadata.TypeRef:0x01000001" }],
});
});
it("names field MemberRefs as fields and leaves mismatched member kinds unnamed", () => {
const result = inspect({
memberRefName: "Empty",
@@ -355,6 +367,13 @@ describe("managed MethodSpec call targets", () => {
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
[
"a property signature",
// PROPERTY | GENERIC, one generic parameter, no parameters, void.
{ memberRefSignature: Buffer.from([0x18, 0x01, 0x00, 0x01]) },
Buffer.from([0x0a, 0x01, 0x0e]),
"does not have a decodable method signature",
],
])(
"reports a MethodSpec that instantiates %s",
(_case, options, methodSpecInstantiation, detail) => {
@@ -926,6 +945,16 @@ it.each([
});
});
it.each([
["a property header", [0x08, 0x00, 0x08]],
["an explicit-this header without has-this", [0x40, 0x00, 0x01]],
])("reports %s as a malformed member signature", (_name, blob) => {
expect(signature(Buffer.from(blob))).toMatchObject({
kind: "unknown",
parse_status: "malformed",
});
});
it("rejects array ranks beyond the CLR limit before rendering dimensions", () => {
expect(
signature(
+22 -17
View File
@@ -338,6 +338,18 @@ interface MethodSignatureHeader {
readonly next: number;
}
// A method signature, whether a member's or one FNPTR carries, uses managed
// DEFAULT/VARARG or the unmanaged C, STDCALL, THISCALL, FASTCALL, and
// UNMANAGED conventions that compilers emit for unmanaged function pointers.
// FIELD, LOCAL_SIG, PROPERTY, and GENERICINST headers are not method
// signatures.
const METHOD_CALLING_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
/**
* Read a method signature's calling convention and counts. A header that is
* not a method signature, sets the reserved bit, or sets EXPLICITTHIS without
* HASTHIS is malformed, so no caller consumes its counts.
*/
const readMethodSignatureHeader = (
blob: Buffer,
offset: number,
@@ -345,6 +357,15 @@ const readMethodSignatureHeader = (
const first = blob[offset];
if (first === undefined)
return malformedSignature("truncated method signature", offset);
if (
!METHOD_CALLING_CONVENTIONS.has(first & 0x0f) ||
(first & 0x80) !== 0 ||
((first & 0x40) !== 0 && (first & 0x20) === 0)
)
return malformedSignature(
`calling convention 0x${first.toString(16)} does not head a method signature`,
offset,
);
let next = offset + 1;
let genericParameterCount: number | null = null;
if ((first & 0x10) !== 0) {
@@ -363,12 +384,6 @@ const readMethodSignatureHeader = (
const MAX_ARRAY_RANK = 32;
// FNPTR carries a method signature: managed DEFAULT/VARARG, or the unmanaged
// C, STDCALL, THISCALL, FASTCALL, and UNMANAGED conventions that compilers
// emit for unmanaged function pointers. FIELD, LOCAL_SIG, PROPERTY, and
// GENERICINST headers are not method signatures.
const FUNCTION_POINTER_CONVENTIONS = new Set([0, 1, 2, 3, 4, 5, 9]);
const arrayShape = (
blob: Buffer,
offset: number,
@@ -523,15 +538,6 @@ const readTypeSignature = (
cursor = argumentCount.next;
} else if (kind === 0x1b) {
const header = readMethodSignatureHeader(blob, cursor + 1);
if (
!FUNCTION_POINTER_CONVENTIONS.has(header.first & 0x0f) ||
(header.first & 0x80) !== 0 ||
((header.first & 0x40) !== 0 && (header.first & 0x20) === 0)
)
return malformedSignature(
`function pointer calling convention 0x${header.first.toString(16)} does not head a method signature`,
cursor + 1,
);
const convention = callingConvention(header.first);
const head =
header.genericParameterCount === null
@@ -659,10 +665,9 @@ export const specSignature = (
/**
* Generic parameter count a method signature declares, 0 when it is not
* generic, or null when the blob is a field signature or does not decode.
* generic, or null when the blob is not a method signature header.
*/
export const methodGenericArity = (blob: Buffer): number | null => {
if (((blob[0] ?? 0) & 0x0f) === 6) return null;
try {
return readMethodSignatureHeader(blob, 0).genericParameterCount ?? 0;
} catch (cause: unknown) {
+5 -3
View File
@@ -37,6 +37,8 @@ interface ManagedPeFixtureOptions {
readonly methodSpecInstantiation?: Buffer;
/** TypeSpec signature blobs, one TypeSpec row each. */
readonly typeSpecSignatures?: readonly Buffer[];
/** Raw ResolutionScope coded value of the TypeRef row; null by default. */
readonly typeRefScopeRaw?: number;
readonly mvid?: Buffer;
readonly pinvoke?: {
readonly importName?: string;
@@ -182,10 +184,10 @@ const typeRefRow = (
name: number,
namespace: number,
stringSize: MetadataIndexSize,
resolutionScopeSize: MetadataIndexSize,
resolutionScope: Buffer,
): Buffer =>
Buffer.concat([
metadataIndex(0, resolutionScopeSize),
resolutionScope,
metadataIndex(name, stringSize),
metadataIndex(namespace, stringSize),
]);
@@ -716,7 +718,7 @@ const fixtureMetadataRows = ({
indexes.attributeName,
indexes.attributeNamespace,
heapSizes.strings,
widths.resolutionScope,
metadataIndex(options.typeRefScopeRaw ?? 0, widths.resolutionScope),
),
],
],
+18 -2
View File
@@ -10,6 +10,7 @@ import {
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
metadataCodedTokenInvalidReason,
metadataRowCursor,
metadataToken,
readMetadataBlob,
@@ -21,7 +22,8 @@ import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js";
* Name the type a TypeDef, TypeRef, or generic-instantiation TypeSpec token
* denotes, in the namespace-qualified form TypeDef `full_name` uses, once per
* token. Other decoded TypeSpecs stay null: the type is unknown. Malformed or
* unsupported rows and blobs are reported to `issues` and also stay null.
* unsupported rows and blobs are reported to `issues` and also stay null,
* except that a TypeRef with an invalid ResolutionScope keeps its readable name.
*/
export const typeNameLookup = (
bytes: Buffer,
@@ -36,7 +38,21 @@ export const typeNameLookup = (
return ranges[row - 1]?.fullName ?? null;
}
const cursor = metadataRowCursor(bytes, layout, 1, row);
cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
const scopeRaw = cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
// A null scope is legal: the type resolves through ExportedType.
const scopeReason = metadataCodedTokenInvalidReason(
scopeRaw,
2,
[0, 26, 35, 1],
layout.rowCounts,
);
if (scopeReason !== null)
issues.push({
code: "invalid-row",
scope: `metadata.TypeRef:${metadataToken(1, row)}`,
offset: cursor.start,
detail: `TypeRef ResolutionScope coded index 0x${scopeRaw.toString(16)} is invalid: ${scopeReason}`,
});
const name = cursor.readIndex(layout.stringIndexSize);
const namespace = cursor.readIndex(layout.stringIndexSize);
return fullName(