[GH-ISSUE #8358] [Bug] Native value trace does not locate recovered call arguments without bindings #6559

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

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

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

trace_native_values keeps recovered call operands inline but does not identify which recovered operands lack cross-function parameter bindings. In the x86-64 Ghidra control below, the call has three recovered arguments; one fixed-parameter binding is present, but the other two have no corresponding binding or located unknown. The result's unknowns array is empty. Its general limitations do say that missing and variadic arguments remain unknown; traversal.status: "complete" describes budget/policy traversal, not a claim of complete argument recovery.

Related: #5523 asks for structured variadic metadata; #8284 concerns a spurious unknown for an empty parameter list; #3083 concerns Ghidra's Apple-arm64 variadic recovery. This report concerns unmatched operands already present in the trace and applies without asserting their cause from the operand count alone.

Reproduction

  1. Build the following source on macOS with clang -arch x86_64 -O1 -g -fno-inline -fno-optimize-sibling-calls -fno-stack-protector fixture.c -o fixture. Do not execute it.
  2. Import it into a disposable Ghidra 12.1.4 project. In a headless post-script set audit_variadic to a user-defined int audit_variadic(int count, ...) with setVarArgs(true), and set audit_fixed to two user-defined integer parameters. Ghidra reported one fixed parameter and hasVarArgs=true for the former; two and false for the latter. REA selected that project read-only.
  3. Open the target via stdio MCP and call trace_native_values for _audit_call_variadic with max_depth: 1, max_functions: 4, max_call_sites: 20, max_nodes: 500, max_edges: 1000, limit: 500. The CALL node has [target, 2, 7, 11]; there is one argument-binding for ordinal 0, no binding for the other operands, and no unknown. The fixed caller has two call arguments, two bindings and no unknowns. Both traces have complete traversal under those budgets.
  4. A fresh Ghidra import without the authored project produced the same counts for this specific build. Each successful MCP call was validated against the advertised compact schema.
#include <stdarg.h>
volatile int audit_global = 5;
__attribute__((noinline)) int audit_variadic(int count, ...) {
  va_list ap; va_start(ap, count); int total = audit_global;
  for (int i = 0; i < count; ++i) total += va_arg(ap, int);
  va_end(ap); return total;
}
__attribute__((noinline)) int audit_fixed(int a, int b) { return a + b + audit_global; }
__attribute__((noinline)) int audit_call_variadic(void) { return audit_variadic(2, 7, 11) + 3; }
__attribute__((noinline)) int audit_call_fixed(void) { return audit_fixed(7, 11) + 3; }
int main(void) { return audit_call_variadic() + audit_call_fixed(); }

Expected behavior

Consider identifying recovered operands that lack a recovered parameter binding at the call site, while retaining the known fixed binding and the general limitations. Do not invent formal parameters or assume every unmatched operand proves a variadic signature; source facts and provider uncertainty must govern interpretation.

Impact and scope

An agent examining located unknowns cannot tell which call requires follow-up despite the operands being present in the graph. One x86-64 Mach-O build, fresh and selected Ghidra projects, was tested; other providers, architectures, and runtime behavior are not established. The target was not executed. SHA-256 of the target and all caller-project files remained unchanged during REA sessions. Ghidra logged some DWARF expression failures during import; the selected prototype was explicitly authored afterward.

REA version

d54a4661da20382ad8456564a08569fdd24131c5 (source build).

Runtime and platform

Node 24.22.1; macOS 26.3 arm64 host; Ghidra 12.1.4; JDK 21.

Target and artifact context

Self-built x86-64 Mach-O; SHA-256 16be6017e676a9bbaf3f346b659f8ae5c7f788a1184199b6dee8a5a8efbe833e.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Selected project: call at 0x1000004b5; recovered argument constants 2, 7, b (hex); only binding has input_index: 1; unknowns: []. Fixed control at 0x1000004ce binds input indexes 1 and 2. The general variadic limitation is present in both traces.

Source-derived reasoning: NativeValueTrace.ts iterates recovered callee parameters and warns if their ordinal exceeds the caller's recovered arguments, but does not separately report recovered caller arguments with no retained parameter ordinal. This does not establish the correct semantics of every unmatched operand.

Additional context

In a disposable experiment, comparing recovered argument indexes with retained parameter ordinals produced a located unknown for x86-64 arguments 1 and 2, while leaving the fixed control unchanged. This is not a proposed production fix: it did not detect the arm64 control's extra stack arguments because Ghidra did not put them in the CALL inputs, and it needs validation for provider truncation, unavailable parameters, multiple call targets, and resource limits. Neither the input binary nor selected projects were modified by REA.

Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0.

Follow-up: paging, budgets, and offline Evidence

On main 6e9ee97a69cb16c7150d8b550e705cba055ee4f0, I reran the same selected-project x86-64 Ghidra 12.1.4 fixture through SDK-schema-validated stdio MCP. The complete variadic trace contained 40 nodes and 47 edges, with three recovered call arguments and only the first binding. Eight pages of five nodes reconstructed the same nodes and edge multiset (edge order is not preserved by page concatenation). All 13 trace records from the full, fixed-control, paged, and budgeted calls survived bundle export and import with identical normalized results. This comparison does not assert that the individual source Evidence links in current main resolve; that separate issue is #8282.

Limits affect what a future located-unknown report can honestly say: max_depth: 0 and max_functions: 1 left the call with no bindings because the callee was not analyzed and returned explicit truncation reasons; max_edges: 1 retained the call node but not its call edge and returned the edge-budget reason. Do not label these as proof of a variadic argument or a fully examined missing binding. A finding should name the call and recovered input index while preserving whether the destination and binding were actually examined and retained.

The same selected-project controls on #8384 head 1e77ce796b7ad0aa0d160cb5238a596ccac69e46 preserved the same graph and limitations. Its source Evidence ownership fix does not itself address unmatched operands. Input and selected-project hashes were unchanged; targets were not executed. This is one fixture/provider/platform and does not prove behavior for multiple targets or provider-side truncation.

Originally created by @morluto on GitHub (Oct 31, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8358 ### 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 `trace_native_values` keeps recovered call operands inline but does not identify which recovered operands lack cross-function parameter bindings. In the x86-64 Ghidra control below, the call has three recovered arguments; one fixed-parameter binding is present, but the other two have no corresponding binding or located unknown. The result's `unknowns` array is empty. Its general limitations do say that missing and variadic arguments remain unknown; `traversal.status: "complete"` describes budget/policy traversal, not a claim of complete argument recovery. Related: #5523 asks for structured variadic metadata; #8284 concerns a spurious unknown for an empty parameter list; #3083 concerns Ghidra's Apple-arm64 variadic recovery. This report concerns unmatched operands **already present in the trace** and applies without asserting their cause from the operand count alone. ### Reproduction 1. Build the following source on macOS with `clang -arch x86_64 -O1 -g -fno-inline -fno-optimize-sibling-calls -fno-stack-protector fixture.c -o fixture`. Do not execute it. 2. Import it into a disposable Ghidra 12.1.4 project. In a headless post-script set `audit_variadic` to a user-defined `int audit_variadic(int count, ...)` with `setVarArgs(true)`, and set `audit_fixed` to two user-defined integer parameters. Ghidra reported one fixed parameter and `hasVarArgs=true` for the former; two and `false` for the latter. REA selected that project read-only. 3. Open the target via stdio MCP and call `trace_native_values` for `_audit_call_variadic` with `max_depth: 1`, `max_functions: 4`, `max_call_sites: 20`, `max_nodes: 500`, `max_edges: 1000`, `limit: 500`. The CALL node has `[target, 2, 7, 11]`; there is one `argument-binding` for ordinal 0, no binding for the other operands, and no unknown. The fixed caller has two call arguments, two bindings and no unknowns. Both traces have complete traversal under those budgets. 4. A fresh Ghidra import without the authored project produced the same counts for this specific build. Each successful MCP call was validated against the advertised compact schema. ```c #include <stdarg.h> volatile int audit_global = 5; __attribute__((noinline)) int audit_variadic(int count, ...) { va_list ap; va_start(ap, count); int total = audit_global; for (int i = 0; i < count; ++i) total += va_arg(ap, int); va_end(ap); return total; } __attribute__((noinline)) int audit_fixed(int a, int b) { return a + b + audit_global; } __attribute__((noinline)) int audit_call_variadic(void) { return audit_variadic(2, 7, 11) + 3; } __attribute__((noinline)) int audit_call_fixed(void) { return audit_fixed(7, 11) + 3; } int main(void) { return audit_call_variadic() + audit_call_fixed(); } ``` ### Expected behavior Consider identifying recovered operands that lack a recovered parameter binding at the call site, while retaining the known fixed binding and the general limitations. Do not invent formal parameters or assume every unmatched operand proves a variadic signature; source facts and provider uncertainty must govern interpretation. ### Impact and scope An agent examining located unknowns cannot tell which call requires follow-up despite the operands being present in the graph. One x86-64 Mach-O build, fresh and selected Ghidra projects, was tested; other providers, architectures, and runtime behavior are not established. The target was not executed. SHA-256 of the target and all caller-project files remained unchanged during REA sessions. Ghidra logged some DWARF expression failures during import; the selected prototype was explicitly authored afterward. ### REA version `d54a4661da20382ad8456564a08569fdd24131c5` (source build). ### Runtime and platform Node 24.22.1; macOS 26.3 arm64 host; Ghidra 12.1.4; JDK 21. ### Target and artifact context Self-built x86-64 Mach-O; SHA-256 `16be6017e676a9bbaf3f346b659f8ae5c7f788a1184199b6dee8a5a8efbe833e`. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Selected project: call at `0x1000004b5`; recovered argument constants `2`, `7`, `b` (hex); only binding has `input_index: 1`; `unknowns: []`. Fixed control at `0x1000004ce` binds input indexes 1 and 2. The general variadic limitation is present in both traces. Source-derived reasoning: [`NativeValueTrace.ts`](https://github.com/morluto/rea/blob/d54a4661da20382ad8456564a08569fdd24131c5/src/application/native/NativeValueTrace.ts#L377) iterates recovered callee parameters and warns if their ordinal exceeds the caller's recovered arguments, but does not separately report recovered caller arguments with no retained parameter ordinal. This does not establish the correct semantics of every unmatched operand. ### Additional context In a disposable experiment, comparing recovered argument indexes with retained parameter ordinals produced a located unknown for x86-64 arguments 1 and 2, while leaving the fixed control unchanged. This is not a proposed production fix: it did not detect the arm64 control's extra stack arguments because Ghidra did not put them in the CALL inputs, and it needs validation for provider truncation, unavailable parameters, multiple call targets, and resource limits. Neither the input binary nor selected projects were modified by REA. Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0. ### Follow-up: paging, budgets, and offline Evidence On main `6e9ee97a69cb16c7150d8b550e705cba055ee4f0`, I reran the same selected-project x86-64 Ghidra 12.1.4 fixture through SDK-schema-validated stdio MCP. The complete variadic trace contained 40 nodes and 47 edges, with three recovered call arguments and only the first binding. Eight pages of five nodes reconstructed the same nodes and edge multiset (edge order is not preserved by page concatenation). All 13 trace records from the full, fixed-control, paged, and budgeted calls survived bundle export and import with identical normalized results. This comparison does not assert that the individual source Evidence links in current main resolve; that separate issue is #8282. Limits affect what a future located-unknown report can honestly say: `max_depth: 0` and `max_functions: 1` left the call with no bindings because the callee was not analyzed and returned explicit truncation reasons; `max_edges: 1` retained the call node but not its call edge and returned the edge-budget reason. Do not label these as proof of a variadic argument or a fully examined missing binding. A finding should name the call and recovered input index while preserving whether the destination and binding were actually examined and retained. The same selected-project controls on #8384 head `1e77ce796b7ad0aa0d160cb5238a596ccac69e46` preserved the same graph and limitations. Its source Evidence ownership fix does not itself address unmatched operands. Input and selected-project hashes were unchanged; targets were not executed. This is one fixture/provider/platform and does not prove behavior for multiple targets or provider-side truncation.
Author
Owner

@morluto commented on GitHub (Nov 1, 2026):

Same binary, different selected prototype

I compared two disposable selected Ghidra 12.1.4 projects for the existing x86-64 fixture (SHA-256 16be6017e676a9bbaf3f346b659f8ae5c7f788a1184199b6dee8a5a8efbe833e) using REA e18594b24754ab95ebd119311cc32a1d1ad6fa13, pinned Node 24.22.1 and SDK stdio MCP with the compact advertised schema. In the second project I explicitly changed the user-defined audit_variadic(int count, ...) prototype to audit_variadic(int count) and saved that owned copy before the REA sessions. The source function really is variadic, so the latter is a deliberately inaccurate analysis annotation, not evidence that the binary changed.

At the same 0x1000004b5 CALL, the variadic project returned the target plus recovered operands 2, 7, b; only the first operand had a parameter-binding edge. The fixed-prototype project returned the target and operand 2 only, with the same single binding. Both traces had unknowns: [] and complete traversal under the selected budgets. In both projects inspect_native_api exposed the one user-defined parameter, but the tested main revision did not expose the prototype's variadic flag in that boundary result. For each project I exported the session and imported it into a fresh target-free MCP server; the retained trace's normalized result matched the live result. The target and both selected project trees' hashes were unchanged by the REA sessions; the target was not executed.

This shows why a located-unmatched-operand finding should describe only recovered operands and available bindings: missing operands are not evidence of absence from the machine call or source signature, and project-supplied signature facts can change what the decompiler emits. It does not establish provider-side truncation behavior or prove a general rule for all calls. Structured prototype visibility is separately discussed in #5523; this control does not claim that any particular PR resolves it.

<!-- gh-comment-id:6555740037 --> @morluto commented on GitHub (Nov 1, 2026): ### Same binary, different selected prototype I compared two disposable selected Ghidra 12.1.4 projects for the existing x86-64 fixture (SHA-256 `16be6017e676a9bbaf3f346b659f8ae5c7f788a1184199b6dee8a5a8efbe833e`) using REA `e18594b24754ab95ebd119311cc32a1d1ad6fa13`, pinned Node 24.22.1 and SDK stdio MCP with the compact advertised schema. In the second project I explicitly changed the user-defined `audit_variadic(int count, ...)` prototype to `audit_variadic(int count)` and saved that owned copy before the REA sessions. The source function really is variadic, so the latter is a deliberately inaccurate analysis annotation, not evidence that the binary changed. At the same `0x1000004b5` CALL, the variadic project returned the target plus recovered operands `2`, `7`, `b`; only the first operand had a parameter-binding edge. The fixed-prototype project returned the target and operand `2` only, with the same single binding. Both traces had `unknowns: []` and complete *traversal* under the selected budgets. In both projects `inspect_native_api` exposed the one user-defined parameter, but the tested main revision did not expose the prototype's variadic flag in that boundary result. For each project I exported the session and imported it into a fresh target-free MCP server; the retained trace's normalized result matched the live result. The target and both selected project trees' hashes were unchanged by the REA sessions; the target was not executed. This shows why a located-unmatched-operand finding should describe only recovered operands and available bindings: missing operands are not evidence of absence from the machine call or source signature, and project-supplied signature facts can change what the decompiler emits. It does not establish provider-side truncation behavior or prove a general rule for all calls. Structured prototype visibility is separately discussed in #5523; this control does not claim that any particular PR resolves it.
Author
Owner

@morluto commented on GitHub (Nov 1, 2026):

Existing static fallback for this fixture

I exercised the recovery an analyst can use with REA e18594b24754ab95ebd119311cc32a1d1ad6fa13, Node 24.22.1, compact SDK stdio MCP, and the same unexecuted x86-64 fixture. In both selected Ghidra 12.1.4 projects from the preceding control, analyze_function retained the same assembly lines before the call: MOV EDI, 0x2, MOV ESI, 0x7, and MOV EDX, 0xb. The deliberately inaccurate fixed prototype changed its pseudocode from a three-operand call to a one-operand call, matching the reduced value trace. inspect_native_instruction separately exposed decoded bytes, register/immediate operand components, and addresses for all three MOVs and the direct CALL. The dossier, trace, and four instruction Evidence records each matched after export/import into a fresh target-free server. The standalone procedure_assembly operation on the fixed project also returned those lines with the selected-project history caveat.

A separate fresh Kuna 1.986 session on the same file returned those register writes through both analyze_function and procedure_assembly; its reconstructed pseudocode displayed all three operands. Its trace_native_values returned an explicit no-def-use-model unknown and no value nodes, and inspect_native_instruction returned capability_unavailable. These are different provider observations, not evidence that Kuna read the selected Ghidra project. The target and Ghidra project trees were unchanged by their sessions; no target was executed.

Thus an analyst has a current assembly-based fallback for this example, and an instruction-level one with Ghidra. Interpreting the register writes as call arguments still requires ABI, control-flow and register-liveness reasoning; the listed observations alone are not a general automatic argument-recovery algorithm. This does not eliminate the missing located-unknown issue or prove that other binaries/providers expose the same details. No autonomous agent-selection trial was run.

<!-- gh-comment-id:6555763294 --> @morluto commented on GitHub (Nov 1, 2026): ### Existing static fallback for this fixture I exercised the recovery an analyst can use with REA `e18594b24754ab95ebd119311cc32a1d1ad6fa13`, Node 24.22.1, compact SDK stdio MCP, and the same unexecuted x86-64 fixture. In both selected Ghidra 12.1.4 projects from the preceding control, `analyze_function` retained the same assembly lines before the call: `MOV EDI, 0x2`, `MOV ESI, 0x7`, and `MOV EDX, 0xb`. The deliberately inaccurate fixed prototype changed its pseudocode from a three-operand call to a one-operand call, matching the reduced value trace. `inspect_native_instruction` separately exposed decoded bytes, register/immediate operand components, and addresses for all three MOVs and the direct CALL. The dossier, trace, and four instruction Evidence records each matched after export/import into a fresh target-free server. The standalone `procedure_assembly` operation on the fixed project also returned those lines with the selected-project history caveat. A separate fresh Kuna 1.986 session on the same file returned those register writes through both `analyze_function` and `procedure_assembly`; its reconstructed pseudocode displayed all three operands. Its `trace_native_values` returned an explicit no-def-use-model unknown and no value nodes, and `inspect_native_instruction` returned `capability_unavailable`. These are different provider observations, not evidence that Kuna read the selected Ghidra project. The target and Ghidra project trees were unchanged by their sessions; no target was executed. Thus an analyst has a current assembly-based fallback for this example, and an instruction-level one with Ghidra. Interpreting the register writes as call arguments still requires ABI, control-flow and register-liveness reasoning; the listed observations alone are not a general automatic argument-recovery algorithm. This does not eliminate the missing located-unknown issue or prove that other binaries/providers expose the same details. No autonomous agent-selection trial was run.
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#6559
No description provided.