[GH-ISSUE #6874] [Bug] Empty optional FunT chunk blocks BEAM module inspection #4212

Closed
opened 2026-10-31 00:47:45 +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/6874

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 BEAM service inspects the repository's compiler-produced OTP 29 fixture successfully. Appending a single zero-byte FunT chunk and updating the IFF form size makes the same inspection fail with BEAM lambda table is truncated. Appending a four-byte zero-count FunT instead succeeds. The source-built CLI reports the same failure for the zero-byte case.

Reproduction

At the tested source revision, install the locked dependencies and build using the repository instructions. Save the script below outside the checkout as repro.mts, then run from the checkout:

FIXTURES=$(mktemp -d)
node node_modules/tsx/dist/cli.mjs /path/to/repro.mts "$FIXTURES"
node scripts/rea.mjs inspect-erlang-beam "$FIXTURES/empty.beam" --json

The service script ran once for each of the three cases. The CLI ran once for the empty case. Neither the BEAM bytecode nor an Erlang emulator was executed.

import { writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const load = (path: string) => import(pathToFileURL(join(process.cwd(), path)).href);
const { createBeamService } = await load('src/composition/erlang.ts');
const { erlangBeamOtp29Fixture } = await load('src/erlang/Beam.fixture.ts');
const directory = process.argv[2];
if (!directory) throw new Error('Pass an existing fixture output directory');
const original = erlangBeamOtp29Fixture();
const u32 = (n: number) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; };
const append = (payload: Buffer) => {
  const b = Buffer.concat([original, Buffer.from('FunT'), u32(payload.length), payload]);
  b.writeUInt32BE(b.length - 8, 4);
  return b;
};
for (const [name, bytes] of [
  ['absent', original],
  ['empty', append(Buffer.alloc(0))],
  ['zero-count', append(u32(0))],
] as const) {
  const path = join(directory, `${name}.beam`);
  await writeFile(path, bytes);
  const service = createBeamService();
  try {
    const result = await service.inspect({ path });
    const facts = result.ok ? {
      ok: true,
      module: result.value.normalized_result.module,
      exports: result.value.normalized_result.exports.length,
      lambdas: result.value.normalized_result.lambdas,
      coverage: result.value.normalized_result.coverage.status,
      funt: result.value.normalized_result.chunks.filter(c => c.name === 'FunT').map(c => c.payload.bytes),
    } : {ok: false, tag: result.error._tag, issues: result.error.issues};
    console.log(JSON.stringify({name, bytes: bytes.length, sha256:createHash('sha256').update(bytes).digest('hex'),...facts}));
  } finally { await service.close(); }
}

Expected behavior

Inspect the module/atom/export/Code facts while retaining the empty optional chunk in the inventory. A zero-byte optional lambda chunk should not discard the rest of the report. REA documents static BEAM chunk/table inspection, including optional tables. Published OTP 29's loader only invokes its lambda-table parser when this chunk has a positive payload size; its chunk scanner accepts zero-length chunks.

The exact choice of lambdas: null versus an explicitly empty array can follow the format owner's absent-table semantics; the observable requirement is successful structural inspection without inventing lambda rows.

Impact and scope

Observed: adding eight bytes representing an empty optional chunk prevents all structural Evidence from being returned for an otherwise successfully inspected compiler fixture. The zero-count control recovers the original module and three exports. Occurrence in real compiler/postprocessor output is unknown. This report does not claim the modified fixture was loaded by Erlang or that runtime loading is part of REA's contract.

REA version

Source revision 3e7e102a518b9112a2a8aa0590ae6f6951fd0635; package.json version 6.7.0. The installed/published REA was not used.

Runtime and platform

Node.js v24.22.1; Linux x86_64; locked tsx 4.23.15. No external analysis provider or Erlang installation was used.

Target and artifact context

The repository's 564-byte erlangBeamOtp29Fixture() compiler-produced module, then separate copies with an appended FunT chunk containing zero bytes (572 bytes total) or a UINT32 zero row count (576 bytes total). No chunk name repeats and all IFF sizes and alignment are updated. Atom, Code, import and export payloads remain byte-identical across the cases.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Selected executed service output:

{"name":"absent","bytes":564,"ok":true,"module":"rea_beam_fixture","exports":3,"lambdas":null,"coverage":"complete","funt":[]}
{"name":"empty","bytes":572,"ok":false,"tag":"AnalysisInputError","issues":[{"path":["path"],"reason":"invalid_format","message":"BEAM lambda table is truncated."}]}
{"name":"zero-count","bytes":576,"ok":true,"module":"rea_beam_fixture","exports":3,"lambdas":[],"coverage":"complete","funt":[4]}

The CLI returned code: "invalid_request", category: "invalid_input", with the same invalid_format issue and BEAM lambda table is truncated. message.

Typed outcome or recovery expected

Successful BEAM structural Evidence retaining module: "rea_beam_fixture", its three exports and the empty FunT chunk inventory, with no lambda rows claimed. Invalid nonempty truncated lambda-table payloads should remain failures.

Analysis or suspected cause

Source-derived: REA checks only whether the FunT key exists before calling parseRows, which rejects payloads shorter than four bytes. OTP treats a zero-size optional lambda chunk as absent and skips that table parser.

Additional context

Searched open and closed issue bodies/titles for FunT, BEAM lambda, BEAM empty lambda, and BEAM empty, and reviewed the supplied open-title index. No matching lambda-chunk report was found. Related open #6816 concerns fallback from an empty AtU8 chunk to a historical Atom table; this report concerns the optional FunT chunk on a current UTF-8 compiler fixture and needs no alternate atom table. Closed #6518 concerns repeated chunk names; none repeat here.

No regression range has been established.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The executed service and CLI reject the complete structural report for a compiler fixture after addition of a zero-byte optional lambda chunk that published OTP skips. The failure is bounded to this input condition; real-world prevalence and runtime loadability of the modified fixture are untested.
  • Workaround status: available
  • Workaround basis: The executed control uses a separate copy whose FunT payload is a four-byte zero count and whose form size is adjusted, preserving the original module/export facts. Keep the original artifact for provenance.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6874 ## 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 BEAM service inspects the repository's compiler-produced OTP 29 fixture successfully. Appending a single zero-byte `FunT` chunk and updating the IFF form size makes the same inspection fail with `BEAM lambda table is truncated.` Appending a four-byte zero-count `FunT` instead succeeds. The source-built CLI reports the same failure for the zero-byte case. ## Reproduction At the tested source revision, install the locked dependencies and build using the repository instructions. Save the script below outside the checkout as `repro.mts`, then run from the checkout: ```sh FIXTURES=$(mktemp -d) node node_modules/tsx/dist/cli.mjs /path/to/repro.mts "$FIXTURES" node scripts/rea.mjs inspect-erlang-beam "$FIXTURES/empty.beam" --json ``` The service script ran once for each of the three cases. The CLI ran once for the empty case. Neither the BEAM bytecode nor an Erlang emulator was executed. ```ts import { writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const load = (path: string) => import(pathToFileURL(join(process.cwd(), path)).href); const { createBeamService } = await load('src/composition/erlang.ts'); const { erlangBeamOtp29Fixture } = await load('src/erlang/Beam.fixture.ts'); const directory = process.argv[2]; if (!directory) throw new Error('Pass an existing fixture output directory'); const original = erlangBeamOtp29Fixture(); const u32 = (n: number) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; }; const append = (payload: Buffer) => { const b = Buffer.concat([original, Buffer.from('FunT'), u32(payload.length), payload]); b.writeUInt32BE(b.length - 8, 4); return b; }; for (const [name, bytes] of [ ['absent', original], ['empty', append(Buffer.alloc(0))], ['zero-count', append(u32(0))], ] as const) { const path = join(directory, `${name}.beam`); await writeFile(path, bytes); const service = createBeamService(); try { const result = await service.inspect({ path }); const facts = result.ok ? { ok: true, module: result.value.normalized_result.module, exports: result.value.normalized_result.exports.length, lambdas: result.value.normalized_result.lambdas, coverage: result.value.normalized_result.coverage.status, funt: result.value.normalized_result.chunks.filter(c => c.name === 'FunT').map(c => c.payload.bytes), } : {ok: false, tag: result.error._tag, issues: result.error.issues}; console.log(JSON.stringify({name, bytes: bytes.length, sha256:createHash('sha256').update(bytes).digest('hex'),...facts})); } finally { await service.close(); } } ``` ## Expected behavior Inspect the module/atom/export/Code facts while retaining the empty optional chunk in the inventory. A zero-byte optional lambda chunk should not discard the rest of the report. REA documents static BEAM chunk/table inspection, including optional tables. Published OTP 29's loader only invokes its lambda-table parser when this chunk has a positive payload size; its chunk scanner accepts zero-length chunks. The exact choice of `lambdas: null` versus an explicitly empty array can follow the format owner's absent-table semantics; the observable requirement is successful structural inspection without inventing lambda rows. ## Impact and scope Observed: adding eight bytes representing an empty optional chunk prevents all structural Evidence from being returned for an otherwise successfully inspected compiler fixture. The zero-count control recovers the original module and three exports. Occurrence in real compiler/postprocessor output is unknown. This report does not claim the modified fixture was loaded by Erlang or that runtime loading is part of REA's contract. ## REA version Source revision `3e7e102a518b9112a2a8aa0590ae6f6951fd0635`; package.json version `6.7.0`. The installed/published REA was not used. ## Runtime and platform Node.js `v24.22.1`; Linux x86_64; locked `tsx` `4.23.15`. No external analysis provider or Erlang installation was used. ## Target and artifact context The repository's 564-byte `erlangBeamOtp29Fixture()` compiler-produced module, then separate copies with an appended `FunT` chunk containing zero bytes (572 bytes total) or a UINT32 zero row count (576 bytes total). No chunk name repeats and all IFF sizes and alignment are updated. Atom, Code, import and export payloads remain byte-identical across the cases. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Selected executed service output: ```jsonl {"name":"absent","bytes":564,"ok":true,"module":"rea_beam_fixture","exports":3,"lambdas":null,"coverage":"complete","funt":[]} {"name":"empty","bytes":572,"ok":false,"tag":"AnalysisInputError","issues":[{"path":["path"],"reason":"invalid_format","message":"BEAM lambda table is truncated."}]} {"name":"zero-count","bytes":576,"ok":true,"module":"rea_beam_fixture","exports":3,"lambdas":[],"coverage":"complete","funt":[4]} ``` The CLI returned `code: "invalid_request"`, `category: "invalid_input"`, with the same `invalid_format` issue and `BEAM lambda table is truncated.` message. - Expected source: [REA's documented optional-table observations](https://github.com/morluto/rea/blob/3e7e102a518b9112a2a8aa0590ae6f6951fd0635/docs/erlang-beam.md#L34-L42), [OTP 29's positive-size lambda-table condition](https://github.com/erlang/otp/blob/550d7b7898706c7822362c42e7b93120c1d1f29a/erts/emulator/beam/beam_file.c#L1689-L1694), and [its chunk scanner](https://github.com/erlang/otp/blob/550d7b7898706c7822362c42e7b93120c1d1f29a/erts/emulator/beam/beam_file.c#L1523-L1576). - Fixture provenance: [compiler-produced fixture and its Erlang source](https://github.com/morluto/rea/blob/3e7e102a518b9112a2a8aa0590ae6f6951fd0635/src/erlang/Beam.fixture.ts#L90-L114). - Failure source: [presence-based FunT parsing](https://github.com/morluto/rea/blob/3e7e102a518b9112a2a8aa0590ae6f6951fd0635/src/erlang/BeamParser.ts#L303-L314) calls [a parser requiring four count bytes](https://github.com/morluto/rea/blob/3e7e102a518b9112a2a8aa0590ae6f6951fd0635/src/erlang/BeamParser.ts#L169-L177). - Evidence provenance: REA service/CLI outputs are observed; OTP acceptance of empty optional lambda chunks is source-derived. - Local verification: production service composition and source-built CLI, using only local inert fixtures. - Reproduction completeness: complete for this structural-inspection failure and its two controls; Erlang loading, MCP transport and the published npm package were not tested. ## Typed outcome or recovery expected Successful BEAM structural Evidence retaining `module: "rea_beam_fixture"`, its three exports and the empty `FunT` chunk inventory, with no lambda rows claimed. Invalid nonempty truncated lambda-table payloads should remain failures. ## Analysis or suspected cause Source-derived: REA checks only whether the `FunT` key exists before calling `parseRows`, which rejects payloads shorter than four bytes. OTP treats a zero-size optional lambda chunk as absent and skips that table parser. ## Additional context Searched open and closed issue bodies/titles for `FunT`, `BEAM lambda`, `BEAM empty lambda`, and `BEAM empty`, and reviewed the supplied open-title index. No matching lambda-chunk report was found. Related open [#6816](https://github.com/morluto/rea/issues/6816) concerns fallback from an empty `AtU8` chunk to a historical `Atom` table; this report concerns the optional `FunT` chunk on a current UTF-8 compiler fixture and needs no alternate atom table. Closed [#6518](https://github.com/morluto/rea/issues/6518) concerns repeated chunk names; none repeat here. No regression range has been established. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The executed service and CLI reject the complete structural report for a compiler fixture after addition of a zero-byte optional lambda chunk that published OTP skips. The failure is bounded to this input condition; real-world prevalence and runtime loadability of the modified fixture are untested. - Workaround status: available - Workaround basis: The executed control uses a separate copy whose FunT payload is a four-byte zero count and whose form size is adjusted, preserving the original module/export facts. Keep the original artifact for provenance.
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#4212
No description provided.