[GH-ISSUE #6663] [Bug] Rust inspection loses compiler-path evidence in sectionless ELF executables #4042

Closed
opened 2026-10-31 00:46:18 +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/6663

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 source RustBinaryService.inspect({path}) workflow returns successful Evidence with rustc_commit: null for an ELF executable with no section header table, although its read-only PT_LOAD segment contains a complete /rustc/<40-hex>/ path. The result states that no compiler path was found in read-only data without indicating that load-segment data was not searched. The control with section headers returns the hash at file offset 4096. Both inputs have identical compiler-path bytes and program headers.

This was observed through the source application service and built-in Rust provider, not through the public CLI, MCP transport, a published package installation, or a real compiler-produced binary.

Reproduction

From the source checkout identified below, with its locked dependencies already installed, save the following complete program as /tmp/rust-sectionless.mts. It constructs benign ELF64 executables and calls the real source application service. The first input declares a named read-only section, the second omits the optional section header table, and the third also changes the data segment to writable. No target is executed and no external provider is used.

import { writeFile, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createHash } from 'node:crypto';
const root = process.cwd();
const { RustBinaryProvider } = await import(pathToFileURL(join(root, 'src/rust/RustBinaryProvider.ts')).href);
const { RustBinaryService } = await import(pathToFileURL(join(root, 'src/application/rust/RustBinaryService.ts')).href);
const out = process.argv[2];
const hash = '1234567890abcdef1234567890abcdef12345678';
const compilerPath = `/rustc/${hash}/library/core/src/panic.rs`;
const image = Buffer.alloc(8192);
const u16 = (at, n) => image.writeUInt16LE(n, at);
const u32 = (at, n) => image.writeUInt32LE(n, at);
const u64 = (at, n) => image.writeBigUInt64LE(BigInt(n), at);
image.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
u16(16, 2); u16(18, 62); u32(20, 1); u64(24, 0x400200);
u64(32, 64); u64(40, 1024); u16(52, 64); u16(54, 56);
u16(56, 2); u16(58, 64); u16(60, 4); u16(62, 1);
for (const [i, offset, address, flags] of [[0, 0, 0x400000, 5], [1, 4096, 0x401000, 4]]) {
  const at = 64 + i * 56;
  u32(at, 1); u32(at + 4, flags); u64(at + 8, offset);
  u64(at + 16, address); u64(at + 24, address); u64(at + 32, 4096);
  u64(at + 40, 4096); u64(at + 48, 4096);
}
const code = Buffer.from('b83c00000031ff0f05', 'hex');
code.copy(image, 512);
const names = Buffer.from('\0.shstrtab\0.text\0.rodata\0');
names.copy(image, 768);
const section = (i, name, type, flags, address, offset, size) => {
  const at = 1024 + i * 64;
  u32(at, name); u32(at + 4, type); u64(at + 8, flags);
  u64(at + 16, address); u64(at + 24, offset); u64(at + 32, size);
  u64(at + 48, 1);
};
section(1, 1, 3, 0, 0, 768, names.length);
section(2, 11, 1, 6, 0x400200, 512, code.length);
section(3, 17, 1, 2, 0x401000, 4096, Buffer.byteLength(compilerPath) + 1);
image.write(compilerPath, 4096);
const sectionless = Buffer.from(image);
sectionless.writeBigUInt64LE(0n, 40); sectionless.writeUInt16LE(0, 58);
sectionless.writeUInt16LE(0, 60); sectionless.writeUInt16LE(0, 62);
const writable = Buffer.from(sectionless);
writable.writeUInt32LE(6, 64 + 56 + 4);
const digest = b => createHash('sha256').update(b).digest('hex');
const service = new RustBinaryService(new RustBinaryProvider());
try {
  for (const [label, bytes] of [['named', image], ['sectionless-readonly', sectionless], ['sectionless-writable', writable]]) {
    const path = join(out, `${label}.elf`);
    await writeFile(path, bytes);
    const result = await service.inspect({path});
    if (!result.ok) throw result.error;
    const evidence = result.value;
    const r = evidence.normalized_result;
    console.log(JSON.stringify({case:label, rustc_commit:r.rustc_commit, auditable_metadata:r.auditable_metadata, unchanged:digest(bytes) === digest(await readFile(path)), format:r.format, architecture:r.architecture, limitations:r.limitations}));
  }
} finally { await service.close(); }

The executed commands were equivalent to the following with the probe and output directory under the investigation scratch root:

mkdir -p /tmp/rea-rust-sectionless
node --import ./node_modules/tsx/dist/loader.mjs /tmp/rust-sectionless.mts /tmp/rea-rust-sectionless
readelf -h -l /tmp/rea-rust-sectionless/sectionless-readonly.elf

The service probe ran once for each of the three cases and exited 0. It also confirmed that inspection left every input unchanged. readelf confirmed the sectionless executable's two load segments and the absence of section headers.

Expected behavior

Preserve the inferred compiler hash and its file offset when a supported ELF executable has a file-backed read-only load segment containing that path. Missing optional section headers should not silently reduce compiler-path coverage to zero. The writable control should continue to omit the hash, and absence of a .dep-v0 section should continue to return auditable_metadata: null.

The Rust inspection contract describes compiler-path evidence in read-only data without requiring a section table. The ELF specification explicitly permits a missing section header table through e_shoff = 0 and e_shnum = 0 (ELF header). Program headers independently describe file-backed segment ranges and permissions (program loading).

Impact and scope

The reproduced valid ELF representation silently loses an available evidence facet and reports no matching path without explaining the coverage gap. Dependency metadata remains null in all three inputs because none declares .dep-v0. Inspection does not alter the files. The frequency of sectionless files in real Rust distributions was not measured. This is a bounded evidence omission; no claim is made about complete compiler identification or runtime dependencies.

REA version

Source package version 6.7.0 at 19520d00087bc92b5c1020918ea3b7c064768bb4.

Runtime and platform

Node.js v24.22.1, Linux x64. Source application service with the built-in Rust dependency provider. No Rust compiler, Hopper, Ghidra, IDA, or cargo-auditable executable was used. OS distribution and kernel version were not recorded.

Target and artifact context

Independently constructed 8192-byte ELF64 little-endian x86-64 executables, each with a read/execute load segment at file offset 0 and a separate data load segment at file offset 4096. The compiler path begins at offset 4096. The named control describes this range as an allocated read-only SHT_PROGBITS section. The sectionless input only clears the section-table locator, entry size, count, and name-table index. The writable control additionally changes the data segment's flags from PF_R to PF_R | PF_W.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed probe returned the following relevant fields; the full output also contained the existing dependency and resource limitations:

named:
  rustc_commit: {"evidence_kind":"inferred","hashes":[{"hash":"1234567890abcdef1234567890abcdef12345678","file_offset":4096}]}
  auditable_metadata: null
  unchanged: true
sectionless-readonly:
  rustc_commit: null
  auditable_metadata: null
  unchanged: true
  limitation: No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.
sectionless-writable:
  rustc_commit: null
  auditable_metadata: null
  unchanged: true

The independent readelf -h -l observation for the failing fixture included:

Type: EXEC (Executable file)
Start of section headers: 0 (bytes into file)
Number of section headers: 0
LOAD  offset 0x0000  vaddr 0x400000  filesz 0x1000  memsz 0x1000  R E  align 0x1000
LOAD  offset 0x1000  vaddr 0x401000  filesz 0x1000  memsz 0x1000  R    align 0x1000

Evidence provenance: observed. Local verification: reproduced through the source application service. Reproduction completeness: complete. No CLI/MCP runtime or published-package behavior is claimed.

Typed outcome or recovery expected

For the read-only sectionless input, restore the same inferred hash and offset as the named-section control. Preserve the writable negative control and the selected artifact's digest. If inspection intentionally cannot search declared read-only segments, explicitly retain that coverage uncertainty instead of asserting that no path was found in read-only data.

Analysis or suspected cause

Source-derived reasoning: the Rust image reader passes only container.sections to compiler inference. Its range selection scans those section records exclusively. The ELF container reader already retains load-region facts separately, but these facts are not supplied to compiler inference. With no section table the section vector is empty even though the read-only load region remains available. The provider limitation then reports no matching read-only path.

Public reachability, not an executed transport claim: CLI and MCP both dispatch to the application service used by the probe.

Additional context

Related open #3397 covers unnamed read-only sections and a missing section-name table while retaining section headers. This case has no section header table at all and requires compiler-path coverage from the declared load segments or an explicit coverage limitation. Retaining unnamed section records alone would not restore this case. Open and closed Rust/compiler-path/sectionless issue searches, refreshed immediately before submission, found no dedicated report for this trigger.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Successful source-service Evidence loses a present compiler-path facet for a valid ELF representation and reports absence without disclosing the unexamined load-region coverage; artifact bytes and other inspected identity facts remain intact.
  • Workaround status: unverified
  • Workaround basis: The named-section control changes the selected file's identity, so it is not an identity-preserving workaround. An external segment-aware reader could inspect the unchanged file, but that alternative workflow was not executed.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6663 ## 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 source `RustBinaryService.inspect({path})` workflow returns successful Evidence with `rustc_commit: null` for an ELF executable with no section header table, although its read-only `PT_LOAD` segment contains a complete `/rustc/<40-hex>/` path. The result states that no compiler path was found in read-only data without indicating that load-segment data was not searched. The control with section headers returns the hash at file offset 4096. Both inputs have identical compiler-path bytes and program headers. This was observed through the source application service and built-in Rust provider, not through the public CLI, MCP transport, a published package installation, or a real compiler-produced binary. ## Reproduction From the source checkout identified below, with its locked dependencies already installed, save the following complete program as `/tmp/rust-sectionless.mts`. It constructs benign ELF64 executables and calls the real source application service. The first input declares a named read-only section, the second omits the optional section header table, and the third also changes the data segment to writable. No target is executed and no external provider is used. ```typescript import { writeFile, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; const root = process.cwd(); const { RustBinaryProvider } = await import(pathToFileURL(join(root, 'src/rust/RustBinaryProvider.ts')).href); const { RustBinaryService } = await import(pathToFileURL(join(root, 'src/application/rust/RustBinaryService.ts')).href); const out = process.argv[2]; const hash = '1234567890abcdef1234567890abcdef12345678'; const compilerPath = `/rustc/${hash}/library/core/src/panic.rs`; const image = Buffer.alloc(8192); const u16 = (at, n) => image.writeUInt16LE(n, at); const u32 = (at, n) => image.writeUInt32LE(n, at); const u64 = (at, n) => image.writeBigUInt64LE(BigInt(n), at); image.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]); u16(16, 2); u16(18, 62); u32(20, 1); u64(24, 0x400200); u64(32, 64); u64(40, 1024); u16(52, 64); u16(54, 56); u16(56, 2); u16(58, 64); u16(60, 4); u16(62, 1); for (const [i, offset, address, flags] of [[0, 0, 0x400000, 5], [1, 4096, 0x401000, 4]]) { const at = 64 + i * 56; u32(at, 1); u32(at + 4, flags); u64(at + 8, offset); u64(at + 16, address); u64(at + 24, address); u64(at + 32, 4096); u64(at + 40, 4096); u64(at + 48, 4096); } const code = Buffer.from('b83c00000031ff0f05', 'hex'); code.copy(image, 512); const names = Buffer.from('\0.shstrtab\0.text\0.rodata\0'); names.copy(image, 768); const section = (i, name, type, flags, address, offset, size) => { const at = 1024 + i * 64; u32(at, name); u32(at + 4, type); u64(at + 8, flags); u64(at + 16, address); u64(at + 24, offset); u64(at + 32, size); u64(at + 48, 1); }; section(1, 1, 3, 0, 0, 768, names.length); section(2, 11, 1, 6, 0x400200, 512, code.length); section(3, 17, 1, 2, 0x401000, 4096, Buffer.byteLength(compilerPath) + 1); image.write(compilerPath, 4096); const sectionless = Buffer.from(image); sectionless.writeBigUInt64LE(0n, 40); sectionless.writeUInt16LE(0, 58); sectionless.writeUInt16LE(0, 60); sectionless.writeUInt16LE(0, 62); const writable = Buffer.from(sectionless); writable.writeUInt32LE(6, 64 + 56 + 4); const digest = b => createHash('sha256').update(b).digest('hex'); const service = new RustBinaryService(new RustBinaryProvider()); try { for (const [label, bytes] of [['named', image], ['sectionless-readonly', sectionless], ['sectionless-writable', writable]]) { const path = join(out, `${label}.elf`); await writeFile(path, bytes); const result = await service.inspect({path}); if (!result.ok) throw result.error; const evidence = result.value; const r = evidence.normalized_result; console.log(JSON.stringify({case:label, rustc_commit:r.rustc_commit, auditable_metadata:r.auditable_metadata, unchanged:digest(bytes) === digest(await readFile(path)), format:r.format, architecture:r.architecture, limitations:r.limitations})); } } finally { await service.close(); } ``` The executed commands were equivalent to the following with the probe and output directory under the investigation scratch root: ```sh mkdir -p /tmp/rea-rust-sectionless node --import ./node_modules/tsx/dist/loader.mjs /tmp/rust-sectionless.mts /tmp/rea-rust-sectionless readelf -h -l /tmp/rea-rust-sectionless/sectionless-readonly.elf ``` The service probe ran once for each of the three cases and exited 0. It also confirmed that inspection left every input unchanged. `readelf` confirmed the sectionless executable's two load segments and the absence of section headers. ## Expected behavior Preserve the inferred compiler hash and its file offset when a supported ELF executable has a file-backed read-only load segment containing that path. Missing optional section headers should not silently reduce compiler-path coverage to zero. The writable control should continue to omit the hash, and absence of a `.dep-v0` section should continue to return `auditable_metadata: null`. The [Rust inspection contract](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/docs/rust-binaries.md#L103-L106) describes compiler-path evidence in read-only data without requiring a section table. The ELF specification explicitly permits a missing section header table through `e_shoff = 0` and `e_shnum = 0` ([ELF header](https://gabi.xinuos.com/elf/02-eheader.html)). Program headers independently describe file-backed segment ranges and permissions ([program loading](https://gabi.xinuos.com/elf/07-pheader.html#segment-permissions)). ## Impact and scope The reproduced valid ELF representation silently loses an available evidence facet and reports no matching path without explaining the coverage gap. Dependency metadata remains null in all three inputs because none declares `.dep-v0`. Inspection does not alter the files. The frequency of sectionless files in real Rust distributions was not measured. This is a bounded evidence omission; no claim is made about complete compiler identification or runtime dependencies. ## REA version Source package version 6.7.0 at `19520d00087bc92b5c1020918ea3b7c064768bb4`. ## Runtime and platform Node.js v24.22.1, Linux x64. Source application service with the built-in Rust dependency provider. No Rust compiler, Hopper, Ghidra, IDA, or cargo-auditable executable was used. OS distribution and kernel version were not recorded. ## Target and artifact context Independently constructed 8192-byte ELF64 little-endian x86-64 executables, each with a read/execute load segment at file offset 0 and a separate data load segment at file offset 4096. The compiler path begins at offset 4096. The named control describes this range as an allocated read-only `SHT_PROGBITS` section. The sectionless input only clears the section-table locator, entry size, count, and name-table index. The writable control additionally changes the data segment's flags from `PF_R` to `PF_R | PF_W`. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence The executed probe returned the following relevant fields; the full output also contained the existing dependency and resource limitations: ```text named: rustc_commit: {"evidence_kind":"inferred","hashes":[{"hash":"1234567890abcdef1234567890abcdef12345678","file_offset":4096}]} auditable_metadata: null unchanged: true sectionless-readonly: rustc_commit: null auditable_metadata: null unchanged: true limitation: No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown. sectionless-writable: rustc_commit: null auditable_metadata: null unchanged: true ``` The independent `readelf -h -l` observation for the failing fixture included: ```text Type: EXEC (Executable file) Start of section headers: 0 (bytes into file) Number of section headers: 0 LOAD offset 0x0000 vaddr 0x400000 filesz 0x1000 memsz 0x1000 R E align 0x1000 LOAD offset 0x1000 vaddr 0x401000 filesz 0x1000 memsz 0x1000 R align 0x1000 ``` Evidence provenance: observed. Local verification: reproduced through the source application service. Reproduction completeness: complete. No CLI/MCP runtime or published-package behavior is claimed. ## Typed outcome or recovery expected For the read-only sectionless input, restore the same inferred hash and offset as the named-section control. Preserve the writable negative control and the selected artifact's digest. If inspection intentionally cannot search declared read-only segments, explicitly retain that coverage uncertainty instead of asserting that no path was found in read-only data. ## Analysis or suspected cause Source-derived reasoning: the [Rust image reader](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/rust/RustBinaryImage.ts#L33-L45) passes only `container.sections` to compiler inference. Its [range selection](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/rust/RustBinaryImage.ts#L109-L139) scans those section records exclusively. The [ELF container reader](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/native/metadata/ElfImage.ts#L93-L101) already retains load-region facts separately, but these facts are not supplied to compiler inference. With no section table the section vector is empty even though the read-only load region remains available. The [provider limitation](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/rust/RustBinaryProvider.ts#L209-L215) then reports no matching read-only path. Public reachability, not an executed transport claim: [CLI](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/cli/rustCommands.ts#L91-L103) and [MCP](https://github.com/morluto/rea/blob/19520d00087bc92b5c1020918ea3b7c064768bb4/src/server/registerRustTools.ts#L20-L32) both dispatch to the application service used by the probe. ## Additional context Related open [#3397](https://github.com/morluto/rea/issues/3397) covers unnamed read-only sections and a missing section-name table while retaining section headers. This case has no section header table at all and requires compiler-path coverage from the declared load segments or an explicit coverage limitation. Retaining unnamed section records alone would not restore this case. Open and closed Rust/compiler-path/sectionless issue searches, refreshed immediately before submission, found no dedicated report for this trigger. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Successful source-service Evidence loses a present compiler-path facet for a valid ELF representation and reports absence without disclosing the unexamined load-region coverage; artifact bytes and other inspected identity facts remain intact. - Workaround status: unverified - Workaround basis: The named-section control changes the selected file's identity, so it is not an identity-preserving workaround. An external segment-aware reader could inspect the unchanged file, but that alternative workflow was not executed.
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#4042
No description provided.