* feat: add Frida instrumentation support * fix: resolve Frida CI checks * test: cover Frida contract validation branches * docs: link Frida instrumentation guide * fix(frida): handle cancellation cleanup safely * fix(frida): address PR review findings * fix(frida): preserve resume and message observations * fix(ci): use canonical registry for rolldown * fix(frida): declare foreign code effects * fix(frida): retain failed startup resources for cleanup retries Keep failed startup session and spawn handles owned until detach, resume, and remote lease cleanup complete. Serialize retries and release each remote reference once. Await CLI provider cleanup while preserving the primary failure and any completed observations. Add regressions for retained startup resources, concurrent cleanup, shared remote leases, and CLI cleanup outcomes. Validation: 41 focused tests pass on the PR head and its latest-main integration; serial static and docs checks pass. Real Linux Frida 17.23.2 workflows and the native spawn/controlled-failure cleanup probe pass. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) --------- Co-authored-by: N0zoM1z0 <1705004398@qq.com>
199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
import { basename } from "node:path";
|
|
|
|
import { FridaInstrumentationService } from "../dist/application/frida/FridaInstrumentationService.js";
|
|
import { FridaInstrumentationManager } from "../dist/frida/FridaInstrumentationManager.js";
|
|
import { isValidFridaRemoteAddress } from "../dist/contracts/fridaRemoteAddress.js";
|
|
|
|
const options = parseArguments(process.argv.slice(2));
|
|
if (options.help) {
|
|
process.stdout.write(
|
|
"Usage: npm run verify:frida [-- --address HOST:PORT --pid PID [remote auth flags]]\n" +
|
|
"Without flags, verifies a local Node.js process created by this script.\n",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const manager = new FridaInstrumentationManager();
|
|
const service = new FridaInstrumentationService(manager);
|
|
const remote =
|
|
options.address === undefined
|
|
? undefined
|
|
: {
|
|
address: options.address,
|
|
...(options.token === undefined ? {} : { token: options.token }),
|
|
...(options.certificate === undefined
|
|
? {}
|
|
: { certificate: options.certificate }),
|
|
...(options.origin === undefined ? {} : { origin: options.origin }),
|
|
...(options.keepaliveInterval === undefined
|
|
? {}
|
|
: { keepaliveInterval: options.keepaliveInterval }),
|
|
};
|
|
|
|
try {
|
|
let targetProcess;
|
|
let input;
|
|
if (remote === undefined) {
|
|
input = {
|
|
mode: "spawn",
|
|
deviceId: "local",
|
|
program: process.execPath,
|
|
argv: ["-e", "setTimeout(() => {}, 1500)"],
|
|
source: {
|
|
sourceKind: "inline",
|
|
source: "send({ rea_frida_verification: 'ok', pid: Process.id });",
|
|
},
|
|
durationMs: 500,
|
|
};
|
|
targetProcess = { pid: undefined, name: basename(process.execPath) };
|
|
} else {
|
|
const inventory = await service.listProcesses({ remote });
|
|
if (!inventory.ok) throw new Error(inventory.error.message);
|
|
targetProcess = inventory.value.processes.find(
|
|
({ pid }) => pid === options.pid,
|
|
);
|
|
if (targetProcess === undefined)
|
|
throw new Error(
|
|
`Selected PID ${String(options.pid)} was not observed remotely`,
|
|
);
|
|
input = {
|
|
mode: "attach",
|
|
remote,
|
|
pid: options.pid,
|
|
source: {
|
|
sourceKind: "inline",
|
|
source: "send({ rea_frida_verification: 'ok', pid: Process.id });",
|
|
},
|
|
durationMs: 500,
|
|
};
|
|
}
|
|
|
|
const result = await service.instrument(input);
|
|
if (!result.ok) throw new Error(result.error.message);
|
|
const observed = result.value.evidence.normalized_result;
|
|
if (
|
|
typeof observed !== "object" ||
|
|
observed === null ||
|
|
Array.isArray(observed) ||
|
|
!Array.isArray(observed.messages)
|
|
)
|
|
throw new Error("Frida returned no message observation");
|
|
const messageObserved = observed.messages.some(
|
|
(message) =>
|
|
typeof message === "object" &&
|
|
message !== null &&
|
|
!Array.isArray(message) &&
|
|
message.type === "send" &&
|
|
typeof message.payload === "object" &&
|
|
message.payload !== null &&
|
|
!Array.isArray(message.payload) &&
|
|
message.payload.rea_frida_verification === "ok" &&
|
|
message.payload.pid === observed.pid,
|
|
);
|
|
if (!messageObserved)
|
|
throw new Error("Selected target did not return the Frida marker");
|
|
const report = {
|
|
ok: result.value.cleanupError === null,
|
|
provider: "frida",
|
|
host_platform: process.platform,
|
|
target_mode: remote === undefined ? "local_spawn" : "remote_attach",
|
|
target_pid: observed.pid,
|
|
target_name: targetProcess.name,
|
|
...(remote === undefined ? {} : { remote_target_observed: true }),
|
|
script_message_observed: true,
|
|
cleanup_error: result.value.cleanupError,
|
|
};
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
if (!report.ok) process.exitCode = 1;
|
|
} catch (cause) {
|
|
process.stderr.write(
|
|
`Frida real-provider verification failed: ${cause instanceof Error ? cause.message : String(cause)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await manager.closeAll().catch(() => undefined);
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
if (args.length === 1 && args[0] === "--help") return { help: true };
|
|
const values = parseOptionValues(args);
|
|
const address = values.get("--address");
|
|
const pidValue = values.get("--pid");
|
|
const pid = parseRemotePid(address, pidValue);
|
|
if (address === undefined) assertNoRemoteOptions(values);
|
|
const keepaliveInterval = parseKeepalive(values.get("--keepalive-interval"));
|
|
return {
|
|
help: false,
|
|
...(address === undefined ? {} : { address }),
|
|
...(pid === undefined ? {} : { pid }),
|
|
...(values.has("--token") ? { token: values.get("--token") } : {}),
|
|
...(values.has("--certificate")
|
|
? { certificate: values.get("--certificate") }
|
|
: {}),
|
|
...(values.has("--origin") ? { origin: values.get("--origin") } : {}),
|
|
...(keepaliveInterval === undefined ? {} : { keepaliveInterval }),
|
|
};
|
|
}
|
|
|
|
function parseOptionValues(args) {
|
|
const values = new Map();
|
|
const valueFlags = new Set([
|
|
"--address",
|
|
"--pid",
|
|
"--token",
|
|
"--certificate",
|
|
"--origin",
|
|
"--keepalive-interval",
|
|
]);
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const flag = args[index];
|
|
const value = args[index + 1];
|
|
if (
|
|
typeof flag !== "string" ||
|
|
!valueFlags.has(flag) ||
|
|
value === undefined ||
|
|
values.has(flag)
|
|
)
|
|
throw new Error(
|
|
"Expected no arguments for local-fixture verification, or --address HOST:PORT --pid PID and optional remote auth flags",
|
|
);
|
|
values.set(flag, value);
|
|
index += 1;
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function parseRemotePid(address, pidValue) {
|
|
if ((address === undefined) !== (pidValue === undefined))
|
|
throw new Error("Remote verification requires both --address and --pid");
|
|
if (address !== undefined && !isValidFridaRemoteAddress(address))
|
|
throw new Error(
|
|
"--address must be a host[:port] without credentials, path, query, or fragment",
|
|
);
|
|
const pid = pidValue === undefined ? undefined : Number(pidValue);
|
|
if (pid !== undefined && (!Number.isSafeInteger(pid) || pid <= 0))
|
|
throw new Error("--pid must be a positive process identifier");
|
|
return pid;
|
|
}
|
|
|
|
function assertNoRemoteOptions(values) {
|
|
for (const flag of [
|
|
"--token",
|
|
"--certificate",
|
|
"--origin",
|
|
"--keepalive-interval",
|
|
])
|
|
if (values.has(flag))
|
|
throw new Error("Remote connection options require --address and --pid");
|
|
}
|
|
|
|
function parseKeepalive(value) {
|
|
const interval = value === undefined ? undefined : Number(value);
|
|
if (
|
|
interval !== undefined &&
|
|
(!Number.isSafeInteger(interval) || interval <= 0)
|
|
)
|
|
throw new Error("--keepalive-interval must be a positive integer");
|
|
return interval;
|
|
}
|