[GH-ISSUE #7685] [Bug] inspect-elf-notes admits a dangling section-name index when no section table exists #4608

Closed
opened 2026-10-31 00:51:16 +00:00 by bsakaguchi · 0 comments
Owner

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

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

node scripts/rea.mjs inspect-elf-notes "$work/with-note.elf" --format json exits 0 for an ELF64 whose header declares no section table (e_shoff=0, e_shnum=0) but selects section-name table index 5. It returns the GNU build ID and coverage: {"status":"complete","examined_tables":1,"examined_entries":1} without identifying the malformed table reference.

Changing the program header from PT_NOTE to PT_NULL also exits 0, with complete coverage, zero tables/entries, and the no-declared-notes limitation. On both inputs, inspect-kernel-module, inspect-go-binary, inspect-rust-binary, and inspect-artifact instead exit 1 with ELF section table is missing.

A control differing from the note-bearing input only in e_shstrndx=0 is admitted by all five commands. Thus absence of section headers is supported; the missed refusal concerns the dangling name-table index.

Reproduction

  1. From the already-built baseline checkout, put Node.js v24.22.1 on PATH.
  2. Run the following script. It creates two malformed 400-byte inputs and a zero-index control, blocks package-manager launches, isolates all CLI state, and removes its temporary files. Each complete matrix was executed twice during independent verification: first with full JSON retained, then with this reduced script.
set -eu
repo="$PWD"
node_bin="$(dirname "$(node -p process.execPath)")"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
python3 - "$work" <<'PY'
import pathlib, struct, sys
work = pathlib.Path(sys.argv[1])
for name in ('bin', 'home', 'config', 'cache', 'data', 'state', 'temp'):
    (work / name).mkdir()
for name in ('npx', 'npm', 'bun'):
    tool = work / 'bin' / name
    tool.write_text('#!/bin/sh\nexit 97\n')
    tool.chmod(0o755)
b = bytearray(400)
b[:7] = bytes([127, 69, 76, 70, 2, 1, 1])
struct.pack_into('<HHIQQQIHHHHHH', b, 16,
                 3, 62, 1, 0, 64, 0, 0, 64, 56, 1, 64, 0, 5)
struct.pack_into('<IIQQQQQQ', b, 64, 4, 4, 200, 0, 0, 36, 36, 4)
struct.pack_into('<III', b, 200, 4, 20, 3)
b[212:216] = bytes([71, 78, 85, 0])
b[216:236] = bytes(range(1, 21))
(work / 'with-note.elf').write_bytes(b)
struct.pack_into('<I', b, 64, 0)
(work / 'without-note.elf').write_bytes(b)
struct.pack_into('<H', b, 62, 0)
struct.pack_into('<I', b, 64, 4)
(work / 'control.elf').write_bytes(b)
PY
for fixture in with-note without-note control; do
  for command in inspect-elf-notes inspect-kernel-module inspect-go-binary inspect-rust-binary inspect-artifact; do
    set +e
    env -i PATH="$work/bin:$node_bin:/usr/bin:/bin" HOME="$work/home" \
      XDG_CONFIG_HOME="$work/config" XDG_CACHE_HOME="$work/cache" \
      XDG_DATA_HOME="$work/data" XDG_STATE_HOME="$work/state" \
      TMPDIR="$work/temp" npm_config_prefix="$work/home/npm-global" \
      node "$repo/scripts/rea.mjs" "$command" "$work/$fixture.elf" \
        --format json > "$work/result.json"
    status=$?
    set -e
    python3 - "$fixture" "$command" "$status" "$work/result.json" <<'PY'
import json, sys
fixture, command, status, path = sys.argv[1:]
r = json.load(open(path))
n = r.get('normalized_result', {})
reason = r.get('details', {}).get('reason')
if r.get('details', {}).get('issues'):
    reason = r['details']['issues'][0]['message'].split(': ')[-1]
print(json.dumps({'fixture': fixture, 'command': command, 'exit': int(status),
                  'code': r.get('code'), 'reason': reason,
                  'coverage': n.get('coverage')}))
PY
  done
done
  1. Compare the exit codes and coverage fields with the table under Evidence. The malformed inputs reproduce the admission mismatch in both runs; the control is admitted in both.

Expected behavior

Reject a header that names section-name table index 5 while declaring no section table. The ELF notes guide promises failure without partial success for malformed ELF headers, separately from unsupported extended numbering. This input uses no extended-numbering markers.

The gABI defines the name-table field as a section-header index and requires SHN_UNDEF when no section-name string table exists. With no section table, there can be no indexed name-table entry. The same invariant already governs the other four tested admissions. A valid sectionless image with SHN_UNDEF should continue to be admitted.

Impact and scope

P2: malformed header metadata receives successful complete Evidence where the documented contract requires refusal. Combining the five inspectors gives inconsistent admission outcomes for the same bytes. The build-ID bytes are decoded correctly; complete note coverage alone is not a general certificate of ELF validity. No crash, hang, data loss, target execution, or security-boundary crossing was observed.

The tested workaround is to require successful inspect-artifact admission before note inspection: it rejects both malformed variants and admits the zero-index control. Verification covers synthetic ELF64 little-endian x86-64 inputs and these five CLI commands. Natural-file prevalence, other name indices, other ELF classes/byte orders, MCP transport, and published-package behavior were not established.

REA version

Exact node scripts/rea.mjs --version stdout: 6.8.0 followed by a newline.
Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Python 3 generated the fixtures. No external analysis engine was used.

Target and artifact context

Inert 400-byte ELF64 little-endian x86-64 ET_DYN metadata. One program header at offset 64 points to a 36-byte note at offset 200: owner GNU, type 3, and the 20-byte build ID 0102030405060708090a0b0c0d0e0f1011121314. No section table exists. The no-note variant changes only the program-header type to PT_NULL; the control changes only the name index from 5 to zero relative to the note-bearing input. The files were read statically, never loaded as programs.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Executed command shape: node scripts/rea.mjs <command> <fixture> --format json.

Command Index 5, PT_NOTE Index 5, PT_NULL Index 0, PT_NOTE control
inspect-elf-notes exit 0; complete; 1 table, 1 entry exit 0; complete; 0 tables, 0 entries exit 0; complete; 1 table, 1 entry
inspect-kernel-module exit 1; invalid_request exit 1; invalid_request exit 0; not_applicable for ET_DYN
inspect-go-binary exit 1; invalid_request exit 1; invalid_request exit 0; no Go build-info record
inspect-rust-binary exit 1; invalid_request exit 1; invalid_request exit 0; no dependency record/compiler hash
inspect-artifact exit 1; target_unavailable exit 1; target_unavailable exit 0; complete-within-substeps

Every refusal names ELF section table is missing. The three format-specific refusals have category invalid_input, with a path issue whose reason is invalid_format. Artifact inspection returns category unsupported_target and constraint file_format_rejected. All command stderr was empty. The no-note result explicitly limits its absence claim to the selected image's declarations; neither accepted malformed input has a malformed-header warning.

Expected-source evidence at the baseline:

  • docs/elf-notes.md:59-64: malformed-header refusal and separate extended-numbering policy.
  • src/domain/elfDeclaredRanges.ts:125-129: the canonical declaration reader rejects a nonzero names index when the section offset is zero.
  • ELF gABI header definitions: the e_shoff, e_shnum, and e_shstrndx definitions establish the absent-table and SHN_UNDEF relationships. The name-index rule is stated for absence of a section-name string table; applying it here follows from absence of its section table.

Failure-source and reachability evidence:

  • src/native/elf/ElfNoteParser.ts:296-350: raw declarations and extended-numbering admission precede a name-index check that runs only with a nonzero section count.
  • src/native/elf/ElfNoteParser.ts:507-525: successful results report complete note coverage.
  • src/native/elf/ElfNotesProvider.ts:68-77: stable artifact bytes are sent directly to that parser.
  • src/cli/binaryDiagnosticsCommands.ts:49-85: supported CLI dispatch to the composed ELF-note service.

Evidence provenance: independent public CLI execution plus source inspection. Local verification: reproduced. Reproduction completeness: complete for the two malformed fixtures and the single-field control; no historical or real-world-frequency claim.

Typed outcome or recovery expected

A nonzero failure identifying the malformed ELF reference, with no successful normalized note result. invalid_request / invalid_input, with path / invalid_format, would match the tested format-specific readers. Preserve successful inspection of a sectionless image that uses SHN_UNDEF.

Analysis or suspected cause

Source-derived: the note parser shares fixed-header and raw-word readers but bypasses the canonical declaration validator. A zero section count skips its name-table branches, so index 5 is ignored. The shared validator used by the native section-directory reader already rejects this exact relationship. This explains the executed discrepancy without requiring a change to the meaning of complete note coverage.

Additional context

The duplicate search covered open and closed issues, open/closed/merged PRs, issue comments, and relevant inline PR reviews. No matching report was found. Closed #6734 concerns error typing for valid extended numbering. The earlier review in merged #4837 concerns nonzero section offset plus zero section count, a different extended-numbering trigger. Closed #6692 and #6869 concern compressed name/note sections. #7324 and #7670 concern kernel-module documentation; #6792, #6663, #6884, and #6880 concern different export, Rust, Dart, and HIP observations. #7403 concerns unsupported DOS/PE error classification. Closed #6161 concerns truncated declared file ranges before provider admission; this report does not claim that its fix regressed.

NOT_ENOUGH_INFO: introducing revision, last known good release, and prevalence in naturally occurring artifacts. No fix was implemented during this verification.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two malformed synthetic ELF inputs violate the documented refusal contract and produce successful complete note Evidence while four other public commands reject the same headers. The observed effect is bounded admission inconsistency, without lost note bytes or data loss.
  • Workaround status: available
  • Workaround basis: The executed inspect-artifact CLI rejects both malformed variants and accepts the zero-index control, allowing callers to require that admission before trusting a note inspection.
Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7685 ### 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 `node scripts/rea.mjs inspect-elf-notes "$work/with-note.elf" --format json` exits 0 for an ELF64 whose header declares no section table (`e_shoff=0`, `e_shnum=0`) but selects section-name table index 5. It returns the GNU build ID and `coverage: {"status":"complete","examined_tables":1,"examined_entries":1}` without identifying the malformed table reference. Changing the program header from `PT_NOTE` to `PT_NULL` also exits 0, with complete coverage, zero tables/entries, and the no-declared-notes limitation. On both inputs, `inspect-kernel-module`, `inspect-go-binary`, `inspect-rust-binary`, and `inspect-artifact` instead exit 1 with `ELF section table is missing`. A control differing from the note-bearing input only in `e_shstrndx=0` is admitted by all five commands. Thus absence of section headers is supported; the missed refusal concerns the dangling name-table index. ### Reproduction 1. From the already-built baseline checkout, put Node.js v24.22.1 on PATH. 2. Run the following script. It creates two malformed 400-byte inputs and a zero-index control, blocks package-manager launches, isolates all CLI state, and removes its temporary files. Each complete matrix was executed twice during independent verification: first with full JSON retained, then with this reduced script. ```sh set -eu repo="$PWD" node_bin="$(dirname "$(node -p process.execPath)")" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT python3 - "$work" <<'PY' import pathlib, struct, sys work = pathlib.Path(sys.argv[1]) for name in ('bin', 'home', 'config', 'cache', 'data', 'state', 'temp'): (work / name).mkdir() for name in ('npx', 'npm', 'bun'): tool = work / 'bin' / name tool.write_text('#!/bin/sh\nexit 97\n') tool.chmod(0o755) b = bytearray(400) b[:7] = bytes([127, 69, 76, 70, 2, 1, 1]) struct.pack_into('<HHIQQQIHHHHHH', b, 16, 3, 62, 1, 0, 64, 0, 0, 64, 56, 1, 64, 0, 5) struct.pack_into('<IIQQQQQQ', b, 64, 4, 4, 200, 0, 0, 36, 36, 4) struct.pack_into('<III', b, 200, 4, 20, 3) b[212:216] = bytes([71, 78, 85, 0]) b[216:236] = bytes(range(1, 21)) (work / 'with-note.elf').write_bytes(b) struct.pack_into('<I', b, 64, 0) (work / 'without-note.elf').write_bytes(b) struct.pack_into('<H', b, 62, 0) struct.pack_into('<I', b, 64, 4) (work / 'control.elf').write_bytes(b) PY for fixture in with-note without-note control; do for command in inspect-elf-notes inspect-kernel-module inspect-go-binary inspect-rust-binary inspect-artifact; do set +e env -i PATH="$work/bin:$node_bin:/usr/bin:/bin" HOME="$work/home" \ XDG_CONFIG_HOME="$work/config" XDG_CACHE_HOME="$work/cache" \ XDG_DATA_HOME="$work/data" XDG_STATE_HOME="$work/state" \ TMPDIR="$work/temp" npm_config_prefix="$work/home/npm-global" \ node "$repo/scripts/rea.mjs" "$command" "$work/$fixture.elf" \ --format json > "$work/result.json" status=$? set -e python3 - "$fixture" "$command" "$status" "$work/result.json" <<'PY' import json, sys fixture, command, status, path = sys.argv[1:] r = json.load(open(path)) n = r.get('normalized_result', {}) reason = r.get('details', {}).get('reason') if r.get('details', {}).get('issues'): reason = r['details']['issues'][0]['message'].split(': ')[-1] print(json.dumps({'fixture': fixture, 'command': command, 'exit': int(status), 'code': r.get('code'), 'reason': reason, 'coverage': n.get('coverage')})) PY done done ``` 3. Compare the exit codes and coverage fields with the table under Evidence. The malformed inputs reproduce the admission mismatch in both runs; the control is admitted in both. ### Expected behavior Reject a header that names section-name table index 5 while declaring no section table. The ELF notes guide promises failure without partial success for malformed ELF headers, separately from unsupported extended numbering. This input uses no extended-numbering markers. The gABI defines the name-table field as a section-header index and requires SHN_UNDEF when no section-name string table exists. With no section table, there can be no indexed name-table entry. The same invariant already governs the other four tested admissions. A valid sectionless image with SHN_UNDEF should continue to be admitted. ### Impact and scope P2: malformed header metadata receives successful complete Evidence where the documented contract requires refusal. Combining the five inspectors gives inconsistent admission outcomes for the same bytes. The build-ID bytes are decoded correctly; complete note coverage alone is not a general certificate of ELF validity. No crash, hang, data loss, target execution, or security-boundary crossing was observed. The tested workaround is to require successful `inspect-artifact` admission before note inspection: it rejects both malformed variants and admits the zero-index control. Verification covers synthetic ELF64 little-endian x86-64 inputs and these five CLI commands. Natural-file prevalence, other name indices, other ELF classes/byte orders, MCP transport, and published-package behavior were not established. ### REA version Exact `node scripts/rea.mjs --version` stdout: `6.8.0` followed by a newline. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Python 3 generated the fixtures. No external analysis engine was used. ### Target and artifact context Inert 400-byte ELF64 little-endian x86-64 ET_DYN metadata. One program header at offset 64 points to a 36-byte note at offset 200: owner GNU, type 3, and the 20-byte build ID `0102030405060708090a0b0c0d0e0f1011121314`. No section table exists. The no-note variant changes only the program-header type to PT_NULL; the control changes only the name index from 5 to zero relative to the note-bearing input. The files were read statically, never loaded as programs. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Executed command shape: `node scripts/rea.mjs <command> <fixture> --format json`. | Command | Index 5, PT_NOTE | Index 5, PT_NULL | Index 0, PT_NOTE control | | --- | --- | --- | --- | | inspect-elf-notes | exit 0; complete; 1 table, 1 entry | exit 0; complete; 0 tables, 0 entries | exit 0; complete; 1 table, 1 entry | | inspect-kernel-module | exit 1; invalid_request | exit 1; invalid_request | exit 0; not_applicable for ET_DYN | | inspect-go-binary | exit 1; invalid_request | exit 1; invalid_request | exit 0; no Go build-info record | | inspect-rust-binary | exit 1; invalid_request | exit 1; invalid_request | exit 0; no dependency record/compiler hash | | inspect-artifact | exit 1; target_unavailable | exit 1; target_unavailable | exit 0; complete-within-substeps | Every refusal names `ELF section table is missing`. The three format-specific refusals have category `invalid_input`, with a `path` issue whose reason is `invalid_format`. Artifact inspection returns category `unsupported_target` and constraint `file_format_rejected`. All command stderr was empty. The no-note result explicitly limits its absence claim to the selected image's declarations; neither accepted malformed input has a malformed-header warning. Expected-source evidence at the baseline: - `docs/elf-notes.md:59-64`: malformed-header refusal and separate extended-numbering policy. - `src/domain/elfDeclaredRanges.ts:125-129`: the canonical declaration reader rejects a nonzero names index when the section offset is zero. - [ELF gABI header definitions](https://gabi.xinuos.com/elf/02-eheader.html): the e_shoff, e_shnum, and e_shstrndx definitions establish the absent-table and SHN_UNDEF relationships. The name-index rule is stated for absence of a section-name string table; applying it here follows from absence of its section table. Failure-source and reachability evidence: - `src/native/elf/ElfNoteParser.ts:296-350`: raw declarations and extended-numbering admission precede a name-index check that runs only with a nonzero section count. - `src/native/elf/ElfNoteParser.ts:507-525`: successful results report complete note coverage. - `src/native/elf/ElfNotesProvider.ts:68-77`: stable artifact bytes are sent directly to that parser. - `src/cli/binaryDiagnosticsCommands.ts:49-85`: supported CLI dispatch to the composed ELF-note service. Evidence provenance: independent public CLI execution plus source inspection. Local verification: reproduced. Reproduction completeness: complete for the two malformed fixtures and the single-field control; no historical or real-world-frequency claim. ### Typed outcome or recovery expected A nonzero failure identifying the malformed ELF reference, with no successful normalized note result. `invalid_request` / `invalid_input`, with `path` / `invalid_format`, would match the tested format-specific readers. Preserve successful inspection of a sectionless image that uses SHN_UNDEF. ### Analysis or suspected cause Source-derived: the note parser shares fixed-header and raw-word readers but bypasses the canonical declaration validator. A zero section count skips its name-table branches, so index 5 is ignored. The shared validator used by the native section-directory reader already rejects this exact relationship. This explains the executed discrepancy without requiring a change to the meaning of complete note coverage. ### Additional context The duplicate search covered open and closed issues, open/closed/merged PRs, issue comments, and relevant inline PR reviews. No matching report was found. Closed #6734 concerns error typing for valid extended numbering. The earlier review in merged #4837 concerns nonzero section offset plus zero section count, a different extended-numbering trigger. Closed #6692 and #6869 concern compressed name/note sections. #7324 and #7670 concern kernel-module documentation; #6792, #6663, #6884, and #6880 concern different export, Rust, Dart, and HIP observations. #7403 concerns unsupported DOS/PE error classification. Closed #6161 concerns truncated declared file ranges before provider admission; this report does not claim that its fix regressed. NOT_ENOUGH_INFO: introducing revision, last known good release, and prevalence in naturally occurring artifacts. No fix was implemented during this verification. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two malformed synthetic ELF inputs violate the documented refusal contract and produce successful complete note Evidence while four other public commands reject the same headers. The observed effect is bounded admission inconsistency, without lost note bytes or data loss. - Workaround status: available - Workaround basis: The executed inspect-artifact CLI rejects both malformed variants and accepts the zero-index control, allowing callers to require that admission before trusting a note inspection.
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#4608
No description provided.