fix(managed): validate spec type operands and transitive reference scopes

This commit is contained in:
morluto committed 2026-10-13 20:19:52 +08:00
1 parent 9b76dd0cf9
commit e59217c834
3 files changed
+83 -20

No files matched your search

+5 -1
View File
@@ -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
+35 -4
View File
@@ -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;
+43 -15
View File
@@ -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(