[GH-ISSUE #6816] [Bug] Empty AtU8 chunk blocks legacy BEAM atom inspection #4164

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

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 bundled BEAM service reads a legacy Atom table successfully, but adding a distinct zero-byte AtU8 chunk makes the same structural inspection fail with AnalysisInputError: BEAM atom table is truncated. No chunk name is repeated. Historical OTP selects the legacy table when the UTF-8 chunk is empty.

Reproduction

From the source checkout, save this script outside the checkout as repro.mts and run node_modules/.bin/tsx /path/to/repro.mts. This exercises the production createBeamService() composition used by both adapters, not the CLI transport. Each of the three cases was executed once.

import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const { createBeamService } = await import(pathToFileURL(join(process.cwd(), 'src/composition/erlang.ts')).href);
const directory = process.argv[2] ?? await mkdtemp(join(tmpdir(), 'rea-beam-atoms-'));
const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; };
const chunk = (name, payload) => Buffer.concat([Buffer.from(name), u32(payload.length), payload, Buffer.alloc((-payload.length) & 3)]);
const atoms = (names) => Buffer.concat([u32(names.length), ...names.flatMap((name) => { const b = Buffer.from(name, 'latin1'); return [Buffer.from([b.length]), b]; })]);
const form = (chunks) => { const body = Buffer.concat([Buffer.from('BEAM'), ...chunks]); return Buffer.concat([Buffer.from('FOR1'), u32(body.length), body]); };
const legacy = chunk('Atom', atoms(['demo', 'old']));
const exp = chunk('ExpT', Buffer.concat([u32(1), u32(2), u32(0), u32(1)]));
const fixtures = [
  ['legacy-control', [legacy, exp]],
  ['empty-atu8', [legacy, chunk('AtU8', Buffer.alloc(0)), exp]],
  ['nonempty-atu8-control', [legacy, chunk('AtU8', atoms(['demo', 'new'])), exp]],
];
for (const [name, chunks] of fixtures) {
  const path = join(directory, `${name}.beam`);
  await writeFile(path, form(chunks));
  const service = createBeamService();
  try {
    const result = await service.inspect({ path });
    if (result.ok) {
      const report = result.value.normalized_result;
      console.log(JSON.stringify({ name, ok: true, module: report.module, function: report.exports[0].function, chunks: report.chunks.map((c) => c.name), coverage: report.coverage.status }));
    } else {
      console.log(JSON.stringify({ name, ok: false, tag: result.error._tag, issues: result.error.issues }));
    }
  } finally { await service.close(); }
}

Expected behavior

Preserve the decoded legacy module/export metadata when the only addition is an empty alternate atom chunk. REA documents inspection of historical Atom tables. Published OTP 20.3 explicitly selects AtU8 only when its payload size is greater than zero, otherwise selecting the Latin-1 Atom table. A nonempty AtU8 table must retain its normal precedence.

Impact and scope

A bounded compatibility failure returns no structural Evidence for this legacy-container combination. Removing the empty alternate chunk in a separate copy restores the decoded facts in the executed control. Occurrence in compiler/tool-generated files is unknown. This report concerns historical metadata inspection: current OTP no longer loads legacy Atom tables. No Erlang loading test was performed.

REA version

Source revision e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4, package.json version 6.7.0. The published npm artifact was not tested.

Runtime and platform

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

Target and artifact context

Synthetic FOR1/BEAM structural containers containing one legacy Atom table and one ExpT row, with an absent, empty, or nonempty alternate AtU8 chunk. These reduced containers have no Code chunk and are not claimed to be loadable modules. All chunk sizes and alignment ranges fit.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed output:

{"name":"legacy-control","ok":true,"module":"demo","function":"old","chunks":["Atom","ExpT"],"coverage":"complete"}
{"name":"empty-atu8","ok":false,"tag":"AnalysisInputError","issues":[{"path":["path"],"reason":"invalid_format","message":"BEAM atom table is truncated."}]}
{"name":"nonempty-atu8-control","ok":true,"module":"demo","function":"new","chunks":["Atom","AtU8","ExpT"],"coverage":"complete"}

Typed outcome or recovery expected

Return successful structural Evidence with module demo, export function old, and both atom chunks inventoried for the empty-alternate case. Keep the nonempty-alternate control resolving its export to new. This restores the same table-selection contract without discarding valid UTF-8 precedence or chunk provenance.

Analysis or suspected cause

Source-derived: REA chooses AtU8 whenever the map contains that name, then passes its zero-byte payload to a parser that requires a four-byte count. Historical OTP's chunk scanner accepts the zero-byte occurrence; its atom-table selection then falls back by size.

Additional context

