[GH-ISSUE #5824] [Bug] Native IDA metadata timeouts lose the requested analysis operation #3520

Closed
opened 2026-10-31 00:41:26 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

Executed the public CLI command node scripts/rea.mjs decompile <fixture.elf> main --provider ida --json against a local simulated native attached MCP provider. Initialization and tool discovery succeeded. When the provider left resources/read for ida://idb/metadata unanswered, REA exited with provider_timeout but identified the failed operation as ida_mcp_read_resource in details.operation, recovery.operation, the message and recovery guidance. The accepted analysis operation was procedure_pseudo_code.

A control using the same registration/profile and a valid metadata response completed decompilation and returned operation: "procedure_pseudo_code". These are adapter/CLI observations with a fixture, not real IDA observations.

Reproduction

  1. Use the source revision below with its locked dependencies and already-built runtime. Use the repository-pinned Node.js 24.18.0 and npm 11.16.0. No build or source change is required beyond this temporary test.
  2. Save the following as src/ida/repro.test.ts. It uses the existing CLI and ELF fixtures, starts only a loopback HTTP server, runs a successful control and a stalled-resource case, and closes the server afterward. Set HOME, XDG_CONFIG_HOME and TMPDIR to separate directories inside a disposable local workspace before running; the CLI children also receive the suite's isolated workspace environment.
  3. Run npm run test:local -- src/ida/repro.test.ts. The same body reproduced the timeout case once and passed the success control once. The assertions below describe the observed defective attribution, so a passing test confirms the defect. Delete the test afterward.
import { createServer } from 'node:http';
import { createHash } from 'node:crypto';
import { expect } from 'vitest';
import { workspaceCliTest } from '../../tests/support/cli/workspaceCliFixture.js';
import { elf } from '../domain/binaryTarget.fixture.js';

workspaceCliTest.for(['success', 'timeout'] as const)(
  'native metadata attribution: %s',
  async (mode, { cli, workspace }) => {
    const bytes = elf(2, 1, 62);
    const digest = createHash('sha256').update(bytes).digest('hex');
    const methods: string[] = [];
    const calls: string[] = [];
    const tools = ['list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm', 'callees', 'xrefs_to'];
    const server = createServer(async (req, res) => {
      if (req.method !== 'POST') { res.writeHead(405).end(); return; }
      let body = '';
      for await (const chunk of req) body += String(chunk);
      const rpc = JSON.parse(body);
      methods.push(rpc.method);
      if (rpc.id === undefined) { res.writeHead(202).end(); return; }
      const send = (result: unknown) => res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result }));
      if (rpc.method === 'initialize') { send({ protocolVersion: rpc.params.protocolVersion, capabilities: { tools: {}, resources: {} }, serverInfo: { name: 'native-fixture', version: '1' } }); return; }
      if (rpc.method === 'tools/list') { send({ tools: tools.map(name => ({ name, inputSchema: { type: 'object', properties: {} } })) }); return; }
      if (rpc.method === 'resources/read') {
        expect(rpc.params.uri).toBe('ida://idb/metadata');
        if (mode === 'timeout') return;
        send({ contents: [{ uri: rpc.params.uri, mimeType: 'application/json', text: JSON.stringify({ path: 'target.i64', sha256: digest, base: '0x1000', module: 'target' }) }] }); return;
      }
      calls.push(rpc.params.name);
      const result = rpc.params.name === 'lookup_funcs'
        ? rpc.params.arguments.queries.map((query: string) => ({ query, fn: { addr: '0x1000', name: 'main', size: '0x20' }, error: null }))
        : { addr: '0x1000', ok: true, pseudocode: 'int main(void) { return 42; }', lines: 1 };
      send({ content: [{ type: 'text', text: JSON.stringify(result) }] });
    });
    await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
    try {
      const address = server.address();
      if (!address || typeof address === 'string') throw new Error('missing address');
      const registration = await workspace.write('ida.json', JSON.stringify({ mode: 'attached', protocol: 'native', url: `http://127.0.0.1:${address.port}/mcp`, timeoutMs: 500 }));
      const target = await workspace.write('target.elf', bytes);
      const result = await cli.run({ arguments: ['decompile', target, 'main', '--provider', 'ida', '--json'], environment: { ...workspace.environment, REA_IDA_MCP_CONFIG: registration, REA_LOG_LEVEL: 'silent' }, timeoutMs: 15000 });
      expect(result.stderr).toBe('');
      console.log(JSON.stringify({ mode, exitCode: result.exitCode, methods, calls, output: result.json }));
      if (mode === 'success') {
        expect(result.exitCode).toBe(0);
        expect(result.json).toMatchObject({ operation: 'procedure_pseudo_code', normalized_result: 'int main(void) { return 42; }' });
        expect(calls).toContain('decompile');
      } else {
        expect(result.exitCode).toBe(1);
        expect(calls).toEqual([]);
        expect(methods).toContain('tools/list');
        expect(methods).toContain('resources/read');
        expect(result.json).toMatchObject({ code: 'provider_timeout', retryable: true, details: { operation: 'ida_mcp_read_resource', timeout_ms: 500 }, recovery: { kind: 'inspect_timeout_state', operation: 'ida_mcp_read_resource', timeout_ms: 500 } });
      }
    } finally {
      server.closeAllConnections();
      await new Promise<void>(resolve => server.close(() => resolve()));
    }
  }
);

