From 9eee709f0cc4064808579c225814898a475ca388 Mon Sep 17 00:00:00 2001 From: Nithilan Kumaran <126580000+sathariels@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:29:04 -0500 Subject: [PATCH] fix(wasm): accept unpadded WABT section rows (#1963) * fix(wasm): accept unpadded WABT section rows WABT 1.0.42 right-aligns section names in a nine-column field. DataCount fills that field, so its objdump row has no leading whitespace. Accept that legal row. Header recognition, the required field shape, and section range checks still reject malformed output. Fixes #1956 * fix(ci): refresh MIPS expectations and Windows test paths Assert admitted soft-float ABI metadata and its committed storage policy through the public target resolver, retain FP64 rejection, and point the curated Windows lane at the moved filesystem reconstruction tests. Validation: reproduced all four prior failures; 97 focused tests and check:ci pass. Native Windows and real Ghidra execution were not performed locally. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- docs/testing.md | 3 +- scripts/verify-wasm-artifact.mjs | 25 +++++ src/wasm/WabtOutput.test.ts | 104 ++++++++++++++++++ src/wasm/WabtOutput.ts | 2 +- .../boundary/filesystem/mipsAbiFlags.test.ts | 23 ++-- 6 files changed, 147 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb7e62067..a8dc167e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -493,7 +493,7 @@ jobs: tests/acceptance/webSourceLocationCli.test.ts tests/boundary/cli/referenceSourceImportStatus.test.ts tests/boundary/filesystem/asarInventoryFreshness.test.ts - tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts + tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts tests/conformance/providers/ghidra/windowsCapabilities.test.ts src/ghidra/GhidraInstallation.test.ts tests/boundary/providers/ghidra/ghidraTransport.test.ts diff --git a/docs/testing.md b/docs/testing.md index 3c0d401b7..45c3dcf84 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1386,7 +1386,8 @@ generation. It installs nothing. Without configuration, the default invocation reports a named skip; `--require-tools` makes missing configuration fail. The verifier creates and validates real modules with custom sections, multiple -import/export kinds, escaped names and empty modules. It compares exact WAT, +import/export kinds, escaped names, an empty module, and a bulk-memory +DataCount section. It compares exact WAT, section output and selected/tool digests through the compiled CLI and real MCP SDK, validates advertised schemas, checks identical CLI/MCP Evidence IDs and session bundle readback, retains invalid/truncated module diagnostics, preserves diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs index ce139c5e2..9a8c53644 100644 --- a/scripts/verify-wasm-artifact.mjs +++ b/scripts/verify-wasm-artifact.mjs @@ -122,6 +122,15 @@ try { new URL("../tests/fixtures/wasm/custom-names.wat", import.meta.url), "utf8", ), + `(module + (memory 1) + (data "x") + (func (export "init") + i32.const 0 + i32.const 0 + i32.const 1 + memory.init 0 + data.drop 0))\n`, ]; for (const [index, source] of sources.entries()) { const fixtureRoot = join(root.path, `fixture-${index}`); @@ -220,6 +229,22 @@ try { '"unicode-é\\\t"', ], ); + if (index === 4) { + assert.match( + headers, + /^DataCount start=0x[\da-f]+ end=0x[\da-f]+ \(size=0x[\da-f]+\) count: 1$/m, + ); + assert.deepEqual( + report.sections.find(({ kind }) => kind === "DataCount"), + { + kind: "DataCount", + start: 0x23, + end: 0x24, + bytes: 1, + description: "count: 1", + }, + ); + } const bundleReply = await client.callTool({ name: "get_evidence_bundle", arguments: {}, diff --git a/src/wasm/WabtOutput.test.ts b/src/wasm/WabtOutput.test.ts index b593a88df..c12402b38 100644 --- a/src/wasm/WabtOutput.test.ts +++ b/src/wasm/WabtOutput.test.ts @@ -33,6 +33,110 @@ it("retains real WABT section ranges, custom section identity and original heade ["test"], ), ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + text.replace( + "\nSection Details:\n", + "\nDataCount start=0x00000040 end=0x0000004c (size=0x0000000c)\nSection Details:\n", + ), + 76, + ["test"], + ), + ).toThrow(/Unrecognized/u); +}); +const dataCountModuleBytes = 0x3d; +const unpaddedDataCountRow = + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001) count: 1"; +const dataCountDump = [ + "", + "module.wasm:\tfile format wasm 0x1", + "", + "Sections:", + "", + " Type start=0x0000000a end=0x0000000e (size=0x00000004) count: 1", + " Function start=0x00000010 end=0x00000012 (size=0x00000002) count: 1", + " Memory start=0x00000014 end=0x00000017 (size=0x00000003) count: 1", + " Export start=0x00000019 end=0x00000021 (size=0x00000008) count: 1", + unpaddedDataCountRow, + " Code start=0x00000026 end=0x00000037 (size=0x00000011) count: 1", + " Data start=0x00000039 end=0x0000003d (size=0x00000004) count: 1", + "", + "Section Details:", + "", +].join("\n"); +it("accepts an unpadded DataCount row and still rejects malformed output and bad ranges", () => { + const parsed = parseWabtOutput(dataCountDump, dataCountModuleBytes, []); + expect(parsed.sections.map(({ kind }) => kind)).toEqual([ + "Type", + "Function", + "Memory", + "Export", + "DataCount", + "Code", + "Data", + ]); + expect(parsed.sections.find(({ kind }) => kind === "DataCount")).toEqual({ + kind: "DataCount", + start: 0x23, + end: 0x24, + bytes: 1, + description: "count: 1", + }); + expect(parsed.headers).toContain(`\n${unpaddedDataCountRow}\n`); + expect(() => + parseWabtOutput( + dataCountDump.replace(unpaddedDataCountRow, "DataCount count: 1"), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace(unpaddedDataCountRow, "Sections:"), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + unpaddedDataCountRow, + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001)count: 1", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/Unrecognized/u); + expect(() => + parseWabtOutput( + dataCountDump.replace("(size=0x00000001)", "(size=0x00000002)"), + dataCountModuleBytes, + [], + ), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + "DataCount start=0x00000023 end=0x00000024 (size=0x00000001)", + "DataCount start=0x00000020 end=0x00000021 (size=0x00000001)", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput(dataCountDump, dataCountModuleBytes - 1, []), + ).toThrow(/ranges/u); + expect(() => + parseWabtOutput( + dataCountDump.replace( + "\nmodule.wasm:\tfile format wasm 0x1\n\nSections:\n\n", + "", + ), + dataCountModuleBytes, + [], + ), + ).toThrow(/header/u); }); it("extracts escaped WAT import/export forms while retaining multiline producer text", async () => { const text = await readFile( diff --git a/src/wasm/WabtOutput.ts b/src/wasm/WabtOutput.ts index f2406dbdf..a434b7d17 100644 --- a/src/wasm/WabtOutput.ts +++ b/src/wasm/WabtOutput.ts @@ -13,7 +13,7 @@ export const parseWabtOutput = ( const sections = []; while (!text.startsWith("\nSection Details:\n", offset)) { const match = - /^[ \t]+(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec( + /^[ \t]*(\w+) start=0x([\da-f]+) end=0x([\da-f]+) \(size=0x([\da-f]+)\) /u.exec( text.slice(offset), ); if (match === null) throw new Error("Unrecognized WABT section row."); diff --git a/tests/boundary/filesystem/mipsAbiFlags.test.ts b/tests/boundary/filesystem/mipsAbiFlags.test.ts index 1c5467d0a..4cab9b702 100644 --- a/tests/boundary/filesystem/mipsAbiFlags.test.ts +++ b/tests/boundary/filesystem/mipsAbiFlags.test.ts @@ -131,7 +131,7 @@ for (const little of [true, false]) { const fp64 = await inspect(bytes, little); assert.ok(fp64.ok); const changed = target({ ...metadata(little), abiFlags: fp64.value }); - assert.match(ghidraMipsUnsupportedReason(changed) ?? "", /register modes/u); + assert.match(ghidraMipsUnsupportedReason(changed) ?? "", /FP64/u); assert.notDeepEqual(ghidraMipsProfileParameters(changed), before); }); } @@ -271,14 +271,6 @@ const unsupported: [string, (bytes: Buffer) => void, RegExp][] = [ }, /MIPS32r2/u, ], - [ - "soft-float", - (b) => { - b[abiOffset + 5] = 0; - b[abiOffset + 7] = 3; - }, - /register modes/u, - ], [ "unspecified FP ABI", (b) => { @@ -309,6 +301,19 @@ for (const [name, change, reason] of unsupported) { ); }); } +it("resolves soft-float ABI metadata and commits its integer-register storage policy", async () => { + const bytes = fixture(); + bytes[abiOffset + 5] = 0; + bytes[abiOffset + 7] = 3; + const resolved = await resolveFixture(bytes); + assert.equal(resolved.mips.abiFlags?.cpr1Size, 0); + assert.equal(resolved.mips.abiFlags?.fpAbi, 3); + assert.equal(ghidraMipsUnsupportedReason(resolved), null); + assert.equal( + ghidraMipsProfileParameters(resolved).mips_soft_float_storage, + "o32-soft-float-prototype-v1", + ); +}); it("preserves the caller's cancellation error during table inspection", async () => { const cancelled = new Error("caller cancellation sentinel"); let checks = 0;