mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
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:
1 parent
02bad95bee
commit
6fef3eaabe
5 files changed
+77
-25
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user