[GH-ISSUE #3979] [Bug] Cancelled Frida script loads lose partial Evidence when native loading rejects #1575

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/3979

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 persistent MCP script load that emits a message, receives cancellation, and then rejects during native loading produces no partial load Evidence. The same cancelled request records source identity, the captured message, and cleanup status when native loading resolves. The cancelled client promise looks the same in both cases, so a caller that stopped waiting misses the load-specific record it would otherwise retrieve later. The new script is released, and the existing session and earlier script remain available.

Reproduction

Use the baseline and prepared dependencies/runtime specified below. From the repository root, run the following ES module on standard input. It uses SDK in-memory MCP transport, the registered production Frida handlers, the real manager and service, and a simulated native DeviceManager. It never attaches to a real process. The recording callback captures precisely what the production handler asks to retain, and the manager wrapper returns its result unchanged.

The fixture starts a persistent session through MCP and loads an earlier script. The candidate load emits marker and waits inside native loading. For the cancellation cases, an awaited ping follows client cancellation before native loading settles, and the probe checks that the server-side signal was aborted. It runs cancellation/native-success, cancellation/native-failure, and native-failure-without-cancellation controls. It then checks cleanup, the earlier script, and a subsequent MCP status request.

node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import { setImmediate } from 'node:timers/promises';
const from = p => import(pathToFileURL(`${process.cwd()}/${p}`).href);
const { Client, InMemoryTransport } = await from('node_modules/@modelcontextprotocol/client/dist/index.mjs');
const { McpServer } = await from('node_modules/@modelcontextprotocol/server/dist/index.mjs');
const { FridaInstrumentationManager } = await from('dist/frida/FridaInstrumentationManager.js');
const { registerFridaTools } = await from('dist/server/registerFridaTools.js');
const { createToolResultDelivery } = await from('dist/server/toolResult.js');
const { silentLogger } = await from('dist/logger.js');
const source = "send('marker');";
async function probe(cancel, failNative) {
  let enter, release, rejectLoad, finishManager;
  const entered = new Promise(r => { enter = r; });
  const held = new Promise((r,j) => { release = r; rejectLoad = j; });
  const completed = new Promise(r => { finishManager = r; });
  let detached = false;
  const scripts = [];
  const nativeSession = {
    pid: 512, detached: { connect() {}, disconnect() {} },
    isDetached: () => detached,
    async detach() { detached = true; },
    async createScript(text) {
      let destroyed = false, listener;
      const script = {
        get isDestroyed() { return destroyed; },
        message: { connect(h) { listener = h; }, disconnect() { listener = undefined; } },
        async load() {
          if (text === source) {
            listener?.({ type: 'send', payload: 'marker' }, null);
            enter();
            await held;
          }
        },
        async unload() { destroyed = true; },
      };
      scripts.push(script);
      return script;
    },
  };
  const device = {
    id: 'local', name: 'Fixture', type: 'local',
    async attach() { return nativeSession; }, async spawn() { return 512; },
    async resume() {}, 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 originalLoad = manager.loadScript.bind(manager);
  manager.loadScript = async (id, input, signal) => {
    const result = await originalLoad(id, input, signal);
    if (input.source === source) finishManager({
      signalAborted: signal?.aborted ?? false, ok: result.ok,
      errorTag: result.ok ? null : result.error._tag,
      hasPartial: !result.ok && result.error.partialObservation != null,
      hasScriptObservation: !result.ok && result.error.script != null,
    });
    return result;
  };
  const records = [];
  const server = new McpServer({ name: 'cancel-reproduction', version: '1' });
  registerFridaTools(server, manager, silentLogger, createToolResultDelivery(undefined), e => {
    records.push(e); return { ok: true, value: null };
  });
  const client = new Client({ name: 'cancel-reproduction', version: '1' });
  const [a,b] = InMemoryTransport.createLinkedPair();
  await server.connect(b); await client.connect(a);
  let timer;
  try {
    const started = await client.callTool({ name: 'start_frida_session', arguments: { mode: 'attach', pid: 512, device_id: 'local' } });
    const id = started.structuredContent.session_id;
    assert.equal(typeof id, 'string');
    const original = await client.callTool({ name: 'load_frida_script', arguments: { session_id: id, source_kind: 'inline', source: 'void 0;' } });
    assert.notEqual(original.isError, true);
    records.length = 0;
    const controller = new AbortController();
    const pending = client.callTool({ name: 'load_frida_script', arguments: { session_id: id, source_kind: 'inline', source } }, { signal: controller.signal }).catch(e => ({ clientRejected: e.message }));
    await entered;
    if (cancel) { controller.abort(new Error('cancel during native load')); await client.ping(); }
    if (failNative) rejectLoad(new Error('native load rejected')); else release();
    const observed = await Promise.race([completed, new Promise((_,j) => { timer = setTimeout(() => j(new Error('manager timeout')), 3000); })]);
    clearTimeout(timer);
    const clientResult = await pending;
    await setImmediate(); await client.ping();
    const retained = records.filter(e => e.operation === 'load_frida_script').map(e => e.normalized_result);
    const status = await client.callTool({ name: 'frida_session_status', arguments: { session_id: id } });
    const live = manager.status(id);
    assert.equal(observed.signalAborted, cancel);
    assert.equal(scripts[0].isDestroyed, false);
    assert.equal(scripts[1].isDestroyed, true);
    assert.equal(detached, false);
    assert.equal(live.scripts.length, 1);
    assert.notEqual(status.isError, true);
    assert.ok(live.messages.some(m => m.payload === 'marker'));
    if (cancel && !failNative) {
      assert.equal(observed.errorTag, 'AnalysisCancelledError');
      assert.equal(retained.length, 1);
      assert.equal(retained[0].source_sha256, createHash('sha256').update(source).digest('hex'));
      assert.equal(retained[0].messages[0].payload, 'marker');
      assert.equal(retained[0].script_cleanup, 'released');
    } else {
      assert.equal(observed.errorTag, 'AnalysisCapabilityUnavailableError');
      assert.equal(retained.length, 0);
    }
    console.log(JSON.stringify({ cancel, failNative, observed, clientRejected: clientResult.clientRejected ?? null, clientIsError: clientResult.isError ?? null, retainedLoadEvidence: retained, newScriptReleased: scripts[1].isDestroyed, originalScriptAvailable: !scripts[0].isDestroyed, sessionAvailable: !detached && live.scripts.length === 1, markerInSessionStatus: live.messages.some(m => m.payload === 'marker') }));
  } finally {
    clearTimeout(timer); release(); await client.close(); await server.close(); await manager.closeAll();
  }
}
await probe(true, false);
await probe(true, true);
await probe(false, true);
JS

Expected behavior

After cancellation arrives during native loading, REA should finish the in-flight call, release the candidate script, and preserve the completed load observation as partial Evidence. That observation should retain the selected source identity, captured message, and cleanup outcome. The existing session and earlier script should remain usable. A native failure without cancellation should remain an ordinary provider failure. The cancellation and partial-fact contracts apply before the load has committed success, including this case where native loading later rejects.

Impact and scope

The cancellation/native-rejection intersection loses a load-specific partial Evidence record with source identity and cleanup state. This is consequential but bounded: the session and earlier script remain usable, and the aggregate status still contains the captured marker. Real-provider frequency is unknown.

A later session status call retains the marker but does not recreate the original cancelled-load Evidence with its source digest and cleanup result. No available operation was demonstrated to recover that missing record after the candidate script was released.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Persistent Frida MCP session with simulated native bindings and an earlier loaded script; no real target process.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

  • Expected source: docs/frida-instrumentation.md:11 specifies native-call cancellation, partial observations, and existing-session load cleanup; docs/mcp-contracts.md:442-448 describes retained cancellation Evidence and retrieval after the SDK stops waiting; AGENTS.md, Architecture and Cleanup, requires preserving partial facts; tests/boundary/mcp/fridaLifecycleMcp.test.ts:587-629 verifies retained cancelled-load observations when native loading completes successfully.
  • Failure source: The executed MCP fixture and its output above. Source-derived explanation: src/frida/FridaInstrumentationManager.ts:427-480 constructs a script observation only after native loading resolves and routes rejection through provider failure; src/application/frida/FridaInstrumentationService.ts:193-202 converts only the script-load cancellation error into partial Evidence; src/server/registerFridaTools.ts:142-182 handles the MCP request and lines 324-337 record only a valid partial Evidence value.

The source explanation is derived from the pinned checkout. Runtime reproduction used the prepared compiled runtime and a simulated native provider at the production boundary. It does not establish actual Frida/device failure timing or real-world frequency. The in-memory recording callback proves that the handler requested no load record; a real Evidence database was not used.

Observed in two local executions of the fixture, each containing one run of each case:

  • Cancellation followed by native success produced an aborted server signal and a manager AnalysisCancelledError tag carrying a script observation. The handler recorded one load_frida_script Evidence item with the expected source SHA-256, the marker message, script_cleanup: "released", and cleanup_error: null.
  • Cancellation followed by native failure produced an aborted server signal and a manager AnalysisCapabilityUnavailableError without a script observation or partial observation. The handler recorded zero load Evidence items.
  • Native failure without cancellation produced an unaborted signal, an ordinary capability-unavailable manager error, a client-visible MCP error, and zero load Evidence items.

In both cancelled cases, the SDK client rejected with Error: cancel during native load. The manager error tag above is an internal boundary observation; it is not a claimed availability-error reply seen by the cancelled client. In all three cases, the candidate script was destroyed, the earlier script stayed loaded, and the session status call succeeded. The aggregate session status still contained marker. The supported loss is the cancelled-load Evidence associating source identity and cleanup with that observation, rather than every copy of the message.

The relevant stable fields printed by the cancellation/native-failure case were:

{
  "cancel": true,
  "failNative": true,
  "observed": {
    "signalAborted": true,
    "ok": false,
    "errorTag": "AnalysisCapabilityUnavailableError",
    "hasPartial": false,
    "hasScriptObservation": false
  },
  "clientRejected": "Error: cancel during native load",
  "clientIsError": null,
  "retainedLoadEvidence": [],
  "newScriptReleased": true,
  "originalScriptAvailable": true,
  "sessionAvailable": true,
  "markerInSessionStatus": true
}

Typed outcome or recovery expected

Run the two cancellation cases with cancellation delivered before the native load settles. Each should produce a cancelled partial load Evidence record containing the source digest, completed marker, candidate script identity, and successful cleanup outcome. The candidate script must be released, while the earlier script and persistent session remain available. Check that the record is available for later Evidence retrieval even though the client promise already rejected. Keep the non-cancelled native-failure case as an ordinary provider failure, so restoration does not hide unrelated failures. The current fixture includes baseline-specific assertions about the missing record; change those expectations to the restored contract when using it as a regression check.

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. Related closed report #2544 concerns cancellation followed by successful native loading and retention as success. This report concerns native rejection after cancellation, with no partial per-load Evidence; the successful-load control now preserves cancellation Evidence.

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/3979 ### 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 persistent MCP script load that emits a message, receives cancellation, and then rejects during native loading produces no partial load Evidence. The same cancelled request records source identity, the captured message, and cleanup status when native loading resolves. The cancelled client promise looks the same in both cases, so a caller that stopped waiting misses the load-specific record it would otherwise retrieve later. The new script is released, and the existing session and earlier script remain available. ### Reproduction Use the baseline and prepared dependencies/runtime specified below. From the repository root, run the following ES module on standard input. It uses SDK in-memory MCP transport, the registered production Frida handlers, the real manager and service, and a simulated native DeviceManager. It never attaches to a real process. The recording callback captures precisely what the production handler asks to retain, and the manager wrapper returns its result unchanged. The fixture starts a persistent session through MCP and loads an earlier script. The candidate load emits `marker` and waits inside native loading. For the cancellation cases, an awaited ping follows client cancellation before native loading settles, and the probe checks that the server-side signal was aborted. It runs cancellation/native-success, cancellation/native-failure, and native-failure-without-cancellation controls. It then checks cleanup, the earlier script, and a subsequent MCP status request. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { pathToFileURL } from 'node:url'; import { setImmediate } from 'node:timers/promises'; const from = p => import(pathToFileURL(`${process.cwd()}/${p}`).href); const { Client, InMemoryTransport } = await from('node_modules/@modelcontextprotocol/client/dist/index.mjs'); const { McpServer } = await from('node_modules/@modelcontextprotocol/server/dist/index.mjs'); const { FridaInstrumentationManager } = await from('dist/frida/FridaInstrumentationManager.js'); const { registerFridaTools } = await from('dist/server/registerFridaTools.js'); const { createToolResultDelivery } = await from('dist/server/toolResult.js'); const { silentLogger } = await from('dist/logger.js'); const source = "send('marker');"; async function probe(cancel, failNative) { let enter, release, rejectLoad, finishManager; const entered = new Promise(r => { enter = r; }); const held = new Promise((r,j) => { release = r; rejectLoad = j; }); const completed = new Promise(r => { finishManager = r; }); let detached = false; const scripts = []; const nativeSession = { pid: 512, detached: { connect() {}, disconnect() {} }, isDetached: () => detached, async detach() { detached = true; }, async createScript(text) { let destroyed = false, listener; const script = { get isDestroyed() { return destroyed; }, message: { connect(h) { listener = h; }, disconnect() { listener = undefined; } }, async load() { if (text === source) { listener?.({ type: 'send', payload: 'marker' }, null); enter(); await held; } }, async unload() { destroyed = true; }, }; scripts.push(script); return script; }, }; const device = { id: 'local', name: 'Fixture', type: 'local', async attach() { return nativeSession; }, async spawn() { return 512; }, async resume() {}, 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 originalLoad = manager.loadScript.bind(manager); manager.loadScript = async (id, input, signal) => { const result = await originalLoad(id, input, signal); if (input.source === source) finishManager({ signalAborted: signal?.aborted ?? false, ok: result.ok, errorTag: result.ok ? null : result.error._tag, hasPartial: !result.ok && result.error.partialObservation != null, hasScriptObservation: !result.ok && result.error.script != null, }); return result; }; const records = []; const server = new McpServer({ name: 'cancel-reproduction', version: '1' }); registerFridaTools(server, manager, silentLogger, createToolResultDelivery(undefined), e => { records.push(e); return { ok: true, value: null }; }); const client = new Client({ name: 'cancel-reproduction', version: '1' }); const [a,b] = InMemoryTransport.createLinkedPair(); await server.connect(b); await client.connect(a); let timer; try { const started = await client.callTool({ name: 'start_frida_session', arguments: { mode: 'attach', pid: 512, device_id: 'local' } }); const id = started.structuredContent.session_id; assert.equal(typeof id, 'string'); const original = await client.callTool({ name: 'load_frida_script', arguments: { session_id: id, source_kind: 'inline', source: 'void 0;' } }); assert.notEqual(original.isError, true); records.length = 0; const controller = new AbortController(); const pending = client.callTool({ name: 'load_frida_script', arguments: { session_id: id, source_kind: 'inline', source } }, { signal: controller.signal }).catch(e => ({ clientRejected: e.message })); await entered; if (cancel) { controller.abort(new Error('cancel during native load')); await client.ping(); } if (failNative) rejectLoad(new Error('native load rejected')); else release(); const observed = await Promise.race([completed, new Promise((_,j) => { timer = setTimeout(() => j(new Error('manager timeout')), 3000); })]); clearTimeout(timer); const clientResult = await pending; await setImmediate(); await client.ping(); const retained = records.filter(e => e.operation === 'load_frida_script').map(e => e.normalized_result); const status = await client.callTool({ name: 'frida_session_status', arguments: { session_id: id } }); const live = manager.status(id); assert.equal(observed.signalAborted, cancel); assert.equal(scripts[0].isDestroyed, false); assert.equal(scripts[1].isDestroyed, true); assert.equal(detached, false); assert.equal(live.scripts.length, 1); assert.notEqual(status.isError, true); assert.ok(live.messages.some(m => m.payload === 'marker')); if (cancel && !failNative) { assert.equal(observed.errorTag, 'AnalysisCancelledError'); assert.equal(retained.length, 1); assert.equal(retained[0].source_sha256, createHash('sha256').update(source).digest('hex')); assert.equal(retained[0].messages[0].payload, 'marker'); assert.equal(retained[0].script_cleanup, 'released'); } else { assert.equal(observed.errorTag, 'AnalysisCapabilityUnavailableError'); assert.equal(retained.length, 0); } console.log(JSON.stringify({ cancel, failNative, observed, clientRejected: clientResult.clientRejected ?? null, clientIsError: clientResult.isError ?? null, retainedLoadEvidence: retained, newScriptReleased: scripts[1].isDestroyed, originalScriptAvailable: !scripts[0].isDestroyed, sessionAvailable: !detached && live.scripts.length === 1, markerInSessionStatus: live.messages.some(m => m.payload === 'marker') })); } finally { clearTimeout(timer); release(); await client.close(); await server.close(); await manager.closeAll(); } } await probe(true, false); await probe(true, true); await probe(false, true); JS ``` ### Expected behavior After cancellation arrives during native loading, REA should finish the in-flight call, release the candidate script, and preserve the completed load observation as partial Evidence. That observation should retain the selected source identity, captured message, and cleanup outcome. The existing session and earlier script should remain usable. A native failure without cancellation should remain an ordinary provider failure. The cancellation and partial-fact contracts apply before the load has committed success, including this case where native loading later rejects. ### Impact and scope The cancellation/native-rejection intersection loses a load-specific partial Evidence record with source identity and cleanup state. This is consequential but bounded: the session and earlier script remain usable, and the aggregate status still contains the captured marker. Real-provider frequency is unknown. A later session status call retains the marker but does not recreate the original cancelled-load Evidence with its source digest and cleanup result. No available operation was demonstrated to recover that missing record after the candidate script was released. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Persistent Frida MCP session with simulated native bindings and an earlier loaded script; no real target process. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence - Expected source: `docs/frida-instrumentation.md:11` specifies native-call cancellation, partial observations, and existing-session load cleanup; `docs/mcp-contracts.md:442-448` describes retained cancellation Evidence and retrieval after the SDK stops waiting; `AGENTS.md`, Architecture and Cleanup, requires preserving partial facts; `tests/boundary/mcp/fridaLifecycleMcp.test.ts:587-629` verifies retained cancelled-load observations when native loading completes successfully. - Failure source: The executed MCP fixture and its output above. Source-derived explanation: `src/frida/FridaInstrumentationManager.ts:427-480` constructs a script observation only after native loading resolves and routes rejection through provider failure; `src/application/frida/FridaInstrumentationService.ts:193-202` converts only the script-load cancellation error into partial Evidence; `src/server/registerFridaTools.ts:142-182` handles the MCP request and lines 324-337 record only a valid partial Evidence value. The source explanation is derived from the pinned checkout. Runtime reproduction used the prepared compiled runtime and a simulated native provider at the production boundary. It does not establish actual Frida/device failure timing or real-world frequency. The in-memory recording callback proves that the handler requested no load record; a real Evidence database was not used. Observed in two local executions of the fixture, each containing one run of each case: - Cancellation followed by native success produced an aborted server signal and a manager `AnalysisCancelledError` tag carrying a script observation. The handler recorded one `load_frida_script` Evidence item with the expected source SHA-256, the `marker` message, `script_cleanup: "released"`, and `cleanup_error: null`. - Cancellation followed by native failure produced an aborted server signal and a manager `AnalysisCapabilityUnavailableError` without a script observation or partial observation. The handler recorded zero load Evidence items. - Native failure without cancellation produced an unaborted signal, an ordinary capability-unavailable manager error, a client-visible MCP error, and zero load Evidence items. In both cancelled cases, the SDK client rejected with `Error: cancel during native load`. The manager error tag above is an internal boundary observation; it is not a claimed availability-error reply seen by the cancelled client. In all three cases, the candidate script was destroyed, the earlier script stayed loaded, and the session status call succeeded. The aggregate session status still contained `marker`. The supported loss is the cancelled-load Evidence associating source identity and cleanup with that observation, rather than every copy of the message. The relevant stable fields printed by the cancellation/native-failure case were: ```json { "cancel": true, "failNative": true, "observed": { "signalAborted": true, "ok": false, "errorTag": "AnalysisCapabilityUnavailableError", "hasPartial": false, "hasScriptObservation": false }, "clientRejected": "Error: cancel during native load", "clientIsError": null, "retainedLoadEvidence": [], "newScriptReleased": true, "originalScriptAvailable": true, "sessionAvailable": true, "markerInSessionStatus": true } ``` ### Typed outcome or recovery expected Run the two cancellation cases with cancellation delivered before the native load settles. Each should produce a cancelled partial load Evidence record containing the source digest, completed marker, candidate script identity, and successful cleanup outcome. The candidate script must be released, while the earlier script and persistent session remain available. Check that the record is available for later Evidence retrieval even though the client promise already rejected. Keep the non-cancelled native-failure case as an ordinary provider failure, so restoration does not hide unrelated failures. The current fixture includes baseline-specific assertions about the missing record; change those expectations to the restored contract when using it as a regression check. ### 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. Related closed report #2544 concerns cancellation followed by successful native loading and retention as success. This report concerns native rejection after cancellation, with no partial per-load Evidence; the successful-load control now preserves cancellation Evidence. 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 in-memory MCP probe on a clean build: cancellation followed by a native rejection left no load record. The manager now reads the observed messages before releasing the candidate script and returns the cancelled-load error with that observation, the cleanup outcome and the sanitized native failure as a diagnostic, so both cancelled cases retain one load_frida_script record (source digest, marker, script_cleanup: released) while the uncancelled failure control stays an ordinary provider failure. Simulated native bindings only. Draft PR follows shortly.

<!-- gh-comment-id:6191422107 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your in-memory MCP probe on a clean build: cancellation followed by a native rejection left no load record. The manager now reads the observed messages before releasing the candidate script and returns the cancelled-load error with that observation, the cleanup outcome and the sanitized native failure as a diagnostic, so both cancelled cases retain one `load_frida_script` record (source digest, marker, `script_cleanup: released`) while the uncancelled failure control stays an ordinary provider failure. Simulated native bindings only. Draft PR follows shortly.
Author
Owner

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

Fixed on main in 048d70755e.

The load rejection branch discarded the per-script FIFO and bypassed cancellation Evidence. The manager now owns one capture object through native completion and awaited cleanup, snapshots it afterward, and retains both the script and capture when cleanup needs a retry. A native rejection accompanying cancellation is a sanitized diagnostic; the caller's cancellation reason stays primary. A failure already observed before cleanup keeps its precedence if cancellation arrives during cleanup. A rejected creation with no returned handle reports no_handle, without claiming no native work occurred.

Validation: 103 focused tests pass across manager/service, public CLI/MCP workflows and Frida contracts; final 38 manager source tests, npm run check, npm run docs:check, and normal commit/push hooks pass. Controls cover cancellation with native success/rejection, messages and truncation during cleanup, failed cleanup and retry, no returned handle, existing script/session preservation, and malformed-file provenance in retained MCP Evidence. These use controlled bindings; real Frida engine/device timing remains unverified.

<!-- gh-comment-id:6263251023 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on main in 048d70755ea9ca1d0d159ac78285d62a7ae00b21. The load rejection branch discarded the per-script FIFO and bypassed cancellation Evidence. The manager now owns one capture object through native completion and awaited cleanup, snapshots it afterward, and retains both the script and capture when cleanup needs a retry. A native rejection accompanying cancellation is a sanitized diagnostic; the caller's cancellation reason stays primary. A failure already observed before cleanup keeps its precedence if cancellation arrives during cleanup. A rejected creation with no returned handle reports `no_handle`, without claiming no native work occurred. Validation: 103 focused tests pass across manager/service, public CLI/MCP workflows and Frida contracts; final 38 manager source tests, `npm run check`, `npm run docs:check`, and normal commit/push hooks pass. Controls cover cancellation with native success/rejection, messages and truncation during cleanup, failed cleanup and retry, no returned handle, existing script/session preservation, and malformed-file provenance in retained MCP Evidence. These use controlled bindings; real Frida engine/device timing remains unverified.
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#1575
No description provided.