fix(managed): retain user-string terminal flag conformance issues

This commit is contained in:
morluto committed 2026-10-13 20:19:53 +08:00
1 parent 1c20e2f511
commit d5b7b0704b
3 files changed
+49 -10

No files matched your search

+4 -2
View File
@@ -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
+6 -1
View File
@@ -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),
);
+39 -7
View File
@@ -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`,
},
};
};