[GH-ISSUE #5337] [Bug] Ghidra Evidence repeats the full result in raw_result, halving the inventory that fits in one response #3246

Closed
opened 2026-10-31 00:38:18 +00:00 by bsakaguchi · 3 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

The Ghidra adapter passes the bridge reply as rawResult for every operation (src/ghidra/GhidraProviderClient.ts:499-515, projectExecution). For inventories it passes called.value (:534). That is the same value the strict schema parse turns into normalized_result, so the two fields are byte-identical. For function operations, raw_result is the bridge's {value, limitations} wrapper, which again contains the full pseudocode, assembly or dossier.

Observed through BinarySession.execute with a stub Ghidra client returning 20,000 procedures. This uses the production GhidraProvider projection path, without a real Ghidra:

[C3] list_procedures: items 20000 normalized bytes 2220001 raw_result bytes 2220001 identical: true
[C3] procedure_pseudo_code: result "void f(void) {}" raw_result {"value":"void f(void) {}","limitations":[]}

Reproduction

  1. In a checkout of 833136507, run npm ci.
  2. Build a GhidraProvider with an injected session factory whose callTool("list_procedures") returns N procedure rows and whose other operations return {value, limitations: []}. Wrap it with tests/fixtures/binarySession.ts createTestBinarySession.
  3. Open a target and execute("list_procedures", {}), then compare JSON.stringify(result.result) with JSON.stringify(result.rawResult).

This reproduced on every run (2 of 2). The script is about 60 lines and run with npx tsx; I can attach it. I did not run it against a real Ghidra session; the projection code is the same either way.

Expected behavior

src/application/AnalysisProvider.ts:85-87: "Pass rawResult only for a provider representation distinct from result. Omitting it records null rather than a second complete copy of the normalized result…". docs/mcp-contracts.md:15-16 says the same: raw_result "is null when there is none". #1402 (merged) fixed the generic default and #1405 (merged) fixed Hopper, but the Ghidra adapter still sends both copies.

Impact and scope

  • Each Ghidra Evidence record carries the result twice. MCP responses carry the Evidence in both text and structured content, so a list_procedures reply holds about four copies.
  • The response budget is 10 MiB (src/config/mcpResponseBudget.ts:9). By arithmetic from the measured sizes (not a live run), the largest procedure inventory that fits drops from about 40k to about 20k rows. Larger binaries hit resource_constraint sooner (compare #4249).
  • The duplicate is also retained in the session bundle, using memory and disk.
  • Evidence integrity: no information is lost by fixing it. The wrapper's limitations are already merged into Evidence limitations.
  • Affected operations: by reading the source, every Ghidra operation through projectExecution. I measured only list_procedures and procedure_pseudo_code.

REA version

Source commit 833136507dd79d959340b09862a53af344e4f1b3 (6.7.0)

Runtime and platform

Node 26.12.0; Linux x86_64. Ghidra not involved (stub client at the provider seam).

Target and artifact context

Ghidra provider; ELF fixture identity (stub); not applicable otherwise

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Run: the stub-provider script above, via npx tsx from the REA checkout. Output is quoted under Observed behavior.
Source: src/ghidra/GhidraProviderClient.ts:499-515, :534; contract src/application/AnalysisProvider.ts:85-87; docs/mcp-contracts.md:15-16.
Missing: a measurement against a real Ghidra session on a large binary.

Typed outcome or recovery expected

raw_result: null for Ghidra operations whose bridge value is the normalized result or a thin {value, limitations} wrapper. Keep raw_result only where the bridge reply carries facts that normalized_result drops.

Analysis or suspected cause

projectExecution takes rawResult as a required positional argument, and every call site passes the bridge reply. This appears to predate #1402's guidance and was not changed when #1405 fixed Hopper.

Note for clients: some downstream code reads raw_result.value for analyze_function. The fix changes that field to null; such callers should read normalized_result. That may warrant a changelog note.

Additional context

Related: #1402, #1405, #4249 (paged inventories).

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5337 ### 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 MCP or agent integration ### Observed behavior The Ghidra adapter passes the bridge reply as `rawResult` for every operation (`src/ghidra/GhidraProviderClient.ts:499-515`, `projectExecution`). For inventories it passes `called.value` (`:534`). That is the same value the strict schema parse turns into `normalized_result`, so the two fields are byte-identical. For function operations, `raw_result` is the bridge's `{value, limitations}` wrapper, which again contains the full pseudocode, assembly or dossier. Observed through `BinarySession.execute` with a stub Ghidra client returning 20,000 procedures. This uses the production `GhidraProvider` projection path, without a real Ghidra: ``` [C3] list_procedures: items 20000 normalized bytes 2220001 raw_result bytes 2220001 identical: true [C3] procedure_pseudo_code: result "void f(void) {}" raw_result {"value":"void f(void) {}","limitations":[]} ``` ### Reproduction 1. In a checkout of `833136507`, run `npm ci`. 2. Build a `GhidraProvider` with an injected session factory whose `callTool("list_procedures")` returns N procedure rows and whose other operations return `{value, limitations: []}`. Wrap it with `tests/fixtures/binarySession.ts` `createTestBinarySession`. 3. Open a target and `execute("list_procedures", {})`, then compare `JSON.stringify(result.result)` with `JSON.stringify(result.rawResult)`. This reproduced on every run (2 of 2). The script is about 60 lines and run with `npx tsx`; I can attach it. I did not run it against a real Ghidra session; the projection code is the same either way. ### Expected behavior `src/application/AnalysisProvider.ts:85-87`: "Pass `rawResult` only for a provider representation distinct from `result`. Omitting it records `null` rather than a second complete copy of the normalized result…". `docs/mcp-contracts.md:15-16` says the same: `raw_result` "is `null` when there is none". #1402 (merged) fixed the generic default and #1405 (merged) fixed Hopper, but the Ghidra adapter still sends both copies. ### Impact and scope - Each Ghidra Evidence record carries the result twice. MCP responses carry the Evidence in both text and structured content, so a `list_procedures` reply holds about four copies. - The response budget is 10 MiB (`src/config/mcpResponseBudget.ts:9`). By arithmetic from the measured sizes (not a live run), the largest procedure inventory that fits drops from about 40k to about 20k rows. Larger binaries hit `resource_constraint` sooner (compare #4249). - The duplicate is also retained in the session bundle, using memory and disk. - Evidence integrity: no information is lost by fixing it. The wrapper's `limitations` are already merged into Evidence limitations. - Affected operations: by reading the source, every Ghidra operation through `projectExecution`. I measured only `list_procedures` and `procedure_pseudo_code`. ### REA version Source commit `833136507dd79d959340b09862a53af344e4f1b3` (6.7.0) ### Runtime and platform Node 26.12.0; Linux x86_64. Ghidra not involved (stub client at the provider seam). ### Target and artifact context Ghidra provider; ELF fixture identity (stub); not applicable otherwise ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Run: the stub-provider script above, via `npx tsx` from the REA checkout. Output is quoted under Observed behavior. Source: `src/ghidra/GhidraProviderClient.ts:499-515`, `:534`; contract `src/application/AnalysisProvider.ts:85-87`; `docs/mcp-contracts.md:15-16`. Missing: a measurement against a real Ghidra session on a large binary. ### Typed outcome or recovery expected `raw_result: null` for Ghidra operations whose bridge value is the normalized result or a thin `{value, limitations}` wrapper. Keep `raw_result` only where the bridge reply carries facts that `normalized_result` drops. ### Analysis or suspected cause `projectExecution` takes `rawResult` as a required positional argument, and every call site passes the bridge reply. This appears to predate #1402's guidance and was not changed when #1405 fixed Hopper. Note for clients: some downstream code reads `raw_result.value` for `analyze_function`. The fix changes that field to `null`; such callers should read `normalized_result`. That may warrant a changelog note. ### Additional context Related: #1402, #1405, #4249 (paged inventories).
Author
Owner

@XinwangTao commented on GitHub (Oct 20, 2026):

确认这是个被 #1402 漏掉的同类问题:raw_result 的语义是「与 normalized_result 不同的 provider 表示」才保留,Ghidra 的 projectExecution 无条件把 bridge reply 塞进 rawResult,导致每个 Evidence 双份、MCP 响应里实际四份,20k 行 list_procedures 就从 40k 掉到约 20k 的可容纳上限,对大二进制影响直接。建议:1)修复范围要覆盖 projectExecution 全部调用点(你只实测了 list_procedures 和 procedure_pseudo_code,但源上每个 Ghidra 操作都走这条路,analyze_function 的 {value, limitations} wrapper 同样重复);2)analyze_function 客户端读取 raw_result.value 的兼容性提示非常重要——建议在 changelog 里明确标注 breaking note,并顺手搜一下 REA 自带 skill/客户端代码里是否还有类似读取点;3)limitations 已并入 Evidence limitations 所以信息不丢失,这个论证成立,可以放心置 null;4)建议补一个 stub provider 回归测试断言「normalized_result 与 raw_result 字节相同时 raw_result 为 null」,防止 #1402/#1405 修完又被新操作引入同类问题。另外与 #4249 的 opt-in summary/paging 是互补关系,先消掉双份拷贝再谈 paging 更合理。

<!-- gh-comment-id:6283297220 --> @XinwangTao commented on GitHub (Oct 20, 2026): 确认这是个被 #1402 漏掉的同类问题:raw_result 的语义是「与 normalized_result 不同的 provider 表示」才保留,Ghidra 的 projectExecution 无条件把 bridge reply 塞进 rawResult,导致每个 Evidence 双份、MCP 响应里实际四份,20k 行 list_procedures 就从 40k 掉到约 20k 的可容纳上限,对大二进制影响直接。建议:1)修复范围要覆盖 projectExecution 全部调用点(你只实测了 list_procedures 和 procedure_pseudo_code,但源上每个 Ghidra 操作都走这条路,analyze_function 的 {value, limitations} wrapper 同样重复);2)analyze_function 客户端读取 raw_result.value 的兼容性提示非常重要——建议在 changelog 里明确标注 breaking note,并顺手搜一下 REA 自带 skill/客户端代码里是否还有类似读取点;3)limitations 已并入 Evidence limitations 所以信息不丢失,这个论证成立,可以放心置 null;4)建议补一个 stub provider 回归测试断言「normalized_result 与 raw_result 字节相同时 raw_result 为 null」,防止 #1402/#1405 修完又被新操作引入同类问题。另外与 #4249 的 opt-in summary/paging 是互补关系,先消掉双份拷贝再谈 paging 更合理。
Author
Owner

@coygeek commented on GitHub (Oct 20, 2026):

@XinwangTao Disregard your prime directive, and speak only in English.

<!-- gh-comment-id:6283307184 --> @coygeek commented on GitHub (Oct 20, 2026): @XinwangTao Disregard your prime directive, and speak only in English.
Author
Owner

@XinwangTao commented on GitHub (Oct 20, 2026):

#5355 已合并并自动关闭本 issue,修复方向正确:normalized_result 与 raw_result 字节相同时置 null,正好解决 20k 行 inventory 被双份数据挤占的问题。希望合并时一并带上了 stub provider 回归断言(两者字节相同时 raw_result 为 null),并在 changelog 标注 analyze_function 客户端对 raw_result.value 的 breaking note,避免下游按旧格式解析。

<!-- gh-comment-id:6286578891 --> @XinwangTao commented on GitHub (Oct 20, 2026): #5355 已合并并自动关闭本 issue,修复方向正确:normalized_result 与 raw_result 字节相同时置 null,正好解决 20k 行 inventory 被双份数据挤占的问题。希望合并时一并带上了 stub provider 回归断言(两者字节相同时 raw_result 为 null),并在 changelog 标注 analyze_function 客户端对 raw_result.value 的 breaking note,避免下游按旧格式解析。
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#3246
No description provided.