diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 1ee211e147..f22221921b 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -97,8 +97,10 @@ assembly. Older member Evidence without `selection` remains readable. `ldstr` anchors keep their build-local `0x70` tokens. `user_strings` lists each distinct token loaded by an inspected method body once, in token order, with its `#US` file offset and decoded value. Values keep their stored UTF-16 code -units, including unpaired surrogates. The terminal flag byte is not -validated: toolchains disagree with ECMA-335 on it and the runtime ignores it. +units, including unpaired surrogates. The terminal flag is checked against +ECMA-335 II.24.2.4. A differing flag is reported with its byte offset as partial +metadata coverage while preserving the readable literal. Toolchain conventions +can differ; this conformance observation does not establish runtime rejection. A missing `#US` stream is one stream-level issue; a malformed item is reported against its token. Either makes coverage partial, and readable literals remain. Normalized CIL digests do not include decoded values. Older member Evidence diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index 20e029b873..5987632ddd 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -362,7 +362,12 @@ export const parseUserStrings = ( for (const token of [...tokens].sort()) { const failures: ManagedParseIssue[] = []; readManagedValue(() => { - const { offset, value } = readMetadataUserString(bytes, layout, token); + const { offset, value, issue } = readMetadataUserString( + bytes, + layout, + token, + ); + if (issue !== null) failures.push(issue); userStrings.push( admitManagedProjection({ token, offset, value }, budget), ); diff --git a/src/dotnet/ManagedMetadataHeaps.ts b/src/dotnet/ManagedMetadataHeaps.ts index 8fca5b860a..18f08abe9b 100644 --- a/src/dotnet/ManagedMetadataHeaps.ts +++ b/src/dotnet/ManagedMetadataHeaps.ts @@ -1,3 +1,4 @@ +import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js"; import { createHash } from "node:crypto"; import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; @@ -202,7 +203,11 @@ export const readMetadataUserString = ( bytes: Buffer, layout: ManagedMetadataLayout, token: string, -): { readonly offset: number; readonly value: string } => { +): { + readonly offset: number; + readonly value: string; + readonly issue: ManagedParseIssue | null; +} => { const heap = layout.userStrings; const scope = `metadata.#US:${token}`; if (heap === null) @@ -237,14 +242,41 @@ export const readMetadataUserString = ( start, ); const units = (decoded.length - 1) / 2; - // The terminal flag byte is not validated. Toolchains disagree with - // ECMA-335 II.24.2.4 (Mono leaves it 0 for ' and -, Roslyn sets it for any - // unit >= 0x80) and the runtime never reads it. - // JSON spends at most six bytes per UTF-16 code unit, the same bound as UTF-8 bytes. - managedDecodeBudget(layout).reserveString(units, start); + // Check the serialized flag independently of literal decoding. A difference + // from ECMA-335 is observable metadata, not proof of runtime rejection. + let expectedFlag = 0; + for (let index = 0; index < units; index++) { + const unit = bytes.readUInt16LE(content + index * 2); + if ( + unit > 0xff || + (unit >= 0x01 && unit <= 0x08) || + (unit >= 0x0e && unit <= 0x1f) || + unit === 0x27 || + unit === 0x2d || + unit === 0x7f + ) { + expectedFlag = 1; + break; + } + } + const flagOffset = content + units * 2; + const actualFlag = bytes[flagOffset]; + // JSON spends at most six bytes per UTF-16 code unit. + const budget = managedDecodeBudget(layout); + budget.reserveString(units, start); + if (actualFlag !== expectedFlag) budget.reserve(512, flagOffset); return { offset: start, - value: bytes.subarray(content, content + units * 2).toString("utf16le"), + value: bytes.subarray(content, flagOffset).toString("utf16le"), + issue: + actualFlag === expectedFlag + ? null + : { + code: "invalid-string", + scope, + offset: flagOffset, + detail: `User string terminal flag ${String(actualFlag)} differs from ECMA-335 II.24.2.4 value ${String(expectedFlag)}; readable UTF-16 is preserved and runtime acceptance is not assessed`, + }, }; };