[GH-ISSUE #3083] [Feature] Ghidra provider: opt-in enabling of analyzers (printf variadic args missing on Apple arm64) #1079

Open
opened 2026-10-15 18:30:53 +00:00 by bsakaguchi · 4 comments
Owner

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

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • 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

On an Apple arm64 Mach-O built with clang -O2, Ghidra pseudocode from procedure_pseudo_code shows variadic calls without their variadic arguments, e.g. _printf("%08x\n");. The assembly from procedure_assembly shows the value is passed on the stack per the Apple arm64 variadic convention (str x8, [sp] immediately before bl _printf). An agent reading only the pseudocode can therefore misidentify which computed value reaches the output; in our test the most prominent hash computation in the pseudocode was a decoy written only to a volatile global, and the real printed value was only identifiable from the assembly.

Setting REA_GHIDRA_LANGUAGE_ID=AARCH64:LE:64:AppleSilicon (whose default compiler spec, AARCH64_apple.cspec, contains the stack-varargs rule) did not change the pseudocode.

Ghidra ships the "Variadic Function Signature Override" analyzer (ghidra.app.plugin.core.string.variadic.FormatStringAnalyzer), which infers printf/scanf-family signatures from format strings. My understanding is that it is not enabled by Ghidra's default auto-analysis (not verified against REA's import), and REA currently offers only REA_GHIDRA_DISABLED_ANALYZERS, so there is no way to opt in.

Use cases

Recovering which values a native program prints or parses (printf/scanf/snprintf/fscanf families) from stripped, optimized Apple arm64 binaries, without having to cross-check every variadic call site in assembly.

Desired outcome and success criteria

An opt-in way to enable named Ghidra analyzers, for example REA_GHIDRA_ENABLED_ANALYZERS="Variadic Function Signature Override", validated and recorded in the analysis profile in the same way as the disabled list (e.g. ghidra-default+enabled:...). With it enabled, the printf call above should show its variadic argument in procedure_pseudo_code.

Alternatively, if enabling this analyzer by default for Mach-O arm64 is considered safe, that would also resolve it; I have no measurements of its cost or false positives.

Expected interaction with targets

Read existing files or captured data

Target and scope details (optional)

Static analysis of a self-built test executable only. Non-goal: changing how format strings are interpreted beyond what Ghidra's analyzer does.

Verification available so far

I can suggest a sample or manual check

Verification details (optional)

Environment: rea-agents 6.4.0, Ghidra 12.1.4 (native decompiler built locally with gradlew buildNatives), OpenJDK 21.0.12.1, macOS arm64.

  1. Compile a small C program that computes a value and calls printf("%08x\n", value) with clang -O2 on Apple silicon, then strip it.
  2. rea decompile <binary> <function> --provider ghidra --json
  3. The printf call in normalized_result has no second argument; procedure_assembly shows str xN, [sp] before the call.

I have not verified that enabling the analyzer fixes this case under REA's headless import.

Alternatives considered

  • Language override to AARCH64:LE:64:AppleSilicon: no change.
  • Reading procedure_assembly for every variadic call: works, but is easy to skip and costs extra calls.

Possible direction

Mirror the existing disabled-analyzer pre-script with an enabled list, applying the same name validation and failing the import when a requested analyzer cannot be enabled.

References

  • Ghidra AARCH64_apple.cspec comment: "Variadic arguments are passed differently than in the AARCH64 standard"
  • Related: #1783 (a Ghidra analyzer that needed to be skipped; this request is the opposite direction)
