[GH-ISSUE #8517] [Bug] ELF export Evidence drops symbol visibility #6635

Closed
opened 2026-11-01 21:12:50 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @morluto on GitHub (Nov 1, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8517

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 the same ELF export row for a default-visible and a protected audit_target, even though the dynamic symbol entries carry different visibility. Complete Evidence identifies the distinct target digests, but neither the row nor its paged export view retains the visibility. The tool's limitation says both classes are included, so it does not falsely claim either one.

Reproduction

On macOS arm64 with Apple clang and LLVM lld, create a shared object with:

__attribute__((visibility("default"), noinline, used))
int audit_target(int x) { return x + 11; }
__attribute__((visibility("default"), used))
int (*volatile audit_func_ptr)(int) = audit_target;

Build the second variant by replacing the first visibility with protected; leave the pointer's visibility as default. Build each with clang -target x86_64-unknown-linux-gnu -fuse-ld=lld -nostdlib -shared -Wl,-soname,fixture.so fixture.c -o fixture.so. Do not execute them.

  1. LLVM llvm-readobj --dyn-symbols --relocations reports a global audit_target with default visibility in one object and STV_PROTECTED in the other. Their pointer relocations also differ: R_X86_64_64 audit_target versus R_X86_64_RELATIVE.
  2. For each, run rea inspect-native-exports <path> --format json: the audit_target rows are equal (value: 0x1340, type: function, binding: global, size 15, version null) and neither has visibility. The two resulting Evidence records do have different artifact digests.
  3. Real stdio MCP inspect_native_exports, then inspect_analysis_view with kind: native-exports, collection: exports, and names: ["audit_target"], also omits the field in both full and compact profiles. Export and import the bundle into a fresh target-free server: the same rows persist.

Targets remained unchanged. Default object SHA-256 a0146cdec01838343a40345efe6ecb2f94ac960d80d7ca2fd0aac504910f6a00; protected object SHA-256 4879fcc9e04097e421bc062de9c06d1e0fa6e9064167a137e8a2cdf76da5fade. These are two controlled fixtures, not a frequency estimate or runtime test.

Expected behavior

Retain the dynamic symbol's actual visibility at the parser boundary and expose it in complete Evidence and selected views; historical records that did not record it must remain unknown rather than be assumed default. ELF gABI symbol visibility distinguishes default-visible preemptable globals from protected definitions' internally bound references. Visibility is one input to resolution, not proof of the loaded runtime binding.

Impact and scope

An agent investigating whether an exported function reference could be interposed cannot distinguish these declarations from REA's export rows alone. No claim is made that REA currently infers a wrong runtime binding. inspect_binary_layout may offer separate ELF observations under its supported Linux/pwntools profile; it was not run on this macOS host.

REA version

7f344af1aa0a78fd4e331dbbc9c30d38e4cfb796 (#8509), Node 24.22.1, macOS arm64.

Runtime and platform

Node 24.22.1, macOS arm64, source-built x86-64 ELF shared objects.

Target and artifact context

Two static ELF fixtures, never executed or modified by REA.

Interface

CLI and MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Both export rows include name: audit_target, value: 0x1340, type: function, binding: global, version: null, and lack visibility. LLVM reports default vs protected; Ghidra and Kuna were used for the related relocation-reference question but no runtime linking was tested.

Typed outcome or recovery expected

A source-derived visibility value or explicit unknown in historical data; preserve fields and correct filtering in complete, paged, offline, and full/compact MCP contracts. Do not conflate ELF symbol visibility with symbol-version hidden.

Analysis or suspected cause

Source-derived: NativeExportParser.ts computes symbol.other & 3 to filter default/protected rows, then projects the export without retaining the value. The owning elfExportSchema has no visibility field. Existing tests cover retaining a protected row but do not assert the visibility distinction.

Additional context

Related to #8510 (relocation-backed incoming pointers), #8221 (selected-library binding), and #8333 (IFUNC semantics). No target execution or dynamic-linker observation was performed.

cc @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0

Originally created by @morluto on GitHub (Nov 1, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8517 ### 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 the same ELF export row for a default-visible and a protected `audit_target`, even though the dynamic symbol entries carry different visibility. Complete Evidence identifies the distinct target digests, but neither the row nor its paged export view retains the visibility. The tool's limitation says both classes are included, so it does not falsely claim either one. ### Reproduction On macOS arm64 with Apple clang and LLVM lld, create a shared object with: ```c __attribute__((visibility("default"), noinline, used)) int audit_target(int x) { return x + 11; } __attribute__((visibility("default"), used)) int (*volatile audit_func_ptr)(int) = audit_target; ``` Build the second variant by replacing the first visibility with `protected`; leave the pointer's visibility as default. Build each with `clang -target x86_64-unknown-linux-gnu -fuse-ld=lld -nostdlib -shared -Wl,-soname,fixture.so fixture.c -o fixture.so`. Do not execute them. 1. LLVM `llvm-readobj --dyn-symbols --relocations` reports a global `audit_target` with default visibility in one object and `STV_PROTECTED` in the other. Their pointer relocations also differ: `R_X86_64_64 audit_target` versus `R_X86_64_RELATIVE`. 2. For each, run `rea inspect-native-exports <path> --format json`: the `audit_target` rows are equal (`value: 0x1340`, `type: function`, `binding: global`, size 15, version null) and neither has visibility. The two resulting Evidence records do have different artifact digests. 3. Real stdio MCP `inspect_native_exports`, then `inspect_analysis_view` with `kind: native-exports`, `collection: exports`, and `names: ["audit_target"]`, also omits the field in both full and compact profiles. Export and import the bundle into a fresh target-free server: the same rows persist. Targets remained unchanged. Default object SHA-256 `a0146cdec01838343a40345efe6ecb2f94ac960d80d7ca2fd0aac504910f6a00`; protected object SHA-256 `4879fcc9e04097e421bc062de9c06d1e0fa6e9064167a137e8a2cdf76da5fade`. These are two controlled fixtures, not a frequency estimate or runtime test. ### Expected behavior Retain the dynamic symbol's actual visibility at the parser boundary and expose it in complete Evidence and selected views; historical records that did not record it must remain unknown rather than be assumed default. ELF [gABI symbol visibility](https://gabi.xinuos.com/elf.pdf) distinguishes default-visible preemptable globals from protected definitions' internally bound references. Visibility is one input to resolution, not proof of the loaded runtime binding. ### Impact and scope An agent investigating whether an exported function reference could be interposed cannot distinguish these declarations from REA's export rows alone. No claim is made that REA currently infers a wrong runtime binding. `inspect_binary_layout` may offer separate ELF observations under its supported Linux/pwntools profile; it was not run on this macOS host. ### REA version `7f344af1aa0a78fd4e331dbbc9c30d38e4cfb796` (#8509), Node 24.22.1, macOS arm64. ### Runtime and platform Node 24.22.1, macOS arm64, source-built x86-64 ELF shared objects. ### Target and artifact context Two static ELF fixtures, never executed or modified by REA. ### Interface CLI and MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Both export rows include `name: audit_target`, `value: 0x1340`, `type: function`, `binding: global`, `version: null`, and lack visibility. LLVM reports default vs protected; Ghidra and Kuna were used for the related relocation-reference question but no runtime linking was tested. ### Typed outcome or recovery expected A source-derived visibility value or explicit unknown in historical data; preserve fields and correct filtering in complete, paged, offline, and full/compact MCP contracts. Do not conflate ELF symbol visibility with symbol-version `hidden`. ### Analysis or suspected cause Source-derived: `NativeExportParser.ts` computes `symbol.other & 3` to filter default/protected rows, then projects the export without retaining the value. The owning `elfExportSchema` has no visibility field. Existing tests cover retaining a protected row but do not assert the visibility distinction. ### Additional context Related to #8510 (relocation-backed incoming pointers), #8221 (selected-library binding), and #8333 (IFUNC semantics). No target execution or dynamic-linker observation was performed. cc @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0
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#6635
No description provided.