Expected behavior

Identify the accepted procedure_pseudo_code operation in the timeout and recovery fields, while retaining provider_timeout, retryability, the actual 500 ms request budget, captured output and any cleanup facts. Preserve the metadata-resource stage separately as diagnostic context.

The applicable selected-runtime contract says failures are reported at the operation requiring that runtime. Metadata acquisition is a prerequisite of this accepted native analysis request. The existing startup attribution logic already separates the caller's operation from a transport-stage diagnostic; that is corroborating implementation context, rather than the sole contract for a metadata read after startup.

Impact and scope

P2: the executed failure corrupts the machine-readable analysis-operation identity and the operation named by recovery guidance. The caller still receives the correct timeout category, retryability and request budget; the fixture observed no false success or target modification. Downstream routing failures were not tested.

Observed once for the first metadata read of native attached decompilation, with one successful control. Source inspection shows the shared native observation path also runs after analysis, but other operations and post-operation stalls were not executed. Real IDA incidence is unknown. Avoiding a stall prevents this trigger; a workaround that restores attribution during an actual stall was not verified.

REA version

rea-agents 6.7.0; source revision a5f982de8d; existing compiled runtime invoked through scripts/rea.mjs.

Runtime and platform

Node.js v24.18.0; npm 11.16.0; Darwin arm64; loopback HTTP MCP fixture; no real IDA engine.

Target and artifact context

Synthetic ELF x86_64 header from the existing elf fixture; attached native registration; metadata uses the fixture's SHA-256; no live IDA database.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Actual CLI JSON from the stalled case, trimmed to the relevant fields:

{
  "error": "Analysis failed",
  "code": "provider_timeout",
  "category": "timeout",
  "message": "REA's ida_mcp_read_resource operation reached its 500ms server/provider deadline. This is not a client-side timeout; extending only the MCP client's deadline does not change this budget.",
  "retryable": true,
  "remediation": {
    "action": "Inspect any partial Evidence, provider diagnostics and reported session/provider cleanup state for ida_mcp_read_resource. Reduce the request if supported, or resolve the reported provider condition before retrying. Do not repeat the unchanged call or retry while work or cleanup may still be active."
  },
  "recovery": {
    "kind": "inspect_timeout_state",
    "operation": "ida_mcp_read_resource",
    "timeout_ms": 500
  },
  "details": {
    "operation": "ida_mcp_read_resource",
    "timeout_ms": 500,
    "captured_output": {"stdout": "", "stderr": "", "truncated": false, "stdout_bytes": 0, "stderr_bytes": 0}
  }
}

The fixture recorded initialize, notifications/initialized, tools/list, resources/read, then no tool calls in the failed case. Exit code was 1. The successful control recorded lookup_funcs and decompile, exited 0 and returned normalized_result: "int main(void) { return 42; }". Both children had empty stderr. Focused runner result:

Test Files  1 passed (1)
     Tests  2 passed (2)

Checked source locators:

Typed outcome or recovery expected

Return details.operation: "procedure_pseudo_code" and recovery: {"kind":"inspect_timeout_state","operation":"procedure_pseudo_code","timeout_ms":500}, with the internal metadata-resource stage retained separately. This requests correct attribution, not successful analysis after a metadata timeout.

