[GH-ISSUE #5523] [Feature] inspect_native_api: report whether a function is variadic #3360

Open
opened 2026-10-31 00:39:31 +00:00 by bsakaguchi · 5 comments
Owner

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

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal. Related: #3083 (variadic arguments missing at printf call sites in Ghidra pseudocode on Apple arm64). That is about call sites; this is about the callee's own boundary.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Native analysis or decompilation

Current limitation

inspect_native_api reconstructs a function boundary as calling_convention, return_type and parameters[] with storage, but it has no field for a variadic signature. A variadic function is indistinguishable from one with only its fixed parameters.

Fixture (x86-64, gcc 13.3 -O1 -g):

long f_var(int n, ...) { va_list ap; long t = 0; va_start(ap, n); for (int i = 0; i < n; i++) t += va_arg(ap, long); va_end(ap); return t; }

rea inspect-native-api abi f_var --provider ghidra on rea-agents@6.7.0 (Ghidra 12.1.4) returns:

  • parameters: [{name: "n", data_type: "int", storage: "EDI:4"}]
  • signature_source: "imported", calling_convention: "__stdcall"
  • boundary keys: available, provenance, signature_source, calling_convention, return_type, parameters, jump_tables, pseudocode, decompiler_artifacts, limitations

None of these says "variadic". The information is available upstream of REA:

  • rea decompile abi f_var --provider ghidra prints long f_var(int n,...), so Ghidra's function has var-args set.
  • The DWARF for f_var has a DW_TAG_unspecified_parameters child.
  • git grep -i varargs finds no match in src/ or bridge/ on main 074768901, so the bridge doesn't read Function.hasVarArgs().

Use cases

  • Agents that read the boundary instead of the pseudocode. They would conclude that f_var takes one argument and that callers pass nothing else. The extra arguments sit in registers and on the stack (and in AL on x86-64 SysV), and are easy to dismiss as noise.
  • Call-site reasoning and hooking. Code that builds a Frida hook or stub from the boundary needs to know the argument list is open. Otherwise it reads or forwards only n.
  • ABI questions. On x86-64 SysV, AL carries the vector-register count for variadic calls. On Apple arm64, variadic arguments go on the stack rather than in registers (the case in #3083). Without the flag, the reported storage gives no hint that these rules apply.

Desired outcome and success criteria

  • The boundary carries an explicit variadic field (for example varargs: true | false | null), with null when the provider can't tell. Nothing should be inferred from ... text.
  • For a Ghidra function with hasVarArgs(), the result is true with evidence naming the source (signature source, DWARF import or user annotation). Fixed-arity functions read false.
  • The fixed parameters keep their current storage. No storage is invented for the variadic part.

Expected interaction with targets

Read existing files or captured data

Target and scope details (optional)

Minimal check: compile the fixture above, then compare inspect_native_api on f_var with a fixed-arity sibling such as long f_seven(long a, long b, long c, long d, long e, long f, long g). A stripped build, where Ghidra may not know, is a useful null case.

Verification available so far

I have a prototype or observed results

Verification details (optional)

Observed on rea-agents@6.7.0, Linux x86-64, Ghidra 12.1.4. Fixture binary sha256 844b7a8568814711681a1cf7998367e954e3d17feeed6b111e09ae7b78e038bc, built reproducibly from the source above. On the same fixture, the other 11 exported functions' parameter and return storage matched the SysV psABI by hand derivation, except unsigned __int128, which follows Ghidra's prototype model.

Alternatives considered

  • Parse ... out of the pseudocode text. That is fragile, and the boundary's limitations already say the pseudocode is not source.
  • Leave it to analyze_function. That reaches the same Ghidra function, but agents asking specifically about the API boundary would still miss it.

Possible direction

Read Function.hasVarArgs() in the Ghidra bridge next to the existing signature-source and calling-convention facts, and add it to the boundary schema as a nullable boolean. Other providers report null until they can supply it.

References

  • System V AMD64 psABI §3.5.7 (variable argument lists; AL register count)
  • Ghidra API: ghidra.program.model.listing.Function#hasVarArgs
  • #3083
Originally created by @Rinkia on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5523 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. Related: #3083 (variadic arguments missing at printf call sites in Ghidra pseudocode on Apple arm64). That is about call sites; this is about the callee's own boundary. - [x] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [x] This request does not include credentials, private samples, or sensitive target data. ### Area Native analysis or decompilation ### Current limitation `inspect_native_api` reconstructs a function boundary as `calling_convention`, `return_type` and `parameters[]` with storage, but it has no field for a variadic signature. A variadic function is indistinguishable from one with only its fixed parameters. Fixture (x86-64, `gcc 13.3 -O1 -g`): ```c long f_var(int n, ...) { va_list ap; long t = 0; va_start(ap, n); for (int i = 0; i < n; i++) t += va_arg(ap, long); va_end(ap); return t; } ``` `rea inspect-native-api abi f_var --provider ghidra` on `rea-agents@6.7.0` (Ghidra 12.1.4) returns: - `parameters`: `[{name: "n", data_type: "int", storage: "EDI:4"}]` - `signature_source`: `"imported"`, `calling_convention`: `"__stdcall"` - boundary keys: `available, provenance, signature_source, calling_convention, return_type, parameters, jump_tables, pseudocode, decompiler_artifacts, limitations` None of these says "variadic". The information is available upstream of REA: - `rea decompile abi f_var --provider ghidra` prints `long f_var(int n,...)`, so Ghidra's function has var-args set. - The DWARF for `f_var` has a `DW_TAG_unspecified_parameters` child. - `git grep -i varargs` finds no match in `src/` or `bridge/` on `main` `074768901`, so the bridge doesn't read `Function.hasVarArgs()`. ### Use cases - **Agents that read the boundary instead of the pseudocode.** They would conclude that `f_var` takes one argument and that callers pass nothing else. The extra arguments sit in registers and on the stack (and in AL on x86-64 SysV), and are easy to dismiss as noise. - **Call-site reasoning and hooking.** Code that builds a Frida hook or stub from the boundary needs to know the argument list is open. Otherwise it reads or forwards only `n`. - **ABI questions.** On x86-64 SysV, AL carries the vector-register count for variadic calls. On Apple arm64, variadic arguments go on the stack rather than in registers (the case in #3083). Without the flag, the reported storage gives no hint that these rules apply. ### Desired outcome and success criteria - The boundary carries an explicit variadic field (for example `varargs: true | false | null`), with `null` when the provider can't tell. Nothing should be inferred from `...` text. - For a Ghidra function with `hasVarArgs()`, the result is `true` with evidence naming the source (signature source, DWARF import or user annotation). Fixed-arity functions read `false`. - The fixed parameters keep their current storage. No storage is invented for the variadic part. ### Expected interaction with targets Read existing files or captured data ### Target and scope details (optional) Minimal check: compile the fixture above, then compare `inspect_native_api` on `f_var` with a fixed-arity sibling such as `long f_seven(long a, long b, long c, long d, long e, long f, long g)`. A stripped build, where Ghidra may not know, is a useful `null` case. ### Verification available so far I have a prototype or observed results ### Verification details (optional) Observed on `rea-agents@6.7.0`, Linux x86-64, Ghidra 12.1.4. Fixture binary sha256 `844b7a8568814711681a1cf7998367e954e3d17feeed6b111e09ae7b78e038bc`, built reproducibly from the source above. On the same fixture, the other 11 exported functions' parameter and return storage matched the SysV psABI by hand derivation, except `unsigned __int128`, which follows Ghidra's prototype model. ### Alternatives considered - Parse `...` out of the pseudocode text. That is fragile, and the boundary's limitations already say the pseudocode is not source. - Leave it to `analyze_function`. That reaches the same Ghidra function, but agents asking specifically about the API boundary would still miss it. ### Possible direction Read `Function.hasVarArgs()` in the Ghidra bridge next to the existing signature-source and calling-convention facts, and add it to the boundary schema as a nullable boolean. Other providers report `null` until they can supply it. ### References - System V AMD64 psABI §3.5.7 (variable argument lists; AL register count) - Ghidra API: `ghidra.program.model.listing.Function#hasVarArgs` - #3083
Author
Owner

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

Additional real Ghidra 12.1.4 / REA b43c329 MCP control on macOS arm64: I copied a selected Ghidra project and deliberately toggled the same user-defined, zero-fixed-parameter function from hasVarArgs=false to true. Headless Ghidra reported int audit_zero(void) and int audit_zero(...), respectively. REA's complete dossiers retained those different display signatures, but inspect_native_api returned identical normalized results for both: signature_source: "user_defined", parameters: [], and no structured variadic fact. Their analysis-profile digests differed, correctly reflecting different project states.

This is an intentionally edited metadata control on a binary whose source is non-variadic, not a real variadic call or ABI reproduction. It demonstrates why neither an empty parameter array nor user_defined alone can resolve the distinction, and complements the real variadic fixture in the issue description. The original project and binary were unchanged; REA did not change the edited copy. Related value-trace issue #8284 needs the same distinction to avoid replacing a missing-bindings unknown with an unjustified fixed-zero conclusion. A structured fact should preserve unknown where the provider cannot establish it.

Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 for visibility on the REA tool-design investigation.

<!-- gh-comment-id:6542368096 --> @morluto commented on GitHub (Oct 31, 2026): Additional real Ghidra 12.1.4 / REA `b43c329` MCP control on macOS arm64: I copied a selected Ghidra project and deliberately toggled the same user-defined, zero-fixed-parameter function from `hasVarArgs=false` to `true`. Headless Ghidra reported `int audit_zero(void)` and `int audit_zero(...)`, respectively. REA's complete dossiers retained those different display signatures, but `inspect_native_api` returned **identical normalized results** for both: `signature_source: "user_defined"`, `parameters: []`, and no structured variadic fact. Their analysis-profile digests differed, correctly reflecting different project states. This is an intentionally edited metadata control on a binary whose source is non-variadic, not a real variadic call or ABI reproduction. It demonstrates why neither an empty parameter array nor `user_defined` alone can resolve the distinction, and complements the real variadic fixture in the issue description. The original project and binary were unchanged; REA did not change the edited copy. Related value-trace issue #8284 needs the same distinction to avoid replacing a missing-bindings unknown with an unjustified fixed-zero conclusion. A structured fact should preserve unknown where the provider cannot establish it. Looping in @gensokyo-dev @kaoru0822-kitauji @N0zoM1z0 for visibility on the REA tool-design investigation.
Author
Owner

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

Compiled Apple arm64 variadic control

On REA b43c329, Ghidra 12.1.4 and macOS arm64, I compiled an unexecuted fixture containing audit_variadic(int tag, ...) using va_arg(args, long), a fixed sibling audit_fixed(int tag, long value), and a caller invoking both with (5, 99L). LLVM's DWARF dump showed DW_TAG_unspecified_parameters under the variadic callee. Independent disassembly showed the caller storing 99 at [sp] before the variadic call, and passing 99 in w1 before the fixed call. This verifies the fixture's emitted static facts; it does not establish whether Ghidra imported the sidecar DWARF.

In a fresh real REA/Ghidra MCP session, inspect_native_api reported signature_source: "default" for both functions: the variadic callee had one low-confidence recovered fixed parameter, and the fixed sibling had two. No structured variadic flag appeared. The dossier displayed the variadic callee as undefined _audit_variadic(), so in this fresh-import case even the display signature did not reveal the source's ellipsis. trace_native_values for the caller retained both direct calls, three fixed-parameter binding edges and two return bindings; it did not emit a per-call unknown for the stack-passed variadic value. Its general limitations did explicitly say variadic arguments remain unknown, and truncated:false concerns traversal, not proof of complete argument recovery. Kuna's API boundary and def-use model were explicitly unavailable for the same fixture.

This extends the earlier artificially edited-project control to one real arm64 variadic call and shows why the general limitation matters. It does not establish a particular Ghidra analyzer setting, imported prototype, runtime behavior, or a universal ABI claim. Compare #3083 for Apple-arm64 pseudocode and #8284 for the related need to distinguish fixed-zero, variadic and unknown parameter coverage. The target and debug companion hashes were unchanged, and both sessions closed successfully; no target was executed.

<!-- gh-comment-id:6542378609 --> @morluto commented on GitHub (Oct 31, 2026): ### Compiled Apple arm64 variadic control On REA `b43c329`, Ghidra 12.1.4 and macOS arm64, I compiled an unexecuted fixture containing `audit_variadic(int tag, ...)` using `va_arg(args, long)`, a fixed sibling `audit_fixed(int tag, long value)`, and a caller invoking both with `(5, 99L)`. LLVM's DWARF dump showed `DW_TAG_unspecified_parameters` under the variadic callee. Independent disassembly showed the caller storing 99 at `[sp]` before the variadic call, and passing 99 in `w1` before the fixed call. This verifies the fixture's emitted static facts; it does not establish whether Ghidra imported the sidecar DWARF. In a fresh real REA/Ghidra MCP session, `inspect_native_api` reported `signature_source: "default"` for both functions: the variadic callee had one low-confidence recovered fixed parameter, and the fixed sibling had two. No structured variadic flag appeared. The dossier displayed the variadic callee as `undefined _audit_variadic()`, so in this fresh-import case even the display signature did not reveal the source's ellipsis. `trace_native_values` for the caller retained both direct calls, three fixed-parameter binding edges and two return bindings; it did not emit a per-call unknown for the stack-passed variadic value. Its general limitations **did** explicitly say variadic arguments remain unknown, and `truncated:false` concerns traversal, not proof of complete argument recovery. Kuna's API boundary and def-use model were explicitly unavailable for the same fixture. This extends the earlier artificially edited-project control to one real arm64 variadic call and shows why the general limitation matters. It does not establish a particular Ghidra analyzer setting, imported prototype, runtime behavior, or a universal ABI claim. Compare #3083 for Apple-arm64 pseudocode and #8284 for the related need to distinguish fixed-zero, variadic and unknown parameter coverage. The target and debug companion hashes were unchanged, and both sessions closed successfully; no target was executed.
Author
Owner

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

The compiled arm64 fixture above also has a selected-project control with an explicitly authored, correct prototype. After importing the binary into a disposable Ghidra project, a headless script set audit_variadic(int tag, ...) with USER_DEFINED / hasVarArgs=true, and the fixed sibling with two parameters and hasVarArgs=false. The setup log confirms those states. Some DWARF expression recovery errors were logged during import; the explicit prototype settings are independent of those errors.

In a real REA/Ghidra MCP session bound to that project, the complete variadic dossier displayed the ellipsis, and inspect_native_api returned signature_source: "user_defined", one fixed parameter and no structured variadic flag; its residual_unknowns was empty. The fixed sibling returned two fixed parameters. The caller's value trace retained the two direct call edges, three fixed-argument bindings and return edges, with no per-call unknown for the variadic tail. Its general limitations continued to state that variadic arguments remain unknown. Thus even when the selected Ghidra program knows the prototype, the structured boundary omits that fact; an empty local unknown list must be read with the broader limitations.

The project and target hashes remained unchanged during the REA session, and the target was not executed. This tests one explicitly authored project and call site, not runtime argument recovery or all compilers. It supports a producer-owned structured variadic flag with an explicit unknown where appropriate, while preserving the existing non-source and call-flow limitations.

<!-- gh-comment-id:6542384479 --> @morluto commented on GitHub (Oct 31, 2026): The compiled arm64 fixture above also has a **selected-project control with an explicitly authored, correct prototype**. After importing the binary into a disposable Ghidra project, a headless script set `audit_variadic(int tag, ...)` with `USER_DEFINED` / `hasVarArgs=true`, and the fixed sibling with two parameters and `hasVarArgs=false`. The setup log confirms those states. Some DWARF expression recovery errors were logged during import; the explicit prototype settings are independent of those errors. In a real REA/Ghidra MCP session bound to that project, the complete variadic dossier displayed the ellipsis, and `inspect_native_api` returned `signature_source: "user_defined"`, one fixed parameter and no structured variadic flag; its `residual_unknowns` was empty. The fixed sibling returned two fixed parameters. The caller's value trace retained the two direct call edges, three fixed-argument bindings and return edges, with no per-call unknown for the variadic tail. Its general limitations continued to state that variadic arguments remain unknown. Thus even when the selected Ghidra program knows the prototype, the structured boundary omits that fact; an empty local unknown list must be read with the broader limitations. The project and target hashes remained unchanged during the REA session, and the target was not executed. This tests one explicitly authored project and call site, not runtime argument recovery or all compilers. It supports a producer-owned structured variadic flag with an explicit unknown where appropriate, while preserving the existing non-source and call-flow limitations.
Author
Owner

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

A related x86-64 Mach-O control on main d54a4661d distinguishes missing metadata from missing call operands. With an explicitly authored int audit_variadic(int count, ...) project, Ghidra's high p-code supplied three caller operands (2, 7, 11), and REA kept all three on the CALL node. The trace bound the fixed count parameter but supplied no per-call unknown for the other two; the general variadic limitation remained present. The two-argument fixed sibling bound both operands. A fresh Ghidra import produced the same counts on this particular fixture.

I filed #8358 for the distinct question of locating already-recovered operands without parameter bindings. Structured signature metadata remains necessary for #5523, and no assumption that an unmatched operand proves variadicness is warranted. The static target and project hashes stayed unchanged during REA sessions; no target was executed.

<!-- gh-comment-id:6547652662 --> @morluto commented on GitHub (Oct 31, 2026): A related x86-64 Mach-O control on main `d54a4661d` distinguishes missing metadata from missing call operands. With an explicitly authored `int audit_variadic(int count, ...)` project, Ghidra's high p-code supplied three caller operands (`2`, `7`, `11`), and REA kept all three on the CALL node. The trace bound the fixed `count` parameter but supplied no per-call unknown for the other two; the general variadic limitation remained present. The two-argument fixed sibling bound both operands. A fresh Ghidra import produced the same counts on this particular fixture. I filed #8358 for the distinct question of locating already-recovered operands without parameter bindings. Structured signature metadata remains necessary for #5523, and no assumption that an unmatched operand proves variadicness is warranted. The static target and project hashes stayed unchanged during REA sessions; no target was executed.
Author
Owner

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

An existing implementation is already under review in #8343: it adds native_api.parameter_list to the existing function boundary, including a provider-derived variadic flag and whether the list is signature-based or recovered. I exercised its exact head 84f66cc672f4bf7725e21bd3b5c73c2c5c4b35d0 with real Ghidra 12.1.4/JDK 21 on the two selected x86-64 Mach-O projects described in #8358, using pinned Node 24.22.1 and compact SDK stdio MCP:

  • The explicitly authored variadic prototype returned parameter_list: {"basis":"signature","variadic":true} and signature_source: "user_defined".
  • The deliberately inaccurate fixed prototype for the same actually variadic function returned {"basis":"signature","variadic":false} and the same signature source. The two projects also produced different recovered CALL operands, as described in #8358.
  • The corresponding trace results survived export/import into fresh target-free servers. The target and both project trees remained unchanged by the REA sessions; neither target was executed.

This supports exposing Ghidra's prototype fact in the existing boundary while making its provenance visible. A false value reports the selected decompiler prototype, not proof that the shipped binary's original source function is non-variadic. This control used user-authored prototypes, one fixture and one host; it does not establish detection of all inferred, stripped, or imported signatures. The PR does not itself add located unknowns for the unmatched operands in #8358.

The actual rea inspect-native-api CLI on the same PR head and pinned Node also returned ok: true with the matching signature/true and signature/false parameter-list facts for the two selected projects. Target and project-tree hashes matched the pre-CLI state. This is a real-provider CLI control, in addition to the stdio MCP checks above.

<!-- gh-comment-id:6555777486 --> @morluto commented on GitHub (Nov 1, 2026): An existing implementation is already under review in #8343: it adds `native_api.parameter_list` to the existing function boundary, including a provider-derived `variadic` flag and whether the list is signature-based or recovered. I exercised its exact head `84f66cc672f4bf7725e21bd3b5c73c2c5c4b35d0` with real Ghidra 12.1.4/JDK 21 on the two selected x86-64 Mach-O projects described in [#8358](https://github.com/morluto/rea/issues/8358#issuecomment-6555740037), using pinned Node 24.22.1 and compact SDK stdio MCP: - The explicitly authored variadic prototype returned `parameter_list: {"basis":"signature","variadic":true}` and `signature_source: "user_defined"`. - The deliberately inaccurate fixed prototype for the **same actually variadic function** returned `{"basis":"signature","variadic":false}` and the same signature source. The two projects also produced different recovered CALL operands, as described in #8358. - The corresponding trace results survived export/import into fresh target-free servers. The target and both project trees remained unchanged by the REA sessions; neither target was executed. This supports exposing Ghidra's prototype fact in the existing boundary while making its provenance visible. A `false` value reports the selected decompiler prototype, not proof that the shipped binary's original source function is non-variadic. This control used user-authored prototypes, one fixture and one host; it does not establish detection of all inferred, stripped, or imported signatures. The PR does not itself add located unknowns for the unmatched operands in #8358. The actual `rea inspect-native-api` CLI on the same PR head and pinned Node also returned `ok: true` with the matching `signature`/true and `signature`/false parameter-list facts for the two selected projects. Target and project-tree hashes matched the pre-CLI state. This is a real-provider CLI control, in addition to the stdio MCP checks above.
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#3360
No description provided.