[GH-ISSUE #6885] [Bug] WASM objdump framing fails when optional name metadata is partially decoded #4219

Closed
opened 2026-10-31 00:47:49 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6885

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

The production WabtArtifactProvider.inspect() returns unreadable_output / Unsupported WABT section header format. for a 49-byte WASM containing a valid exported empty function and an optional name section whose valid module-name subsection precedes a truncated function-name subsection.

All three real WABT 1.0.42 commands exit zero, retaining the module, its export and the optional-metadata warning. Controls with the same core module and either a valid name section or an initially malformed name section succeed. No provider was mocked and no target bytecode was executed.

Reproduction

Use the checkout at the SHA below, its locked dependencies, Node.js 24.22.1, and the official WABT 1.0.42 Linux x64 release. The downloaded release archive matched its published SHA-256, 84895407a6bbb80e918f33b16b2fb2206021c150b6bc9ff6f761263a745ab131.

Save the executed source-boundary probe below as /tmp/name-probe.mjs. From the checkout root run the same command structure used in the probe, choosing an empty output directory and the absolute WABT bin directory:

mise exec node@24.22.1 -- node --import ./node_modules/tsx/dist/loader.mjs /tmp/name-probe.mjs /tmp/name-probe-output /absolute/path/to/wabt-1.0.42/bin
Executed probe: constructs three inert fixtures, records producer output, and calls the production provider
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { execFileSync, spawnSync } from 'node:child_process';

const evidenceRoot = resolve(process.argv[2]);
const bin = resolve(process.argv[3]);
await mkdir(evidenceRoot, { recursive: true });
const { WabtArtifactProvider } = await import(pathToFileURL(resolve('src/wasm/WabtArtifactProvider.ts')));
const { inspectWasmArtifactInputSchema } = await import(pathToFileURL(resolve('src/domain/wasm/wasmArtifact.ts')));
const { projectAnalysisError } = await import(pathToFileURL(resolve('src/domain/analysisErrorProjection.ts')));
const base = Buffer.from('0061736d0100000001040160000003020100070501016600000a040102000b', 'hex');
const namePrefix = Buffer.from([0, 6, 5, ...Buffer.from('hello')]);
const cases = [
  ['valid-name', namePrefix],
  ['valid-name-then-truncated-subsection', Buffer.concat([namePrefix, Buffer.from([1, 2, 1])])],
  ['truncated-name-only', Buffer.from([0, 6, 5, ...Buffer.from('name')])],
];
const results = [];
for (const [label, payload] of cases) {
  const custom = Buffer.concat([Buffer.from([4]), Buffer.from('name'), payload]);
  const bytes = Buffer.concat([base, Buffer.from([0, custom.length]), custom]);
  const path = join(evidenceRoot, label + '.wasm');
  await writeFile(path, bytes);
  const producer = [];
  for (const [tool, args] of [
    ['wasm-validate', ['--enable-all', '--ignore-custom-section-errors', path]],
    ['wasm-objdump', ['-h', '-x', path]],
    ['wasm2wat', ['--enable-all', '--ignore-custom-section-errors', path]],
  ]) {
    const run = spawnSync(join(bin, tool), args, { encoding: 'utf8' });
    producer.push({ tool, arguments: args.map(a => a === path ? label + '.wasm' : a), exit: run.status, stdout: run.stdout, stderr: run.stderr });
  }
  const provider = new WabtArtifactProvider({ ...process.env, REA_WABT_BIN_DIRECTORY: bin });
  const result = await provider.inspect(inspectWasmArtifactInputSchema.parse({ path }));
  await provider.close();
  results.push({
    case: label,
    bytes: bytes.length,
    sha256: createHash('sha256').update(bytes).digest('hex'),
    producer,
    rea: result.ok ? {
      ok: true,
      validation: result.value.result.validation,
      debug_name_metadata: result.value.result.debug_name_metadata,
      interface: result.value.result.interface,
      custom_section_warnings: result.value.result.custom_section_warnings,
    } : { ok: false, error: projectAnalysisError(result.error) },
  });
}
const output = {
  command: 'mise exec node@24.22.1 -- node --import ./node_modules/tsx/dist/loader.mjs ../evidence/languages-wabt-name-probe.mjs ../evidence/languages-wabt-name ../evidence/languages-wabt/wabt-1.0.42/bin',
  source: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(),
  runtime: process.version,
  platform: execFileSync('uname', ['-srmo'], { encoding: 'utf8' }).trim(),
  wabt_version: execFileSync(join(bin, 'wasm-validate'), ['--version'], { encoding: 'utf8' }).trim(),
  fixture_base_hex: base.toString('hex'),
  results,
};
await writeFile(join(evidenceRoot, 'results.json'), JSON.stringify(output, null, 2));
console.log(JSON.stringify(output, null, 2));

Each of the three source-provider cases ran once in the final comparative probe. The failing fixture is exactly:

0061736d0100000001040160000003020100070501016600000a040102000b0010046e616d6500060568656c6c6f010201

Expected behavior

Return the valid core inspection, retain the WABT warning and raw custom-section bytes, and mark optional debug metadata uninterpretable. The documented optional custom-section contract says core validation remains authoritative when optional metadata cannot be interpreted. The initially malformed control already exhibits that behavior.

Impact and scope

P2: optional debug metadata blocks the entire inspection of this otherwise valid core module, including its export interface. Verified on one reduced 49-byte artifact; production prevalence and published-package behavior were not measured. This report concerns recovery from incomplete optional metadata, not acceptance of invalid core WASM.

A verified read-only workaround is to inspect the same original bytes directly with WABT: its validation, section dump and decoded WAT all succeed and retain the export and warning. That bypasses REA's normalized Evidence output. Editing/removing the name section was not tested and would change artifact identity.

REA version

Source main SHA 2443b949a48c3b2eba687e7381682e49e909d5b9.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64 GNU/Linux; repository-locked dependencies and official WABT 1.0.42 Linux x64 executables. No installed REA was used.

Target and artifact context

Synthetic core WASM module exporting f: () -> (), plus a name custom section containing module name hello followed by subsection bytes 01 02 01 (declared two-byte payload with only one byte present). Core bytes are identical in all controls.

Interface

Other — direct production WabtArtifactProvider.inspect() source boundary. CLI and MCP were not exercised.

Earliest failure stage

Transport or result projection

Relevant output or evidence

The failing artifact's SHA-256 is e80ef9007c79ed750db8d86f95995f6399ce1171d1c38c31bf0ef31a2336eddc.

case                                      WABT exits      REA result
valid-name                                0, 0, 0         success; debug_name_metadata=complete
valid-name-then-truncated-subsection       0, 0, 0         unreadable_output
truncated-name-only                       0, 0, 0         success; debug_name_metadata=uninterpretable

WABT warning for the failing case:
0000030: warning: invalid sub-section size: extends past end

REA error details:
operation: inspect_wasm_artifact
reason: Unsupported WABT section header format.

The captured objdump output starts with:

module.wasm:    file format wasm 0x1
module name: <hello>

Sections:

The retained decoded WAT contains the valid export and the original custom payload:

(module $hello
  (type (;0;) (func))
  (func (;0;) (type 0))
  (export "f" (func 0))
  (@custom "name" "\00\06\05hello\01\02\01"))

Typed outcome or recovery expected

Successful valid-core inspection with the existing optional-metadata warning/limitation fields, preserving exact original artifact identity and raw producer output.

Analysis or suspected cause

Source-derived explanation: moduleNames() returns null after the later framing failure. parseWabtWat() clears every debug module name, including the earlier successfully decoded one. WABT has already printed that earlier module name in its objdump preamble. parseWabtOutput() constructs a nameless preamble from the cleared list and rejects the actual output. Keeping optional metadata unknown must not make the core section framing depend on an empty module-name list.

Additional context

Closed #6812 reported strict validation rejecting uninterpretable optional metadata. That initial malformed-metadata case now succeeds, as the executed control shows. This report is a remaining failure at a later boundary: WABT has decoded a module name before a subsequent subsection fails, and REA rejects the resulting objdump preamble with unreadable_output despite all WABT commands succeeding. Open #5395 concerns a different debug-name collision in interface comparison. No source files were changed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The real production provider rejects all core inspection results for the reduced artifact even though the selected real WABT profile validates and decodes it; the failure is bounded to optional metadata with this framing pattern.
  • Workaround status: available
  • Workaround basis: The same probe directly ran the three real WABT commands on the original bytes; each succeeded and retained the export and diagnostic. This preserves read-only artifact inspection, although REA normalized Evidence is unavailable.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6885 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction ## Observed behavior The production `WabtArtifactProvider.inspect()` returns `unreadable_output` / `Unsupported WABT section header format.` for a 49-byte WASM containing a valid exported empty function and an optional `name` section whose valid module-name subsection precedes a truncated function-name subsection. All three real WABT 1.0.42 commands exit zero, retaining the module, its export and the optional-metadata warning. Controls with the same core module and either a valid name section or an initially malformed name section succeed. No provider was mocked and no target bytecode was executed. ## Reproduction Use the checkout at the SHA below, its locked dependencies, Node.js 24.22.1, and the official WABT 1.0.42 Linux x64 release. The downloaded release archive matched its published SHA-256, `84895407a6bbb80e918f33b16b2fb2206021c150b6bc9ff6f761263a745ab131`. Save the executed source-boundary probe below as `/tmp/name-probe.mjs`. From the checkout root run the same command structure used in the probe, choosing an empty output directory and the absolute WABT bin directory: ```sh mise exec node@24.22.1 -- node --import ./node_modules/tsx/dist/loader.mjs /tmp/name-probe.mjs /tmp/name-probe-output /absolute/path/to/wabt-1.0.42/bin ``` <details> <summary>Executed probe: constructs three inert fixtures, records producer output, and calls the production provider</summary> ```js import { createHash } from 'node:crypto'; import { mkdir, writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { execFileSync, spawnSync } from 'node:child_process'; const evidenceRoot = resolve(process.argv[2]); const bin = resolve(process.argv[3]); await mkdir(evidenceRoot, { recursive: true }); const { WabtArtifactProvider } = await import(pathToFileURL(resolve('src/wasm/WabtArtifactProvider.ts'))); const { inspectWasmArtifactInputSchema } = await import(pathToFileURL(resolve('src/domain/wasm/wasmArtifact.ts'))); const { projectAnalysisError } = await import(pathToFileURL(resolve('src/domain/analysisErrorProjection.ts'))); const base = Buffer.from('0061736d0100000001040160000003020100070501016600000a040102000b', 'hex'); const namePrefix = Buffer.from([0, 6, 5, ...Buffer.from('hello')]); const cases = [ ['valid-name', namePrefix], ['valid-name-then-truncated-subsection', Buffer.concat([namePrefix, Buffer.from([1, 2, 1])])], ['truncated-name-only', Buffer.from([0, 6, 5, ...Buffer.from('name')])], ]; const results = []; for (const [label, payload] of cases) { const custom = Buffer.concat([Buffer.from([4]), Buffer.from('name'), payload]); const bytes = Buffer.concat([base, Buffer.from([0, custom.length]), custom]); const path = join(evidenceRoot, label + '.wasm'); await writeFile(path, bytes); const producer = []; for (const [tool, args] of [ ['wasm-validate', ['--enable-all', '--ignore-custom-section-errors', path]], ['wasm-objdump', ['-h', '-x', path]], ['wasm2wat', ['--enable-all', '--ignore-custom-section-errors', path]], ]) { const run = spawnSync(join(bin, tool), args, { encoding: 'utf8' }); producer.push({ tool, arguments: args.map(a => a === path ? label + '.wasm' : a), exit: run.status, stdout: run.stdout, stderr: run.stderr }); } const provider = new WabtArtifactProvider({ ...process.env, REA_WABT_BIN_DIRECTORY: bin }); const result = await provider.inspect(inspectWasmArtifactInputSchema.parse({ path })); await provider.close(); results.push({ case: label, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex'), producer, rea: result.ok ? { ok: true, validation: result.value.result.validation, debug_name_metadata: result.value.result.debug_name_metadata, interface: result.value.result.interface, custom_section_warnings: result.value.result.custom_section_warnings, } : { ok: false, error: projectAnalysisError(result.error) }, }); } const output = { command: 'mise exec node@24.22.1 -- node --import ./node_modules/tsx/dist/loader.mjs ../evidence/languages-wabt-name-probe.mjs ../evidence/languages-wabt-name ../evidence/languages-wabt/wabt-1.0.42/bin', source: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), runtime: process.version, platform: execFileSync('uname', ['-srmo'], { encoding: 'utf8' }).trim(), wabt_version: execFileSync(join(bin, 'wasm-validate'), ['--version'], { encoding: 'utf8' }).trim(), fixture_base_hex: base.toString('hex'), results, }; await writeFile(join(evidenceRoot, 'results.json'), JSON.stringify(output, null, 2)); console.log(JSON.stringify(output, null, 2)); ``` </details> Each of the three source-provider cases ran once in the final comparative probe. The failing fixture is exactly: ```text 0061736d0100000001040160000003020100070501016600000a040102000b0010046e616d6500060568656c6c6f010201 ``` ## Expected behavior Return the valid core inspection, retain the WABT warning and raw custom-section bytes, and mark optional debug metadata `uninterpretable`. The documented [optional custom-section contract](https://github.com/morluto/rea/blob/2443b949a48c3b2eba687e7381682e49e909d5b9/docs/wasm-artifacts.md#L75-L81) says core validation remains authoritative when optional metadata cannot be interpreted. The initially malformed control already exhibits that behavior. ## Impact and scope P2: optional debug metadata blocks the entire inspection of this otherwise valid core module, including its export interface. Verified on one reduced 49-byte artifact; production prevalence and published-package behavior were not measured. This report concerns recovery from incomplete optional metadata, not acceptance of invalid core WASM. A verified read-only workaround is to inspect the same original bytes directly with WABT: its validation, section dump and decoded WAT all succeed and retain the export and warning. That bypasses REA's normalized Evidence output. Editing/removing the name section was not tested and would change artifact identity. ## REA version Source main SHA `2443b949a48c3b2eba687e7381682e49e909d5b9`. ## Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64 GNU/Linux; repository-locked dependencies and official WABT 1.0.42 Linux x64 executables. No installed REA was used. ## Target and artifact context Synthetic core WASM module exporting `f: () -> ()`, plus a `name` custom section containing module name `hello` followed by subsection bytes `01 02 01` (declared two-byte payload with only one byte present). Core bytes are identical in all controls. ## Interface Other — direct production `WabtArtifactProvider.inspect()` source boundary. CLI and MCP were not exercised. ## Earliest failure stage Transport or result projection ## Relevant output or evidence The failing artifact's SHA-256 is `e80ef9007c79ed750db8d86f95995f6399ce1171d1c38c31bf0ef31a2336eddc`. ```text case WABT exits REA result valid-name 0, 0, 0 success; debug_name_metadata=complete valid-name-then-truncated-subsection 0, 0, 0 unreadable_output truncated-name-only 0, 0, 0 success; debug_name_metadata=uninterpretable WABT warning for the failing case: 0000030: warning: invalid sub-section size: extends past end REA error details: operation: inspect_wasm_artifact reason: Unsupported WABT section header format. ``` The captured objdump output starts with: ```text module.wasm: file format wasm 0x1 module name: <hello> Sections: ``` The retained decoded WAT contains the valid export and the original custom payload: ```wat (module $hello (type (;0;) (func)) (func (;0;) (type 0)) (export "f" (func 0)) (@custom "name" "\00\06\05hello\01\02\01")) ``` ## Typed outcome or recovery expected Successful valid-core inspection with the existing optional-metadata warning/limitation fields, preserving exact original artifact identity and raw producer output. ## Analysis or suspected cause Source-derived explanation: [`moduleNames()`](https://github.com/morluto/rea/blob/2443b949a48c3b2eba687e7381682e49e909d5b9/src/wasm/WabtWat.ts#L19-L40) returns `null` after the later framing failure. [`parseWabtWat()`](https://github.com/morluto/rea/blob/2443b949a48c3b2eba687e7381682e49e909d5b9/src/wasm/WabtWat.ts#L135-L141) clears every debug module name, including the earlier successfully decoded one. WABT has already printed that earlier module name in its objdump preamble. [`parseWabtOutput()`](https://github.com/morluto/rea/blob/2443b949a48c3b2eba687e7381682e49e909d5b9/src/wasm/WabtOutput.ts#L12-L19) constructs a nameless preamble from the cleared list and rejects the actual output. Keeping optional metadata unknown must not make the core section framing depend on an empty module-name list. ## Additional context Closed [#6812](https://github.com/morluto/rea/issues/6812) reported strict validation rejecting uninterpretable optional metadata. That initial malformed-metadata case now succeeds, as the executed control shows. This report is a remaining failure at a later boundary: WABT has decoded a module name before a subsequent subsection fails, and REA rejects the resulting objdump preamble with `unreadable_output` despite all WABT commands succeeding. Open #5395 concerns a different debug-name collision in interface comparison. No source files were changed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The real production provider rejects all core inspection results for the reduced artifact even though the selected real WABT profile validates and decodes it; the failure is bounded to optional metadata with this framing pattern. - Workaround status: available - Workaround basis: The same probe directly ran the three real WABT commands on the original bytes; each succeeded and retained the export and diagnostic. This preserves read-only artifact inspection, although REA normalized Evidence is unavailable.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4219
No description provided.