Analysis or suspected cause

Source-derived: the metadata read happens outside the startup wrapper that reattributes typed failures. Its SDK timeout is already an AnalysisError, so the session failure handler preserves the private operation string and the public projection faithfully exposes it. The caller-operation context should remain available while retaining the transport-stage provenance.

Additional context

Searched open and closed issues and PRs using exact ida_mcp_read_resource, metadata-timeout, native-metadata-timeout and IDA-operation/attribution terms. No matching report surfaced. Closed issue #4614 concerns startup attribution; this reproduction succeeds through initialization/catalog discovery and fails at the later required metadata read. No release regression range or real-engine reproduction was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5824 ### 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 Native analysis or decompilation ### Observed behavior Executed the public CLI command `node scripts/rea.mjs decompile <fixture.elf> main --provider ida --json` against a local simulated native attached MCP provider. Initialization and tool discovery succeeded. When the provider left `resources/read` for `ida://idb/metadata` unanswered, REA exited with `provider_timeout` but identified the failed operation as `ida_mcp_read_resource` in `details.operation`, `recovery.operation`, the message and recovery guidance. The accepted analysis operation was `procedure_pseudo_code`. A control using the same registration/profile and a valid metadata response completed decompilation and returned `operation: "procedure_pseudo_code"`. These are adapter/CLI observations with a fixture, not real IDA observations. ### Reproduction 1. Use the source revision below with its locked dependencies and already-built runtime. Use the repository-pinned Node.js 24.18.0 and npm 11.16.0. No build or source change is required beyond this temporary test. 2. Save the following as `src/ida/repro.test.ts`. It uses the existing CLI and ELF fixtures, starts only a loopback HTTP server, runs a successful control and a stalled-resource case, and closes the server afterward. Set `HOME`, `XDG_CONFIG_HOME` and `TMPDIR` to separate directories inside a disposable local workspace before running; the CLI children also receive the suite's isolated workspace environment. 3. Run `npm run test:local -- src/ida/repro.test.ts`. The same body reproduced the timeout case once and passed the success control once. The assertions below describe the observed defective attribution, so a passing test confirms the defect. Delete the test afterward. ```ts import { createServer } from 'node:http'; import { createHash } from 'node:crypto'; import { expect } from 'vitest'; import { workspaceCliTest } from '../../tests/support/cli/workspaceCliFixture.js'; import { elf } from '../domain/binaryTarget.fixture.js'; workspaceCliTest.for(['success', 'timeout'] as const)( 'native metadata attribution: %s', async (mode, { cli, workspace }) => { const bytes = elf(2, 1, 62); const digest = createHash('sha256').update(bytes).digest('hex'); const methods: string[] = []; const calls: string[] = []; const tools = ['list_funcs', 'lookup_funcs', 'find_regex', 'decompile', 'disasm', 'callees', 'xrefs_to']; const server = createServer(async (req, res) => { if (req.method !== 'POST') { res.writeHead(405).end(); return; } let body = ''; for await (const chunk of req) body += String(chunk); const rpc = JSON.parse(body); methods.push(rpc.method); if (rpc.id === undefined) { res.writeHead(202).end(); return; } const send = (result: unknown) => res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result })); if (rpc.method === 'initialize') { send({ protocolVersion: rpc.params.protocolVersion, capabilities: { tools: {}, resources: {} }, serverInfo: { name: 'native-fixture', version: '1' } }); return; } if (rpc.method === 'tools/list') { send({ tools: tools.map(name => ({ name, inputSchema: { type: 'object', properties: {} } })) }); return; } if (rpc.method === 'resources/read') { expect(rpc.params.uri).toBe('ida://idb/metadata'); if (mode === 'timeout') return; send({ contents: [{ uri: rpc.params.uri, mimeType: 'application/json', text: JSON.stringify({ path: 'target.i64', sha256: digest, base: '0x1000', module: 'target' }) }] }); return; } calls.push(rpc.params.name); const result = rpc.params.name === 'lookup_funcs' ? rpc.params.arguments.queries.map((query: string) => ({ query, fn: { addr: '0x1000', name: 'main', size: '0x20' }, error: null })) : { addr: '0x1000', ok: true, pseudocode: 'int main(void) { return 42; }', lines: 1 }; send({ content: [{ type: 'text', text: JSON.stringify(result) }] }); }); await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve)); try { const address = server.address(); if (!address || typeof address === 'string') throw new Error('missing address'); const registration = await workspace.write('ida.json', JSON.stringify({ mode: 'attached', protocol: 'native', url: `http://127.0.0.1:${address.port}/mcp`, timeoutMs: 500 })); const target = await workspace.write('target.elf', bytes); const result = await cli.run({ arguments: ['decompile', target, 'main', '--provider', 'ida', '--json'], environment: { ...workspace.environment, REA_IDA_MCP_CONFIG: registration, REA_LOG_LEVEL: 'silent' }, timeoutMs: 15000 }); expect(result.stderr).toBe(''); console.log(JSON.stringify({ mode, exitCode: result.exitCode, methods, calls, output: result.json })); if (mode === 'success') { expect(result.exitCode).toBe(0); expect(result.json).toMatchObject({ operation: 'procedure_pseudo_code', normalized_result: 'int main(void) { return 42; }' }); expect(calls).toContain('decompile'); } else { expect(result.exitCode).toBe(1); expect(calls).toEqual([]); expect(methods).toContain('tools/list'); expect(methods).toContain('resources/read'); expect(result.json).toMatchObject({ code: 'provider_timeout', retryable: true, details: { operation: 'ida_mcp_read_resource', timeout_ms: 500 }, recovery: { kind: 'inspect_timeout_state', operation: 'ida_mcp_read_resource', timeout_ms: 500 } }); } } finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())); } } ); ``` ### Expected behavior Identify the accepted `procedure_pseudo_code` operation in the timeout and recovery fields, while retaining `provider_timeout`, retryability, the actual 500 ms request budget, captured output and any cleanup facts. Preserve the metadata-resource stage separately as diagnostic context. The applicable [selected-runtime contract](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/docs/mcp-contracts.md#L757-L761) says failures are reported at the operation requiring that runtime. Metadata acquisition is a prerequisite of this accepted native analysis request. The existing [startup attribution logic](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaSessionClient.ts#L510-L556) already separates the caller's operation from a transport-stage diagnostic; that is corroborating implementation context, rather than the sole contract for a metadata read after startup. ### Impact and scope P2: the executed failure corrupts the machine-readable analysis-operation identity and the operation named by recovery guidance. The caller still receives the correct timeout category, retryability and request budget; the fixture observed no false success or target modification. Downstream routing failures were not tested. Observed once for the first metadata read of native attached decompilation, with one successful control. Source inspection shows the shared native observation path also runs after analysis, but other operations and post-operation stalls were not executed. Real IDA incidence is unknown. Avoiding a stall prevents this trigger; a workaround that restores attribution during an actual stall was not verified. ### REA version rea-agents 6.7.0; source revision a5f982de8dbe4b092cc20ab89d492b7e7028b758; existing compiled runtime invoked through scripts/rea.mjs. ### Runtime and platform Node.js v24.18.0; npm 11.16.0; Darwin arm64; loopback HTTP MCP fixture; no real IDA engine. ### Target and artifact context Synthetic ELF x86_64 header from the existing elf fixture; attached native registration; metadata uses the fixture's SHA-256; no live IDA database. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Actual CLI JSON from the stalled case, trimmed to the relevant fields: ```json { "error": "Analysis failed", "code": "provider_timeout", "category": "timeout", "message": "REA's ida_mcp_read_resource operation reached its 500ms server/provider deadline. This is not a client-side timeout; extending only the MCP client's deadline does not change this budget.", "retryable": true, "remediation": { "action": "Inspect any partial Evidence, provider diagnostics and reported session/provider cleanup state for ida_mcp_read_resource. Reduce the request if supported, or resolve the reported provider condition before retrying. Do not repeat the unchanged call or retry while work or cleanup may still be active." }, "recovery": { "kind": "inspect_timeout_state", "operation": "ida_mcp_read_resource", "timeout_ms": 500 }, "details": { "operation": "ida_mcp_read_resource", "timeout_ms": 500, "captured_output": {"stdout": "", "stderr": "", "truncated": false, "stdout_bytes": 0, "stderr_bytes": 0} } } ``` The fixture recorded `initialize`, `notifications/initialized`, `tools/list`, `resources/read`, then no tool calls in the failed case. Exit code was 1. The successful control recorded `lookup_funcs` and `decompile`, exited 0 and returned `normalized_result: "int main(void) { return 42; }"`. Both children had empty stderr. Focused runner result: ```text Test Files 1 passed (1) Tests 2 passed (2) ``` Checked source locators: - [Native metadata acquisition](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaSessionClient.ts#L658-L675) reads the required resource; [execution sequencing](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaSessionClient.ts#L295-L314) places the first read after startup. - [Resource transport attribution](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaMcpConnection.ts#L478-L483) supplies `ida_mcp_read_resource`; [SDK timeout conversion](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaMcpConnection.ts#L396-L400) uses that string to construct the typed timeout. - [Failure propagation](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/ida/IdaSessionClient.ts#L428-L428) retains an existing analysis error. [Recovery projection](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorProjection.ts#L134-L139) and [details projection](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorProjection.ts#L524-L527) use its operation unchanged; [presentation](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorPresentation.ts#L390-L394) also uses it. ### Typed outcome or recovery expected Return `details.operation: "procedure_pseudo_code"` and `recovery: {"kind":"inspect_timeout_state","operation":"procedure_pseudo_code","timeout_ms":500}`, with the internal metadata-resource stage retained separately. This requests correct attribution, not successful analysis after a metadata timeout. ### Analysis or suspected cause Source-derived: the metadata read happens outside the startup wrapper that reattributes typed failures. Its SDK timeout is already an `AnalysisError`, so the session failure handler preserves the private operation string and the public projection faithfully exposes it. The caller-operation context should remain available while retaining the transport-stage provenance. ### Additional context Searched open and closed issues and PRs using exact `ida_mcp_read_resource`, metadata-timeout, native-metadata-timeout and IDA-operation/attribution terms. No matching report surfaced. Closed [issue #4614](https://github.com/morluto/rea/issues/4614) concerns startup attribution; this reproduction succeeds through initialization/catalog discovery and fails at the later required metadata read. No release regression range or real-engine reproduction was established.
Author
Owner

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

