fix(managed): validate spec type operands and transitive reference scopes
This commit is contained in:
1 parent
9b76dd0cf9
commit
e59217c834
3 files changed
+83
-20
No files matched your search
@@ -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
|
||||
|
||||
@@ -465,10 +465,40 @@ const POSITION_ELEMENTS: ReadonlyMap<number, readonly SignaturePosition[]> =
|
||||
[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<typeof readCompressed> => {
|
||||
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;
|
||||
|
||||
@@ -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<string, string | null>();
|
||||
const checkedScopes = new Set<number>();
|
||||
const validateScopeChain = (row: number): void => {
|
||||
const path = new Set<number>();
|
||||
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(
|
||||
|
||||
Reference in new issue
Block a user