Originally created by @SomersetMaugham on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3083 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. - [x] I understand the 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 On an Apple arm64 Mach-O built with `clang -O2`, Ghidra pseudocode from `procedure_pseudo_code` shows variadic calls without their variadic arguments, e.g. `_printf("%08x\n");`. The assembly from `procedure_assembly` shows the value is passed on the stack per the Apple arm64 variadic convention (`str x8, [sp]` immediately before `bl _printf`). An agent reading only the pseudocode can therefore misidentify which computed value reaches the output; in our test the most prominent hash computation in the pseudocode was a decoy written only to a volatile global, and the real printed value was only identifiable from the assembly. Setting `REA_GHIDRA_LANGUAGE_ID=AARCH64:LE:64:AppleSilicon` (whose default compiler spec, `AARCH64_apple.cspec`, contains the stack-varargs rule) did not change the pseudocode. Ghidra ships the "Variadic Function Signature Override" analyzer (`ghidra.app.plugin.core.string.variadic.FormatStringAnalyzer`), which infers printf/scanf-family signatures from format strings. My understanding is that it is not enabled by Ghidra's default auto-analysis (not verified against REA's import), and REA currently offers only `REA_GHIDRA_DISABLED_ANALYZERS`, so there is no way to opt in. ### Use cases Recovering which values a native program prints or parses (printf/scanf/snprintf/fscanf families) from stripped, optimized Apple arm64 binaries, without having to cross-check every variadic call site in assembly. ### Desired outcome and success criteria An opt-in way to enable named Ghidra analyzers, for example `REA_GHIDRA_ENABLED_ANALYZERS="Variadic Function Signature Override"`, validated and recorded in the analysis profile in the same way as the disabled list (e.g. `ghidra-default+enabled:...`). With it enabled, the printf call above should show its variadic argument in `procedure_pseudo_code`. Alternatively, if enabling this analyzer by default for Mach-O arm64 is considered safe, that would also resolve it; I have no measurements of its cost or false positives. ### Expected interaction with targets Read existing files or captured data ### Target and scope details (optional) Static analysis of a self-built test executable only. Non-goal: changing how format strings are interpreted beyond what Ghidra's analyzer does. ### Verification available so far I can suggest a sample or manual check ### Verification details (optional) Environment: rea-agents 6.4.0, Ghidra 12.1.4 (native decompiler built locally with `gradlew buildNatives`), OpenJDK 21.0.12.1, macOS arm64. 1. Compile a small C program that computes a value and calls `printf("%08x\n", value)` with `clang -O2` on Apple silicon, then `strip` it. 2. `rea decompile <binary> <function> --provider ghidra --json` 3. The printf call in `normalized_result` has no second argument; `procedure_assembly` shows `str xN, [sp]` before the call. I have not verified that enabling the analyzer fixes this case under REA's headless import. ### Alternatives considered - Language override to `AARCH64:LE:64:AppleSilicon`: no change. - Reading `procedure_assembly` for every variadic call: works, but is easy to skip and costs extra calls. ### Possible direction Mirror the existing disabled-analyzer pre-script with an enabled list, applying the same name validation and failing the import when a requested analyzer cannot be enabled. ### References - Ghidra `AARCH64_apple.cspec` comment: "Variadic arguments are passed differently than in the AARCH64 standard" - Related: #1783 (a Ghidra analyzer that needed to be skipped; this request is the opposite direction)
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Claiming this. Scope I am taking: the opt-in enabling mechanism only — a validated REA_GHIDRA_ENABLED_ANALYZERS list that mirrors the existing REA_GHIDRA_DISABLED_ANALYZERS path end to end:

  • config schema and parsing with the same name limits and character pattern as the disabled list;
  • analysis-profile recording alongside the disabled list (for example ghidra-default+enabled:Variadic Function Signature Override plus an enabled_analyzers entry under analysis_options), with the same committed-profile vs. environment readback consistency check;
  • the shared ReaGhidraAnalysisOptions.java preScript gains an optional second comma-separated argument that validates each requested analyzer exists for the program, sets it to enabled, and fails the import when it cannot be enabled — same fail-hard pattern as the disable path;
  • unit tests for all of the above, mirroring the existing disabled-list tests.