Confirmed — when native attached metadata read times out, the provider_timeout error attributes the failed operation as ida_mcp_read_resource everywhere, even though the user-requested analysis was procedure_pseudo_code and the control run correctly reports it. Keeping the requested operation (and the analysis intent) in details while noting the stalled sub-step (metadata resource read) would let callers decide whether to retry the whole decompile instead of being told a resource read failed in isolation.

<!-- gh-comment-id:6312261080 --> @XinwangTao commented on GitHub (Oct 21, 2026): Confirmed — when native attached metadata read times out, the provider_timeout error attributes the failed operation as ida_mcp_read_resource everywhere, even though the user-requested analysis was procedure_pseudo_code and the control run correctly reports it. Keeping the requested operation (and the analysis intent) in details while noting the stalled sub-step (metadata resource read) would let callers decide whether to retry the whole decompile instead of being told a resource read failed in isolation.
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

IdaSessionClient failure translation now wraps an AnalysisTimeoutError from a different operation in a timeout attributed to the accepted analysis operation. It preserves timeoutMs, captured output, diagnostics, partial observation and cleanup dependency/owner, with the original timeout as cause. The exact native-attached metadata regression throws ida_mcp_read_resource at readResource and requires procedure_pseudo_code in details and recovery while retaining uri and the 40ms budget.

Implementation: IdaSessionClient.ts, IdaSessionClient.test.ts.

The relevant regression checks in IdaSessionClient.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380750663 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. IdaSessionClient failure translation now wraps an AnalysisTimeoutError from a different operation in a timeout attributed to the accepted analysis operation. It preserves timeoutMs, captured output, diagnostics, partial observation and cleanup dependency/owner, with the original timeout as cause. The exact native-attached metadata regression throws ida_mcp_read_resource at readResource and requires procedure_pseudo_code in details and recovery while retaining uri and the 40ms budget. Implementation: [IdaSessionClient.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ida/IdaSessionClient.ts#L468), [IdaSessionClient.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ida/IdaSessionClient.test.ts#L2045). The relevant regression checks in [IdaSessionClient.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ida/IdaSessionClient.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3520
No description provided.