[GH-ISSUE #3978] [Bug] Frida session status loses observed detachment when resume settles #1573

Closed
opened 2026-10-16 21:07:19 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

A spawned Frida session can report running after REA has already observed its detachment. When the native detach notification arrives while resume_frida_session is waiting for native resume to settle, a subsequent frida_session_status call loses the recorded detached state. The incorrect state persists across repeated status reads. This was reproduced through the registered MCP tools with a simulated native device, without starting a real target.

Reproduction

Use the baseline in the REA version and Runtime and platform fields, with its locked dependencies installed and its compiled runtime available. From the repository root, run the following fixture. It connects an MCP client to the production Frida tool registration and injects a simulated device manager. It does not load native Frida bindings, read REA user configuration, execute target code or access an external service.

Each case calls start_frida_session with spawn mode, program fixture and device local, dispatches resume_frida_session, and calls frida_session_status before and twice after native resume settles. The during-resume case sets native detachment and delivers the subscribed notification before settlement. The other cases keep the session attached or deliver detachment after settlement.

node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const load = p => import(pathToFileURL(`${process.cwd()}/${p}`).href);
const { Client, InMemoryTransport } = await load('node_modules/@modelcontextprotocol/client/dist/index.mjs');
const { McpServer } = await load('node_modules/@modelcontextprotocol/server/dist/index.mjs');
const { FridaInstrumentationManager } = await load('dist/frida/FridaInstrumentationManager.js');
const { registerFridaTools } = await load('dist/server/registerFridaTools.js');
const { createToolResultDelivery } = await load('dist/server/toolResult.js');
const { silentLogger } = await load('dist/logger.js');
for (const timing of ['attached', 'during-resume', 'after-resume']) {
  let detached = false, listener;
  const entered = Promise.withResolvers(), settle = Promise.withResolvers();
  const markDetached = () => { detached = true; listener('process-terminated', null); };
  const session = {
    pid: 512,
    detached: { connect(fn) { listener = fn; }, disconnect() {} },
    isDetached: () => detached,
    async detach() { markDetached(); },
    async createScript() { throw Error('unused'); },
  };
  const device = {
    id: 'local', name: 'Fixture', type: 'local',
    async spawn() { return 512; }, async attach() { return session; },
    async resume() { entered.resolve(); await settle.promise; },
    async enumerateProcesses() { return []; },
    async getProcessByPid() { return { pid: 512, name: 'Fixture' }; },
  };
  const deviceManager = {
    async getDeviceById() { return device; }, async enumerateDevices() { return [device]; },
    async addRemoteDevice() { return device; }, async removeRemoteDevice() {},
  };
  const manager = new FridaInstrumentationManager({ deviceManager });
  const server = new McpServer({ name: 'lifecycle-probe', version: '1' });
  registerFridaTools(server, manager, silentLogger, createToolResultDelivery(undefined));
  const client = new Client({ name: 'lifecycle-probe', version: '1' });
  const [a,b] = InMemoryTransport.createLinkedPair();
  await server.connect(b); await client.connect(a);
  const call = (name, args) => client.callTool({ name, arguments: args });
  try {
    const start = await call('start_frida_session', { mode: 'spawn', program: 'fixture', device_id: 'local' });
    assert.equal(start.isError, undefined);
    assert.equal(start.structuredContent.state, 'paused');
    const args = { session_id: start.structuredContent.session_id };
    const getState = async () => {
      const result = await call('frida_session_status', args);
      assert.equal(result.isError, undefined);
      return result.structuredContent.normalized_result.state;
    };
    const pending = call('resume_frida_session', args);
    await entered.promise;
    if (timing === 'during-resume') markDetached();
    const beforeSettlement = await getState();
    settle.resolve();
    const resume = await pending;
    assert.equal(resume.isError, undefined);
    if (timing === 'after-resume') markDetached();
    const status = [await getState(), await getState()];
    const expected = timing === 'attached' ? 'running' : 'detached';
    const result = { timing, nativeDetached: detached, beforeSettlement, resume: resume.structuredContent, status, expected, contractMet: status.every(s => s === expected) };
    console.log(JSON.stringify(result));
    if (timing === 'during-resume') assert.equal(beforeSettlement, 'detached');
    else assert.equal(result.contractMet, true);
    await call('close_frida_session', args);
  } finally { settle.resolve(); await client.close(); await server.close(); await manager.closeAll(); }
}
JS

Expected behavior

Session status should preserve the observed native detachment. The during-resume case should report detached before settlement and in both later status reads. The attached control should report running after settlement, and the after-resume-detachment control should report detached. A successful resume acknowledgement can remain running according to its separate operation contract; it does not establish that instrumentation remains attached.

Impact and scope

A bounded event ordering replaces a known session-detachment observation with an incorrect live state and can misdirect later instrumentation decisions. Both controls retain correct observations. Real-provider frequency and downstream effects were not measured, and broad workflow failure or severe harm was not established.

Two later status reads preserve the incorrect state in the reproduced case. No practical caller workaround preserving the same session observation was demonstrated; the investigation did not establish that every alternative recovery is unavailable.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Simulated native Frida device; paused spawned session; no real target process.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

  • Expected source: src/contracts/fridaToolContracts.ts:221-247 promises observed Frida session state, including detached; AGENTS.md, Product and Authority, requires preservation of observations and session semantics. The separate resume acknowledgement is specified at src/contracts/fridaToolContracts.ts:185-191.
  • Failure source: The executed MCP fixture and output above show the status changing from detached to running after resume settlement. Source corroboration is the detach callback at src/frida/FridaInstrumentationManager.ts:319-321, the resume completion at src/frida/FridaInstrumentationManager.ts:537-540, and the cached status projection at src/frida/FridaInstrumentationManager.ts:617-632.

The state-assignment explanation is source-derived. The production service constructs status Evidence from the manager's status and labels it observed at src/application/frida/FridaInstrumentationService.ts:217-224 and src/application/frida/FridaInstrumentationService.ts:364-383. The fixture exercises the actual registered handlers and service with provider injection, consistent with the boundary approach in tests/boundary/mcp/fridaLifecycleMcp.test.ts. A target exiting during resume is a possible real-world trigger, not a reproduced real-provider event.

Two complete executions produced the following output. In both, status first observed detached during the pending resume and then incorrectly returned running twice after settlement while the native session remained detached. Both controls passed. The fixture deliberately prints contractMet: false for the failing case and exits successfully so all controls and cleanup run.

{"timing":"attached","nativeDetached":false,"beforeSettlement":"paused","resume":{"state":"running"},"status":["running","running"],"expected":"running","contractMet":true}
{"timing":"during-resume","nativeDetached":true,"beforeSettlement":"detached","resume":{"state":"running"},"status":["running","running"],"expected":"detached","contractMet":false}
{"timing":"after-resume","nativeDetached":true,"beforeSettlement":"paused","resume":{"state":"running"},"status":["detached","detached"],"expected":"detached","contractMet":true}

The notification was delivered and processed, as shown by the intervening detached status. The reproduction establishes a deterministic ordering failure with a simulated provider. It does not establish real Frida scheduling or how often a target detaches before resume settles.

Typed outcome or recovery expected

Repeat all three fixture cases. The during-resume case must retain detached in both status reads after settlement, without a second detach notification, and print contractMet: true. Keep the attached control reporting running and the after-resume-detachment control reporting detached. Preserve the distinction between successful target resume and continued instrumentation attachment, including the existing successful-resume acknowledgement.

Analysis or suspected cause

The source references above explain the observed behavior. Runtime claims are limited to the explicitly described simulation boundaries.

Additional context

Runtime claims above describe the supplied local reproduction at the baseline commit. Real-provider frequency remains unmeasured.

The relevant failure code is unchanged on current origin/main fc712b3750.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3978 ### 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 Process capture or runtime observation ### Observed behavior A spawned Frida session can report `running` after REA has already observed its detachment. When the native detach notification arrives while `resume_frida_session` is waiting for native resume to settle, a subsequent `frida_session_status` call loses the recorded `detached` state. The incorrect state persists across repeated status reads. This was reproduced through the registered MCP tools with a simulated native device, without starting a real target. ### Reproduction Use the baseline in the REA version and Runtime and platform fields, with its locked dependencies installed and its compiled runtime available. From the repository root, run the following fixture. It connects an MCP client to the production Frida tool registration and injects a simulated device manager. It does not load native Frida bindings, read REA user configuration, execute target code or access an external service. Each case calls `start_frida_session` with spawn mode, program `fixture` and device `local`, dispatches `resume_frida_session`, and calls `frida_session_status` before and twice after native resume settles. The `during-resume` case sets native detachment and delivers the subscribed notification before settlement. The other cases keep the session attached or deliver detachment after settlement. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; const load = p => import(pathToFileURL(`${process.cwd()}/${p}`).href); const { Client, InMemoryTransport } = await load('node_modules/@modelcontextprotocol/client/dist/index.mjs'); const { McpServer } = await load('node_modules/@modelcontextprotocol/server/dist/index.mjs'); const { FridaInstrumentationManager } = await load('dist/frida/FridaInstrumentationManager.js'); const { registerFridaTools } = await load('dist/server/registerFridaTools.js'); const { createToolResultDelivery } = await load('dist/server/toolResult.js'); const { silentLogger } = await load('dist/logger.js'); for (const timing of ['attached', 'during-resume', 'after-resume']) { let detached = false, listener; const entered = Promise.withResolvers(), settle = Promise.withResolvers(); const markDetached = () => { detached = true; listener('process-terminated', null); }; const session = { pid: 512, detached: { connect(fn) { listener = fn; }, disconnect() {} }, isDetached: () => detached, async detach() { markDetached(); }, async createScript() { throw Error('unused'); }, }; const device = { id: 'local', name: 'Fixture', type: 'local', async spawn() { return 512; }, async attach() { return session; }, async resume() { entered.resolve(); await settle.promise; }, async enumerateProcesses() { return []; }, async getProcessByPid() { return { pid: 512, name: 'Fixture' }; }, }; const deviceManager = { async getDeviceById() { return device; }, async enumerateDevices() { return [device]; }, async addRemoteDevice() { return device; }, async removeRemoteDevice() {}, }; const manager = new FridaInstrumentationManager({ deviceManager }); const server = new McpServer({ name: 'lifecycle-probe', version: '1' }); registerFridaTools(server, manager, silentLogger, createToolResultDelivery(undefined)); const client = new Client({ name: 'lifecycle-probe', version: '1' }); const [a,b] = InMemoryTransport.createLinkedPair(); await server.connect(b); await client.connect(a); const call = (name, args) => client.callTool({ name, arguments: args }); try { const start = await call('start_frida_session', { mode: 'spawn', program: 'fixture', device_id: 'local' }); assert.equal(start.isError, undefined); assert.equal(start.structuredContent.state, 'paused'); const args = { session_id: start.structuredContent.session_id }; const getState = async () => { const result = await call('frida_session_status', args); assert.equal(result.isError, undefined); return result.structuredContent.normalized_result.state; }; const pending = call('resume_frida_session', args); await entered.promise; if (timing === 'during-resume') markDetached(); const beforeSettlement = await getState(); settle.resolve(); const resume = await pending; assert.equal(resume.isError, undefined); if (timing === 'after-resume') markDetached(); const status = [await getState(), await getState()]; const expected = timing === 'attached' ? 'running' : 'detached'; const result = { timing, nativeDetached: detached, beforeSettlement, resume: resume.structuredContent, status, expected, contractMet: status.every(s => s === expected) }; console.log(JSON.stringify(result)); if (timing === 'during-resume') assert.equal(beforeSettlement, 'detached'); else assert.equal(result.contractMet, true); await call('close_frida_session', args); } finally { settle.resolve(); await client.close(); await server.close(); await manager.closeAll(); } } JS ``` ### Expected behavior Session status should preserve the observed native detachment. The `during-resume` case should report `detached` before settlement and in both later status reads. The attached control should report `running` after settlement, and the after-resume-detachment control should report `detached`. A successful resume acknowledgement can remain `running` according to its separate operation contract; it does not establish that instrumentation remains attached. ### Impact and scope A bounded event ordering replaces a known session-detachment observation with an incorrect live state and can misdirect later instrumentation decisions. Both controls retain correct observations. Real-provider frequency and downstream effects were not measured, and broad workflow failure or severe harm was not established. Two later status reads preserve the incorrect state in the reproduced case. No practical caller workaround preserving the same session observation was demonstrated; the investigation did not establish that every alternative recovery is unavailable. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Simulated native Frida device; paused spawned session; no real target process. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence - Expected source: `src/contracts/fridaToolContracts.ts:221-247` promises observed Frida session state, including `detached`; `AGENTS.md`, Product and Authority, requires preservation of observations and session semantics. The separate resume acknowledgement is specified at `src/contracts/fridaToolContracts.ts:185-191`. - Failure source: The executed MCP fixture and output above show the status changing from `detached` to `running` after resume settlement. Source corroboration is the detach callback at `src/frida/FridaInstrumentationManager.ts:319-321`, the resume completion at `src/frida/FridaInstrumentationManager.ts:537-540`, and the cached status projection at `src/frida/FridaInstrumentationManager.ts:617-632`. The state-assignment explanation is source-derived. The production service constructs status Evidence from the manager's status and labels it observed at `src/application/frida/FridaInstrumentationService.ts:217-224` and `src/application/frida/FridaInstrumentationService.ts:364-383`. The fixture exercises the actual registered handlers and service with provider injection, consistent with the boundary approach in `tests/boundary/mcp/fridaLifecycleMcp.test.ts`. A target exiting during resume is a possible real-world trigger, not a reproduced real-provider event. Two complete executions produced the following output. In both, status first observed `detached` during the pending resume and then incorrectly returned `running` twice after settlement while the native session remained detached. Both controls passed. The fixture deliberately prints `contractMet: false` for the failing case and exits successfully so all controls and cleanup run. ```json {"timing":"attached","nativeDetached":false,"beforeSettlement":"paused","resume":{"state":"running"},"status":["running","running"],"expected":"running","contractMet":true} {"timing":"during-resume","nativeDetached":true,"beforeSettlement":"detached","resume":{"state":"running"},"status":["running","running"],"expected":"detached","contractMet":false} {"timing":"after-resume","nativeDetached":true,"beforeSettlement":"paused","resume":{"state":"running"},"status":["detached","detached"],"expected":"detached","contractMet":true} ``` The notification was delivered and processed, as shown by the intervening `detached` status. The reproduction establishes a deterministic ordering failure with a simulated provider. It does not establish real Frida scheduling or how often a target detaches before resume settles. ### Typed outcome or recovery expected Repeat all three fixture cases. The `during-resume` case must retain `detached` in both status reads after settlement, without a second detach notification, and print `contractMet: true`. Keep the attached control reporting `running` and the after-resume-detachment control reporting `detached`. Preserve the distinction between successful target resume and continued instrumentation attachment, including the existing successful-resume acknowledgement. ### Analysis or suspected cause The source references above explain the observed behavior. Runtime claims are limited to the explicitly described simulation boundaries. ### Additional context Runtime claims above describe the supplied local reproduction at the baseline commit. Real-provider frequency remains unmeasured. The relevant failure code is unchanged on current `origin/main` fc712b3750cbe16755c4f78c1f90fdf09fbaca31.
Author
Owner

@yankikucuk commented on GitHub (Oct 15, 2026):

Working on this. Reproduced with your MCP probe on a clean build: the during-resume case read detached before settlement and running twice afterwards. Resume completion now keeps the session detached when detachment was observed through the notification or the provider state, so the probe reports detached for both later reads while the attached and after-resume controls are unchanged; the resume acknowledgement itself is untouched. Simulated native bindings only. Draft PR follows shortly.

<!-- gh-comment-id:6191492361 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your MCP probe on a clean build: the during-resume case read detached before settlement and running twice afterwards. Resume completion now keeps the session detached when detachment was observed through the notification or the provider state, so the probe reports detached for both later reads while the attached and after-resume controls are unchanged; the resume acknowledgement itself is untouched. Simulated native bindings only. Draft PR follows shortly.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in 69fe86c5c7.

Root cause: a mutable session-state union represented both instrumentation attachment and target execution. Both native resume continuations could overwrite an earlier detach observation. The manager now retains detachment and successful target-resume acknowledgement as independent facts, and derives the public state from those facts. Resume still acknowledges success without claiming the instrumentation remains attached.

The nearby close path had the same observable defect: after detach and successful cleanup-resume, a failed remote-device removal retained a session reporting running. The same structural change fixes it; retry retains detached and does not repeat successful detach/resume.

Validation on pinned Node 24.18.0: registered MCP regressions initially failed in both affected orderings (2 failed,35 passed); after the change,114 tests passed across manager, service, registered MCP lifecycle/discovery and CLI boundaries. npm run check and npm run docs:check passed. Tests use simulated native Frida objects through production workflows; real-provider event frequency, native targets and Linux/Windows behavior were not exercised. No performance improvement is claimed.

External cross-check: Frida exposes target resume as a separate Promise operation; its detached signal / isDetached concern the session. Context7 and DeepWiki were navigation aids, not proof of scheduling guarantees. Official Frida lifecycle notes describe is_detached as reconciliation for a signal emitted before listener registration.

<!-- gh-comment-id:6264822703 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on main in 69fe86c5c7c31082d1846842b3afd00343499d75. Root cause: a mutable session-state union represented both instrumentation attachment and target execution. Both native resume continuations could overwrite an earlier detach observation. The manager now retains detachment and successful target-resume acknowledgement as independent facts, and derives the public state from those facts. Resume still acknowledges success without claiming the instrumentation remains attached. The nearby close path had the same observable defect: after detach and successful cleanup-resume, a failed remote-device removal retained a session reporting `running`. The same structural change fixes it; retry retains `detached` and does not repeat successful detach/resume. Validation on pinned Node 24.18.0: registered MCP regressions initially failed in both affected orderings (2 failed,35 passed); after the change,114 tests passed across manager, service, registered MCP lifecycle/discovery and CLI boundaries. `npm run check` and `npm run docs:check` passed. Tests use simulated native Frida objects through production workflows; real-provider event frequency, native targets and Linux/Windows behavior were not exercised. No performance improvement is claimed. External cross-check: Frida exposes target resume as a separate Promise<void> operation; its detached signal / isDetached concern the session. Context7 and [DeepWiki](https://deepwiki.com/search/for-node-sessiondetached-and-d_2f88f481-a540-4ec2-85f2-0886a1666118) were navigation aids, not proof of scheduling guarantees. [Official Frida lifecycle notes](https://frida.re/news/2021/07/18/frida-15-0-released/) describe is_detached as reconciliation for a signal emitted before listener registration.
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#1573
No description provided.