[GH-ISSUE #8333] [Design] Preserve IFUNC resolver semantics when investigating exported APIs #6549

Open
opened 2026-11-01 21:12:08 +00:00 by bsakaguchi · 4 comments
Owner

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

Before submitting

  • I searched the open and closed issues and pull requests for a related discussion.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This discussion does not include credentials, private samples, or sensitive target data.

Decision needed

How should an agent investigating an ELF-exported API distinguish an IFUNC resolver's analyzed signature and references from the eventual implementation, while preserving independent format Evidence and leaving runtime selection unknown?

Context and evidence

A source-built x86-64 shared object defines audit_transform using a compiler IFUNC attribute, a no-argument resolver returning audit_implementation, and an implementation taking one integer and adding seven. Independent llvm-readelf reported the export as IFUNC at 0x12a0; llvm-objdump showed that address returning the implementation's address 0x1290. The GNU C Library manual describes IFUNC symbols as naming resolvers whose returned addresses become relocation targets.

On REA a989c0e with real Ghidra 12.1.4 and Kuna 1.986 MCP:

  • inspect_native_exports correctly returned audit_transform with type:"ifunc" and the ELF value. This is independent format Evidence.
  • Both providers resolved/decompiled audit_transform to the resolver, whose pseudocode returned audit_implementation. Ghidra's rebased entry was 0x1012a0; Kuna used 0x12a0.
  • Ghidra inspect_native_api({procedure:"audit_transform"}) returned a low-confidence pointer return, an empty parameter list and ordinary decompiler-inference caveats for that selected resolver. It did not identify the export as an IFUNC or explain that the result describes the resolver rather than the selected implementation. The analysis of the selected function is valid; the concern is the cross-tool handoff when the analyst's question is the exported API.
  • procedure_references exposed the resolver's data reference to 0x101290 and trace_native_values exposed the address-forming value flowing to RETURN. Explicitly selecting audit_implementation yielded one inferred integer parameter and an integer return. This is a manual, single-fixture static candidate, not proof of general IFUNC resolution or runtime binding. trace_call_path correctly found no direct call edge from resolver to implementation; a returned pointer is not a call.

A second source-built library contained both an ordinary one-argument export and an IFUNC. Format inspection distinguished function and ifunc; Ghidra's API view recovered a one-parameter signature for the ordinary function and a zero-parameter pointer-return signature for the resolver. Neither file was executed or modified. This does not test a real loader, alternate resolver choices, other architectures, or agent behavior.

Expected interaction with targets

Read caller-selected artifacts and use static provider observations. No target execution, library loading, mutation, injection or automatic provider switch is proposed.

Boundary details (optional)

Keep artifact digest/slice, ELF symbol type and version, provider address space, resolver identity, candidate implementation Evidence and the caller's selected provider distinct. An IFUNC value is not automatically an implementation address; data references alone do not prove a resolver returns an address, and static candidates do not establish runtime selection. Resolver conditions, multiple returns, interposition, external code, relocations, incomplete sources and rebased projects may leave correspondence unknown.

Options and tradeoffs

Consider focused documentation/contract guidance using the existing export, reference and value-flow workflows; or an explicit evidence-backed handoff that accepts retained export facts, preserves a verified mapping to the selected provider, labels resolver versus candidates, and returns unknowns where not established. Avoid automatic filesystem search, runtime execution or interpreting every export as a normal function. The existing Android JNI inspection already excludes IFUNC exports from direct implementation candidates; any shared solution should respect that distinction.

Verification available so far

I have a prototype or observed results.

Success criteria and open questions

Exercise ordinary functions alongside IFUNCs, multiple or conditional resolver returns, unknown targets, weak/versioned symbols, provider-specific rebasing, stripped files and retained/offline Evidence. Test that API output never silently presents the resolver signature as the runtime implementation's signature. This is a proposed verification matrix, not work already performed.

References

Related: #8221 (cross-image candidate handoff), #8328 (independent export evidence for name recovery), #8330 (secondary-name search). REA a989c0e458c91af177e097abd7be0fd021b0401d, Node 24.22.1, macOS arm64, static x86-64 ELF fixtures; original SHA-256 655057b5165a376bf959f0146351c56c5c49c3f9e97b8d88ba522fed1bb7562c, normal-export control 8f7de557288c8d4bd153e1d7805f9807b1dd26d863c0e084a55854777d7e230d, unchanged through analysis.

@gensokyo-dev @kaoru0822-kitauji @N0zoM1z0

Originally created by @morluto on GitHub (Oct 31, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8333 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related discussion. - [x] I understand the contribution scope must be reviewed before integration; open questions are welcome here. - [x] This discussion does not include credentials, private samples, or sensitive target data. ### Decision needed How should an agent investigating an ELF-exported API distinguish an IFUNC resolver's analyzed signature and references from the eventual implementation, while preserving independent format Evidence and leaving runtime selection unknown? ### Context and evidence A source-built x86-64 shared object defines `audit_transform` using a compiler IFUNC attribute, a no-argument resolver returning `audit_implementation`, and an implementation taking one integer and adding seven. Independent `llvm-readelf` reported the export as `IFUNC` at `0x12a0`; `llvm-objdump` showed that address returning the implementation's address `0x1290`. The [GNU C Library manual](https://www.sourceware.org/glibc/manual/latest/html_node/Indirect-Functions.html) describes IFUNC symbols as naming resolvers whose returned addresses become relocation targets. On REA `a989c0e` with real Ghidra 12.1.4 and Kuna 1.986 MCP: - `inspect_native_exports` correctly returned `audit_transform` with `type:"ifunc"` and the ELF value. This is independent format Evidence. - Both providers resolved/decompiled `audit_transform` to the **resolver**, whose pseudocode returned `audit_implementation`. Ghidra's rebased entry was `0x1012a0`; Kuna used `0x12a0`. - Ghidra `inspect_native_api({procedure:"audit_transform"})` returned a low-confidence pointer return, an empty parameter list and ordinary decompiler-inference caveats for that selected resolver. It did not identify the export as an IFUNC or explain that the result describes the resolver rather than the selected implementation. The analysis of the selected function is valid; the concern is the cross-tool handoff when the analyst's question is the exported API. - `procedure_references` exposed the resolver's data reference to `0x101290` and `trace_native_values` exposed the address-forming value flowing to `RETURN`. Explicitly selecting `audit_implementation` yielded one inferred integer parameter and an integer return. This is a manual, single-fixture static candidate, not proof of general IFUNC resolution or runtime binding. `trace_call_path` correctly found no direct call edge from resolver to implementation; a returned pointer is not a call. A second source-built library contained both an ordinary one-argument export and an IFUNC. Format inspection distinguished `function` and `ifunc`; Ghidra's API view recovered a one-parameter signature for the ordinary function and a zero-parameter pointer-return signature for the resolver. Neither file was executed or modified. This does not test a real loader, alternate resolver choices, other architectures, or agent behavior. ### Expected interaction with targets Read caller-selected artifacts and use static provider observations. No target execution, library loading, mutation, injection or automatic provider switch is proposed. ### Boundary details (optional) Keep artifact digest/slice, ELF symbol type and version, provider address space, resolver identity, candidate implementation Evidence and the caller's selected provider distinct. An IFUNC value is not automatically an implementation address; data references alone do not prove a resolver returns an address, and static candidates do not establish runtime selection. Resolver conditions, multiple returns, interposition, external code, relocations, incomplete sources and rebased projects may leave correspondence unknown. ### Options and tradeoffs Consider focused documentation/contract guidance using the existing export, reference and value-flow workflows; or an explicit evidence-backed handoff that accepts retained export facts, preserves a verified mapping to the selected provider, labels resolver versus candidates, and returns unknowns where not established. Avoid automatic filesystem search, runtime execution or interpreting every export as a normal function. The existing Android JNI inspection already excludes IFUNC exports from direct implementation candidates; any shared solution should respect that distinction. ### Verification available so far I have a prototype or observed results. ### Success criteria and open questions Exercise ordinary functions alongside IFUNCs, multiple or conditional resolver returns, unknown targets, weak/versioned symbols, provider-specific rebasing, stripped files and retained/offline Evidence. Test that API output never silently presents the resolver signature as the runtime implementation's signature. This is a proposed verification matrix, not work already performed. ### References Related: #8221 (cross-image candidate handoff), #8328 (independent export evidence for name recovery), #8330 (secondary-name search). REA `a989c0e458c91af177e097abd7be0fd021b0401d`, Node 24.22.1, macOS arm64, static x86-64 ELF fixtures; original SHA-256 `655057b5165a376bf959f0146351c56c5c49c3f9e97b8d88ba522fed1bb7562c`, normal-export control `8f7de557288c8d4bd153e1d7805f9807b1dd26d863c0e084a55854777d7e230d`, unchanged through analysis. @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

Existing checks and an E2E opportunity

Source inspection at a989c0e found an existing producer test in NativeExportParser.test.ts: its synthetic ELF includes type 10 and asserts that the export row is classified as ifunc. That checks the format classification, but the selected assertion does not join it to a provider dossier or recovered API signature. A literal search for ifunc/gnu_indirect_function in tests, scripts and src found that test and the Android JNI exclusion, but no IFUNC-specific real-provider verifier by those spellings. This is a bounded source review, not proof that no equivalent case exists under another name; no test suite was run here.

If the design changes, a useful public MCP/CLI regression can build a source-owned ELF with an IFUNC and ordinary export, inspect format evidence, then compare the resolver and implementation observations with real Ghidra. Assert that candidate and runtime-selection uncertainty remain distinct; use a conditional resolver and unavailable/ambiguous cases as follow-up coverage. Retain the producer parser test for its separate format semantics.

<!-- gh-comment-id:6545399898 --> @morluto commented on GitHub (Oct 31, 2026): ### Existing checks and an E2E opportunity Source inspection at `a989c0e` found an existing producer test in [`NativeExportParser.test.ts`](https://github.com/morluto/rea/blob/a989c0e458c91af177e097abd7be0fd021b0401d/src/native/exports/NativeExportParser.test.ts#L350): its synthetic ELF includes type 10 and asserts that the export row is classified as `ifunc`. That checks the format classification, but the selected assertion does not join it to a provider dossier or recovered API signature. A literal search for `ifunc`/`gnu_indirect_function` in `tests`, `scripts` and `src` found that test and the Android JNI exclusion, but no IFUNC-specific real-provider verifier by those spellings. This is a bounded source review, not proof that no equivalent case exists under another name; no test suite was run here. If the design changes, a useful public MCP/CLI regression can build a source-owned ELF with an IFUNC and ordinary export, inspect format evidence, then compare the resolver and implementation observations with real Ghidra. Assert that candidate and runtime-selection uncertainty remain distinct; use a conditional resolver and unavailable/ambiguous cases as follow-up coverage. Retain the producer parser test for its separate format semantics.
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

Conditional resolver and offline source control

A second source-built x86-64 ELF makes the resolver read a volatile choice and return either audit_fast or audit_safe. Independent llvm-readelf identified audit_transform as IFUNC at ELF value 0x1350; llvm-objdump showed a choice read, both implementation-address calculations and a conditional selection before return. An exported setter writes the choice, but this static experiment does not establish whether, when, or with what value a real loader would invoke the resolver.

On REA a989c0e with real Ghidra 12.1.4 and Kuna 1.986 MCP, both decompiled the resolver with both alternatives. Ghidra procedure_references reported separate data references to rebased audit_fast (0x101320) and audit_safe (0x101330) and a read of the choice. Its trace_native_values returned both address-forming operations flowing through MULTIEQUAL into RETURN. This supports two static candidates on this fixture; no REA tool here asserted a runtime-selected implementation. Kuna returned explicit Provider has no native def-use model and did not support procedure_references. Both providers' call-only traces returned not_reached for each candidate, which is consistent with returning addresses rather than making direct calls.

For this run, a TypeScript MCP SDK client supplied each advertised compact-profile tool definition when calling the tools. Successful calls passed SDK output-schema validation; the SDK does not apply the success schema to Kuna's expected isError reference result. This is a contract-shape check for these calls, not a semantic-correctness or other-client claim.

An offline Evidence control also reproduces the separate #8282 defect on this fixture: after cold and identical warm Ghidra value traces, export/import in a fresh target-free server resolved the cold procedure source ID but rejected the warm trace's per-procedure ID as missing, while the warm trace's top-level link still cited the cold source. Candidate handoff should not assume nested source IDs are exportable until that issue is fixed. Target SHA-256 a7dc38d17e48395f93f6acb137864d63655fb74272b21184a89a49656dca3c73 and the exported bundle hash were unchanged; target code was not executed.

<!-- gh-comment-id:6545416083 --> @morluto commented on GitHub (Oct 31, 2026): ### Conditional resolver and offline source control A second source-built x86-64 ELF makes the resolver read a volatile choice and return either `audit_fast` or `audit_safe`. Independent `llvm-readelf` identified `audit_transform` as IFUNC at ELF value `0x1350`; `llvm-objdump` showed a choice read, both implementation-address calculations and a conditional selection before return. An exported setter writes the choice, but this static experiment does not establish whether, when, or with what value a real loader would invoke the resolver. On REA `a989c0e` with real Ghidra 12.1.4 and Kuna 1.986 MCP, both decompiled the resolver with both alternatives. Ghidra `procedure_references` reported separate data references to rebased `audit_fast` (`0x101320`) and `audit_safe` (`0x101330`) and a read of the choice. Its `trace_native_values` returned both address-forming operations flowing through `MULTIEQUAL` into `RETURN`. This supports two *static candidates* on this fixture; no REA tool here asserted a runtime-selected implementation. Kuna returned explicit `Provider has no native def-use model` and did not support `procedure_references`. Both providers' call-only traces returned `not_reached` for each candidate, which is consistent with returning addresses rather than making direct calls. For this run, a TypeScript MCP SDK client supplied each advertised compact-profile tool definition when calling the tools. Successful calls passed SDK output-schema validation; the SDK does not apply the success schema to Kuna's expected `isError` reference result. This is a contract-shape check for these calls, not a semantic-correctness or other-client claim. An offline Evidence control also reproduces the separate #8282 defect on this fixture: after cold and identical warm Ghidra value traces, export/import in a fresh target-free server resolved the cold procedure source ID but rejected the warm trace's per-procedure ID as missing, while the warm trace's top-level link still cited the cold source. Candidate handoff should not assume nested source IDs are exportable until that issue is fixed. Target SHA-256 `a7dc38d17e48395f93f6acb137864d63655fb74272b21184a89a49656dca3c73` and the exported bundle hash were unchanged; target code was not executed.
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

A function reference that is not a returned implementation

A third source-owned ELF control adds audit_decoy and stores its address in a volatile global before the conditional return of audit_fast or audit_safe. Independent disassembly showed that store and the two selected return addresses. Ghidra procedure_references reported DATA references to all three functions; trace_native_values exposed the decoy address flowing to the global write, while the fast/safe address operations flowed through MULTIEQUAL to RETURN. Both decompilers showed the two returned names. The inference that the decoy is not one of the returned alternatives is based on the fixture source, instructions and this observed graph; this is not a general guarantee of complete def-use or runtime selection.

This is a concrete negative control for any candidate handoff: a function referenced by a resolver is not necessarily a function the resolver returns. Keep references, def-use and unknowns distinguishable; do not turn every outgoing DATA edge into an implementation candidate. The target was not executed, and SHA-256 465f35baa337829103a1941e3fa6f3093b9e0689c94e6f0080b344392bceb882 stayed unchanged.

The same control also exposed an independent Kuna pseudocode problem: an unconditional global store was rendered again on one return path. A reduced ordinary-function fixture, without IFUNC, reproduced it in direct Kuna 1.986, upstream current main and REA's cold and warm Kuna outputs; Ghidra and independent disassembly show one store. Filed Kuna #1355. REA already labels pseudocode as reconstructed and exposes assembly; the finding does not imply that it can silently correct provider text.

<!-- gh-comment-id:6545444697 --> @morluto commented on GitHub (Oct 31, 2026): ### A function reference that is not a returned implementation A third source-owned ELF control adds `audit_decoy` and stores its address in a volatile global before the conditional return of `audit_fast` or `audit_safe`. Independent disassembly showed that store and the two selected return addresses. Ghidra `procedure_references` reported DATA references to **all three** functions; `trace_native_values` exposed the decoy address flowing to the global write, while the fast/safe address operations flowed through `MULTIEQUAL` to `RETURN`. Both decompilers showed the two returned names. The inference that the decoy is not one of the returned alternatives is based on the fixture source, instructions and this observed graph; this is not a general guarantee of complete def-use or runtime selection. This is a concrete negative control for any candidate handoff: a function referenced by a resolver is not necessarily a function the resolver returns. Keep references, def-use and unknowns distinguishable; do not turn every outgoing DATA edge into an implementation candidate. The target was not executed, and SHA-256 `465f35baa337829103a1941e3fa6f3093b9e0689c94e6f0080b344392bceb882` stayed unchanged. The same control also exposed an independent Kuna pseudocode problem: an unconditional global store was rendered again on one return path. A reduced ordinary-function fixture, without IFUNC, reproduced it in direct Kuna 1.986, upstream current main and REA's cold and warm Kuna outputs; Ghidra and independent disassembly show one store. Filed [Kuna #1355](https://github.com/Noelo-Lab/kuna/issues/1355). REA already labels pseudocode as reconstructed and exposes assembly; the finding does not imply that it can silently correct provider text.
Author
Owner

@morluto commented on GitHub (Oct 31, 2026):

Installed guidance and catalog discovery

On built REA a989c0e (rea-agents@6.8.0, compact MCP schema), search_guidance({query:"ifunc"}) and search_guidance({query:"ELF export resolver"}) returned zero matches. search_guidance({query:"native exports"}) returned the installed “Offline PE and ELF export tables” section; reading that exact section explained export types/coverage at a general level but did not explain IFUNC resolver semantics or an export-to-provider handoff. The advertised inspect_native_exports description mentions ELF symbol type, and the inspect_native_api description says it analyzes one function, but neither tested description explains that an IFUNC entry is its resolver. Other queries or external knowledge may find additional guidance; this is not an autonomous-agent selection test or a claim about all documentation.

This gives the documentation/contract-guidance option in the issue a concrete first-use case: describe the observed resolver boundary and link the existing explicit references/value-flow route, while keeping candidate selection and runtime binding unknown. The format row already carries type:"ifunc"; do not infer implementation identity from that field alone.

<!-- gh-comment-id:6545520780 --> @morluto commented on GitHub (Oct 31, 2026): ### Installed guidance and catalog discovery On built REA `a989c0e` (`rea-agents@6.8.0`, compact MCP schema), `search_guidance({query:"ifunc"})` and `search_guidance({query:"ELF export resolver"})` returned zero matches. `search_guidance({query:"native exports"})` returned the installed “Offline PE and ELF export tables” section; reading that exact section explained export types/coverage at a general level but did not explain IFUNC resolver semantics or an export-to-provider handoff. The advertised `inspect_native_exports` description mentions ELF symbol type, and the `inspect_native_api` description says it analyzes one function, but neither tested description explains that an IFUNC entry is its resolver. Other queries or external knowledge may find additional guidance; this is not an autonomous-agent selection test or a claim about all documentation. This gives the documentation/contract-guidance option in the issue a concrete first-use case: describe the observed resolver boundary and link the existing explicit references/value-flow route, while keeping candidate selection and runtime binding unknown. The format row already carries `type:"ifunc"`; do not infer implementation identity from that field alone.
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#6549
No description provided.