rea/src/application/frida/FridaInstrumentationManager...

1061 lines
35 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type {
Device,
DeviceManager,
FridaMessage,
Process,
Script,
Session,
Signal,
} from "frida";
import { FridaInstrumentationManager } from "../../frida/FridaInstrumentationManager.js";
const signal = <Handler extends (...args: never[]) => void>() => {
let connected: Handler | undefined;
const value: Signal<Handler> & {
emit: (...args: Parameters<Handler>) => void;
} = {
connect: (handler) => {
connected = handler;
},
disconnect: (handler) => {
if (connected === handler) connected = undefined;
},
emit: (...args) => connected?.(...args),
};
return value;
};
class FakeScript implements Script {
readonly message =
signal<(message: FridaMessage, data: Buffer | null) => void>();
isDestroyed = false;
loadFailure: Error | undefined;
unloadFailureCount = 0;
unloadCalls = 0;
constructor(
readonly payload: unknown = { event: "loaded" },
readonly beforeLoad: () => Promise<void> = async () => {},
readonly description = "script loaded",
readonly stack = "",
) {}
async load(): Promise<void> {
await this.beforeLoad();
if (this.loadFailure !== undefined) throw this.loadFailure;
this.message.emit(
{
type: "send",
payload: this.payload,
description: this.description,
stack: this.stack,
},
null,
);
}
async unload(): Promise<void> {
this.unloadCalls += 1;
if (this.unloadFailureCount > 0) {
this.unloadFailureCount -= 1;
throw new Error("script unload failed");
}
if (this.isDestroyed) throw new Error("Script is destroyed");
this.isDestroyed = true;
}
}
class FakeSession implements Session {
readonly detached = signal<() => void>();
readonly scripts: FakeScript[] = [];
nextPayload: unknown = { event: "loaded" };
nextDescription = "script loaded";
nextStack = "";
nextLoadFailure: Error | undefined;
nextUnloadFailureCount = 0;
detachFailureCount = 0;
scriptLoadGate: Promise<void> | undefined;
onScriptLoadStarted: (() => void) | undefined;
private detachedState = false;
readonly pid = 512;
isDetached(): boolean {
return this.detachedState;
}
async createScript(): Promise<Script> {
const script = new FakeScript(
this.nextPayload,
async () => {
this.onScriptLoadStarted?.();
await this.scriptLoadGate;
},
this.nextDescription,
this.nextStack,
);
script.loadFailure = this.nextLoadFailure;
script.unloadFailureCount = this.nextUnloadFailureCount;
this.nextPayload = { event: "loaded" };
this.nextDescription = "script loaded";
this.nextStack = "";
this.nextLoadFailure = undefined;
this.nextUnloadFailureCount = 0;
this.scripts.push(script);
return script;
}
async detach(): Promise<void> {
if (this.detachFailureCount > 0) {
this.detachFailureCount -= 1;
throw new Error("session detach failed");
}
for (const script of this.scripts) script.isDestroyed = true;
this.markDetached();
}
markDetached(): void {
this.detachedState = true;
this.detached.emit();
}
markDetachedWithoutSignal(): void {
this.detachedState = true;
}
}
class FakeDevice implements Device {
readonly id = "local";
readonly name = "Local System";
readonly type = "local";
readonly process = { pid: 512, name: "fixture" } satisfies Process;
readonly session = new FakeSession();
resumed: number[] = [];
resumeFailureCount = 0;
readonly spawned: string[] = [];
async enumerateProcesses(): Promise<Process[]> {
return [this.process];
}
async getProcessByPid(): Promise<Process> {
return this.process;
}
async spawn(program: string): Promise<number> {
this.spawned.push(program);
return this.process.pid;
}
async resume(pid: number): Promise<void> {
if (this.resumeFailureCount > 0) {
this.resumeFailureCount -= 1;
throw new Error("target resume failed");
}
this.resumed.push(pid);
}
async attach(): Promise<Session> {
return this.session;
}
}
class FakeDeviceManager implements DeviceManager {
readonly device = new FakeDevice();
removedRemote: string[] = [];
removeRemoteFailureCount = 0;
removeRemoteFailureMessage = "remote device removal failed";
async enumerateDevices(): Promise<Device[]> {
return [this.device];
}
async getDeviceById(): Promise<Device> {
return this.device;
}
async addRemoteDevice(): Promise<Device> {
return this.device;
}
async removeRemoteDevice(address: string): Promise<void> {
if (this.removeRemoteFailureCount > 0) {
this.removeRemoteFailureCount -= 1;
throw new Error(this.removeRemoteFailureMessage);
}
this.removedRemote.push(address);
}
}
const startLocalAttachedSession = async (
bindings = new FakeDeviceManager(),
) => {
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
return { bindings, manager, started };
};
// eslint-disable-next-line max-lines-per-function -- related manager lifecycle cases share the same focused fake provider.
describe("FridaInstrumentationManager", () => {
it("retains a failed startup's spawn and remote lease until cleanup succeeds", async () => {
const bindings = new FakeDeviceManager();
bindings.device.attach = async () => {
throw new Error("attach failed after spawn");
};
bindings.device.resumeFailureCount = 2;
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
remote: { address: "127.0.0.1:27042" },
program: "/tmp/fixture",
});
if (started.ok) throw new Error("Attach unexpectedly succeeded");
expect(started.error.cleanupIncomplete).toBe(true);
expect(bindings.device.resumed).toEqual([]);
expect(bindings.removedRemote).toEqual([]);
await expect(manager.closeAll()).rejects.toThrow("cleanup was incomplete");
expect(bindings.device.resumed).toEqual([]);
expect(bindings.removedRemote).toEqual([]);
// A lookup could now select a different device with the same PID. Cleanup
// must use the original provider handle, not select the target again.
const replacement = new FakeDevice();
bindings.getDeviceById = async () => replacement;
bindings.addRemoteDevice = async () => replacement;
await Promise.all([manager.closeAll(), manager.closeAll()]);
expect(bindings.device.resumed).toEqual([512]);
expect(replacement.resumed).toEqual([]);
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
await manager.closeAll();
expect(bindings.device.resumed).toEqual([512]);
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
});
it("retains an acquired session when startup fails before registration", async () => {
const bindings = new FakeDeviceManager();
bindings.device.session.detached.connect = () => {
throw new Error("detached signal registration failed");
};
bindings.device.session.detachFailureCount = 1;
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
if (started.ok)
throw new Error("Signal registration unexpectedly succeeded");
expect(started.error.cleanupIncomplete).toBe(true);
expect(bindings.device.session.isDetached()).toBe(false);
expect(bindings.device.resumed).toEqual([]);
await manager.closeAll();
expect(bindings.device.session.isDetached()).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
});
it("does not release another session's remote lease when retrying failed startup removal", async () => {
const bindings = new FakeDeviceManager();
bindings.device.attach = async () => {
throw new Error("attach failed");
};
bindings.removeRemoteFailureCount = 1;
const remote = { address: "127.0.0.1:27042" };
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const failed = await manager.startSession({
mode: "spawn",
remote,
program: "/tmp/fixture",
});
if (failed.ok) throw new Error("Attach unexpectedly succeeded");
expect(failed.error.cleanupIncomplete).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
bindings.device.attach = async () => bindings.device.session;
const active = await manager.startSession({
mode: "attach",
remote,
pid: 512,
});
if (!active.ok) throw active.error;
const removedWhileSessionAttached: boolean[] = [];
const remove = bindings.removeRemoteDevice.bind(bindings);
bindings.removeRemoteDevice = async (address) => {
removedWhileSessionAttached.push(!bindings.device.session.isDetached());
await remove(address);
};
await manager.closeAll();
expect(removedWhileSessionAttached).toEqual([false]);
expect(bindings.removedRemote).toEqual([remote.address]);
expect(bindings.device.resumed).toEqual([512]);
});
it("loads instrumentation before resuming a spawned target", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
expect(started.value.state).toBe("paused");
expect(bindings.device.resumed).toEqual([]);
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({ event: 'loaded' });",
});
expect(loaded.ok).toBe(true);
if (!loaded.ok) return;
expect(loaded.value.messages).toContainEqual(
expect.objectContaining({
type: "send",
payload: { event: "loaded" },
}),
);
expect(loaded.value.sourceSha256).toMatch(/^[a-f0-9]{64}$/u);
expect(bindings.device.resumed).toEqual([]);
const resumed = await manager.resumeSession(started.value.sessionId);
expect(resumed.ok).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
expect(manager.status(started.value.sessionId)?.targetResumeObserved).toBe(
true,
);
expect(manager.status(started.value.sessionId)?.state).toBe("running");
await manager.closeAll();
expect(bindings.device.session.isDetached()).toBe(true);
});
it("removes a remote device after process discovery", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", token: "secret-token" },
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const listed = await manager.listProcesses({
remote: { address: "127.0.0.1:27042", token: "secret-token" },
});
expect(listed.ok).toBe(true);
if (listed.ok) {
expect(listed.value.processes[0]?.pid).toBe(512);
expect(listed.value.cleanupError).toBeNull();
}
expect(bindings.removedRemote).toEqual([]);
await manager.closeSession(started.value.sessionId);
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
await manager.closeAll();
});
it("preserves remote discovery facts when cleanup fails and retries cleanup", async () => {
const bindings = new FakeDeviceManager();
bindings.removeRemoteFailureCount = 1;
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const listed = await manager.listProcesses({
remote: { address: "127.0.0.1:27042" },
});
expect(listed.ok).toBe(true);
if (listed.ok) {
expect(listed.value.processes).toEqual([
{ pid: 512, name: "fixture", identifier: null },
]);
expect(listed.value.cleanupError).toContain(
"remote device removal failed",
);
}
await manager.closeAll();
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
});
it("does not expose remote credentials in shutdown cleanup failures", async () => {
const token = "synthetic-close-token";
const certificate = "synthetic-close-certificate";
const bindings = new FakeDeviceManager();
bindings.removeRemoteFailureCount = 2;
bindings.removeRemoteFailureMessage = `${token} ${certificate}`;
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", token, certificate },
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
let failure = "";
try {
await manager.closeAll();
} catch (cause: unknown) {
failure =
cause instanceof AggregateError
? cause.errors.map((entry: unknown) => String(entry)).join("; ")
: String(cause);
}
expect(failure).not.toBe("");
expect(failure).not.toContain(token);
expect(failure).not.toContain(certificate);
});
it("unloads scripts, resumes a paused spawn, and detaches without killing it", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(true);
expect(bindings.device.session.scripts[0]?.isDestroyed).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
expect(bindings.device.session.isDetached()).toBe(true);
});
it("retains a failed script handle until a later cleanup retry releases it", async () => {
const bindings = new FakeDeviceManager();
bindings.device.session.nextLoadFailure = new Error("script load failed");
bindings.device.session.nextUnloadFailureCount = 1;
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(false);
if (!loaded.ok) expect(loaded.error.cleanupIncomplete).toBe(true);
expect(manager.status(started.value.sessionId)?.scripts).toHaveLength(1);
expect(bindings.device.session.scripts[0]?.unloadCalls).toBe(1);
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(true);
expect(bindings.device.session.scripts[0]?.unloadCalls).toBe(2);
expect(bindings.device.session.scripts[0]?.isDestroyed).toBe(true);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});
it("closes a session when its target already destroyed the script", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
const script = bindings.device.session.scripts[0];
expect(script).toBeDefined();
if (script === undefined) return;
script.isDestroyed = true;
bindings.device.session.markDetached();
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(true);
expect(script.unloadCalls).toBe(0);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});
it("does not resume a paused spawn after its parent session detached", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
bindings.device.session.markDetachedWithoutSignal();
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(true);
expect(bindings.device.resumed).toEqual([]);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});
it("uses successful parent-session detach to release a script after unload fails", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
const script = bindings.device.session.scripts[0];
expect(script).toBeDefined();
if (script === undefined) return;
script.unloadFailureCount = 1;
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(true);
expect(script.isDestroyed).toBe(true);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});
it("keeps a paused spawn paused when script unload and session detach fail", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
const script = bindings.device.session.scripts[0];
expect(script).toBeDefined();
if (script === undefined) return;
script.unloadFailureCount = 1;
bindings.device.session.detachFailureCount = 1;
const closed = await manager.closeSession(started.value.sessionId);
expect(closed.ok).toBe(false);
expect(bindings.device.resumed).toEqual([]);
expect(manager.status(started.value.sessionId)?.scripts).toHaveLength(1);
});
it("retries resuming a spawn after detach succeeded but resume failed", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
deviceId: "local",
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
bindings.device.resumeFailureCount = 1;
const firstClose = await manager.closeSession(started.value.sessionId);
expect(firstClose.ok).toBe(false);
expect(bindings.device.session.isDetached()).toBe(true);
expect(manager.status(started.value.sessionId)).toBeDefined();
const retry = await manager.closeSession(started.value.sessionId);
expect(retry.ok).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
expect(manager.status(started.value.sessionId)).toBeUndefined();
});
it("keeps a remote device while a spawned target resume is pending", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "spawn",
remote: { address: "127.0.0.1:27042" },
program: "/tmp/fixture",
});
expect(started.ok).toBe(true);
if (!started.ok) return;
bindings.device.resumeFailureCount = 1;
const firstClose = await manager.closeSession(started.value.sessionId);
expect(firstClose.ok).toBe(false);
expect(bindings.removedRemote).toEqual([]);
const retry = await manager.closeSession(started.value.sessionId);
expect(retry.ok).toBe(true);
expect(bindings.device.resumed).toEqual([512]);
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
});
it("keeps a remote device while script and session cleanup are pending", async () => {
const bindings = new FakeDeviceManager();
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042" },
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('ready');",
});
expect(loaded.ok).toBe(true);
const script = bindings.device.session.scripts[0];
expect(script).toBeDefined();
if (script === undefined) return;
script.unloadFailureCount = 1;
bindings.device.session.detachFailureCount = 1;
const firstClose = await manager.closeSession(started.value.sessionId);
expect(firstClose.ok).toBe(false);
expect(bindings.removedRemote).toEqual([]);
const retry = await manager.closeSession(started.value.sessionId);
expect(retry.ok).toBe(true);
expect(script.isDestroyed).toBe(true);
expect(bindings.removedRemote).toEqual(["127.0.0.1:27042"]);
});
});
it("does not release a newer remote lease when retrying an older session close", async () => {
const bindings = new FakeDeviceManager();
bindings.removeRemoteFailureCount = 1;
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
const remote = { address: "127.0.0.1:27042" };
const first = await manager.startSession({
mode: "attach",
remote,
pid: 512,
});
expect(first.ok).toBe(true);
if (!first.ok) return;
const failedClose = await manager.closeSession(first.value.sessionId);
expect(failedClose.ok).toBe(false);
const second = await manager.startSession({
mode: "attach",
remote,
pid: 512,
});
expect(second.ok).toBe(true);
if (!second.ok) return;
const retry = await manager.closeSession(first.value.sessionId);
expect(retry.ok).toBe(true);
expect(bindings.removedRemote).toEqual([]);
expect(manager.status(second.value.sessionId)).toBeDefined();
const secondClose = await manager.closeSession(second.value.sessionId);
expect(secondClose.ok).toBe(true);
expect(bindings.removedRemote).toEqual([remote.address]);
});
it("attributes messages to their producing script and returns each load's messages", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
bindings.device.session.nextPayload = { tag: "A" };
const first = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({tag: 'A'});",
});
expect(first.ok).toBe(true);
if (!first.ok) return;
bindings.device.session.nextPayload = { tag: "B" };
const second = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({tag: 'B'});",
});
expect(second.ok).toBe(true);
if (!second.ok) return;
expect(first.value.messages).toEqual([
expect.objectContaining({
script_id: first.value.scriptId,
payload: { tag: "A" },
}),
]);
expect(second.value.messages).toEqual([
expect.objectContaining({
script_id: second.value.scriptId,
payload: { tag: "B" },
}),
]);
expect(manager.status(started.value.sessionId)?.messages).toEqual([
expect.objectContaining({ script_id: first.value.scriptId }),
expect.objectContaining({ script_id: second.value.scriptId }),
]);
await manager.closeAll();
});
it("keeps an active script message bucket after its last message is evicted", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
const session = bindings.device.session;
expect(started.ok).toBe(true);
if (!started.ok) return;
session.nextPayload = { tag: "A" };
const first = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({tag: 'A'});",
});
expect(first.ok).toBe(true);
if (!first.ok) return;
let finishSecondLoad: (() => void) | undefined;
let markSecondLoadStarted: (() => void) | undefined;
const secondLoadGate = new Promise<void>((resolve) => {
finishSecondLoad = resolve;
});
const secondLoadStarted = new Promise<void>((resolve) => {
markSecondLoadStarted = resolve;
});
session.nextPayload = { tag: "B2" };
session.scriptLoadGate = secondLoadGate;
session.onScriptLoadStarted = () => markSecondLoadStarted?.();
const secondLoading = manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({tag: 'B2'});",
});
await secondLoadStarted;
const secondScript = session.scripts[1];
expect(secondScript).toBeDefined();
if (secondScript === undefined) return;
secondScript.message.emit({ type: "send", payload: { tag: "B1" } }, null);
const firstScript = session.scripts[0];
expect(firstScript).toBeDefined();
if (firstScript === undefined) return;
for (let index = 0; index < 800; index += 1)
firstScript.message.emit(
{ type: "send", payload: "x".repeat(32_700) },
null,
);
finishSecondLoad?.();
const second = await secondLoading;
expect(second.ok).toBe(true);
if (second.ok) {
expect(second.value.messages).toContainEqual(
expect.objectContaining({
script_id: second.value.scriptId,
payload: { tag: "B2" },
}),
);
expect(second.value.messagesTruncated).toBe(true);
}
expect(manager.status(started.value.sessionId)?.messages).toContainEqual(
expect.objectContaining({
script_id: second.ok ? second.value.scriptId : "",
payload: { tag: "B2" },
}),
);
await manager.closeAll();
});
it("retains the newest contiguous messages across repeated budget evictions", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
bindings.device.session.nextPayload = { index: -1 };
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send({index: -1});",
});
expect(loaded.ok).toBe(true);
const script = bindings.device.session.scripts[0];
expect(script).toBeDefined();
if (script === undefined) return;
// About 500 of these fit the 16 MiB budget, so 2,000 sends evict and
// compact the queue several times.
const filler = "x".repeat(32_000);
for (let index = 0; index < 2_000; index += 1)
script.message.emit({ type: "send", payload: { index, filler } }, null);
const status = manager.status(started.value.sessionId);
expect(status?.messagesTruncated).toBe(true);
const indexes = (status?.messages ?? []).map(
(message) => (message as { payload: { index: number } }).payload.index,
);
expect(indexes.length).toBeGreaterThan(400);
expect(indexes.at(-1)).toBe(1_999);
expect(indexes).toEqual(
Array.from(
{ length: indexes.length },
(_, offset) => 2_000 - indexes.length + offset,
),
);
await manager.closeAll();
});
it("serializes a final remote release against a new same-endpoint session", async () => {
const bindings = new FakeDeviceManager();
let enteredRemoval: (() => void) | undefined;
const removalEntered = new Promise<void>((resolve) => {
enteredRemoval = resolve;
});
let finishRemoval: (() => void) | undefined;
const removalGate = new Promise<void>((resolve) => {
finishRemoval = resolve;
});
bindings.removeRemoteDevice = async (address) => {
enteredRemoval?.();
await removalGate;
bindings.removedRemote.push(address);
};
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const remote = { address: "127.0.0.1:27042", token: "same-token" };
const first = await manager.startSession({
mode: "attach",
remote,
pid: 512,
});
expect(first.ok).toBe(true);
if (!first.ok) return;
const closing = manager.closeSession(first.value.sessionId);
await removalEntered;
let secondStarted = false;
const secondPromise = manager
.startSession({ mode: "attach", remote, pid: 512 })
.then((result) => {
secondStarted = true;
return result;
});
await Promise.resolve();
expect(secondStarted).toBe(false);
finishRemoval?.();
expect((await closing).ok).toBe(true);
const second = await secondPromise;
expect(second.ok).toBe(true);
if (second.ok) await manager.closeSession(second.value.sessionId);
expect(bindings.removedRemote).toEqual([
"127.0.0.1:27042",
"127.0.0.1:27042",
]);
});
it("redacts remote credentials and maps a missing optional binding", async () => {
const secret = "synthetic-secret-token";
const remoteManager = new FakeDeviceManager();
remoteManager.addRemoteDevice = async () => {
throw new Error(`authorization rejected: ${secret}`);
};
const remote = new FridaInstrumentationManager({
deviceManager: remoteManager,
});
const failed = await remote.listProcesses({
remote: {
address: "wss://host/path?token=synthetic-url-token",
token: secret,
},
});
expect(failed.ok).toBe(false);
if (!failed.ok) {
expect(failed.error.message).not.toContain(secret);
expect(failed.error.message).not.toContain("synthetic-url-token");
}
const missing = new FridaInstrumentationManager({
deviceManagerLoader: async () => {
throw new Error("Cannot find package 'frida'");
},
});
const unavailable = await missing.listDevices();
expect(unavailable.ok).toBe(false);
if (!unavailable.ok)
expect(unavailable.error.message).toContain("Cannot find package 'frida'");
});
it("redacts remote credentials echoed by a target script", async () => {
const token = "synthetic-token-from-auth";
const certificate = "synthetic-certificate-from-auth";
const bindings = new FakeDeviceManager();
bindings.device.session.nextPayload = { token, certificate };
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
const started = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", token, certificate },
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('credential payload');",
});
expect(loaded.ok).toBe(true);
if (loaded.ok) {
const serialized = JSON.stringify(loaded.value.messages);
expect(serialized).not.toContain(token);
expect(serialized).not.toContain(certificate);
expect(serialized).toContain("[redacted]");
}
await manager.closeAll();
});
it("bounds oversized Frida message metadata before retaining it", async () => {
const bindings = new FakeDeviceManager();
bindings.device.session.nextDescription = "d".repeat(1_000_000);
bindings.device.session.nextStack = "s".repeat(1_000_000);
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('metadata');",
});
expect(loaded.ok).toBe(true);
if (loaded.ok) {
expect(loaded.value.messagesTruncated).toBe(true);
const message = loaded.value.messages[0];
expect(message).toMatchObject({
description_truncated: true,
stack_truncated: true,
});
expect(JSON.stringify(message).length).toBeLessThan(100_000);
}
await manager.closeAll();
});
it("serializes script loading against concurrent close and unloads the loaded script", async () => {
const bindings = new FakeDeviceManager();
let finishScriptLoad: (() => void) | undefined;
const scriptLoadGate = new Promise<void>((resolve) => {
finishScriptLoad = resolve;
});
let markScriptLoadStarted: (() => void) | undefined;
const scriptLoadStarted = new Promise<void>((resolve) => {
markScriptLoadStarted = resolve;
});
bindings.device.session.scriptLoadGate = scriptLoadGate;
bindings.device.session.onScriptLoadStarted = () => markScriptLoadStarted?.();
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
const loading = manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('loaded');",
});
await scriptLoadStarted;
let closed = false;
const closing = manager.closeSession(started.value.sessionId);
void closing.then(() => {
closed = true;
});
expect(manager.closeSession(started.value.sessionId)).toBe(closing);
await Promise.resolve();
expect(closed).toBe(false);
finishScriptLoad?.();
expect((await loading).ok).toBe(true);
expect((await closing).ok).toBe(true);
expect(bindings.device.session.scripts[0]?.isDestroyed).toBe(true);
expect(bindings.device.session.isDetached()).toBe(true);
});
it("names the unknown session or script ID it rejects", async () => {
const { manager, started } = await startLocalAttachedSession();
if (!started.ok) throw started.error;
const missing = "00000000-0000-4000-8000-000000000009";
const sessionIssue = {
path: ["session_id"],
reason: "invalid_value",
message: `No open Frida session ${missing}; start one with start_frida_session.`,
};
for (const result of [
await manager.closeSession(missing),
await manager.resumeSession(missing),
await manager.loadScript(missing, { sourceKind: "inline", source: "" }),
await manager.unloadScript(missing, missing),
])
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError", issues: [sessionIssue] },
});
expect(
await manager.unloadScript(started.value.sessionId, missing),
).toMatchObject({
ok: false,
error: {
issues: [
{
path: ["script_id"],
reason: "invalid_value",
message: `No loaded Frida script ${missing} in this session.`,
},
],
},
});
await manager.closeAll();
});