diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index c167f40e2d..f84a16a8a9 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -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 diff --git a/src/dotnet/ManagedMemberInspector.test.ts b/src/dotnet/ManagedMemberInspector.test.ts index 6d53f86beb..9e3a8cc6b7 100644 --- a/src/dotnet/ManagedMemberInspector.test.ts +++ b/src/dotnet/ManagedMemberInspector.test.ts @@ -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( diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 180ad2e6ae..1819504d14 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -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) { diff --git a/src/dotnet/ManagedPe.fixture.ts b/src/dotnet/ManagedPe.fixture.ts index 948a66b7be..7ea2b06b47 100644 --- a/src/dotnet/ManagedPe.fixture.ts +++ b/src/dotnet/ManagedPe.fixture.ts @@ -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), ), ], ], diff --git a/src/dotnet/ManagedTypeNames.ts b/src/dotnet/ManagedTypeNames.ts index 992749183c..1728c2efa0 100644 --- a/src/dotnet/ManagedTypeNames.ts +++ b/src/dotnet/ManagedTypeNames.ts @@ -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(