fix(wasm): preserve partial name observations and coverage

Retain module-name displays observed before optional metadata failure so strict objdump framing can use those facts. Preserve uncertainty across name sections and report unknown completeness when unattributed producer warnings concern skipped custom metadata; keep exact warnings, core validation, WAT and artifact provenance.

Validation: focused source/process regressions and 61 real WABT1.0.42 CLI/MCP verifier cases pass. The unmodified official macOS arm64 archive matches its published SHA-256. Coverage includes outer/inner malformed name metadata, valid empty-map controls, stable core/interface facts and cleanup. No inspected WASM target was executed.

Fixes #6885
This commit is contained in:
morluto
2026-10-24 02:54:07 +08:00
parent 8b3902d377
commit 1e688e4201
6 changed files with 335 additions and 84 deletions
+11 -4
View File
@@ -75,10 +75,17 @@ selected byte identity and tool provenance.
Core validation remains authoritative when an optional custom section cannot
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
`debug_name_metadata` reports whether the optional name section was absent,
fully interpreted, or uninterpretable. Older portable reports retain their
original strict command profile; missing metadata coverage is `unknown` and
missing warning history is `null`. The raw custom section remains in the
decoded WAT. An uninterpretable name section leaves debug module names unknown.
fully interpreted, uninterpretable, or unknown. WABT custom-section warnings
whose section attribution is unknown prevent a complete debug-name claim, even
when module names were decoded. They do not establish that the name section is
malformed. Exact warnings retain their producing tool and text. Older portable
reports retain their original strict command profile; missing metadata coverage
is `unknown` and missing warning history is `null`. The raw custom section
remains in the decoded WAT. An uninterpretable name section leaves complete
debug-name coverage unknown. Module names decoded before a later metadata
failure remain available for exact objdump framing, including names containing
newlines or NUL; those partial observations do not establish complete optional
metadata.
The tool profile preserves upstream release/source identity, resolved executable
paths, executable digests, exact banners and arguments. Executable digests are
+112
View File
@@ -429,6 +429,118 @@ try {
cases += 2;
}
}
// Source-owned inert core and raw optional payloads reproduce #6885 without
// asking wat2wasm to repair or regenerate the malformed name metadata.
const core = Buffer.from(
"0061736d0100000001040160000003020100070501016600000a040102000b",
"hex",
);
for (const [label, payload, metadata, observedName] of [
["valid-name", Buffer.from("00060568656c6c6f", "hex"), "complete", true],
[
"valid-name-map",
Buffer.from("00060568656c6c6f010100", "hex"),
"complete",
true,
],
[
"inner-name-error",
Buffer.from("00060568656c6c6f010101", "hex"),
"unknown",
true,
],
[
"partial-name",
Buffer.from("00060568656c6c6f010201", "hex"),
"uninterpretable",
true,
],
[
"initial-name-error",
Buffer.from("0006056e616d65", "hex"),
"uninterpretable",
false,
],
]) {
const fixtureRoot = join(root.path, label);
await mkdir(fixtureRoot);
const path = join(fixtureRoot, "module.wasm"),
custom = Buffer.concat([Buffer.from([4]), Buffer.from("name"), payload]),
original = Buffer.concat([core, Buffer.from([0, custom.length]), custom]);
await writeFile(path, original);
const validation = await tool(
"wasm-validate",
["--enable-all", "--ignore-custom-section-errors", "module.wasm"],
fixtureRoot,
),
dump = await tool(
"wasm-objdump",
["-h", "-x", "module.wasm"],
fixtureRoot,
),
decoded = await tool(
"wasm2wat",
["--enable-all", "--ignore-custom-section-errors", "module.wasm"],
fixtureRoot,
);
const input = { path, glue_paths: [], candidate_paths: [] };
const cli = parseEvidence(await inspectCli(input));
const reply = await callTool({
name: "inspect_wasm_artifact",
arguments: input,
});
assert.notEqual(reply.isError, true, mcpTextValue(reply));
assert.ok(
ajv.validate(advertised.outputSchema, reply.structuredContent),
JSON.stringify(ajv.errors),
);
const mcp = parseEvidence(reply.structuredContent);
assert.deepEqual(cli, mcp);
const report = mcp.normalized_result;
assert.equal(report.validation, "valid");
assert.equal(report.artifact.sha256, sha256(original));
assert.equal(report.artifact.bytes, original.length);
assert.equal(report.debug_name_metadata, metadata);
assert.equal(report.interface.state, "complete");
assert.deepEqual(report.exports, ['(export "f" (func 0))']);
assert.deepEqual(
report.interface.entries.map(({ direction, name, signature }) => ({
direction,
name,
signature,
})),
[
{
direction: "export",
name: "f",
signature: { kind: "func", params: [], results: [] },
},
],
);
assert.equal(
report.headers.includes("module name: <hello>\n"),
observedName,
);
assert.equal(report.sections.at(-1).end, original.length);
assert.equal(report.wat.text, decoded.stdout);
assert.equal(report.wat.sha256, sha256(decoded.stdout));
assert.ok(report.wat.text.includes('(@custom "name"'));
const warnings = [
{ tool: "wasm-validate", text: validation.stderr },
{ tool: "wasm-objdump", text: dump.stderr },
{ tool: "wasm2wat", text: decoded.stderr },
].filter(({ text }) => text !== "");
assert.equal(warnings.length, metadata === "complete" ? 0 : 3);
assert.deepEqual(report.custom_section_warnings, warnings);
if (metadata !== "complete")
assert.ok(
report.limitations.some((text) =>
text.includes("complete debug-name coverage remains unknown"),
),
);
assert.deepEqual(await readFile(path), original);
cases += 2;
}
cases += await verifyWabtCleanup(entrypoint, environment, childTempRoot);
} catch (cause) {
failures.push(cause);
+129 -69
View File
@@ -207,76 +207,136 @@ const createWabtArtifactFixture = async () => {
return { selectedRoot, path, bytes, dump };
};
it("keeps core WASM usable when optional name-section metadata is malformed", async () => {
const fixture = await createWabtArtifactFixture();
const diagnostic =
"0000011: warning: invalid sub-section size: extends past end\n";
const dump = [
"",
"module.wasm:\tfile format wasm 0x1",
"",
"Sections:",
"",
' Custom start=0x0000000a end=0x0000004c (size=0x00000042) "name"',
"",
"Section Details:",
"",
].join("\n");
const provider = new WabtArtifactProvider(
{ REA_WABT_BIN_DIRECTORY: fixture.selectedRoot },
(input) => {
const version = input.arguments[0] === "--version";
const isObjdump = input.command.includes("wasm-objdump");
const isDecoder = input.command.includes("wasm2wat");
const stdout = version
? "1.0.42\n"
: isObjdump
? dump
: isDecoder
? '(module (@custom "name" "abc"))\n'
: "";
const stderr = version ? "" : diagnostic;
return spawnOwnedProviderProcess({
...input,
hostEnvironment: nodeHostEnvironment(input.hostEnvironment),
command: process.execPath,
arguments: [
"-e",
`process.stdout.write(${JSON.stringify(stdout)}); process.stderr.write(${JSON.stringify(stderr)});`,
],
expectedCommand: null,
it.each([
{
label: "initially malformed",
preamble: "",
payload: "abc",
diagnostic:
"0000011: warning: invalid sub-section size: extends past end\n",
metadata: "uninterpretable",
},
{
label: "partially decoded",
preamble: "module name: <hello>\n",
payload: String.raw`\00\06\05hello\01\02\01`,
diagnostic:
"0000011: warning: invalid sub-section size: extends past end\n",
metadata: "uninterpretable",
},
{
label: "an inner map rejected by WABT",
preamble: "module name: <hello>\n",
payload: String.raw`\00\06\05hello\01\01\01`,
diagnostic:
"000002f: warning: invalid name count 1, only 0 bytes left in section\n",
metadata: "unknown",
},
{
label: "a valid empty function-name map",
preamble: "module name: <hello>\n",
payload: String.raw`\00\06\05hello\01\01\00`,
diagnostic: "",
metadata: "complete",
},
{
label: "a valid name with an unattributed custom-section warning",
preamble: "module name: <hello>\n",
payload: String.raw`\00\06\05hello\01\01\00`,
diagnostic:
"0000030: warning: unable to read u32 leb128: linking metadata version\n",
metadata: "unknown",
},
])(
"keeps core WASM usable when optional name metadata is $label",
async ({ preamble, payload, diagnostic, metadata }) => {
const fixture = await createWabtArtifactFixture();
const dump = [
"",
`module.wasm:\tfile format wasm 0x1\n${preamble}`.trimEnd(),
"",
"Sections:",
"",
' Custom start=0x0000000a end=0x0000004c (size=0x00000042) "name"',
"",
"Section Details:",
"",
].join("\n");
const provider = new WabtArtifactProvider(
{ REA_WABT_BIN_DIRECTORY: fixture.selectedRoot },
(input) => {
const version = input.arguments[0] === "--version";
const isObjdump = input.command.includes("wasm-objdump");
const isDecoder = input.command.includes("wasm2wat");
const stdout = version
? "1.0.42\n"
: isObjdump
? dump
: isDecoder
? `(module (@custom "name" "${payload}"))\n`
: "";
const stderr = version ? "" : diagnostic;
return spawnOwnedProviderProcess({
...input,
hostEnvironment: nodeHostEnvironment(input.hostEnvironment),
command: process.execPath,
arguments: [
"-e",
`process.stdout.write(${JSON.stringify(stdout)}); process.stderr.write(${JSON.stringify(stderr)});`,
],
expectedCommand: null,
});
},
);
try {
const result = await provider.inspect({
path: fixture.path,
glue_paths: [],
candidate_paths: [],
});
},
);
try {
const result = await provider.inspect({
path: fixture.path,
glue_paths: [],
candidate_paths: [],
});
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
validation: "valid",
debug_name_metadata: "uninterpretable",
custom_section_warnings: [
{ tool: "wasm-validate", text: diagnostic },
{ tool: "wasm-objdump", text: diagnostic },
{ tool: "wasm2wat", text: diagnostic },
],
sections: [{ kind: "Custom", start: 10, end: 76, description: '"name"' }],
limitations: expect.arrayContaining([
expect.stringContaining("optional WebAssembly name custom section"),
]),
});
expect(
wasmArtifactSchema.parse(result.value.result).tool_profile.commands[0]
?.arguments,
).toContain("--ignore-custom-section-errors");
expect(await readFile(fixture.path)).toEqual(fixture.bytes);
} finally {
await provider.close();
}
});
if (!result.ok) throw result.error;
const report = wasmArtifactSchema.parse(result.value.result);
expect(report).toMatchObject({
validation: "valid",
debug_name_metadata: metadata,
custom_section_warnings:
diagnostic === ""
? []
: [
{ tool: "wasm-validate", text: diagnostic },
{ tool: "wasm-objdump", text: diagnostic },
{ tool: "wasm2wat", text: diagnostic },
],
sections: [
{ kind: "Custom", start: 10, end: 76, description: '"name"' },
],
});
if (metadata !== "complete")
expect(report.limitations).toContainEqual(
expect.stringContaining(
"complete debug-name coverage remains unknown",
),
);
else
expect(report.limitations.join("\n")).not.toContain(
"complete debug-name coverage remains unknown",
);
expect(report.artifact.sha256).toBe(
createHash("sha256").update(fixture.bytes).digest("hex"),
);
expect(report.tool_profile.commands[0]?.arguments).toContain(
"--ignore-custom-section-errors",
);
expect(await readFile(fixture.path)).toEqual(fixture.bytes);
expect(report.headers).toBe(
dump.slice(0, dump.indexOf("Section Details:\n")),
);
expect(report.wat.text).toBe(`(module (@custom "name" "${payload}"))\n`);
} finally {
await provider.close();
}
},
);
const assertModeOutcome = (
mode: OutcomeMode,
+13 -2
View File
@@ -431,12 +431,23 @@ export class WabtArtifactProvider implements WasmArtifactPort {
exports,
customSectionNames,
debugModuleNames,
debugNameMetadata,
debugNameMetadata: decodedNameMetadata,
interface: interfaceFacts,
} = parseWabtWat(wat.stdout.text);
// WAT framing reads module names, not every inner name map. A producer
// warning may concern a skipped body or another custom section; without
// section attribution it cannot establish complete debug-name coverage.
const debugNameMetadata =
decodedNameMetadata === "complete" && customSectionWarnings.length > 0
? "unknown"
: decodedNameMetadata;
if (debugNameMetadata === "uninterpretable")
optionalLimitations.push(
"The optional WebAssembly name custom section could not be interpreted; its original custom-section payload remains in decoded WAT, while debug module names are unavailable.",
"The optional WebAssembly name custom section could not be fully interpreted; its original payload remains in decoded WAT. Observed module-name displays are retained for producer framing; complete debug-name coverage remains unknown.",
);
else if (debugNameMetadata === "unknown")
optionalLimitations.push(
"WABT custom-section warning attribution is unknown, so complete debug-name coverage remains unknown. Observed module-name displays are retained for producer framing.",
);
if (customSectionWarnings.length > 0)
optionalLimitations.push(
+35
View File
@@ -203,6 +203,41 @@ it.each([
debugNameMetadata: "uninterpretable",
});
});
it.each([
String.raw`\00\06\05hello\01\02\01`,
String.raw`\00\07\05hello\00`,
String.raw`\00\06\05hello\00\06\05later`,
])(
"preserves a module name already observed before malformed metadata: %s",
(payload) => {
expect(
parseWabtWat(`(module (@custom "name" "${payload}"))`),
).toMatchObject({
debugModuleNames: ["hello"],
debugNameMetadata: "uninterpretable",
});
},
);
it("stops before a module name that WABT rejects as out of order", () => {
expect(
parseWabtWat(String.raw`(module (@custom "name" "\01\00\00\06\05hello"))`),
).toMatchObject({
debugModuleNames: [],
debugNameMetadata: "uninterpretable",
});
});
it("keeps observed names across custom sections while coverage stays uninterpretable", () => {
expect(
parseWabtWat(String.raw`(module
(@custom "name" "\00\06\05hello")
(@custom "name" "\00\06\05short\01\02\01")
(@custom "name" "\00\06\05later"))`),
).toMatchObject({
customSectionNames: ["name", "name", "name"],
debugModuleNames: ["hello", "short", "later"],
debugNameMetadata: "uninterpretable",
});
});
it("frames custom section names at their NUL-terminated objdump display", async () => {
const text = await golden();
expect(parseWabtOutput(text, 76, ["test\0hidden"]).sections.at(-1)).toEqual({
+35 -9
View File
@@ -1,8 +1,18 @@
import { parseWabtInterface } from "./WabtInterface.js";
import { watString, watStringEnd } from "./WabtString.js";
import { err, ok, type Result } from "../domain/result.js";
interface WabtNameMetadataFailure {
readonly _tag: "WabtNameMetadataFailure";
readonly reason: string;
/** Names WABT already observed before rejecting the remaining metadata. */
readonly partial: readonly string[];
}
/** Read only module-name metadata from WABT's preserved name-section payload. */
const moduleNames = (bytes: Uint8Array): string[] | null => {
const moduleNames = (
bytes: Uint8Array,
): Result<readonly string[], WabtNameMetadataFailure> => {
const names: string[] = [];
let offset = 0;
const uint32 = (end: number): number => {
@@ -17,28 +27,41 @@ const moduleNames = (bytes: Uint8Array): string[] | null => {
throw new Error("Incomplete WABT name-section metadata.");
};
try {
let previousType = -1;
while (offset < bytes.length) {
const type = uint32(bytes.length);
if (type <= previousType)
throw new Error("Duplicate or out-of-order WABT name subsection.");
previousType = type;
const size = uint32(bytes.length);
const end = offset + size;
if (end > bytes.length)
throw new Error("Incomplete WABT name-section metadata.");
if (type === 0 && size !== 0) {
const length = uint32(end);
if (offset + length !== end)
const nameEnd = offset + length;
if (nameEnd > end)
throw new Error("Incomplete WABT module-name metadata.");
names.push(
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes.subarray(offset, end),
bytes.subarray(offset, nameEnd),
),
);
// WABT calls OnModuleName before rejecting trailing subsection bytes.
if (nameEnd !== end)
throw new Error("Unfinished WABT module-name subsection.");
}
offset = end;
}
} catch {
return null;
} catch (cause) {
if (!(cause instanceof Error)) throw cause;
return err({
_tag: "WabtNameMetadataFailure",
reason: cause.message,
partial: names,
});
}
return names;
return ok(names);
};
/** Extract exact top-level WABT WAT forms; no raw WASM decoding or import/export name unescaping. */
@@ -133,12 +156,15 @@ export const parseWabtWat = (text: string) => {
nameString.end + payloadPrefix[0].length,
);
const names = moduleNames(payload.bytes);
if (names === null) {
// Framing needs every observed display, even when optional
// metadata coverage is incomplete in this or an earlier section.
debugModuleNames.push(
...(names.ok ? names.value : names.error.partial),
);
if (!names.ok) {
debugNameMetadata = "uninterpretable";
debugModuleNames.length = 0;
} else if (debugNameMetadata !== "uninterpretable") {
debugNameMetadata = "complete";
debugModuleNames.push(...names);
}
}
}