From 264862bf2e3612c9cb6fcfb861843cbba4e1893f Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:41:50 +0800 Subject: [PATCH] fix(ida): preserve registration recovery facts in readiness Fixes #5771. Preserve the configuration owner's parsed registration settings and remediation in IDA readiness, retain caller-selected paths, and distinguish configuration selection from validity and live connection. Verified with 84 relevant tests, CLI/MCP negative-case parity, static/docs gates, independent source review and repeated actual Codex recovery tasks. No real IDA engine/GUI analysis qualification. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) --- docs/ida-provider.md | 9 +++++ src/ida/IdaProvider.test.ts | 78 ++++++++++++++++++++++++++++++++++++- src/ida/IdaProvider.ts | 65 +++++++++++++++++++------------ 3 files changed, 126 insertions(+), 26 deletions(-) diff --git a/docs/ida-provider.md b/docs/ida-provider.md index f4a2c95dd6..ecaa2c66d8 100644 --- a/docs/ida-provider.md +++ b/docs/ida-provider.md @@ -41,6 +41,15 @@ rea function /absolute/path/to/program main --json rea decompile /absolute/path/to/program main --json ``` +If the registration cannot be read or validated, provider diagnostics retain its +selected `registration_path` and the configuration owner's setting/constraint +facts in `configuration_details.settings`, plus `configuration_remediation`. +`configured` means a path was selected, not that it is valid or an IDA connection +was established; failed registrations remain unavailable. Correct +`REA_IDA_MCP_CONFIG` in the next CLI invocation's environment or the MCP server's +launch environment. Restart/reconnect that server after changing its environment; +an export in another shell does not update a running server. + Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows. Attached profiles, including native mode, refuse universal Mach-O inputs at admission: every slice shares the recorded whole-file hash, so REA cannot verify which slice the GUI loaded. Open a thin file extracted with `lipo -thin` in both IDA and REA, or use headless mode, which analyzes the selected slice. REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched. diff --git a/src/ida/IdaProvider.test.ts b/src/ida/IdaProvider.test.ts index e0d5a2180e..9ce6d84045 100644 --- a/src/ida/IdaProvider.test.ts +++ b/src/ida/IdaProvider.test.ts @@ -1,8 +1,84 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { parseConfig } from "../config/parseConfig.js"; import { IdaProvider } from "./IdaProvider.js"; +it.each([ + { state: "unconfigured", selected: false, content: undefined }, + { state: "missing", selected: true, content: undefined }, + { state: "malformed", selected: true, content: "{" }, + { state: "invalid", selected: true, content: "{}" }, +])( + "preserves the owning registration setting and selection for $state IDA readiness", + async ({ selected, content }) => { + const root = await createTestTempDirectory("rea-ida-recovery-"); + const path = join(root, "registration.json"); + if (content !== undefined) await writeFile(path, content); + const config = parseConfig(selected ? { REA_IDA_MCP_CONFIG: path } : {}); + if (!config.ok) throw config.error; + const connect = vi.fn(() => { + throw new Error("Readiness must not connect to IDA"); + }); + const provider = new IdaProvider(config.value, {}, connect); + try { + const snapshot = await provider.inspectReadinessSnapshot(); + expect(snapshot.availability).toMatchObject({ + status: "unavailable", + code: selected ? "open_options_invalid" : "not_configured", + reason: expect.stringContaining("REA_IDA_MCP_CONFIG"), + diagnostics: { + configured: selected, + registration_path: selected ? path : null, + live_connection_probed: false, + configuration_details: { + settings: [ + { setting: "REA_IDA_MCP_CONFIG", constraint: expect.any(String) }, + ], + }, + configuration_remediation: + expect.stringContaining("REA_IDA_MCP_CONFIG"), + }, + }); + expect(connect).not.toHaveBeenCalled(); + } finally { + const closed = await provider.closeOwnedResources(); + if (!closed.ok) throw closed.error; + } + }, +); + +it("keeps a selected valid registration distinct from a live IDA connection", async () => { + const root = await createTestTempDirectory("rea-ida-configured-"); + const path = join(root, "registration.json"); + await writeFile( + path, + JSON.stringify({ mode: "attached", url: "http://127.0.0.1:13337/mcp" }), + ); + const config = parseConfig({ REA_IDA_MCP_CONFIG: path }); + if (!config.ok) throw config.error; + const connect = vi.fn(() => { + throw new Error("Readiness must not connect to IDA"); + }); + const provider = new IdaProvider(config.value, {}, connect); + try { + expect(await provider.inspectAvailability()).toMatchObject({ + status: "available", + diagnostics: { + configured: true, + registration_path: path, + live_connection_probed: false, + }, + }); + expect(connect).not.toHaveBeenCalled(); + } finally { + const closed = await provider.closeOwnedResources(); + if (!closed.ok) throw closed.error; + } +}); + describe("IDA provider target support", () => { it("declines headerless raw-binary targets", () => { const config = parseConfig({}); diff --git a/src/ida/IdaProvider.ts b/src/ida/IdaProvider.ts index f48dbccc58..88bc8a3b30 100644 --- a/src/ida/IdaProvider.ts +++ b/src/ida/IdaProvider.ts @@ -7,6 +7,7 @@ import { } from "../domain/binaryTargetTypes.js"; import { err, ok } from "../domain/result.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { AnalysisClient, AnalysisProviderBoundClientFactory, @@ -171,7 +172,31 @@ export class IdaProvider implements AnalysisProviderCandidate { config.value.protocol === "native", ) : this.capabilities(); - if (config.ok && config.value.mode === "headless") { + const registrationDiagnostics = { + configured: this.config.idaMcpConfigPath !== undefined, + registration_path: this.config.idaMcpConfigPath ?? null, + live_connection_probed: false, + }; + if (!config.ok) { + const failure = projectAnalysisError(config.error); + return { + availability: { + status: "unavailable", + code: + this.config.idaMcpConfigPath === undefined + ? "not_configured" + : "open_options_invalid", + reason: failure.message, + diagnostics: { + ...registrationDiagnostics, + configuration_details: failure.details ?? null, + configuration_remediation: failure.remediation.action, + }, + }, + capabilities, + }; + } + if (config.value.mode === "headless") { const privacy = privateRuntimeRootCapability(); if (!privacy.available) return { @@ -180,9 +205,8 @@ export class IdaProvider implements AnalysisProviderCandidate { code: privacy.code, reason: privacy.reason, diagnostics: { - configured: true, + ...registrationDiagnostics, mode: "headless", - live_connection_probed: false, private_workspace_available: false, windows_native_failure: { stage: privacy.failureStage, @@ -193,28 +217,19 @@ export class IdaProvider implements AnalysisProviderCandidate { capabilities, }; } - const availability: ProviderAvailability = config.ok - ? { - status: "available" as const, - code: null, - reason: null, - diagnostics: { - configured: true, - mode: config.value.mode, - transport: "command" in config.value ? "stdio" : "http", - live_connection_probed: false, - }, - } - : { - status: "unavailable" as const, - code: - this.config.idaMcpConfigPath === undefined - ? "not_configured" - : "open_options_invalid", - reason: config.error.message, - diagnostics: { configured: false, live_connection_probed: false }, - }; - return { availability, capabilities }; + return { + availability: { + status: "available" as const, + code: null, + reason: null, + diagnostics: { + ...registrationDiagnostics, + mode: config.value.mode, + transport: "command" in config.value ? "stdio" : "http", + }, + }, + capabilities, + }; } inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport { const diagnostics = {