Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
344fc71
fix(frida): drain admitted operations and honor queued cancellation
rohitg00 Oct 12, 2026
32af2ab
fix(frida): settle cancelled loads before queued resume
rohitg00 Oct 13, 2026
f01062f
chore: merge upstream main into Frida lifecycle fixes
rohitg00 Oct 13, 2026
68bf1ae
test(frida): share lifecycle fixtures without duplicating assertions
rohitg00 Oct 13, 2026
5792141
fix(frida): preserve operation admission before session close
rohitg00 Oct 13, 2026
16d063c
chore: merge upstream CI fixes into Frida lifecycle branch
rohitg00 Oct 13, 2026
235517c
chore: integrate upstream CI test correction
rohitg00 Oct 13, 2026
443b404
test(apple): bound cold archive fixture compilation
rohitg00 Oct 13, 2026
79ba2a8
chore: merge upstream CI and provenance test fixes
rohitg00 Oct 13, 2026
1c53762
fix(frida): retain cancelled script creation evidence
rohitg00 Oct 13, 2026
e439438
test(release): scope preflight schema to selected jobs
rohitg00 Oct 13, 2026
97ec5a5
test(ci): include cold-cache Apple fixture timeout correction
rohitg00 Oct 13, 2026
bc6f334
test(ci): merge shared release and Apple fixture fixes
rohitg00 Oct 13, 2026
08e5d8f
chore: merge upstream release test correction
rohitg00 Oct 13, 2026
4f12f5e
chore: merge upstream CI and dyld fixture corrections
rohitg00 Oct 13, 2026
37c055e
chore(ci): merge upstream baseline fixes
morluto Oct 13, 2026
6a7b4c6
fix(process): report no remaining time after a provider deadline fires
kaoru0822-kitauji Oct 13, 2026
d41542d
chore(ci): retain concurrent upstream baseline synchronization
morluto Oct 13, 2026
99eafa5
fix(frida): preserve lifecycle fixes with captured main deadline owner
morluto Oct 13, 2026
6267bee
fix(comparison): recheck historical calls-only changes (#2223)
dakjdakd Oct 13, 2026
abc535d
fix(ci): isolate Linux Hopper process fixture inventory
morluto Oct 13, 2026
41aa691
fix(android): make DEX descriptor pattern portable
morluto Oct 13, 2026
2c5912c
fix: repair the static-check failures on the default branch (#2895)
XiaoBinGan Oct 13, 2026
ae23cd6
chore: refresh frida lifecycle PR with main
codex Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/frida-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ For remote devices, pass a host with an optional port (`host[:port]`; bracket IP

MCP exposes `list_frida_devices`, `list_frida_processes`, `start_frida_session`, `load_frida_script`, `resume_frida_session`, `unload_frida_script`, `frida_session_status`, and `close_frida_session`. A spawned process starts paused so scripts can load first. Closing unloads session scripts and detaches. If a spawn remains paused, REA resumes it only after script cleanup or session detach confirms the instrumentation is released; if both cleanup paths fail, the target stays paused and the cleanup error is retained. REA never kills the target. MCP sessions end with the server process. The one-shot CLI command `instrument-with-frida` accepts one inline script or script file, observes messages for a bounded duration, and returns Evidence with the source hash and captured messages.

Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. A failed cleanup remains reported rather than treating an unregistered session as fully released.
Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. Server shutdown stops admitting new Frida operations and waits for admitted startup, discovery, and queued work before releasing their resources. A failed cleanup remains reported and can be retried by closing the server again, rather than treating an unregistered session as fully released.

Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. A cancelled script load on an existing session releases only its new script before queued operations can run; the session and previously loaded scripts remain available. Once the provider commits a successful load, later request cancellation does not unload that script; use `unload_frida_script` to release it. If script cleanup fails, the script remains owned and a paused target cannot resume until the pending instrumentation is released. Queued script loads, resumes, and unloads check cancellation before dispatching native effects. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.

The [Frida Core API](https://frida.re/docs/frida-core-api/) documents device management, sessions, and script loading; the [JavaScript API](https://frida.re/docs/javascript-api/) documents target-side instrumentation. [Luma](https://luma.frida.re/) is Frida's standalone GUI; REA does not automate or control it. `npm run verify:frida` requires a Linux host with the optional Frida binding and uses a short-lived local Node.js fixture created by the verifier. Add `-- --address HOST:PORT --pid PID` to run the separate remote attach lane against a caller-selected target. Mock tests do not establish real device/server compatibility.
22 changes: 22 additions & 0 deletions src/application/frida/FridaInstrumentationManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,28 @@ it("serializes script loading against concurrent close and unloads the loaded sc
expect(bindings.device.session.isDetached()).toBe(true);
});

it("drains startup when the provider loader initiates shutdown", async () => {
const bindings = new FakeDeviceManager();
let closing: Promise<void> | undefined;
const manager = new FridaInstrumentationManager({
deviceManagerLoader: async () => {
closing = manager.closeAll();
return bindings;
},
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
if (!started.ok) throw started.error;
expect(closing).toBeDefined();
await closing;
expect(bindings.device.session.isDetached()).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});

it("names the unknown session or script ID it rejects", async () => {
const { manager, started } = await startLocalAttachedSession();
if (!started.ok) throw started.error;
Expand Down
23 changes: 21 additions & 2 deletions src/application/frida/FridaInstrumentationPort.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
AnalysisError,
AnalysisErrorOptions,
} from "../../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { OptionalProviderLoadFailure } from "../OptionalObservationProviders.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { Result } from "../../domain/result.js";
Expand Down Expand Up @@ -60,6 +64,15 @@ export interface FridaScriptObservation {
readonly messagesTruncated: boolean;
}

export class FridaScriptLoadCancelledError extends AnalysisCancelledError {
constructor(
readonly script: FridaScriptObservation,
options?: AnalysisErrorOptions,
) {
super("load_frida_script", options);
}
}

export interface FridaSessionStatus extends FridaSessionObservation {
/** True only after REA has successfully resumed this spawned target. */
readonly targetResumeObserved: boolean;
Expand Down Expand Up @@ -100,14 +113,20 @@ export interface FridaInstrumentationPort {
startSession(
input: StartFridaSessionInput,
): Promise<Result<FridaSessionObservation, AnalysisError>>;
/** Own cancellation and new-script cleanup before returning a final load outcome. */
loadScript(
sessionId: string,
source: FridaScriptSource,
signal?: AbortSignal,
): Promise<Result<FridaScriptObservation, AnalysisError>>;
resumeSession(sessionId: string): Promise<Result<null, AnalysisError>>;
resumeSession(
sessionId: string,
signal?: AbortSignal,
): Promise<Result<null, AnalysisError>>;
unloadScript(
sessionId: string,
scriptId: string,
signal?: AbortSignal,
): Promise<Result<null, AnalysisError>>;
status(sessionId: string): FridaSessionStatus | undefined;
closeSession(
Expand Down
72 changes: 72 additions & 0 deletions src/application/frida/FridaInstrumentationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from "node:path";

import { expect, it } from "vitest";

import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
import type { FridaInstrumentationPort } from "./FridaInstrumentationPort.js";
import { FridaInstrumentationService } from "./FridaInstrumentationService.js";
import {
Expand Down Expand Up @@ -569,3 +570,74 @@ it("cleans up a session and returns typed cancellation after a pending start", a
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
expect(closed).toEqual(["00000000-0000-4000-8000-000000000001"]);
});

it("retains a cancelled provider load's source and messages in one-shot Evidence", async () => {
const controller = new AbortController();
const script = {
...createScriptObservation(),
messages: [{ type: "send", payload: "cancelled-load" }],
};
const service = new FridaInstrumentationService(
createProvider({
async loadScript() {
controller.abort(new Error("cancel during native load"));
return {
ok: false,
error: new FridaScriptLoadCancelledError(script, {
cause: controller.signal.reason,
}),
};
},
}),
);
const result = await service.instrument(
{
mode: "spawn",
program: "/fixture",
source: { sourceKind: "inline", source: "send('cancelled-load')" },
durationMs: 0,
},
controller.signal,
);
if (result.ok) throw new Error("Cancelled instrumentation succeeded");
expect(result.error.partialObservation).toMatchObject({
normalized_result: {
source_kind: "inline",
source_sha256: script.sourceSha256,
messages: script.messages,
instrumentation_cleanup: "released",
},
});
});

it("preserves a provider-committed load when cancellation arrives during result delivery", async () => {
const controller = new AbortController();
let unloads = 0;
const script = createScriptObservation();
const service = new FridaInstrumentationService(
createProvider({
loadScript() {
const completed = Promise.resolve({ ok: true as const, value: script });
void completed.then(() =>
controller.abort(new Error("late cancellation")),
);
return completed;
},
async unloadScript() {
unloads += 1;
return { ok: true, value: null };
},
}),
);
const loaded = await service.loadScript(
createSession().sessionId,
{ sourceKind: "inline", source: "send('ready')" },
controller.signal,
);
expect(controller.signal.aborted).toBe(true);
if (!loaded.ok) throw loaded.error;
expect(loaded.value.normalized_result).toMatchObject({
script_id: script.scriptId,
});
expect(unloads).toBe(0);
});
48 changes: 42 additions & 6 deletions src/application/frida/FridaInstrumentationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
import type {
FridaDeviceSelector,
FridaInstrumentationPort,
Expand Down Expand Up @@ -181,8 +182,17 @@ export class FridaInstrumentationService {
async loadScript(
sessionId: string,
source: FridaScriptSource,
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> {
const loaded = await this.#provider.loadScript(sessionId, source);
if (signal?.aborted)
return err(
new AnalysisCancelledError("load_frida_script", {
cause: signal.reason,
}),
);
const loaded = await this.#provider.loadScript(sessionId, source, signal);
if (!loaded.ok && loaded.error instanceof FridaScriptLoadCancelledError)
return err(cancelledScriptEvidence(sessionId, loaded.error));
return loaded.ok
? ok(
fridaEvidence("load_frida_script", scriptOutput(loaded.value), {
Expand All @@ -192,12 +202,12 @@ export class FridaInstrumentationService {
: loaded;
}

resumeSession(sessionId: string) {
return this.#provider.resumeSession(sessionId);
resumeSession(sessionId: string, signal?: AbortSignal) {
return this.#provider.resumeSession(sessionId, signal);
}

unloadScript(sessionId: string, scriptId: string) {
return this.#provider.unloadScript(sessionId, scriptId);
unloadScript(sessionId: string, scriptId: string, signal?: AbortSignal) {
return this.#provider.unloadScript(sessionId, scriptId, signal);
}

status(sessionId: string): FridaSessionStatus | undefined {
Expand Down Expand Up @@ -243,14 +253,19 @@ export class FridaInstrumentationService {
const loaded = await this.#provider.loadScript(
started.value.sessionId,
input.source,
signal,
);
let scriptObservation: FridaScriptObservation | undefined;
if (loaded.ok) scriptObservation = loaded.value;
else if (loaded.error instanceof FridaScriptLoadCancelledError)
scriptObservation = loaded.error.script;
if (signal?.aborted)
return this.#cancelSession(
"instrument_with_frida",
started.value,
input,
signal.reason,
loaded.ok ? loaded.value : undefined,
scriptObservation,
);
if (!loaded.ok) {
const closed = await this.#provider.closeSession(started.value.sessionId);
Expand All @@ -267,6 +282,7 @@ export class FridaInstrumentationService {
if (started.value.mode === "spawn") {
const resumed = await this.#provider.resumeSession(
started.value.sessionId,
signal,
);
if (signal?.aborted)
return this.#cancelSession(
Expand Down Expand Up @@ -462,3 +478,23 @@ const sessionStatusOutput = (status: FridaSessionStatus) => ({
messages: status.messages,
messages_truncated: status.messagesTruncated,
});

const cancelledScriptEvidence = (
sessionId: string,
error: FridaScriptLoadCancelledError,
): AnalysisCancelledError =>
new AnalysisCancelledError(error.operation, {
cause: error.cause,
cleanup: error.cleanup,
partialObservation: fridaEvidence(
error.operation,
{
...scriptOutput(error.script),
cleanup_error: error.cleanup?.reason ?? null,
script_cleanup: error.cleanupIncomplete
? "incomplete_or_unknown"
: "released",
},
{ session_id: sessionId },
),
});
Loading
Loading