Open and closed searches for BEAM, atom, AtU8, and empty BEAM found no matching report. Related closed #6518 concerns repeated occurrences of the same chunk name; this reproduction has distinct Atom and AtU8 names.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: REA rejects an otherwise inspectable legacy atom/export report when an alternate chunk that historical OTP ignores is present; the REA failure is observed, while compatibility is source-derived and real-world prevalence is unknown.
  • Workaround status: available
  • Workaround basis: The executed legacy-only control recovers the exact module/export facts after omitting the empty alternate chunk in a separate synthetic copy; retain the original artifact for provenance.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6816 ## 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 bundled BEAM service reads a legacy `Atom` table successfully, but adding a distinct zero-byte `AtU8` chunk makes the same structural inspection fail with `AnalysisInputError: BEAM atom table is truncated.` No chunk name is repeated. Historical OTP selects the legacy table when the UTF-8 chunk is empty. ## Reproduction From the source checkout, save this script outside the checkout as `repro.mts` and run `node_modules/.bin/tsx /path/to/repro.mts`. This exercises the production `createBeamService()` composition used by both adapters, not the CLI transport. Each of the three cases was executed once. ```ts import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const { createBeamService } = await import(pathToFileURL(join(process.cwd(), 'src/composition/erlang.ts')).href); const directory = process.argv[2] ?? await mkdtemp(join(tmpdir(), 'rea-beam-atoms-')); const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; }; const chunk = (name, payload) => Buffer.concat([Buffer.from(name), u32(payload.length), payload, Buffer.alloc((-payload.length) & 3)]); const atoms = (names) => Buffer.concat([u32(names.length), ...names.flatMap((name) => { const b = Buffer.from(name, 'latin1'); return [Buffer.from([b.length]), b]; })]); const form = (chunks) => { const body = Buffer.concat([Buffer.from('BEAM'), ...chunks]); return Buffer.concat([Buffer.from('FOR1'), u32(body.length), body]); }; const legacy = chunk('Atom', atoms(['demo', 'old'])); const exp = chunk('ExpT', Buffer.concat([u32(1), u32(2), u32(0), u32(1)])); const fixtures = [ ['legacy-control', [legacy, exp]], ['empty-atu8', [legacy, chunk('AtU8', Buffer.alloc(0)), exp]], ['nonempty-atu8-control', [legacy, chunk('AtU8', atoms(['demo', 'new'])), exp]], ]; for (const [name, chunks] of fixtures) { const path = join(directory, `${name}.beam`); await writeFile(path, form(chunks)); const service = createBeamService(); try { const result = await service.inspect({ path }); if (result.ok) { const report = result.value.normalized_result; console.log(JSON.stringify({ name, ok: true, module: report.module, function: report.exports[0].function, chunks: report.chunks.map((c) => c.name), coverage: report.coverage.status })); } else { console.log(JSON.stringify({ name, ok: false, tag: result.error._tag, issues: result.error.issues })); } } finally { await service.close(); } } ``` ## Expected behavior Preserve the decoded legacy module/export metadata when the only addition is an empty alternate atom chunk. REA documents inspection of historical `Atom` tables. Published OTP 20.3 explicitly selects `AtU8` only when its payload size is greater than zero, otherwise selecting the Latin-1 `Atom` table. A nonempty `AtU8` table must retain its normal precedence. ## Impact and scope A bounded compatibility failure returns no structural Evidence for this legacy-container combination. Removing the empty alternate chunk in a separate copy restores the decoded facts in the executed control. Occurrence in compiler/tool-generated files is unknown. This report concerns historical metadata inspection: current OTP no longer loads legacy `Atom` tables. No Erlang loading test was performed. ## REA version Source revision `e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4`, package.json version 6.7.0. The published npm artifact was not tested. ## Runtime and platform Node.js v24.22.1; Linux x86_64; locked tsx 4.23.15. No Erlang emulator or external analysis provider was used. ## Target and artifact context Synthetic `FOR1`/`BEAM` structural containers containing one legacy `Atom` table and one `ExpT` row, with an absent, empty, or nonempty alternate `AtU8` chunk. These reduced containers have no Code chunk and are not claimed to be loadable modules. All chunk sizes and alignment ranges fit. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed output: ```jsonl {"name":"legacy-control","ok":true,"module":"demo","function":"old","chunks":["Atom","ExpT"],"coverage":"complete"} {"name":"empty-atu8","ok":false,"tag":"AnalysisInputError","issues":[{"path":["path"],"reason":"invalid_format","message":"BEAM atom table is truncated."}]} {"name":"nonempty-atu8-control","ok":true,"module":"demo","function":"new","chunks":["Atom","AtU8","ExpT"],"coverage":"complete"} ``` - Expected source: [REA's historical atom-table contract](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/docs/erlang-beam.md#L34-L38) and [OTP 20.3's size-based atom-table selection](https://github.com/erlang/otp/blob/a113f6117fd696ea6f84ed754055b4ec97a7ccb2/erts/emulator/beam/beam_load.c#L685-L695). - Failure source: The executed service output above and [REA's presence-based alternate-table selection](https://github.com/morluto/rea/blob/e2efd10b70a2b5f15a7ac1b5a457a9a1dbcab4d4/src/erlang/BeamParser.ts#L259-L268). - Evidence provenance: observed for the REA failure/control; static for historical OTP compatibility. - Local verification: reproduced through the shared service; Erlang loading was not run. - Reproduction completeness: complete for the structural-inspection failure and controls. ## Typed outcome or recovery expected Return successful structural Evidence with module `demo`, export function `old`, and both atom chunks inventoried for the empty-alternate case. Keep the nonempty-alternate control resolving its export to `new`. This restores the same table-selection contract without discarding valid UTF-8 precedence or chunk provenance. ## Analysis or suspected cause Source-derived: REA chooses `AtU8` whenever the map contains that name, then passes its zero-byte payload to a parser that requires a four-byte count. Historical OTP's [chunk scanner accepts the zero-byte occurrence](https://github.com/erlang/otp/blob/a113f6117fd696ea6f84ed754055b4ec97a7ccb2/erts/emulator/beam/beam_load.c#L1266-L1288); its atom-table selection then falls back by size. ## Additional context Open and closed searches for BEAM, atom, AtU8, and empty BEAM found no matching report. Related closed [#6518](https://github.com/morluto/rea/issues/6518) concerns repeated occurrences of the same chunk name; this reproduction has distinct `Atom` and `AtU8` names. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: REA rejects an otherwise inspectable legacy atom/export report when an alternate chunk that historical OTP ignores is present; the REA failure is observed, while compatibility is source-derived and real-world prevalence is unknown. - Workaround status: available - Workaround basis: The executed legacy-only control recovers the exact module/export facts after omitting the empty alternate chunk in a separate synthetic copy; retain 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#4164
No description provided.