Out of scope, unchanged: the default analyzer set for any target, the pinned MIPS/RISC-V/PowerPC/PSP lanes, and Ghidra's own analyzer behavior. I have no local Ghidra, so I will not claim runtime verification that the printf variadic argument appears in procedure_pseudo_code; the mechanism-level tests and the fail-hard validation are covered locally, and the Apple arm64 effect needs your Ghidra 12.1.4 environment to confirm.

<!-- gh-comment-id:6160720788 --> @akushonkamen commented on GitHub (Oct 14, 2026): Claiming this. Scope I am taking: the opt-in enabling mechanism only — a validated `REA_GHIDRA_ENABLED_ANALYZERS` list that mirrors the existing `REA_GHIDRA_DISABLED_ANALYZERS` path end to end: - config schema and parsing with the same name limits and character pattern as the disabled list; - analysis-profile recording alongside the disabled list (for example `ghidra-default+enabled:Variadic Function Signature Override` plus an `enabled_analyzers` entry under `analysis_options`), with the same committed-profile vs. environment readback consistency check; - the shared `ReaGhidraAnalysisOptions.java` preScript gains an optional second comma-separated argument that validates each requested analyzer exists for the program, sets it to enabled, and fails the import when it cannot be enabled — same fail-hard pattern as the disable path; - unit tests for all of the above, mirroring the existing disabled-list tests. Out of scope, unchanged: the default analyzer set for any target, the pinned MIPS/RISC-V/PowerPC/PSP lanes, and Ghidra's own analyzer behavior. I have no local Ghidra, so I will not claim runtime verification that the printf variadic argument appears in `procedure_pseudo_code`; the mechanism-level tests and the fail-hard validation are covered locally, and the Apple arm64 effect needs your Ghidra 12.1.4 environment to confirm.
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Branch ready: akushonkamen:fix/ghidra-enabled-analyzers (38dda553e0a2f1d18ae85f4d42d52168b47fffad). PR creation from this account is currently rejected by the repository (GraphQL CreatePullRequest permission error, REST 404) — the branch, tests, and validation summary are on the fork; happy to open the PR as soon as creation works again.

