diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index f84a16a8a9..37ebcdbcb5 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -96,7 +96,11 @@ assembly. Older member Evidence without `selection` remains readable. `member_refs[].parent_name`, `call_edges[].target_declaring_type`, and `field_accesses[].field_declaring_type` name the type that declares each -target. TypeDef and TypeRef names use the namespace-qualified `full_name` +target. Referenced spec signatures validate encoded type coordinates against +the metadata tables, including nested type arguments and custom modifiers. +TypeRef resolution scopes are checked transitively with cycle detection; +readable names survive malformed scopes as partial evidence. +TypeDef and TypeRef names use the namespace-qualified `full_name` form, and nested types keep bare names. A generic-instantiation TypeSpec parent names its generic type, such as ``System.Collections.Generic.List`1``. Other well-formed TypeSpec forms and ModuleRef or MethodDef parents stay diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index d5b922bf15..dccea81b7d 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -465,10 +465,40 @@ const POSITION_ELEMENTS: ReadonlyMap = [0x10, ["return", "parameter", "field"]], ]); +/** Decode a type operand once; spec callers also validate its metadata coordinate. */ +const readTypeOperand = ( + blob: Buffer, + offset: number, + layout?: ManagedMetadataLayout, + tables: readonly number[] = [2, 1, 27], +): ReturnType => { + const token = readCompressed(blob, offset); + if (layout !== undefined) { + const reason = + token.value === 0 + ? "type operand is null" + : metadataCodedTokenInvalidReason( + token.value, + 2, + tables, + layout.rowCounts, + ); + if (reason !== null) + throw managedFailure( + "invalid-row", + "signature", + `TypeDefOrRefOrSpec operand 0x${token.value.toString(16)} is invalid: ${reason}`, + offset, + ); + } + return token; +}; + const readTypeSignature = ( blob: Buffer, offset: number, base: SignaturePosition, + layout?: ManagedMetadataLayout, ): { readonly value: string; readonly next: number } => { const stack: TypeContinuation[] = []; let cursor = offset; @@ -495,7 +525,7 @@ const readTypeSignature = ( stack.push({ kind: "suffix", suffix }); cursor += 1; } else if (kind === 0x1f || kind === 0x20) { - const token = readCompressed(blob, cursor + 1); + const token = readTypeOperand(blob, cursor + 1, layout); const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`; // A modifier frame on top was pushed by the directly preceding byte; // keep an adjacent run together and in source order. @@ -504,7 +534,7 @@ const readTypeSignature = ( else stack.push({ kind: "modifiers", modifiers: [modifier] }); cursor = token.next; } else if (kind === 0x11 || kind === 0x12) { - const token = readCompressed(blob, cursor + 1); + const token = readTypeOperand(blob, cursor + 1, layout); value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`; cursor = token.next; } else if (kind === 0x1e || kind === 0x13) { @@ -524,7 +554,7 @@ const readTypeSignature = ( "generic instantiation does not name a class or value type", cursor + 1, ); - const token = readCompressed(blob, cursor + 2); + const token = readTypeOperand(blob, cursor + 2, layout, [2, 1]); const argumentCount = readCompressed(blob, token.next); if (argumentCount.value === 0) return malformedSignature( @@ -646,7 +676,7 @@ export const specSignature = ( const types: string[] = []; // Each Type consumes at least one byte, so a false count fails on truncation. for (let index = 0; index < count; index += 1) { - const type = readTypeSignature(blob, cursor, "type"); + const type = readTypeSignature(blob, cursor, "type", layout); types.push(type.value); cursor = type.next; } @@ -657,6 +687,7 @@ export const specSignature = ( if ( !(cause instanceof ManagedReaderFailure) || (cause.issue.code !== "invalid-blob" && + cause.issue.code !== "invalid-row" && cause.issue.code !== "unsupported-signature") ) throw cause; diff --git a/src/dotnet/ManagedTypeNames.ts b/src/dotnet/ManagedTypeNames.ts index 1728c2efa0..0c6a4d9972 100644 --- a/src/dotnet/ManagedTypeNames.ts +++ b/src/dotnet/ManagedTypeNames.ts @@ -1,3 +1,4 @@ +import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import { fullName, @@ -32,27 +33,54 @@ export const typeNameLookup = ( issues: ManagedParseIssue[], ): ((token: string | null) => string | null) => { const names = new Map(); + const checkedScopes = new Set(); + const validateScopeChain = (row: number): void => { + const path = new Set(); + let current = row; + while (!checkedScopes.has(current)) { + const cursor = metadataRowCursor(bytes, layout, 1, current); + if (path.has(current)) { + issues.push({ + code: "invalid-row", + scope: `metadata.TypeRef:${metadataToken(1, current)}`, + offset: cursor.start, + detail: "TypeRef ResolutionScope chain contains a cycle", + }); + break; + } + // Admit the chain's visited entry and retained validation-cache entry. + managedDecodeBudget(layout).reserve(128, cursor.start); + path.add(current); + const raw = cursor.readIndex(layout.codedIndexSize("ResolutionScope")); + const reason = metadataCodedTokenInvalidReason( + raw, + 2, + [0, 26, 35, 1], + layout.rowCounts, + ); + if (reason !== null) { + issues.push({ + code: "invalid-row", + scope: `metadata.TypeRef:${metadataToken(1, current)}`, + offset: cursor.start, + detail: `TypeRef ResolutionScope coded index 0x${raw.toString(16)} is invalid: ${reason}`, + }); + break; + } + // Null scopes are legal; non-TypeRef scopes terminate local validation. + if (raw === 0 || (raw & 3) !== 3) break; + current = Math.floor(raw / 4); + } + for (const visited of path) checkedScopes.add(visited); + }; const definitionName = (table: 1 | 2, row: number): string | null => { if (table === 2) { metadataRowOffset(layout, 2, row); return ranges[row - 1]?.fullName ?? null; } const cursor = metadataRowCursor(bytes, layout, 1, row); - 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}`, - }); + cursor.readIndex(layout.codedIndexSize("ResolutionScope")); + validateScopeChain(row); const name = cursor.readIndex(layout.stringIndexSize); const namespace = cursor.readIndex(layout.stringIndexSize); return fullName(