[GH-ISSUE #6792] [Bug] ELF export inventory reports complete absence when only dynamic-segment symbols remain #4141

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

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-native-exports returns coverage.status: "complete", an empty export array, and the limitation “The ELF has no dynamic symbol table, so it exports no dynamic symbols” when an ELF shared library retains a section table containing only its null entry. Its program headers and dynamic symbols remain intact: the system loader still resolves and calls exported_answer, returning 7.

The same library before the section-table reduction correctly lists exported_answer. Removing the section table entirely instead produces the existing unsupported coverage result. Each of the three CLI/control combinations was executed once before this draft was written.

Reproduction

On Linux x86_64, use a prepared checkout of the revision below, a C compiler, and Python 3. Set REA_ENTRYPOINT to that checkout's absolute scripts/rea.mjs path and run in a disposable directory:

printf 'int exported_answer(void) { return 7; }\n' > demo.c
cc -shared -fPIC -o original.so demo.c
python3 - <<'PY'
from pathlib import Path
import ctypes, struct
original = Path('original.so').read_bytes()
assert original[:6] == b'\x7fELF\x02\x01'
reduced = bytearray(original)
# ELF64 little-endian e_shnum=1 and e_shstrndx=SHN_UNDEF.
# Keep the existing SHT_NULL header, every program header, and all loadable bytes.
struct.pack_into('<HH', reduced, 60, 1, 0)
Path('null-only.so').write_bytes(reduced)
absent = bytearray(original)
struct.pack_into('<Q', absent, 40, 0)
struct.pack_into('<HH', absent, 60, 0, 0)
Path('no-sections.so').write_bytes(absent)
for name in ['original.so', 'null-only.so', 'no-sections.so']:
    library = ctypes.CDLL(str(Path(name).resolve()))
    print(name, library.exported_answer())
PY
node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/original.so" --json
node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/null-only.so" --json
node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/no-sections.so" --json

The executed probe used Python subprocess to run these same compiler and CLI arguments, saved stdout/stderr, and isolated HOME/XDG state. The loader control executes only the self-authored function above; REA's inspection remains static.

Expected behavior

A missing SHT_DYNSYM section must not establish that a file has no dynamic exports when loadable dynamic metadata remains available. Resolve that metadata if supported, or report unsupported/unknown coverage, as the existing no-section-table branch does. The coverage schema defines complete coverage as every table entry having been read.

Impact and scope

The static inventory falsely establishes export absence for the tested library layout, which can mislead native-binding investigation. The function remains present and callable; no target corruption or REA crash was observed. This was reproduced once for an ELF64 little-endian shared library on Linux x86_64. Other architectures, operating systems, and naturally distributed binaries were not tested.

REA version

6.7.0 manifest; source revision bd4f5e9c197ba942ed026c9fed6ff3fae663901d.

Runtime and platform

Node.js v24.22.1; Linux x86_64; GCC 16.2.1. No external analysis provider was used.

Target and artifact context

Self-authored shared library with one exported function, plus two copies with only ELF section-table header fields changed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

All three CLI calls exited 0 with empty stderr. Reduced saved results:

[
  {"file":"original.so","loader_exported_answer":7,"coverage":{"status":"complete","examined_entries":5,"exports":1},"exports":["exported_answer"],"issues":[]},
  {"file":"null-only.so","loader_exported_answer":7,"coverage":{"status":"complete","examined_entries":0,"exports":0},"exports":[],"issues":[]},
  {"file":"no-sections.so","loader_exported_answer":7,"coverage":{"status":"unsupported","examined_entries":0,"exports":0},"exports":[],"issues":[]}
]

The no-section result explains that dynamic-segment enumeration is not implemented. The null-only result instead asserts no dynamic symbols. These are observed public CLI results, with independent system-loader controls. No probe was rerun after drafting.

Typed outcome or recovery expected

Return non-complete coverage with an explanation when dynamic export enumeration cannot be established from the available metadata. Do not silently convert missing section-directory coverage into an empty complete inventory.

Analysis or suspected cause

Source-derived: the ELF parser searches the section directory for SHT_DYNSYM and immediately returns coverage([], 0, 0) when it finds none. The earlier branch handles an entirely absent section table conservatively, but this branch does not inspect PT_DYNAMIC before asserting absence.

Additional context

NOT_ENOUGH_INFO: first affected release and last known good release were not established. This report concerns a false completeness/absence claim; implementing a new dynamic-segment decoder is not required to represent the current limitation honestly.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The public CLI silently reports complete absence of exports for a locally loadable shared library whose exported function is independently resolved and called. The observed failure is bounded to this inventory and does not modify target data.
  • Workaround status: unverified
  • Workaround basis: An original build retaining its dynamic-symbol section works in the control, but access to such a build for an affected artifact is not established. Clearing the section table changes the reported coverage to unsupported and does not recover the missing inventory.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6792 ## 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-native-exports` returns `coverage.status: "complete"`, an empty export array, and the limitation “The ELF has no dynamic symbol table, so it exports no dynamic symbols” when an ELF shared library retains a section table containing only its null entry. Its program headers and dynamic symbols remain intact: the system loader still resolves and calls `exported_answer`, returning 7. The same library before the section-table reduction correctly lists `exported_answer`. Removing the section table entirely instead produces the existing `unsupported` coverage result. Each of the three CLI/control combinations was executed once before this draft was written. ## Reproduction On Linux x86_64, use a prepared checkout of the revision below, a C compiler, and Python 3. Set `REA_ENTRYPOINT` to that checkout's absolute `scripts/rea.mjs` path and run in a disposable directory: ```sh printf 'int exported_answer(void) { return 7; }\n' > demo.c cc -shared -fPIC -o original.so demo.c python3 - <<'PY' from pathlib import Path import ctypes, struct original = Path('original.so').read_bytes() assert original[:6] == b'\x7fELF\x02\x01' reduced = bytearray(original) # ELF64 little-endian e_shnum=1 and e_shstrndx=SHN_UNDEF. # Keep the existing SHT_NULL header, every program header, and all loadable bytes. struct.pack_into('<HH', reduced, 60, 1, 0) Path('null-only.so').write_bytes(reduced) absent = bytearray(original) struct.pack_into('<Q', absent, 40, 0) struct.pack_into('<HH', absent, 60, 0, 0) Path('no-sections.so').write_bytes(absent) for name in ['original.so', 'null-only.so', 'no-sections.so']: library = ctypes.CDLL(str(Path(name).resolve())) print(name, library.exported_answer()) PY node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/original.so" --json node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/null-only.so" --json node "$REA_ENTRYPOINT" inspect-native-exports "$PWD/no-sections.so" --json ``` The executed probe used Python `subprocess` to run these same compiler and CLI arguments, saved stdout/stderr, and isolated HOME/XDG state. The loader control executes only the self-authored function above; REA's inspection remains static. ## Expected behavior A missing `SHT_DYNSYM` section must not establish that a file has no dynamic exports when loadable dynamic metadata remains available. Resolve that metadata if supported, or report unsupported/unknown coverage, as the existing no-section-table branch does. The [coverage schema](https://github.com/morluto/rea/blob/bd4f5e9c197ba942ed026c9fed6ff3fae663901d/src/domain/native/nativeExports.ts#L103-L109) defines complete coverage as every table entry having been read. ## Impact and scope The static inventory falsely establishes export absence for the tested library layout, which can mislead native-binding investigation. The function remains present and callable; no target corruption or REA crash was observed. This was reproduced once for an ELF64 little-endian shared library on Linux x86_64. Other architectures, operating systems, and naturally distributed binaries were not tested. ## REA version 6.7.0 manifest; source revision `bd4f5e9c197ba942ed026c9fed6ff3fae663901d`. ## Runtime and platform Node.js v24.22.1; Linux x86_64; GCC 16.2.1. No external analysis provider was used. ## Target and artifact context Self-authored shared library with one exported function, plus two copies with only ELF section-table header fields changed. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence All three CLI calls exited 0 with empty stderr. Reduced saved results: ```json [ {"file":"original.so","loader_exported_answer":7,"coverage":{"status":"complete","examined_entries":5,"exports":1},"exports":["exported_answer"],"issues":[]}, {"file":"null-only.so","loader_exported_answer":7,"coverage":{"status":"complete","examined_entries":0,"exports":0},"exports":[],"issues":[]}, {"file":"no-sections.so","loader_exported_answer":7,"coverage":{"status":"unsupported","examined_entries":0,"exports":0},"exports":[],"issues":[]} ] ``` The no-section result explains that dynamic-segment enumeration is not implemented. The null-only result instead asserts no dynamic symbols. These are observed public CLI results, with independent system-loader controls. No probe was rerun after drafting. ## Typed outcome or recovery expected Return non-complete coverage with an explanation when dynamic export enumeration cannot be established from the available metadata. Do not silently convert missing section-directory coverage into an empty complete inventory. ## Analysis or suspected cause Source-derived: [the ELF parser](https://github.com/morluto/rea/blob/bd4f5e9c197ba942ed026c9fed6ff3fae663901d/src/native/exports/NativeExportParser.ts#L528-L539) searches the section directory for `SHT_DYNSYM` and immediately returns `coverage([], 0, 0)` when it finds none. The earlier branch handles an entirely absent section table conservatively, but this branch does not inspect `PT_DYNAMIC` before asserting absence. ## Additional context NOT_ENOUGH_INFO: first affected release and last known good release were not established. This report concerns a false completeness/absence claim; implementing a new dynamic-segment decoder is not required to represent the current limitation honestly. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The public CLI silently reports complete absence of exports for a locally loadable shared library whose exported function is independently resolved and called. The observed failure is bounded to this inventory and does not modify target data. - Workaround status: unverified - Workaround basis: An original build retaining its dynamic-symbol section works in the control, but access to such a build for an affected artifact is not established. Clearing the section table changes the reported coverage to unsupported and does not recover the missing inventory.
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#4141
No description provided.