Repository navigation
fix(ghidra): reject uncommitted handshake reports before the first query - #8630
Merged
Merged
Conversation
The handshake check ran only inside startSession, which a query reached only when the profile committed a startup input, for PE or for an existing project. Any other session started lazily inside callTool, so analysis seeds, disabled analyzers, extension results, MIPS soft-float storage or a RISC-V calling convention that the profile did not commit were never checked and the query returned its result. Every query now runs the checked start first. The client shares one startup attempt, so there is still one launch, and a provider restarted within the session is checked again. That leaves startSession as the only start in the provider client, so the PE data-type path reuses its session info. Recovery guidance is unchanged: the former eager-start condition now selects only whether a completed handshake counts as a running provider. Fixes #8612 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
[Medium impact] The PR appears safe to merge; no blocking issues were found. SummaryEvery Ghidra query now checks the handshake before dispatch. The PE data-type path also reuses the checked session information instead of calling
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Query] --> B[Check operation and input]
B --> C[Start or reuse shared startup]
C --> D{Handshake reports match profile?}
D -->|Yes| E[Dispatch query]
D -->|No| F[Close owned provider]
F --> G[Return retained failure]
G --> H[Reopen retires rejected client]
Reviews (2) · Last reviewed commit: "Merge origin/main into fix/8612-ghidra-h..." · Reviewed by Greptile |
coyoteXujie
pushed a commit
to coyoteXujie/rea
that referenced
this pull request
Nov 2, 2026
…ery (morluto#8630) The handshake check ran only inside startSession, which a query reached only when the profile committed a startup input, for PE or for an existing project. Any other session started lazily inside callTool, so analysis seeds, disabled analyzers, extension results, MIPS soft-float storage or a RISC-V calling convention that the profile did not commit were never checked and the query returned its result. Every query now runs the checked start first. The client shares one startup attempt, so there is still one launch, and a provider restarted within the session is checked again. That leaves startSession as the only start in the provider client, so the PE data-type path reuses its session info. Recovery guidance is unchanged: the former eager-start condition now selects only whether a completed handshake counts as a running provider. Fixes morluto#8612 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
6 of 12 tasks
Entrpi
added a commit
to Entrpi/rea
that referenced
this pull request
Nov 4, 2026
main's morluto#8630 starts every Ghidra query's session and checks its handshake first, which supersedes this branch's eager start of every RISC-V session. Take main's start and drop the branch's riscv term. The condition main kept selects recovery guidance only; it now also counts a committed load preparation, a startup input like the calling-convention policy. A double-float image can have the first without the second.
7 of 22 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke: A Ghidra session whose first query was not
health, on a target with no eager-start condition, started the bridge insidecallTool, so a handshake report the profile did not commit was never checked and the query returned its result.The fix: Every query now runs the existing checked start first, so the session fails closed with
ProviderAdapterErrorand closes before any query runs; there is still one launch.The risk: Low. Recovery guidance is deliberately unchanged, and the first query now waits for the handshake in the provider client instead of inside
callTool, through the same shared startup attempt. Not run against real Ghidra.Summary
Fixes #8612.
checkSessionObservationsrejects handshake reports the analysis profile did not commit, but onlystartSessionruns it, and a query reachedstartSessiononly when an eager-start condition held. Any other session now gets the same check before its first query.Problem and expected behavior
Trigger: a plain x86_64 ELF with no committed seeds, disabled analyzers or extensions, whose handshake carries one of those reports, and whose first query is, for example,
list_procedures.Violated invariant: each report binds in both directions, so a session that receives a report its profile did not commit fails closed before it returns any analysis result, whatever the first operation is.
This also reached mutating operations: on the base revision
set_address_namewas dispatched to such a session.Change and scope
All production changes are in
src/ghidra/GhidraProviderClient.ts:executerunsstartSessionbefore every query. The real client shares one startup attempt, socallToolreuses that launch. Because the check runs per query, a provider restarted within the session is checked again.startSessionis now the onlyclient.startcall in the provider client. The PEinspect_native_data_typepath reuses the session info it already holds instead of starting again unchecked.Paths and reports considered
execute, non-health, no eager termexecute, rawclient.starton the PE data-type pathexecuteIfReadyopen_binaryretires the rejected client; its replacement launches and is checked.The five reports
checkSessionObservationsbinds (analysis seeds, disabled analyzers, extension results, MIPS soft-float storage, RISC-V calling convention) are all tested. The PDB report is not profile-bound, so it is unchanged.Decision for maintainers: recovery guidance
The issue proposes the same fix but notes that it would change recovery guidance, and asks for a maintainer scope decision before implementation. No maintainer has replied. This PR avoids that change rather than deciding it:
handshakeEstablishesProvider, and selects only whether a completed handshake counts as a running provider.start.Removing that condition would give one rule, but it would also put "Call open_binary" in front of one-shot CLI runs on ELF and Mach-O targets.
exitedProviderMessagehas no caller-lifetime split, and PE one-shot runs already see this text (both source-derived). That is a separate decision, so I left it out. If you want one rule, it means deleting the condition and updating those two tests, and probably a lifetime split for that message.Not included
ghidraProjectHistoryEvidencerecords them as historical project reports rather than outcomes of this launch. The issue also excludes these sessions from its scope. This is source-derived; I did not add a test for it.executeIfReadybetween handshake settlement and the check, andcallTool's own internalstart, which aclose()landing just after the check could relaunch. Both are runs of microtasks with no macrotask in them (source-derived) and both already exist on eager-start sessions, so the complexity is not justified.riscveager-start term for its ownelf_load_preparationreport. This PR removes the need for a new eager-start term, but the two will conflict textually at that condition.Contribution scope: ordinary fix within existing boundaries; the required maintainer scope decision for new capabilities is not applicable. The reporter's request for a decision is covered above and is why "Material scope" below stays unticked.
Contract and boundary impact
createGhidraProviderClientowns the handshake check; the changed stage isexecute, before dispatch. The rawclient.starton the PE data-type path is removed as superseded.handshakeEstablishesProvideris retained for recovery guidance only.execution_failurefrom its first query instead of a result. Both adapters reach this through the provider client; MCP is tested here and the CLI path is source-derived.client.close()path, and failed cleanup stays retained and retryable.docs/public/product-catalog.json), package, or installation impact: none.Expected effects (check known effects; use documentation-only on its own).
Unchecked items do not establish absence; leave uncertainties visible.
(These are the existing Ghidra session effects, including the bridge scripts; this PR adds none. The one launch is unchanged, and a rejected session now runs no query.)
Evidence and regression coverage
src/ghidra/GhidraProvider.behavior.test.ts: 5 reports x 7 first operations (health,list_documents,list_procedures,list_strings,inspect_native_load_image,procedure_assembly,set_address_name), each pinningstarts: 1, calls: 0, closes: 1and the reason.executeIfReady; a reopened client; a restart within the session; concurrent callers on one shared startup; failed rejection cleanup retried on close; refused input starts nothing; caller cancellation reaches the checked start.health, both give reopen guidance. These pass on base and with the fix, by design.tests/boundary/mcp/ghidraReopen.test.ts: public MCP workflow with the production client, a real child and a real socket, using the simulated bridge. Covers rejection before the first query, no query reaching the bridge, release of the child and runtime root, retry, reopen, and retained failed cleanup.tests/boundary/mcp/ghidraStartupCancellationMcp.test.tsobserves callers instart, where they now wait; "commits exact provider..." pinsstartCount1 (it was 0); three fakes stubbedstartto fail and now succeed.tests/fixtures/fakeGhidra.mjstakes optionalhandshakeReports.GhidraProviderClient.tsreverted to8250bf232): 42 failed, 203 passed acrossGhidraProvider.behavior.test.ts,ghidraReopen.test.tsandghidraStartupCancellationMcp.test.ts. All fivehealthcases pass; the non-healthcases fail because the query ran. The two MCP regressions fail because the query returned a normal result, and the two cancellation cases fail because callers never reachstart.list_stringsas the first query):{"error":{"code":"execution_failure","retryable":false,"details":{"provider_id":"ghidra","operation":"list_strings","diagnostics":{"reason":"The Ghidra session reported analysis seeds that the profile did not commit."}}}}messageandremediationare omitted here; they carry the existing genericrea doctortext.)list_stringson the production client against the simulated bridge, so this says nothing about real Ghidra's cost): 1 launch on both; cold first query 60 ms on base and 59-60 ms with the fix; warm 45-48 µs per query on base and 45-46 µs with the fix; test-process CPU 55-67 µs and 56-57 µs per query; response 3,078 bytes on both. Agent tokens and process-family peak memory were not measured; the response size is unchanged.NOT_ENOUGH_INFO. The MCP tests skip on Windows and did not run there. There is no CLI-level regression test.Boundary and lifecycle regressions covered: callers sharing one handshake, cancellation during startup, a rejected session's cleanup and its retry, restart within a session, and reopen. Cancellation during the rejection's own
close()is not separately tested;close()takes no signal and this path is shared with eager-start sessions.For evidence-bearing changes:
Validation performed
Run on macOS arm64, Node.js 24.22.1, on the commit in this PR.
npx vitest run src/ghidra tests/boundary/mcp/ghidra tests/boundary/providers/ghidra tests/boundary/providers/startupCloseWaiters tests/boundary/filesystem/ghidra tests/process-global/ghidra src/providers src/application/binary/CompositeProvider— 78 files passed; 1600 tests passed, 3 skipped.npm run check:fast— passed.npm run format:check— passed.npm run docs:checkandnpm run metadata:check— not run; no docs or catalog changed.Compatibility, safety, and release
Author readiness
Human owner: @kaoru0822-kitauji
Declarations entered by Claude Code on the owner's standing written instruction.
Before merge
type(scope): outcome.🤖 Generated with Claude Code