fix(ghidra): report selected project identity failures (#8379)

* fix(ghidra): report selected project identity failures

* fix(ghidra): retain refusal until client shutdown
This commit is contained in:
morluto authored and GitHub committed 2026-11-04 19:58:50 +08:00
1 parent 85be0f70a6
commit 2d16a40a49
11 files changed
+605 -77

No files matched your search

+21 -8
View File
@@ -179,6 +179,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
private List<FunctionEntry> procedureInventory;
private List<InventoryItem> stringInventory;
private DecompInterface decompiler;
// A refused Program may only report its admission failure to the client.
private String identityAdmissionFailure;
/** Per-function native decompiler limit from the session; 0 leaves it unbounded. */
private int decompileTimeoutSeconds;
// DecompileProcess converts seconds to an int millisecond timer delay.
@@ -224,14 +226,17 @@ public final class ReaGhidraBridge extends HeadlessScript {
throw new IllegalStateException("Ghidra provider version does not match the session");
}
String importedSha256 = currentProgram.getExecutableSHA256();
if (importedSha256 == null ||
!constantTimeEquals(
importedSha256.toLowerCase(Locale.ROOT),
descriptor.targetSha256
)) {
throw new IllegalStateException(
"Ghidra imported-byte digest does not match the admitted target"
);
if (importedSha256 == null || !importedSha256.matches("(?i)[0-9a-f]{64}")) {
identityAdmissionFailure = "import_identity_unavailable";
}
else if (!constantTimeEquals(importedSha256.toLowerCase(Locale.ROOT),
descriptor.targetSha256)) {
identityAdmissionFailure = "import_identity_mismatch";
}
if (identityAdmissionFailure != null) {
end(false);
serve(descriptor);
return;
}
admittedTargetSha256 = descriptor.targetSha256;
// GhidraScript wraps run() in a transaction. End it before serving so
@@ -405,6 +410,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
writeSuccess(writer, request.id, result);
return;
}
if (identityAdmissionFailure != null) {
// Keep the producer alive until the client has consumed the
// refusal and closed or sent shutdown. Every other request
// remains refused, including further startup attempts.
writeFailure(writer, request.id, identityAdmissionFailure,
"Ghidra imported identity admission failed");
continue;
}
decompileTimeoutSeconds = request.decompileTimeoutSeconds() == null
? descriptor.decompileTimeoutSeconds() : request.decompileTimeoutSeconds();
writeSuccess(writer, request.id, handleRequest(request, descriptor));
+5 -2
View File
@@ -1450,8 +1450,11 @@ the original database; REA reads selected files only for fingerprinting and copy
This mode does not copy unrelated sibling projects, rerun repair scripts, or
admit bridge database edits. Cleanup removes the owned copy.
The bridge checks the program's recorded import SHA-256 against the admitted
target digest; mismatches and project changes fail explicitly. Reopen the target
to resolve a new profile after intentionally changing the source project.
target digest. An unavailable or malformed recorded digest is reported as an
unverified identity, distinct from a known mismatch; neither admits the program
or triggers a fresh import. Select a matching target or explicitly configure a
different project or program. Changes to the source project also fail admission;
reopen the target to resolve a new profile after intentionally changing it.
The profile records the caller path, resolved project/name, program and content
fingerprint. Original analysis settings and completeness remain **unknown**;
+7
View File
@@ -17,6 +17,7 @@ import {
requireMcpOutput,
} from "./mcp-verifier-results.mjs";
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
@@ -317,6 +318,11 @@ async function verifyNativeValueAdapters(
await client.close();
await transport.close();
}
const projectIdentity = await verifyGhidraProjectIdentity({
target,
entrypoint,
env,
});
return {
mocked: false,
cli: true,
@@ -324,6 +330,7 @@ async function verifyNativeValueAdapters(
inline_evidence: true,
direct_function_comparison: true,
boundaries,
project_identity: projectIdentity,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,
@@ -0,0 +1,249 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
appendFile,
copyFile,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { basename, join } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
const exec = promisify(execFile);
/** Verify selected-project identity refusal through real CLI and MCP adapters. */
export async function verifyGhidraProjectIdentity({ target, entrypoint, env }) {
if (process.platform === "win32")
return {
status: "not-applicable",
reason: "Existing Ghidra projects are not admitted on Windows.",
};
assert.ok(env.GHIDRA_INSTALL_DIR, "Ghidra installation is required");
const root = await mkdtemp(join(env.TMPDIR, "rea-project-identity-"));
const targetPath = target.path;
const different = join(root, "different-target");
const headless = join(env.GHIDRA_INSTALL_DIR, "support", "analyzeHeadless");
const fixtureEnv = {
...env,
HOME: root,
XDG_CONFIG_HOME: join(root, "config"),
XDG_CACHE_HOME: join(root, "cache"),
XDG_DATA_HOME: join(root, "data"),
JDK_JAVA_OPTIONS: `-Duser.home="${root}"`,
};
const program = basename(targetPath);
const known = join(root, "known.gpr");
const unknown = join(root, "unknown.gpr");
const runtimeRootsBefore = (await readdir(env.TMPDIR))
.filter((name) => name.startsWith("rea-ghidra-"))
.sort();
const sha = async (path) =>
createHash("sha256")
.update(await readFile(path))
.digest("hex");
try {
await copyFile(targetPath, different);
await appendFile(different, "\nREA_IDENTITY_VERIFIER\n");
assert.notEqual(await sha(different), await sha(targetPath));
await writeFile(
join(root, "ClearImportDigest.java"),
`import ghidra.app.script.GhidraScript;
public class ClearImportDigest extends GhidraScript {
@Override public void run() {
currentProgram.setExecutableSHA256(null);
println("REA_DIGEST_AFTER=" + currentProgram.getExecutableSHA256());
}
}\n`,
);
await exec(
headless,
[root, "known", "-import", targetPath, "-noanalysis"],
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
);
const cleared = await exec(
headless,
[
root,
"unknown",
"-import",
targetPath,
"-noanalysis",
"-scriptPath",
root,
"-postScript",
"ClearImportDigest.java",
],
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
);
assert.match(cleared.stdout, /REA_DIGEST_AFTER=unknown/u);
const before = await projectState([known, unknown, targetPath, different]);
const knownEnv = {
...env,
REA_GHIDRA_PROJECT_PATH: known,
REA_GHIDRA_PROGRAM_NAME: program,
};
const unknownEnv = {
...env,
REA_GHIDRA_PROJECT_PATH: unknown,
REA_GHIDRA_PROGRAM_NAME: program,
};
const mismatch = await selectedProjectMcp({
entrypoint,
env: knownEnv,
first: different,
matching: targetPath,
expected: "import_identity_mismatch",
});
const unavailable = await selectedProjectMcp({
entrypoint,
env: unknownEnv,
first: targetPath,
expected: "import_identity_unavailable",
});
for (const [label, selectedEnv, input, expected] of [
["mismatch", knownEnv, different, "import_identity_mismatch"],
["unavailable", unknownEnv, targetPath, "import_identity_unavailable"],
]) {
let failed;
try {
await exec(
process.execPath,
[
entrypoint,
"decompile",
input,
"0x0",
"--provider",
"ghidra",
"--format",
"json",
],
{ env: selectedEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
);
} catch (cause) {
assert.equal(cause.code, 1, `${label} CLI exit`);
failed = JSON.parse(cause.stdout);
}
assert.ok(failed, `${label} CLI must refuse`);
assert.equal(failed.code, "configuration_invalid");
assert.equal(failed.details.diagnostics.remote_code, expected);
}
assert.deepEqual(
await projectState([known, unknown, targetPath, different]),
before,
);
assert.deepEqual(
(await readdir(env.TMPDIR))
.filter((name) => name.startsWith("rea-ghidra-"))
.sort(),
runtimeRootsBefore,
"Selected-project checks left a Ghidra runtime directory",
);
return {
status: "verified",
mismatch,
unavailable,
cli_refusals: true,
source_files_unchanged: true,
};
} finally {
await rm(root, { recursive: true, force: true });
}
}
async function selectedProjectMcp({
entrypoint,
env,
first,
matching,
expected,
}) {
const client = new Client({
name: "rea-project-identity-verifier",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
try {
await client.connect(transport);
const tools = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const call = (name, arguments_) =>
client.callTool(
{ name, arguments: arguments_ },
{ timeout: 180000, toolDefinition: tools.get(name) },
);
assert.notEqual(
(await call("open_binary", { path: first, provider_id: "ghidra" }))
.isError,
true,
);
const rejected = requireMcpToolError(await call("list_documents", {}));
assert.equal(
rejected.code,
"configuration_invalid",
JSON.stringify({
code: rejected.code,
message: rejected.message,
failure_kind: rejected.details?.diagnostics?.failure_kind,
failure_message: rejected.details?.diagnostics?.failure_message,
stdout: rejected.details?.diagnostics?.stdout?.text?.slice(-2500),
stderr: rejected.details?.diagnostics?.stderr?.text?.slice(-1000),
}),
);
assert.equal(rejected.details.diagnostics.remote_code, expected);
if (matching !== undefined) {
assert.notEqual(
(await call("open_binary", { path: matching, provider_id: "ghidra" }))
.isError,
true,
);
assert.notEqual((await call("list_documents", {})).isError, true);
}
assert.notEqual((await call("close_binary", {})).isError, true);
return {
refused: expected,
matching_target_reopened: matching !== undefined,
};
} finally {
await client.close();
await transport.close();
}
}
async function projectState(paths) {
const files = [];
const walk = async (dir) => {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) await walk(path);
else if (entry.isFile()) files.push(path);
}
};
for (const path of paths) {
files.push(path);
if (path.endsWith(".gpr")) await walk(`${path.slice(0, -4)}.rep`);
}
files.sort();
return Promise.all(
files.map(async (path) => [
path,
createHash("sha256")
.update(await readFile(path))
.digest("hex"),
]),
);
}
+2
View File
@@ -28,6 +28,8 @@ const GHIDRA_ADMISSION_CODES = [
"prepared_options_exceed_bound",
"output_unobserved",
"handshake_identity",
"import_identity_unavailable",
"import_identity_mismatch",
"response_shape",
"response_json",
"seed_path_differs",
+20
View File
@@ -8,6 +8,7 @@ import {
type GhidraSessionFailureFactory,
} from "./GhidraSessionError.js";
import type { GhidraSessionInfo } from "./GhidraSessionValues.js";
import { GhidraAdmissionError } from "./GhidraAdmissionError.js";
/** Inputs needed to complete the post-connection bridge handshake. */
export interface GhidraStartupHandshakeOptions {
@@ -42,6 +43,25 @@ export async function completeGhidraStartupHandshake(
options.deadline.interruption !== undefined
? interruptionFailure(options)
: ping.error;
if (
failure.kind === "remote" &&
(failure.remoteCode === "import_identity_unavailable" ||
failure.remoteCode === "import_identity_mismatch")
) {
const admitted = options.failure(
"admission",
failure.message,
new GhidraAdmissionError(failure.remoteCode, undefined, {
cause: failure,
}),
{ remoteCode: failure.remoteCode },
);
return err(
failure.cleanupFailure === undefined
? admitted
: admitted.withCleanupFailure(failure.cleanupFailure),
);
}
return err(failure);
}
const parsed = options.parseSessionInfo(ping.value);
@@ -89,6 +89,7 @@ import {
} from "./GhidraSessionValues.js";
import type { Result } from "../domain/result.js";
import { GhidraSessionError } from "./GhidraSessionError.js";
import { GhidraAdmissionError } from "./GhidraAdmissionError.js";
import { silentLogger } from "../logger.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
@@ -4173,6 +4174,46 @@ describe("Ghidra deadline guidance", () => {
});
});
it.each([
["import_identity_unavailable", "did not report a valid imported SHA-256"],
["import_identity_mismatch", "differs from the selected target"],
] as const)(
"reports fresh-import %s without blaming project configuration",
async (code, message) => {
const failure = new GhidraSessionError(
"admission",
"Identity admission failed",
{},
{
cause: new GhidraAdmissionError(code),
remoteCode: code,
},
);
const ghidra = provider(installationHost(), () => ({
start: async () => err(failure),
callTool: async () => err(failure),
close: async () => ok(null),
}));
onTestFinished(async () => {
await ghidra.closeOwnedResources();
});
const target = executableTarget("elf", "x86_64");
const profile = await ghidra.resolveAnalysisProfile(target);
if (!profile.ok || profile.value.profile === null)
throw new Error("Expected profile");
const result = await ghidra
.createClient(target, profile.value.profile)
.execute("list_documents", {});
if (result.ok) throw new Error("Expected identity admission failure");
const projected = projectAnalysisError(result.error);
expect(projected.code).toBe("execution_failure");
expect(projected.message).toContain(message);
expect(JSON.stringify(projected.details)).not.toContain(
"REA_GHIDRA_PROJECT_PATH",
);
},
);
describe("Ghidra startup deadline for a finished invocation", () => {
const startupLimit = {
setting: "ghidra.startup_timeout_ms",
+27
View File
@@ -9,6 +9,7 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.
import { artifactFilePrimaryFacts } from "../artifacts/ArtifactFileOperation.js";
import { fileURLToPath } from "node:url";
import { GHIDRA_BRIDGE_SOURCES } from "./GhidraBridgeSources.js";
import { GhidraAdmissionError } from "./GhidraAdmissionError.js";
import {
createAnalysisExecution,
@@ -1239,6 +1240,32 @@ const projectPrimarySessionError = (
physicalCause.reason,
context,
);
if (
failure.kind === "admission" &&
failure.cause instanceof GhidraAdmissionError &&
(failure.cause.code === "import_identity_unavailable" ||
failure.cause.code === "import_identity_mismatch")
) {
const unavailable = failure.cause.code === "import_identity_unavailable";
const constraint = unavailable
? "The Ghidra program did not report a valid imported SHA-256, so REA cannot verify its target identity"
: "The Ghidra program's imported SHA-256 differs from the selected target's SHA-256";
const remediationAction = unavailable
? "Select a Ghidra program whose imported identity can be verified, or investigate the missing identity in Ghidra. Configure a different project or program when starting REA. REA did not fall back to a fresh import."
: "Select a target matching the configured program and retry, or configure a matching project or program when starting REA. Resolve any reported cleanup before reopening. REA did not fall back to a fresh import.";
if (limits.ghidraExistingProject !== undefined)
return new ConfigurationError(constraint, {
...context,
settings: [{ setting: "REA_GHIDRA_PROJECT_PATH", constraint }],
remediationAction,
});
return new ProviderAdapterError("ghidra", operation, {
...context,
userMessage: `${constraint}.`,
remediationAction:
"Inspect the retained Ghidra diagnostics and target identity before retrying the import.",
});
}
// A well-formed address that lacks one exact source offset keeps the
// bridge's reason and the caller's exact spelling, not schema advice.
const fileOffset =
@@ -29,6 +29,9 @@ type FixtureMode =
| "remote_error"
| "decompile_timed_out"
| "remote_startup_error"
| "import_identity_unavailable"
| "import_identity_mismatch"
| "late_identity_failure"
| "shutdown_error"
| "invalid_shutdown_ack"
| "hang_after_start"
@@ -29,6 +29,7 @@ import type { ProgressUpdate } from "../../../../src/application/ProgressReporte
import { err, ok } from "../../../../src/domain/result.js";
import * as GhidraTransport from "../../../../src/ghidra/GhidraTransport.js";
import { GhidraClient } from "../../../../src/ghidra/GhidraClient.js";
import { GhidraAdmissionError } from "../../../../src/ghidra/GhidraAdmissionError.js";
import { ProviderRunLineage } from "../../../../src/process/ProviderRunLineage.js";
import { PrivateRuntimeRoot } from "../../../../src/process/PrivateRuntimeRoot.js";
import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
@@ -1017,6 +1018,45 @@ it("retains launcher diagnostics when an input error rejects the startup handsha
expect(result.error.diagnostics).not.toHaveProperty("diagnostic_scope");
});
it.each(
(
["import_identity_unavailable", "import_identity_mismatch"] as const
).flatMap((code) =>
(HOST_TRANSPORT === "unix-socket"
? ([HOST_TRANSPORT, "authenticated-loopback-tcp"] as const)
: ([HOST_TRANSPORT] as const)
).map((transport) => [code, transport] as const),
),
)(
"retains structured %s from %s startup and cleans the failed bridge",
async (code, transport) => {
const launcher = new FixtureLauncher(code);
const client = clientFor(launcher, { transport });
const result = await client.start();
if (result.ok) throw new Error("Expected identity admission failure");
expect(result.error.kind).toBe("admission");
expect(result.error.cause).toBeInstanceOf(GhidraAdmissionError);
expect(result.error.cause).toMatchObject({ code });
expect(client.isReady()).toBe(false);
await expect(waitForExit(launcher.processes[0])).resolves.toBe(true);
expect(launcher.requests).toEqual(["ping", "shutdown"]);
await expect(access(launcher.runtimeRoots[0] ?? "")).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it("does not classify a later remote error as an identity admission failure", async () => {
const client = clientFor(new FixtureLauncher("late_identity_failure"));
expect((await client.start()).ok).toBe(true);
const result = await client.callTool("list_strings", {});
if (result.ok) throw new Error("Expected the fixture's later remote failure");
expect(result.error.kind).toBe("remote");
expect(result.error.remoteCode).toBe("import_identity_mismatch");
expect(result.error.cause).not.toBeInstanceOf(GhidraAdmissionError);
expect(client.isReady()).toBe(true);
});
describe("GhidraClient", () => {
it("completes an exact, fragmented post-analysis handshake", async () => {
const launcher = new FixtureLauncher("fragmented");
@@ -2073,10 +2113,73 @@ describe("GhidraClient shutdown diagnostics", () => {
);
});
it("preserves failed startup and retained Ghidra cleanup capabilities until a verified retry", async () => {
const fixture = new FixtureLauncher("wrong_identity");
let cleanupAllowed = false;
let cleanupAttempts = 0;
it.each([
["wrong_identity", "protocol"],
["import_identity_unavailable", "admission"],
] as const)(
"preserves %s and retained Ghidra cleanup until a verified retry",
async (mode, kind) => {
const fixture = new FixtureLauncher(mode);
let cleanupAllowed = false;
let cleanupAttempts = 0;
const launcher: GhidraLauncher = {
launch: async (session) => {
const launched = await fixture.launch(session);
if (!launched.ok) return launched;
return ok({
...launched.value,
cleanup: async () => {
cleanupAttempts += 1;
return cleanupAllowed
? { cleaned: true as const, signaled: false }
: {
cleaned: false as const,
reason: "fixture process cleanup unconfirmed",
};
},
});
},
};
const client = clientFor(launcher);
onTestFinished(async () => {
cleanupAllowed = true;
await client.close();
});
const failed = await client.start();
expect(failed).toMatchObject({
ok: false,
error: { kind, cleanupFailure: { cleanupIncomplete: true } },
});
if (failed.ok) throw new Error("Expected failed startup");
if (mode === "import_identity_unavailable")
expect(failed.error.cause).toMatchObject({ code: mode });
const runtime = fixture.runtimeRoots[0];
if (runtime === undefined)
throw new Error("Fixture runtime was not captured");
await access(runtime);
expect(cleanupAttempts).toBe(1);
expect(await client.close()).toMatchObject({
ok: false,
error: { cleanupIncomplete: true },
});
cleanupAllowed = true;
expect(await client.close()).toEqual(ok(null));
expect(failed.error.cleanupFailure?.cleanupIncomplete).toBe(false);
expect(cleanupAttempts).toBe(3);
await expect(access(runtime)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("retains the identity refusal when cancellation arrives during final startup cleanup", async () => {
const fixture = new FixtureLauncher("import_identity_mismatch");
let entered: (() => void) | undefined;
let release: (() => void) | undefined;
const cleanupStarted = new Promise<void>((resolve) => {
entered = resolve;
});
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const launcher: GhidraLauncher = {
launch: async (session) => {
const launched = await fixture.launch(session);
@@ -2084,39 +2187,40 @@ it("preserves failed startup and retained Ghidra cleanup capabilities until a ve
return ok({
...launched.value,
cleanup: async () => {
cleanupAttempts += 1;
return cleanupAllowed
? { cleaned: true as const, signaled: false }
: {
cleaned: false as const,
reason: "fixture process cleanup unconfirmed",
};
entered?.();
await gate;
return { cleaned: true as const, signaled: false };
},
});
},
};
const client = clientFor(launcher);
onTestFinished(async () => {
cleanupAllowed = true;
await client.close();
});
expect(await client.start()).toMatchObject({
ok: false,
error: { kind: "protocol", cleanupFailure: { cleanupIncomplete: true } },
});
const runtime = fixture.runtimeRoots[0];
if (runtime === undefined)
throw new Error("Fixture runtime was not captured");
await access(runtime);
expect(cleanupAttempts).toBe(1);
expect(await client.close()).toMatchObject({
ok: false,
error: { cleanupIncomplete: true },
});
cleanupAllowed = true;
expect(await client.close()).toEqual(ok(null));
expect(cleanupAttempts).toBe(3);
await expect(access(runtime)).rejects.toMatchObject({ code: "ENOENT" });
const client = clientFor(launcher, { startupTimeoutMs: 10_000 });
const controller = new AbortController();
const started = client.start(controller.signal);
await withVerifierCleanup(async () => {
await awaitAcknowledgement(
cleanupStarted,
started,
"identity refusal cleanup",
);
controller.abort("cancel during final cleanup");
release?.();
const result = await started;
if (result.ok) throw new Error("Expected the earlier identity refusal");
expect(result.error.kind).toBe("admission");
expect(result.error.cause).toMatchObject({
code: "import_identity_mismatch",
});
expect(client.isReady()).toBe(false);
await expect(access(fixture.runtimeRoots[0] ?? "")).rejects.toMatchObject({
code: "ENOENT",
});
}, [
() => release?.(),
async () => {
await client.close();
},
]);
});
describe.skipIf(process.platform === "win32")(
@@ -2370,42 +2474,50 @@ it.each([null, "caller stopped", new Error("caller stopped")])(
},
);
it("preserves a handshake protocol error observed before the final startup caller cancels", async () => {
const controller = new AbortController();
let cancelled = false;
const logger = pino(
{
level: "warn",
hooks: {
logMethod(args, method) {
const fields = args[0];
if (
typeof fields === "object" &&
fields !== null &&
"method" in fields &&
fields.method === "ping" &&
!cancelled
) {
cancelled = true;
controller.abort(null);
}
method.apply(this, args);
it.each([
["malformed", "protocol"],
["import_identity_unavailable", "admission"],
] as const)(
"preserves %s observed before the final startup caller cancels",
async (mode, kind) => {
const controller = new AbortController();
let cancelled = false;
const logger = pino(
{
level: "warn",
hooks: {
logMethod(args, method) {
const fields = args[0];
if (
typeof fields === "object" &&
fields !== null &&
"method" in fields &&
fields.method === "ping" &&
!cancelled
) {
cancelled = true;
controller.abort(null);
}
method.apply(this, args);
},
},
},
},
{ write: () => {} },
);
const launcher = new FixtureLauncher("malformed");
const client = clientFor(launcher, { logger });
const result = await client.start(controller.signal);
expect(cancelled).toBe(true);
if (result.ok) throw new Error("Expected handshake protocol failure");
expect(result.error.kind).toBe("protocol");
expect(await waitForExit(launcher.processes[0])).toBe(true);
await expect(access(launcher.runtimeRoots[0] ?? "")).rejects.toMatchObject({
code: "ENOENT",
});
});
{ write: () => {} },
);
const launcher = new FixtureLauncher(mode);
const client = clientFor(launcher, { logger });
const result = await client.start(controller.signal);
expect(cancelled).toBe(true);
if (result.ok) throw new Error("Expected handshake protocol failure");
expect(result.error.kind).toBe(kind);
if (mode === "import_identity_unavailable")
expect(result.error.cause).toMatchObject({ code: mode });
expect(await waitForExit(launcher.processes[0])).toBe(true);
await expect(access(launcher.runtimeRoots[0] ?? "")).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it("redacts the transport credential when refreshing startup diagnostics", async () => {
let token = "";
+51
View File
@@ -182,6 +182,40 @@ const handleRequest = (socket, server, request, state) => {
handlePing(socket, request, state);
return;
}
if (
["import_identity_unavailable", "import_identity_mismatch"].includes(
state.mode,
) &&
request.method !== "shutdown"
) {
socket.write(
`${JSON.stringify({
id: request.id,
ok: false,
error: {
code: state.mode,
message: "Ghidra imported identity admission failed",
},
})}\n`,
);
return;
}
if (
state.mode === "late_identity_failure" &&
request.method === "list_strings"
) {
socket.write(
`${JSON.stringify({
id: request.id,
ok: false,
error: {
code: "import_identity_mismatch",
message: "Unexpected later failure",
},
})}\n`,
);
return;
}
if (
state.mode === "old_oom_then_malformed" &&
request.method === "list_documents" &&
@@ -341,6 +375,23 @@ const handleRequest = (socket, server, request, state) => {
};
const handlePing = (socket, request, state) => {
if (
["import_identity_unavailable", "import_identity_mismatch"].includes(
state.mode,
)
) {
socket.write(
`${JSON.stringify({
id: request.id,
ok: false,
error: {
code: state.mode,
message: "Ghidra project identity admission failed",
},
})}\n`,
);
return;
}
if (state.mode === "remote_startup_error") {
socket.write(
`${JSON.stringify({