[GH-ISSUE #3397] [Bug] Unnamed ELF read-only sections lose embedded Rust compiler-path evidence #1261

Closed
opened 2026-10-16 21:03:31 +00:00 by bsakaguchi · 1 comment
Owner

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

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

inspect-rust-binary returns rustc_commit: null for a valid ELF whose allocated read-only data section has no name, even though that section contains a complete compiler path. The same bytes in a named section return the hash and its correct file offset. Removing the optional section-name table also returns null. The result says that no compiler path was found in read-only data without exposing the skipped coverage.

All three public CLI commands exit 0. The compiler-path bytes and their offset are identical in all variants. The omission was reproduced through the public CLI with independently constructed inputs.

Reproduction

  1. Start from a clean source checkout and build the runtime.
git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. Run the following complete fixture builder and three CLI inspections from the repository root. No Rust compiler or external analysis engine is needed. The builder creates a small ELF64 executable with a read-only load segment and an allocated, file-backed SHT_PROGBITS section. The unnamed variant changes only that section's four-byte name index to zero. The third variant removes the optional section-name table.
work_dir="$(mktemp -d)"
trap 'rm -rf "$work_dir"' EXIT
cat > "$work_dir/create.mjs" <<'EOF'
import { writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const out = process.argv[2] ?? dirname(fileURLToPath(import.meta.url));
const bytes = Buffer.alloc(768);
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
const u16 = (at, value) => bytes.writeUInt16LE(value, at);
const u32 = (at, value) => bytes.writeUInt32LE(value, at);
const u64 = (at, value) => bytes.writeBigUInt64LE(BigInt(value), at);
u16(16, 2); u16(18, 62); u32(20, 1);
u64(32, 64); u64(40, 128);
u16(52, 64); u16(54, 56); u16(56, 1);
u16(58, 64); u16(60, 3); u16(62, 1);
u32(64, 1); u32(68, 4); u64(72, 0); u64(80, 0x400000);
u64(96, bytes.length); u64(104, bytes.length); u64(112, 1);
const names = Buffer.from('\0.shstrtab\0.rodata\0');
names.copy(bytes, 512);
u32(192, 1); u32(196, 3); u64(216, 512); u64(224, names.length); u64(240, 1);
const compilerPath = Buffer.from('/rustc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/library/core');
compilerPath.copy(bytes, 640);
u32(256, 11); u32(260, 1); u64(264, 2); u64(272, 0x400000 + 640);
u64(280, 640); u64(288, compilerPath.length); u64(304, 1);
writeFileSync(join(out, 'readonly-named.elf'), bytes);
const unnamed = Buffer.from(bytes);
unnamed.writeUInt32LE(0, 256);
writeFileSync(join(out, 'readonly-unnamed.elf'), unnamed);
const noNames = Buffer.from(unnamed);
noNames.writeUInt16LE(0, 62); noNames.fill(0, 192, 256);
writeFileSync(join(out, 'readonly-no-names.elf'), noNames);
EOF
node "$work_dir/create.mjs" "$work_dir"
node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-named.elf" --json
node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-unnamed.elf" --json
node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-no-names.elf" --json
  1. Compare normalized_result.rustc_commit in the three results. The named control returns the inferred hash at file offset 640. Both other variants return null and the absence limitation quoted below.

Expected behavior

All three inspections should retain the same inferred compiler hash at file offset 640. Empty section names and an absent section-name string table are valid ELF representations. The section's type, allocation flag, file range, read-only permissions, and embedded path remain sufficient to identify its read-only data. The missing dependency section should continue to return auditable_metadata: null.

docs/rust-binaries.md:42 describes compiler-path inference from read-only data. tests/boundary/rust/rustBinaryImage.test.ts:115 explicitly preserves such evidence when a section's name cannot fit the name-decoding window.

Impact and scope

Impact level: P2. Assessment status: supported. A documented static evidence facet silently loses a present compiler path for the reproduced valid ELF representations and reports absence without exposing skipped coverage. No artifact bytes are modified by inspection. Prevalence in compiler-produced binaries was not measured.

Workaround status: unverified. An external ELF reader could inspect the same read-only bytes, but that workflow was not executed. Naming a section in an artifact copy changes its bytes and digest, so the passing named control does not preserve the selected artifact's identity as a workaround.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0 on macOS 27.0.1. The compiled checkout runtime was used, and REA reports version 6.4.0. The host architecture was not recorded in the reproduction evidence; the fixture architecture is x86-64.

Target and artifact context

Three independently constructed 768-byte ELF64 little-endian x86-64 executables contain /rustc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/library/core at file offset 640. Their data section is allocated, file-backed SHT_PROGBITS, and read-only. The named control has a .rodata name; the unnamed variant sets the name index to zero; the final variant removes the optional section-name table. The compiler-path bytes and file offset remain identical. No .dep-v0 dependency section is present, and no target program or external analysis engine needs to run.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The observed named-control value of normalized_result.rustc_commit, followed by the observed value and limitation for both other variants:

readonly-named.elf:
{"evidence_kind":"inferred","hashes":[{"hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","file_offset":640}]}

readonly-unnamed.elf and readonly-no-names.elf:
rustc_commit: null
No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.

All three commands exit 0.

Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete.

Typed outcome or recovery expected

Repeat the three commands. Each must retain the same hash and offset while preserving the selected file's own digest and leaving auditable_metadata null. A control with the data section made writable must still omit the hash, so restoration does not turn compiler inference into an unrestricted byte-string scan. This writable control is a restoration requirement; its result was not recorded in the reproduction evidence.

Analysis or suspected cause

Source-derived reasoning: src/go/GoElfImage.ts:144 skips section recording without a name table, and src/go/GoElfImage.ts:263 omits unnamed sections. src/rust/RustBinaryImage.ts:109 scans only the recorded read-only ranges. These paths explain how unchanged compiler-path bytes in valid read-only sections can disappear from inference when the sections lack names. The CLI outputs above establish the failure; this explanation comes from source inspection.

Additional context

  • Related open issue #2536 loses Go build metadata from a later writable segment in a sectionless PIE; this issue loses Rust compiler-path evidence from omitted unnamed read-only section records.
  • Related completed issue #1659 concerns Ghidra-defined Rust string-search coverage and missing limitations; this issue concerns the built-in Rust compiler-path inspector without an external analysis engine.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3397 ### 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 `inspect-rust-binary` returns `rustc_commit: null` for a valid ELF whose allocated read-only data section has no name, even though that section contains a complete compiler path. The same bytes in a named section return the hash and its correct file offset. Removing the optional section-name table also returns null. The result says that no compiler path was found in read-only data without exposing the skipped coverage. All three public CLI commands exit 0. The compiler-path bytes and their offset are identical in all variants. The omission was reproduced through the public CLI with independently constructed inputs. ### Reproduction 1. Start from a clean source checkout and build the runtime. ```sh git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout --detach 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. Run the following complete fixture builder and three CLI inspections from the repository root. No Rust compiler or external analysis engine is needed. The builder creates a small ELF64 executable with a read-only load segment and an allocated, file-backed `SHT_PROGBITS` section. The unnamed variant changes only that section's four-byte name index to zero. The third variant removes the optional section-name table. ```sh work_dir="$(mktemp -d)" trap 'rm -rf "$work_dir"' EXIT cat > "$work_dir/create.mjs" <<'EOF' import { writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const out = process.argv[2] ?? dirname(fileURLToPath(import.meta.url)); const bytes = Buffer.alloc(768); bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]); const u16 = (at, value) => bytes.writeUInt16LE(value, at); const u32 = (at, value) => bytes.writeUInt32LE(value, at); const u64 = (at, value) => bytes.writeBigUInt64LE(BigInt(value), at); u16(16, 2); u16(18, 62); u32(20, 1); u64(32, 64); u64(40, 128); u16(52, 64); u16(54, 56); u16(56, 1); u16(58, 64); u16(60, 3); u16(62, 1); u32(64, 1); u32(68, 4); u64(72, 0); u64(80, 0x400000); u64(96, bytes.length); u64(104, bytes.length); u64(112, 1); const names = Buffer.from('\0.shstrtab\0.rodata\0'); names.copy(bytes, 512); u32(192, 1); u32(196, 3); u64(216, 512); u64(224, names.length); u64(240, 1); const compilerPath = Buffer.from('/rustc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/library/core'); compilerPath.copy(bytes, 640); u32(256, 11); u32(260, 1); u64(264, 2); u64(272, 0x400000 + 640); u64(280, 640); u64(288, compilerPath.length); u64(304, 1); writeFileSync(join(out, 'readonly-named.elf'), bytes); const unnamed = Buffer.from(bytes); unnamed.writeUInt32LE(0, 256); writeFileSync(join(out, 'readonly-unnamed.elf'), unnamed); const noNames = Buffer.from(unnamed); noNames.writeUInt16LE(0, 62); noNames.fill(0, 192, 256); writeFileSync(join(out, 'readonly-no-names.elf'), noNames); EOF node "$work_dir/create.mjs" "$work_dir" node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-named.elf" --json node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-unnamed.elf" --json node scripts/rea.mjs inspect-rust-binary "$work_dir/readonly-no-names.elf" --json ``` 3. Compare `normalized_result.rustc_commit` in the three results. The named control returns the inferred hash at file offset 640. Both other variants return null and the absence limitation quoted below. ### Expected behavior All three inspections should retain the same inferred compiler hash at file offset 640. Empty section names and an absent section-name string table are valid ELF representations. The section's type, allocation flag, file range, read-only permissions, and embedded path remain sufficient to identify its read-only data. The missing dependency section should continue to return `auditable_metadata: null`. `docs/rust-binaries.md:42` describes compiler-path inference from read-only data. `tests/boundary/rust/rustBinaryImage.test.ts:115` explicitly preserves such evidence when a section's name cannot fit the name-decoding window. ### Impact and scope Impact level: P2. Assessment status: supported. A documented static evidence facet silently loses a present compiler path for the reproduced valid ELF representations and reports absence without exposing skipped coverage. No artifact bytes are modified by inspection. Prevalence in compiler-produced binaries was not measured. Workaround status: unverified. An external ELF reader could inspect the same read-only bytes, but that workflow was not executed. Naming a section in an artifact copy changes its bytes and digest, so the passing named control does not preserve the selected artifact's identity as a workaround. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0 on macOS 27.0.1. The compiled checkout runtime was used, and REA reports version 6.4.0. The host architecture was not recorded in the reproduction evidence; the fixture architecture is x86-64. ### Target and artifact context Three independently constructed 768-byte ELF64 little-endian x86-64 executables contain `/rustc/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/library/core` at file offset 640. Their data section is allocated, file-backed `SHT_PROGBITS`, and read-only. The named control has a `.rodata` name; the unnamed variant sets the name index to zero; the final variant removes the optional section-name table. The compiler-path bytes and file offset remain identical. No `.dep-v0` dependency section is present, and no target program or external analysis engine needs to run. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The observed named-control value of `normalized_result.rustc_commit`, followed by the observed value and limitation for both other variants: ```text readonly-named.elf: {"evidence_kind":"inferred","hashes":[{"hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","file_offset":640}]} readonly-unnamed.elf and readonly-no-names.elf: rustc_commit: null No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown. All three commands exit 0. ``` Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. ### Typed outcome or recovery expected Repeat the three commands. Each must retain the same hash and offset while preserving the selected file's own digest and leaving `auditable_metadata` null. A control with the data section made writable must still omit the hash, so restoration does not turn compiler inference into an unrestricted byte-string scan. This writable control is a restoration requirement; its result was not recorded in the reproduction evidence. ### Analysis or suspected cause Source-derived reasoning: `src/go/GoElfImage.ts:144` skips section recording without a name table, and `src/go/GoElfImage.ts:263` omits unnamed sections. `src/rust/RustBinaryImage.ts:109` scans only the recorded read-only ranges. These paths explain how unchanged compiler-path bytes in valid read-only sections can disappear from inference when the sections lack names. The CLI outputs above establish the failure; this explanation comes from source inspection. ### Additional context - Related open issue [#2536](https://github.com/morluto/rea/issues/2536) loses Go build metadata from a later writable segment in a sectionless PIE; this issue loses Rust compiler-path evidence from omitted unnamed read-only section records. - Related completed issue [#1659](https://github.com/morluto/rea/issues/1659) concerns Ghidra-defined Rust string-search coverage and missing limitations; this issue concerns the built-in Rust compiler-path inspector without an external analysis engine.
Author
Owner

@CassieFox23 commented on GitHub (Oct 21, 2026):

Thanks, coygeek, for the fixture builder. From reading main at 01788bac8, not a run: this still looks valid. The file src/native/metadata/ElfImage.ts:69 drops every section whose name is empty. It also drops every section whose name table is unavailable. That happens before the read-only check at line 80 can keep it. A name index of 0 decodes to an empty name (src/native/metadata/NativeSectionDirectory.ts:181-193). A missing shstrtab sets name_state to unavailable (line 172). So neither unnamed variant reaches the read-only scan in src/rust/RustBinaryImage.ts:122-127. PR #3477 closed on 2026-10-20 without merging. Likely area: native container parsing.

🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊

<!-- gh-comment-id:6321732802 --> @CassieFox23 commented on GitHub (Oct 21, 2026): Thanks, coygeek, for the fixture builder. From reading main at 01788bac8, not a run: this still looks valid. The file `src/native/metadata/ElfImage.ts:69` drops every section whose name is empty. It also drops every section whose name table is unavailable. That happens before the read-only check at line 80 can keep it. A name index of 0 decodes to an empty name (`src/native/metadata/NativeSectionDirectory.ts:181-193`). A missing `shstrtab` sets `name_state` to unavailable (line 172). So neither unnamed variant reaches the read-only scan in `src/rust/RustBinaryImage.ts:122-127`. PR #3477 closed on 2026-10-20 without merging. Likely area: `native container parsing`. 🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊
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#1261
No description provided.