mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(frida): reject an unreadable script file before starting a session (#2211)
* fix(frida): reject an unreadable script file before starting a session The shared instrument_with_frida workflow started the Frida session, which attaches or spawns the target, before the provider read a file script source. A missing script file then failed loadScript with a bare invalid_request, and session cleanup resumed the paused spawn, so the target ran without instrumentation. Check that the selected file is a readable regular file first and report the path, or access_denied for EACCES/EPERM, before any device effect. Fixes #2209 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frida): let cancellation win over the script file check An already-aborted request with an unreadable file source returned the input or access-denied error instead of AnalysisCancelledError. Check the signal before the script file preflight and again after it, so file sources keep the existing cancellation semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(frida): keep the pre-start checks out of instrument Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae412654d5
commit
c9aecde580
2 files changed
+127
No files matched your search
@@ -1,3 +1,6 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import type { FridaInstrumentationPort } from "./FridaInstrumentationPort.js";
|
||||
@@ -102,6 +105,66 @@ it.each(["startSession", "instrument"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[join(tmpdir(), "rea-missing-frida-script.js"), "no file exists there"],
|
||||
[tmpdir(), "it is not a regular file"],
|
||||
])(
|
||||
"rejects the unreadable script file %s before starting Frida",
|
||||
async (path, reason) => {
|
||||
let starts = 0;
|
||||
const provider = createProvider({
|
||||
async startSession() {
|
||||
starts += 1;
|
||||
return { ok: true, value: createSession() };
|
||||
},
|
||||
});
|
||||
const service = new FridaInstrumentationService(provider);
|
||||
|
||||
const result = await service.instrument({
|
||||
mode: "spawn",
|
||||
deviceId: "local",
|
||||
program: "/tmp/fixture",
|
||||
source: { sourceKind: "file", path },
|
||||
durationMs: 1,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect((result.error as AnalysisInputError).issues).toEqual([
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: `Cannot read Frida script file ${path}: ${reason}.`,
|
||||
},
|
||||
]);
|
||||
expect(starts).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it("reports cancellation before checking a file script source", async () => {
|
||||
const service = new FridaInstrumentationService(createProvider());
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error("cancelled before call"));
|
||||
|
||||
const result = await service.instrument(
|
||||
{
|
||||
mode: "spawn",
|
||||
deviceId: "local",
|
||||
program: "/tmp/fixture",
|
||||
source: {
|
||||
sourceKind: "file",
|
||||
path: join(tmpdir(), "rea-missing-frida-script.js"),
|
||||
},
|
||||
durationMs: 1,
|
||||
},
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
});
|
||||
|
||||
it("does not resume a spawned target when cancellation arrives during script loading", async () => {
|
||||
const controller = new AbortController();
|
||||
let resumes = 0;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonObjectSchema, jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import type {
|
||||
@@ -19,6 +23,64 @@ import type {
|
||||
StartFridaSessionInput,
|
||||
} from "./FridaInstrumentationPort.js";
|
||||
|
||||
const unreadableScriptFile = async (
|
||||
source: FridaScriptSource,
|
||||
): Promise<AnalysisError | undefined> => {
|
||||
if (source.sourceKind !== "file") return undefined;
|
||||
let reason: string;
|
||||
let cause: unknown;
|
||||
try {
|
||||
if (!(await stat(source.path)).isFile())
|
||||
reason = "it is not a regular file";
|
||||
else {
|
||||
await access(source.path, constants.R_OK);
|
||||
return undefined;
|
||||
}
|
||||
} catch (failure: unknown) {
|
||||
cause = failure;
|
||||
const code =
|
||||
failure instanceof Error && "code" in failure ? failure.code : undefined;
|
||||
if (code === "EACCES" || code === "EPERM")
|
||||
return new AnalysisAccessDeniedError(
|
||||
"instrument_with_frida",
|
||||
source.path,
|
||||
code,
|
||||
{ cause },
|
||||
);
|
||||
reason =
|
||||
code === "ENOENT" ? "no file exists there" : String(code ?? failure);
|
||||
}
|
||||
return new AnalysisInputError(
|
||||
"instrument_with_frida",
|
||||
cause === undefined ? undefined : { cause },
|
||||
[
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: `Cannot read Frida script file ${source.path}: ${reason}.`,
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject an unreadable script file before any attach or spawn effect;
|
||||
* cancellation before or during the check still wins.
|
||||
*/
|
||||
const rejectBeforeStart = async (
|
||||
source: FridaScriptSource,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<AnalysisError | undefined> => {
|
||||
const cancelled = () =>
|
||||
new AnalysisCancelledError(
|
||||
"instrument_with_frida",
|
||||
signal?.reason === undefined ? {} : { cause: signal.reason },
|
||||
);
|
||||
if (signal?.aborted) return cancelled();
|
||||
const unreadable = await unreadableScriptFile(source);
|
||||
return signal?.aborted ? cancelled() : unreadable;
|
||||
};
|
||||
|
||||
/** Shared CLI/MCP workflows for Frida's device and instrumentation operations. */
|
||||
export class FridaInstrumentationService {
|
||||
readonly #provider: FridaInstrumentationPort;
|
||||
@@ -163,6 +225,8 @@ export class FridaInstrumentationService {
|
||||
AnalysisError
|
||||
>
|
||||
> {
|
||||
const rejected = await rejectBeforeStart(input.source, signal);
|
||||
if (rejected !== undefined) return err(rejected);
|
||||
const started = await this.#startSession(
|
||||
input,
|
||||
signal,
|
||||
|
||||
Reference in new issue
Block a user