From ecfac46c8d887fa5c93cc7c3e30077db3ec879b7 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:16:07 +0800 Subject: [PATCH] fix(ghidra): retain neutral recovery for observed SIGKILL --- docs/ghidra-provider.md | 6 +++++ src/ghidra/GhidraProvider.behavior.test.ts | 31 ++++++++++++++++++++++ src/ghidra/GhidraProviderClient.ts | 12 +++++++++ 3 files changed, 49 insertions(+) diff --git a/docs/ghidra-provider.md b/docs/ghidra-provider.md index 88a450505b..92ff7385e7 100644 --- a/docs/ghidra-provider.md +++ b/docs/ghidra-provider.md @@ -24,6 +24,12 @@ undefined. Installation `application.properties` has a fixed 1 MiB read allowanc with no runtime override. The failure retains that allowance and directs recovery at the selected installation metadata. +Retained process diagnostics can report termination by `SIGKILL`; the signal alone +cannot establish an OOM kill. REA gives neutral recovery guidance and preserves the +signal. Confirm resource causes from process output or host/container records; +operation-scoped JVM `OutOfMemoryError` observations retain their separate resource +classification. Automatic kernel or cgroup OOM attribution is not implemented. + Provider selection and profile identity follow [MCP contracts](mcp-contracts.md#identity-and-discovery). One deep provider stays bound to the target; runtime failure never selects another diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 573193c34d..29bc0dd876 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -3690,6 +3690,37 @@ const issueOutput = [ ].join("\n"); describe("Ghidra OOME error projection", () => { + it("preserves observed SIGKILL without guessing its cause or installation recovery", async () => { + const projected = await projectFailure("process", { + exit_code: null, + exit_signal: "SIGKILL", + failure_output: failureOutput(), + }); + expect(projected.code).toBe("execution_failure"); + expect(projected.message).toContain("SIGKILL"); + expect(projected.message).toContain("does not establish its cause"); + expect(projected.remediation.action).toContain( + "host or container termination records", + ); + expect(projected.remediation.action).not.toContain("rea doctor"); + expect(projected.remediation.action).not.toContain( + "GHIDRA_HEADLESS_MAXMEM", + ); + }); + + it("keeps operation-scoped JVM memory evidence stronger than an exit signal", async () => { + const projected = await projectFailure("process", { + exit_signal: "SIGKILL", + failure_output: failureOutput( + "", + "java.lang.OutOfMemoryError: Java heap space", + ), + }); + expect(projected.code).toBe("resource_constraint"); + expect(projected.message).toContain("Java heap space"); + expect(projected.remediation.action).toContain("GHIDRA_HEADLESS_MAXMEM"); + }); + it("recognizes the headless analyzer's real detail-before-class output", async () => { const projected = await projectFailure("process", { stdout: output(issueOutput), diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index dd6ba8a1a6..a73b146e45 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -1008,6 +1008,18 @@ const projectPrimarySessionError = ( }), }); } + if ( + (failure.kind === "start" || + failure.kind === "process" || + failure.kind === "protocol") && + failure.diagnostics.exit_signal === "SIGKILL" + ) + return new ProviderAdapterError("ghidra", operation, { + ...context, + userMessage: `Ghidra process diagnostics report termination by SIGKILL. The signal alone does not establish its cause or an out-of-memory failure.${uncertainty === undefined ? "" : ` ${uncertainty}`}`, + remediationAction: + "Review the retained process output and host or container termination records before choosing a resource-specific remedy. Export useful Evidence, close the binary and resolve any cleanup failure, then call open_binary for the same target to start a new Ghidra session. Session-only annotations are not retained.", + }); const exited = exitedProviderMessage(operation, failure, health); if (exited !== undefined) return new ProviderAdapterError("ghidra", operation, {