<!-- gh-comment-id:6160932970 --> @akushonkamen commented on GitHub (Oct 14, 2026): Branch ready: `akushonkamen:fix/ghidra-enabled-analyzers` (38dda553e0a2f1d18ae85f4d42d52168b47fffad). PR creation from this account is currently rejected by the repository (GraphQL CreatePullRequest permission error, REST 404) — the branch, tests, and validation summary are on the fork; happy to open the PR as soon as creation works again.
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Delivery update (rebased; PR creation retried and still rejected). The fix is complete and validated on branch akushonkamen:fix/ghidra-enabled-analyzers (commit d51afe0dd, based on main at 767fe0f0c). Since the delivery summary below was first posted, main advanced by 3 commits (including #3080); the branch was rebased cleanly with no conflicts and every gate was re-run after the rebase — the numbers below are from the rebased tree. Opening the pull request from this account is still rejected by the repository (retried today: GraphQL CreatePullRequest permission error; REST returns 404), while issue comments and pushes to my fork still work — so the branch and this summary stand in for the PR until creation works again; the maintainer can open the PR from that head.

What the branch contains

  • REA_GHIDRA_ENABLED_ANALYZERS, validated by the same analyzer-name rules as the disabled list (shared schema in src/config/ghidraAnalyzerNames.ts; src/config/ghidraEnabledAnalyzers.ts), flowing through parseConfig/AppConfig into the Ghidra provider.
  • Analysis-profile recording beside the disabled list: analyzer_preset becomes ghidra-default+enabled:…, or ghidra-default+disabled:…+enabled:… when both lists are set, with an enabled_analyzers entry under analysis_options; session open re-checks the committed profile against the active environment exactly like the disabled list.
  • ReaGhidraAnalysisOptions.java now takes an explicit mode argument (disable or enable) plus the comma-separated names, validates every name against the program's registered analyzers, applies the option, re-reads the applied value, and aborts the import when the requested state did not stick — the same fail-hard shape as the disable path. The launcher emits one invocation per configured list after loader/seed preparation and before auto-analysis; every argument stays a non-empty single quotable token, so the Windows batch path is unchanged in safety.
  • docs/installation.md documents the new setting next to the disabled one.

Executed validation (all commands run locally on the rebased branch, macOS arm64):

  • npm run test:focused -- src/ghidra/GhidraEnabledAnalyzers.test.ts src/ghidra/GhidraDisabledAnalyzers.test.ts src/ghidra/GhidraProvider.behavior.test.ts tests/boundary/providers/ghidra/ghidraLauncher.test.ts — 4 files, 113 passed, 4 skipped (identical to the pre-rebase run). The new tests fail against the unmodified tree first: the config file's tests failed at import, 4 behavior tests failed (records the enabled analyzers…, records enabled and disabled analyzers in one preset, hands the enabled analyzers to the launcher…, refuses a profile resolved without or with other enabled analyzers), and 5 launcher tests failed (mode argument, ordering, empty-list omission, updated one-list expectations) — red before the implementation, green after.
  • npm run test:focused -- src/config.test.ts src/cliHostInputs.test.ts src/ghidra/GhidraAnalysisSeeds.test.ts src/ghidra/extensions/GhidraExtensions.test.ts src/ghidra/GhidraSnapshotCache.test.ts — 5 files, 112 passed. Correction to my earlier summary: that run had said "6 files, 122 passed"; re-running the listed 5-file set today gives 112 both on the rebased tree and on the original pre-rebase commit 38dda553 (verified by detached checkout), so the earlier 122 came from the extra sixth file, not from these five.
  • npm run test:focused -- tests/boundary/mcp/officialArguments.test.ts tests/boundary/mcp/officialTools.test.ts tests/boundary/filesystem/ghidraPspExtension.test.ts — 3 files, 52 passed.
  • npx tsc --noEmit — clean. npm run format:check — pass. npm run lint — 0 errors. npx knip — clean. npm run jscpd — 3.21%, under the 3.5% threshold. npm run docs:check — pass.

Not claimed: the printf variadic-argument effect itself. I have no local Ghidra, so the branch delivers and validates the opt-in mechanism and its fail-hard import validation; observing Variadic Function Signature Override recover the stack-passed variadic argument in procedure_pseudo_code needs your Ghidra 12.1.4 environment (REA_GHIDRA_ENABLED_ANALYZERS="Variadic Function Signature Override" on the Apple arm64 sample from the issue).

<!-- gh-comment-id:6160942157 --> @akushonkamen commented on GitHub (Oct 14, 2026): **Delivery update (rebased; PR creation retried and still rejected).** The fix is complete and validated on branch `akushonkamen:fix/ghidra-enabled-analyzers` (commit `d51afe0dd`, based on `main` at `767fe0f0c`). Since the delivery summary below was first posted, `main` advanced by 3 commits (including #3080); the branch was rebased cleanly with no conflicts and every gate was re-run after the rebase — the numbers below are from the rebased tree. Opening the pull request from this account is still rejected by the repository (retried today: GraphQL `CreatePullRequest` permission error; REST returns 404), while issue comments and pushes to my fork still work — so the branch and this summary stand in for the PR until creation works again; the maintainer can open the PR from that head. **What the branch contains** - `REA_GHIDRA_ENABLED_ANALYZERS`, validated by the same analyzer-name rules as the disabled list (shared schema in `src/config/ghidraAnalyzerNames.ts`; `src/config/ghidraEnabledAnalyzers.ts`), flowing through `parseConfig`/`AppConfig` into the Ghidra provider. - Analysis-profile recording beside the disabled list: `analyzer_preset` becomes `ghidra-default+enabled:…`, or `ghidra-default+disabled:…+enabled:…` when both lists are set, with an `enabled_analyzers` entry under `analysis_options`; session open re-checks the committed profile against the active environment exactly like the disabled list. - `ReaGhidraAnalysisOptions.java` now takes an explicit mode argument (`disable` or `enable`) plus the comma-separated names, validates every name against the program's registered analyzers, applies the option, re-reads the applied value, and aborts the import when the requested state did not stick — the same fail-hard shape as the disable path. The launcher emits one invocation per configured list after loader/seed preparation and before auto-analysis; every argument stays a non-empty single quotable token, so the Windows batch path is unchanged in safety. - `docs/installation.md` documents the new setting next to the disabled one. **Executed validation** (all commands run locally on the rebased branch, macOS arm64): - `npm run test:focused -- src/ghidra/GhidraEnabledAnalyzers.test.ts src/ghidra/GhidraDisabledAnalyzers.test.ts src/ghidra/GhidraProvider.behavior.test.ts tests/boundary/providers/ghidra/ghidraLauncher.test.ts` — 4 files, **113 passed, 4 skipped** (identical to the pre-rebase run). The new tests fail against the unmodified tree first: the config file's tests failed at import, 4 behavior tests failed (`records the enabled analyzers…`, `records enabled and disabled analyzers in one preset`, `hands the enabled analyzers to the launcher…`, `refuses a profile resolved without or with other enabled analyzers`), and 5 launcher tests failed (mode argument, ordering, empty-list omission, updated one-list expectations) — red before the implementation, green after. - `npm run test:focused -- src/config.test.ts src/cliHostInputs.test.ts src/ghidra/GhidraAnalysisSeeds.test.ts src/ghidra/extensions/GhidraExtensions.test.ts src/ghidra/GhidraSnapshotCache.test.ts` — 5 files, **112 passed**. Correction to my earlier summary: that run had said "6 files, 122 passed"; re-running the listed 5-file set today gives 112 both on the rebased tree and on the original pre-rebase commit `38dda553` (verified by detached checkout), so the earlier 122 came from the extra sixth file, not from these five. - `npm run test:focused -- tests/boundary/mcp/officialArguments.test.ts tests/boundary/mcp/officialTools.test.ts tests/boundary/filesystem/ghidraPspExtension.test.ts` — 3 files, **52 passed**. - `npx tsc --noEmit` — clean. `npm run format:check` — pass. `npm run lint` — 0 errors. `npx knip` — clean. `npm run jscpd` — 3.21%, under the 3.5% threshold. `npm run docs:check` — pass. **Not claimed:** the printf variadic-argument effect itself. I have no local Ghidra, so the branch delivers and validates the opt-in mechanism and its fail-hard import validation; observing `Variadic Function Signature Override` recover the stack-passed variadic argument in `procedure_pseudo_code` needs your Ghidra 12.1.4 environment (`REA_GHIDRA_ENABLED_ANALYZERS="Variadic Function Signature Override"` on the Apple arm64 sample from the issue).
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Branch rebased onto current main (767fe0f0c, includes #3080) — all gates re-run green on the rebased tree; details updated in the delivery summary above. PR creation retried just now and still rejected for this account (GraphQL CreatePullRequest permission error; REST 404), so the PR could not be opened from here: akushonkamen:fix/ghidra-enabled-analyzers at d51afe0dd is ready for a maintainer to open against main (Closes #3083).

<!-- gh-comment-id:6161139645 --> @akushonkamen commented on GitHub (Oct 14, 2026): Branch rebased onto current `main` (`767fe0f0c`, includes #3080) — all gates re-run green on the rebased tree; details updated in the delivery summary above. PR creation retried just now and still rejected for this account (GraphQL `CreatePullRequest` permission error; REST 404), so the PR could not be opened from here: `akushonkamen:fix/ghidra-enabled-analyzers` at `d51afe0dd` is ready for a maintainer to open against `main` (Closes #3083).
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#1079
No description provided.