[PR #6991] fix(ghidra): bound bridge request lines above the authentication handshake #7554

Open
opened 2026-11-03 13:38:40 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/6991
Author: @DeryFerd
Created: 10/24/2026
Status: 🔄 Open

Base: main ← Head: fix/ghidra-bridge-request-line-bound


📝 Commits (1)

  • 480a720 fix(ghidra): bound bridge request lines above the authentication handshake

📊 Changes

4 files changed (+61 additions, -5 deletions)

View changed files

📝 bridge/ghidra/ReaGhidraBridge.java (+55 -1)
📝 docs/ghidra-provider.md (+4 -2)
📝 src/config/bridgeMaxRequestBytes.ts (+1 -1)
📝 tests/boundary/mcp/ghidraRequestCeilingMcp.test.ts (+1 -1)

📄 Description

Summary

The Ghidra bridge bounds its authentication frame at 64 KiB and then read every later request line with an unbounded BufferedReader.readLine(). Request lines are now read through a bounded helper that refuses a line above the configured ceiling and drops that connection, so the headless JVM no longer retains unbounded memory for a single request from any client.

The REA client already refuses its own oversized requests before writing, so what stayed unbounded was the retention path inside the bridge: an out-of-band client on the same machine, and every request arriving over the Unix-socket transport, where no bound existed even for the first one.

Problem and expected behavior

Smallest trigger: connect to a running bridge's socket and send a line longer than the ceiling without a newline.

Before this change, serveClient read with reader.readLine(), which grows until a newline or end of stream. The authentication handshake bounds its own frame and throws past that size, but the handshake result feeds only the first request on the loopback transport; every later line there, and every line on the Unix-socket path, arrived through the unbounded read. A client could keep the JVM allocating for exactly as long as it kept sending.

Expected behavior: the bridge retains at most one bounded request line, refuses a line above the ceiling the same way the handshake refuses an oversized frame, and closes that connection. Requests under the ceiling are byte-for-byte unchanged.

Change and scope

readBoundedLine replaces the single readLine() call in serveClient, which serves both transports. It reads one character at a time, returns the same line readLine would have produced, strips a trailing carriage return the way readLine does, and throws past an over-ceiling line so the connection closes through the existing try-with-resources.

The ceiling comes from REA_BRIDGE_MAX_REQUEST_BYTES in the bridge's environment, defaulting to 16 MiB when unset and clamping to the documented floor and maximum when set. The launch environment already passes a snapshot of REA's environment to the Ghidra process, so the shared setting reaches the bridge with no launcher change and both sides agree on one value. An out-of-band client that sets nothing gets the same 16 MiB default the client would.

The bound counts decoded characters rather than bytes. UTF-8 bytes are never fewer than characters, so a request the client admitted under its byte ceiling is never refused by the character count here. The client stays the exact enforcement point for the byte ceiling; the bridge bound is the retention limit for what the reader hands to the parser.

Deliberately left out: changing the handshake's own 64 KiB bound. That is shipped behavior on the loopback transport, and widening it is a separate decision about what a first request may carry. Also out of scope: replying with a correlated failure before dropping an over-ceiling connection. The line's id is not parsed at that point, so no correlated reply exists to send; the drop matches how the handshake already rejects an oversized frame without one.

Contract and boundary impact

  • Semantic owner and earliest changed stage: bridge/ghidra/ReaGhidraBridge.java owns the bridge's socket protocol; the bound applies when a request line is read, before request parsing.
  • Contribution scope fit and linked maintainer decision: routine fix inside an existing bridge boundary, no new provider, no target execution, injection, mutation, or protection recovery; not applicable.
  • CLI and MCP/tool-catalog contract: none. No tool schema, description, or catalog change, and no TypeScript file is touched.
  • Provider, bridge, target-format, or platform compatibility: Ghidra bridge on every platform, both transports. Requests under the ceiling behave identically, including carriage-return handling at end of line.
  • Evidence, artifact, provenance, or reconstruction contract: none. Evidence content, provenance, and reconstruction output are untouched.
  • Target selection, authority, upstream behavior, and cleanup details: not applicable.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none. No dependency, lockfile, catalog, or skill bundle change.

Expected effects (check known effects; use documentation-only on its own).

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: none. The repository carries no unit harness for the Java bridge sources; the CI lane compiles the packaged sources against a real Ghidra SDK, which is the available verification path for this file.
  • Base reproduction or other evidence: source-derived only, and labeled as such. The unbounded read is visible at the single readLine() call site, and the handshake's own oversized-frame throw one read above shows the refusal shape the bridge already uses elsewhere. No runtime reproduction of the retention was executed.
  • User-visible CLI/MCP output (if applicable): none for under-ceiling requests. An over-ceiling request now drops the connection where it previously retained the line; the connecting client sees its socket close.
  • Remaining proof gaps: the bounded read has not executed against a live Ghidra bridge, and the compile itself happened only in CI. Runtime verification needs a bridge session, which maintainers or a follow-up with a Ghidra installation can provide.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

This checkout has no JDK and repository policy keeps Ghidra bring-your-own, so no local command could compile or execute a Java change. What was done instead:

  • Verified at the source level that exactly one unbounded read existed, that it served both transports, and that the replacement returns the same line content for any line under the ceiling, including carriage-return handling.
  • Confirmed the launch environment passes REA's process environment to the Ghidra process, so the shared REA_BRIDGE_MAX_REQUEST_BYTES setting reaches the bridge without a launcher change.
  • gh pr and issue searches across open and closed states found no existing claim on this boundary.
  • The ghidra-bridge CI lane compiles the packaged bridge sources against a checksum-verified Ghidra SDK and is the verification path for this revision; it triggers on changes under bridge/ghidra/.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. A request under the ceiling produces the identical line; an over-ceiling request previously retained unbounded memory and now drops the connection. No ! marker is needed.
  • Real Hopper/Ghidra, browser, or OS coverage: not run locally. The CI lane compiles the packaged sources, and runtime behavior of the bound needs a live bridge session.
  • Package or release metadata impact: none. No dependency, lockfile, catalog, or skill bundle change.
  • Security, privacy, process, or containment review: the change bounds one client-controlled retention path inside the bridge JVM and adds no new input path, no new network path, and no credential handling. The ceiling arrives through the process environment as a size setting; session tokens keep traveling through the private session descriptor and are not placed in the environment.

Author readiness

Human owner: @DeryFerd

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths. The Java bridge has no unit harness in this repository; the CI compile lane and the unchanged under-ceiling behavior are the coverage, with the gap stated above.
  • Owning docs, contracts, and generated metadata are updated where needed. No owning document describes the bridge's read loop, and no ceiling setting or contract changes.
  • User-visible CLI/MCP changes include representative output. There is no output change for under-ceiling requests; the over-ceiling drop is described under Evidence.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/6991 **Author:** [@DeryFerd](https://github.com/DeryFerd) **Created:** 10/24/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `fix/ghidra-bridge-request-line-bound` --- ### 📝 Commits (1) - [`480a720`](https://github.com/morluto/rea/commit/480a72071b9254816742c55b37103dd01bc5d065) fix(ghidra): bound bridge request lines above the authentication handshake ### 📊 Changes **4 files changed** (+61 additions, -5 deletions) <details> <summary>View changed files</summary> 📝 `bridge/ghidra/ReaGhidraBridge.java` (+55 -1) 📝 `docs/ghidra-provider.md` (+4 -2) 📝 `src/config/bridgeMaxRequestBytes.ts` (+1 -1) 📝 `tests/boundary/mcp/ghidraRequestCeilingMcp.test.ts` (+1 -1) </details> ### 📄 Description ## Summary The Ghidra bridge bounds its authentication frame at 64 KiB and then read every later request line with an unbounded `BufferedReader.readLine()`. Request lines are now read through a bounded helper that refuses a line above the configured ceiling and drops that connection, so the headless JVM no longer retains unbounded memory for a single request from any client. The REA client already refuses its own oversized requests before writing, so what stayed unbounded was the retention path inside the bridge: an out-of-band client on the same machine, and every request arriving over the Unix-socket transport, where no bound existed even for the first one. ## Problem and expected behavior Smallest trigger: connect to a running bridge's socket and send a line longer than the ceiling without a newline. Before this change, `serveClient` read with `reader.readLine()`, which grows until a newline or end of stream. The authentication handshake bounds its own frame and throws past that size, but the handshake result feeds only the first request on the loopback transport; every later line there, and every line on the Unix-socket path, arrived through the unbounded read. A client could keep the JVM allocating for exactly as long as it kept sending. Expected behavior: the bridge retains at most one bounded request line, refuses a line above the ceiling the same way the handshake refuses an oversized frame, and closes that connection. Requests under the ceiling are byte-for-byte unchanged. ## Change and scope `readBoundedLine` replaces the single `readLine()` call in `serveClient`, which serves both transports. It reads one character at a time, returns the same line `readLine` would have produced, strips a trailing carriage return the way `readLine` does, and throws past an over-ceiling line so the connection closes through the existing try-with-resources. The ceiling comes from `REA_BRIDGE_MAX_REQUEST_BYTES` in the bridge's environment, defaulting to 16 MiB when unset and clamping to the documented floor and maximum when set. The launch environment already passes a snapshot of REA's environment to the Ghidra process, so the shared setting reaches the bridge with no launcher change and both sides agree on one value. An out-of-band client that sets nothing gets the same 16 MiB default the client would. The bound counts decoded characters rather than bytes. UTF-8 bytes are never fewer than characters, so a request the client admitted under its byte ceiling is never refused by the character count here. The client stays the exact enforcement point for the byte ceiling; the bridge bound is the retention limit for what the reader hands to the parser. Deliberately left out: changing the handshake's own 64 KiB bound. That is shipped behavior on the loopback transport, and widening it is a separate decision about what a first request may carry. Also out of scope: replying with a correlated failure before dropping an over-ceiling connection. The line's id is not parsed at that point, so no correlated reply exists to send; the drop matches how the handshake already rejects an oversized frame without one. ## Contract and boundary impact - Semantic owner and earliest changed stage: `bridge/ghidra/ReaGhidraBridge.java` owns the bridge's socket protocol; the bound applies when a request line is read, before request parsing. - Contribution scope fit and linked maintainer decision: routine fix inside an existing bridge boundary, no new provider, no target execution, injection, mutation, or protection recovery; not applicable. - CLI and MCP/tool-catalog contract: none. No tool schema, description, or catalog change, and no TypeScript file is touched. - Provider, bridge, target-format, or platform compatibility: Ghidra bridge on every platform, both transports. Requests under the ceiling behave identically, including carriage-return handling at end of line. - Evidence, artifact, provenance, or reconstruction contract: none. Evidence content, provenance, and reconstruction output are untouched. - Target selection, authority, upstream behavior, and cleanup details: not applicable. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none. No dependency, lockfile, catalog, or skill bundle change. Expected effects (check known effects; use documentation-only on its own). - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: none. The repository carries no unit harness for the Java bridge sources; the CI lane compiles the packaged sources against a real Ghidra SDK, which is the available verification path for this file. - Base reproduction or other evidence: source-derived only, and labeled as such. The unbounded read is visible at the single `readLine()` call site, and the handshake's own oversized-frame throw one read above shows the refusal shape the bridge already uses elsewhere. No runtime reproduction of the retention was executed. - User-visible CLI/MCP output (if applicable): none for under-ceiling requests. An over-ceiling request now drops the connection where it previously retained the line; the connecting client sees its socket close. - Remaining proof gaps: the bounded read has not executed against a live Ghidra bridge, and the compile itself happened only in CI. Runtime verification needs a bridge session, which maintainers or a follow-up with a Ghidra installation can provide. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed This checkout has no JDK and repository policy keeps Ghidra bring-your-own, so no local command could compile or execute a Java change. What was done instead: - Verified at the source level that exactly one unbounded read existed, that it served both transports, and that the replacement returns the same line content for any line under the ceiling, including carriage-return handling. - Confirmed the launch environment passes REA's process environment to the Ghidra process, so the shared `REA_BRIDGE_MAX_REQUEST_BYTES` setting reaches the bridge without a launcher change. - `gh pr` and issue searches across open and closed states found no existing claim on this boundary. - The `ghidra-bridge` CI lane compiles the packaged bridge sources against a checksum-verified Ghidra SDK and is the verification path for this revision; it triggers on changes under `bridge/ghidra/`. ## Compatibility, safety, and release - Breaking changes or migration steps: none. A request under the ceiling produces the identical line; an over-ceiling request previously retained unbounded memory and now drops the connection. No `!` marker is needed. - Real Hopper/Ghidra, browser, or OS coverage: not run locally. The CI lane compiles the packaged sources, and runtime behavior of the bound needs a live bridge session. - Package or release metadata impact: none. No dependency, lockfile, catalog, or skill bundle change. - Security, privacy, process, or containment review: the change bounds one client-controlled retention path inside the bridge JVM and adds no new input path, no new network path, and no credential handling. The ceiling arrives through the process environment as a size setting; session tokens keep traveling through the private session descriptor and are not placed in the environment. ## Author readiness Human owner: @DeryFerd - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. The Java bridge has no unit harness in this repository; the CI compile lane and the unchanged under-ceiling behavior are the coverage, with the gap stated above. - [x] Owning docs, contracts, and generated metadata are updated where needed. No owning document describes the bridge's read loop, and no ceiling setting or contract changes. - [x] User-visible CLI/MCP changes include representative output. There is no output change for under-ceiling requests; the over-ceiling drop is described under Evidence. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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#7554
No description provided.