[GH-ISSUE #6830] [Bug] Go pointer strings crossing adjacent ELF load segments are rejected as unmapped #4176

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

The bundled Go service rejects an eight-byte compiler string that straddles two adjacent, nonoverlapping ELF load segments, returning AnalysisInputError, invalid_format, and Virtual address or string range is not file mapped. Every string byte has exactly one file mapping with the same contiguous file/address translation. The same compiler string succeeds with one load segment, and with two segments when moved four bytes earlier to end exactly at the boundary.

Reproduction

With this source checkout's locked dependencies available, save the following script outside the checkout as /tmp/rea-go-repro.mts. From the checkout root run:

PATH="/usr/local/bin:$PATH" node_modules/.bin/tsx /tmp/rea-go-repro.mts /tmp/rea-go-repro-fixtures

Each case in this exact script was executed once. It calls the production createGoBinaryService() composition shared by CLI/MCP; neither adapter transport was invoked. The repository's authored fixture supplies inert bytes. No target was loaded or executed.

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const load = (p) => import(pathToFileURL(join(process.cwd(), p)).href);
const { createGoBinaryFixture } = await load('tests/fixtures/go/image.ts');
const { createGoBinaryService } = await load('src/composition/go.ts');
const { readBinaryContainer } = await load('src/native/metadata/readBinaryContainer.ts');
const directory = process.argv[2]; await mkdir(directory, { recursive: true });
const fixture = createGoBinaryFixture({ encoding: 'pointer', moduleText: 'path\tdemo\n' });
const control = Buffer.alloc(8192); fixture.bytes.copy(control);
control.writeBigUInt64LE(8192n, 160); control.writeBigUInt64LE(8192n, 168);
control.writeBigUInt64LE(4096n, 176); control.writeBigUInt64LE(32n, 416);
fixture.bytes.copy(control, 4092, 2048, 2056);
fixture.moduleBytes.copy(control, 5000);
control.writeBigUInt64LE(0x10ffcn, 1536); control.writeBigUInt64LE(0x11388n, 1552);
const split = Buffer.from(control); split.writeUInt16LE(2, 56);
split.writeBigUInt64LE(4096n, 160); split.writeBigUInt64LE(4096n, 168);
split.writeUInt32LE(1, 184); split.writeUInt32LE(6, 188);
split.writeBigUInt64LE(4096n, 192); split.writeBigUInt64LE(0x11000n, 200);
split.writeBigUInt64LE(4096n, 216); split.writeBigUInt64LE(4096n, 224);
split.writeBigUInt64LE(4096n, 232);
const contained = Buffer.from(split); fixture.bytes.copy(contained, 4088, 2048, 2056);
contained.writeBigUInt64LE(0x10ff8n, 1536);
const service = createGoBinaryService();
try {
  for (const [name, bytes, offset] of [['one-load', control, 4092], ['crossing-loads', split, 4092], ['contained', contained, 4088]]) {
    const image = readBinaryContainer(bytes);
    const covered = Array.from({ length: 8 }, (_, i) => {
      const address = 0x10000n + BigInt(offset + i);
      const hits = image.mappings.filter(m => address >= m.address && address < m.address + BigInt(m.size));
      return hits.length === 1 && hits[0].offset + Number(address - hits[0].address) === offset + i;
    }).every(Boolean);
    const path = join(directory, name + '.elf'); await writeFile(path, bytes);
    const result = await service.inspect({ path });
    console.log(JSON.stringify({ name, all_version_bytes_mapped: covered, ok: result.ok,
      ...(result.ok ? { go_version: result.value.normalized_result.build_info.go_version } : {
        error: result.error._tag, reason: result.error.issues[0].reason,
        message: result.error.issues[0].message.split(': ').slice(1).join(': ') }) }));
  }
} finally { await service.close(); }

Expected behavior

Read go1.26.0 from file bytes 4092–4099 and retain the module metadata. Documented historical pointer support includes pointer-backed original-file source ranges; it does not require each contiguous string to stay inside one program-header entry. The existing native export reader already reads strings across adjacent file mappings. This report concerns contiguous, consistently mapped bytes only.

Impact and scope

Observed: a bounded supported-format metadata inspection fails entirely instead of returning present compiler/module facts. The file is unchanged. Real compiler output with this layout, other widths/encodings/formats, OS loading, and a standard Go reader comparison were not tested; the provisional assessment reflects that unestablished reach and compatibility interpretation.

REA version

Source revision 9066444f6b8e9ffe1607a9b02cde3895a37db431, main; package.json reports 6.7.0. This is a source-service reproduction, not verification of an installed npm package.

Runtime and platform

Measured Node.js v26.12.1, Linux 7.2.8-9-omarchy x86_64 (process.arch: x64), locked tsx 4.23.15. No Go compiler, Erlang emulator, disassembler or device was used.

Target and artifact context

Synthetic 8192-byte ELF64, little endian, amd64; historical eight-byte pointer encoding. The build-info header and its 32-byte named discovery section fit in the first load. The crossing string starts at file offset 4092 / VA 0x10ffc. The split uses adjacent 4096-byte PT_LOAD entries at file offsets 0 and 4096, VAs 0x10000 and 0x11000, both PF_R|PF_W and p_align 4096. Module bytes at file offset 5000 fit in the second segment.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Actual stdout from the script above; the error message is the production error's issue message with only its selected-path prefix omitted:

{"name":"one-load","all_version_bytes_mapped":true,"ok":true,"go_version":"go1.26.0"}
{"name":"crossing-loads","all_version_bytes_mapped":true,"ok":false,"error":"AnalysisInputError","reason":"invalid_format","message":"Virtual address or string range is not file mapped"}
{"name":"contained","all_version_bytes_mapped":true,"ok":true,"go_version":"go1.26.0"}

Evidence provenance: observed service results and synthetic file mappings; source-derived diagnosis. Local verification: reproduced. Reproduction completeness: complete for the stated source-service cases. Public CLI, stdio MCP, installed release, other hosts and real compiler-produced occurrences were not tested. NOT_ENOUGH_INFO: real-world frequency and a last-known-good version are unknown.

Typed outcome or recovery expected

Recover the same compiler/module bytes and original file offsets from the unchanged artifact. Keep failures for genuinely unmapped, contradictory or zero-filled nonempty string ranges. No fix or restored implementation was tested.

Analysis or suspected cause

readPointerString resolves the entire compiler string through one mappedFileOffset call. mappedFileOffset first requires one mapping to contain the whole requested range. Neither split mapping contains all eight bytes, so it reports an unmapped range before considering the fully mapped union. This is source-derived and matches the executed controls.

Additional context

Targeted open-and-closed searches for Go/mapped, Go/string/segment, and adjacent mappings found no exact report. Related closed #6716 and #6779 concern inline strings leaving a named discovery section while remaining within one mapping; this case uses pointer strings crossing two load mappings. Closed #6688 fixed the analogous native PE export-name case, not Go pointer reads.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The executed service loses all build metadata for a fully byte-covered synthetic compiler string at a load boundary; controls recover the same metadata. Real producer occurrence and whether broader cross-load support is intended remain unverified.
  • Workaround status: unverified
  • Workaround basis: The synthetic controls establish recovery after changing layout in a copy, which changes artifact identity. No alternate route recovering the affected unchanged artifact was tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6830 ## 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 Native analysis or decompilation ## Observed behavior The bundled Go service rejects an eight-byte compiler string that straddles two adjacent, nonoverlapping ELF load segments, returning `AnalysisInputError`, `invalid_format`, and `Virtual address or string range is not file mapped`. Every string byte has exactly one file mapping with the same contiguous file/address translation. The same compiler string succeeds with one load segment, and with two segments when moved four bytes earlier to end exactly at the boundary. ## Reproduction With this source checkout's locked dependencies available, save the following script outside the checkout as `/tmp/rea-go-repro.mts`. From the checkout root run: ```sh PATH="/usr/local/bin:$PATH" node_modules/.bin/tsx /tmp/rea-go-repro.mts /tmp/rea-go-repro-fixtures ``` Each case in this exact script was executed once. It calls the production `createGoBinaryService()` composition shared by CLI/MCP; neither adapter transport was invoked. The repository's authored fixture supplies inert bytes. No target was loaded or executed. ```ts import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const load = (p) => import(pathToFileURL(join(process.cwd(), p)).href); const { createGoBinaryFixture } = await load('tests/fixtures/go/image.ts'); const { createGoBinaryService } = await load('src/composition/go.ts'); const { readBinaryContainer } = await load('src/native/metadata/readBinaryContainer.ts'); const directory = process.argv[2]; await mkdir(directory, { recursive: true }); const fixture = createGoBinaryFixture({ encoding: 'pointer', moduleText: 'path\tdemo\n' }); const control = Buffer.alloc(8192); fixture.bytes.copy(control); control.writeBigUInt64LE(8192n, 160); control.writeBigUInt64LE(8192n, 168); control.writeBigUInt64LE(4096n, 176); control.writeBigUInt64LE(32n, 416); fixture.bytes.copy(control, 4092, 2048, 2056); fixture.moduleBytes.copy(control, 5000); control.writeBigUInt64LE(0x10ffcn, 1536); control.writeBigUInt64LE(0x11388n, 1552); const split = Buffer.from(control); split.writeUInt16LE(2, 56); split.writeBigUInt64LE(4096n, 160); split.writeBigUInt64LE(4096n, 168); split.writeUInt32LE(1, 184); split.writeUInt32LE(6, 188); split.writeBigUInt64LE(4096n, 192); split.writeBigUInt64LE(0x11000n, 200); split.writeBigUInt64LE(4096n, 216); split.writeBigUInt64LE(4096n, 224); split.writeBigUInt64LE(4096n, 232); const contained = Buffer.from(split); fixture.bytes.copy(contained, 4088, 2048, 2056); contained.writeBigUInt64LE(0x10ff8n, 1536); const service = createGoBinaryService(); try { for (const [name, bytes, offset] of [['one-load', control, 4092], ['crossing-loads', split, 4092], ['contained', contained, 4088]]) { const image = readBinaryContainer(bytes); const covered = Array.from({ length: 8 }, (_, i) => { const address = 0x10000n + BigInt(offset + i); const hits = image.mappings.filter(m => address >= m.address && address < m.address + BigInt(m.size)); return hits.length === 1 && hits[0].offset + Number(address - hits[0].address) === offset + i; }).every(Boolean); const path = join(directory, name + '.elf'); await writeFile(path, bytes); const result = await service.inspect({ path }); console.log(JSON.stringify({ name, all_version_bytes_mapped: covered, ok: result.ok, ...(result.ok ? { go_version: result.value.normalized_result.build_info.go_version } : { error: result.error._tag, reason: result.error.issues[0].reason, message: result.error.issues[0].message.split(': ').slice(1).join(': ') }) })); } } finally { await service.close(); } ``` ## Expected behavior Read `go1.26.0` from file bytes 4092–4099 and retain the module metadata. [Documented historical pointer support](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/docs/go-binaries.md#L112-L117) includes pointer-backed original-file source ranges; it does not require each contiguous string to stay inside one program-header entry. The [existing native export reader](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/src/native/exports/NativeExportParser.ts#L118-L177) already reads strings across adjacent file mappings. This report concerns contiguous, consistently mapped bytes only. ## Impact and scope Observed: a bounded supported-format metadata inspection fails entirely instead of returning present compiler/module facts. The file is unchanged. Real compiler output with this layout, other widths/encodings/formats, OS loading, and a standard Go reader comparison were not tested; the provisional assessment reflects that unestablished reach and compatibility interpretation. ## REA version Source revision [9066444f6b8e9ffe1607a9b02cde3895a37db431](https://github.com/morluto/rea/commit/9066444f6b8e9ffe1607a9b02cde3895a37db431), main; `package.json` reports 6.7.0. This is a source-service reproduction, not verification of an installed npm package. ## Runtime and platform Measured Node.js v26.12.1, Linux 7.2.8-9-omarchy x86_64 (`process.arch: x64`), locked tsx 4.23.15. No Go compiler, Erlang emulator, disassembler or device was used. ## Target and artifact context Synthetic 8192-byte ELF64, little endian, amd64; historical eight-byte pointer encoding. The build-info header and its 32-byte named discovery section fit in the first load. The crossing string starts at file offset 4092 / VA 0x10ffc. The split uses adjacent 4096-byte PT_LOAD entries at file offsets 0 and 4096, VAs 0x10000 and 0x11000, both PF_R|PF_W and p_align 4096. Module bytes at file offset 5000 fit in the second segment. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Actual stdout from the script above; the error message is the production error's issue message with only its selected-path prefix omitted: ```jsonl {"name":"one-load","all_version_bytes_mapped":true,"ok":true,"go_version":"go1.26.0"} {"name":"crossing-loads","all_version_bytes_mapped":true,"ok":false,"error":"AnalysisInputError","reason":"invalid_format","message":"Virtual address or string range is not file mapped"} {"name":"contained","all_version_bytes_mapped":true,"ok":true,"go_version":"go1.26.0"} ``` Evidence provenance: observed service results and synthetic file mappings; source-derived diagnosis. Local verification: reproduced. Reproduction completeness: complete for the stated source-service cases. Public CLI, stdio MCP, installed release, other hosts and real compiler-produced occurrences were not tested. NOT_ENOUGH_INFO: real-world frequency and a last-known-good version are unknown. ## Typed outcome or recovery expected Recover the same compiler/module bytes and original file offsets from the unchanged artifact. Keep failures for genuinely unmapped, contradictory or zero-filled nonempty string ranges. No fix or restored implementation was tested. ## Analysis or suspected cause [readPointerString](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/src/go/GoBinaryImage.ts#L347-L373) resolves the entire compiler string through one `mappedFileOffset` call. [mappedFileOffset](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/src/native/metadata/BinaryContainer.ts#L214-L242) first requires one mapping to contain the whole requested range. Neither split mapping contains all eight bytes, so it reports an unmapped range before considering the fully mapped union. This is source-derived and matches the executed controls. ## Additional context Targeted open-and-closed searches for Go/mapped, Go/string/segment, and adjacent mappings found no exact report. Related closed [#6716](https://github.com/morluto/rea/issues/6716) and [#6779](https://github.com/morluto/rea/issues/6779) concern **inline strings leaving a named discovery section while remaining within one mapping**; this case uses pointer strings crossing **two load mappings**. Closed [#6688](https://github.com/morluto/rea/issues/6688) fixed the analogous native PE export-name case, not Go pointer reads. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The executed service loses all build metadata for a fully byte-covered synthetic compiler string at a load boundary; controls recover the same metadata. Real producer occurrence and whether broader cross-load support is intended remain unverified. - Workaround status: unverified - Workaround basis: The synthetic controls establish recovery after changing layout in a copy, which changes artifact identity. No alternate route recovering the affected unchanged artifact was tested.
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#4176
No description provided.