fix(ghidra): report unusable existing-project selections as REA_GHIDRA_PROJECT_PATH configuration errors (#7922)

A selected .gpr marker that does not exist and a target that existing-project mode cannot serve both surfaced as execution_failure with no setting and a doctor action that cannot help. Both now return configuration_invalid naming REA_GHIDRA_PROJECT_PATH. The ENOENT diagnostic of the missing marker is kept.

Fixes #7417
Fixes #7416
This commit is contained in:
Hasan Zayni authored and GitHub committed 2026-10-29 07:56:38 +08:00
1 parent 844bb46351
commit 32b3722d2f
4 files changed
+57 -10

No files matched your search

+11 -1
View File
@@ -24,6 +24,7 @@ import {
} from "../filesystem/RegularFile.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { GhidraProjectSelection } from "../config/ghidraExistingProject.js";
import { ghidraProgramNameSchema } from "../config/ghidraExistingProject.js";
@@ -59,9 +60,11 @@ export type GhidraProjectCommitment = z.infer<
const selectionFailure = (
message: string,
setting = "REA_GHIDRA_PROJECT_PATH",
diagnostics?: Readonly<Record<string, JsonValue>>,
): ConfigurationError =>
new ConfigurationError(message, {
settings: [{ setting, constraint: message }],
...(diagnostics === undefined ? {} : { diagnostics }),
});
const entriesFor = (name: string) => [`${name}.gpr`, `${name}.rep`] as const;
@@ -176,7 +179,14 @@ export const resolveGhidraProject = async (
// Admit the caller's exact marker before canonicalizing its parent.
// Darwin realpath(file/..) can succeed despite native lookup rejecting
// that traversal; hashing the canonical sibling would select a new input.
if (!(await lstat(selection.path)).isFile())
const marker = await lstat(selection.path).catch(absentProjectEntry);
if (marker === undefined)
throw selectionFailure(
`Existing Ghidra project marker does not exist: ${selection.path}`,
"REA_GHIDRA_PROJECT_PATH",
{ system_code: "ENOENT", syscall: "lstat" },
);
if (!marker.isFile())
throw selectionFailure(
`Existing Ghidra project marker must be a regular file: ${selection.path}`,
);
@@ -4620,6 +4620,26 @@ it("reports every Windows existing-project capability unavailable before open",
}
});
it("reports an unsupported existing-project target as an invalid REA_GHIDRA_PROJECT_PATH setting", async () => {
const ghidra = provider(installationHost(), undefined, {
REA_GHIDRA_PROJECT_PATH: join(process.cwd(), "selected.gpr"),
});
try {
const resolved = await ghidra.resolveAnalysisProfile({
...executableTarget("elf", "x86_64"),
format: "dos-com",
} as unknown as BinaryTarget);
expect(resolved.ok).toBe(false);
if (resolved.ok) return;
expect(resolved.error).toMatchObject({
_tag: "ConfigurationError",
settings: [{ setting: "REA_GHIDRA_PROJECT_PATH" }],
});
} finally {
await ghidra.closeOwnedResources();
}
});
it("keeps saved project reports as shared historical Evidence without applying fresh overrides", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-history-project-"));
await mkdir(join(root, "history.rep"));
+11 -9
View File
@@ -3,7 +3,7 @@ import {
ghidraPinnedLanguage,
resolveGhidraAnalysisProfile,
} from "./GhidraAnalysisProfile.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { resolveGhidraProject } from "./GhidraProjectSnapshot.js";
import { DeferredAnalysisClient } from "../providers/DeferredAnalysisClient.js";
import type { ProviderInspectionScope } from "../application/ProviderInspectionScope.js";
@@ -419,14 +419,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
isMachOFormat(target.format)
)
)
return err(
new ProviderAdapterError("ghidra", "resolve_analysis_profile", {
diagnostics: {
reason:
"Existing-project mode supports native ELF/PE/Mach-O programs on POSIX without fresh-import loader preparation or pinned language overrides; use fresh import for this target/host.",
},
}),
);
return err(existingProjectUnsupported());
const project = await resolveGhidraProject(
this.config.ghidraExistingProject,
target,
@@ -671,6 +664,15 @@ export class GhidraProvider implements AnalysisProviderCandidate {
}
}
/** The selected project, not the target or provider, is what this host cannot serve. */
const existingProjectUnsupported = (): ConfigurationError => {
const constraint =
"Existing-project mode supports native ELF/PE/Mach-O programs on POSIX without fresh-import loader preparation or pinned language overrides; unset REA_GHIDRA_PROJECT_PATH to use fresh import for this target/host.";
return new ConfigurationError(constraint, {
settings: [{ setting: "REA_GHIDRA_PROJECT_PATH", constraint }],
});
};
const providerFailureText = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);
@@ -196,6 +196,21 @@ it.skipIf(process.platform === "win32")(
},
);
it("reports a missing project marker as an invalid REA_GHIDRA_PROJECT_PATH selection", async () => {
const root = await createTestTempDirectory("rea-ghidra-project-missing-");
await expect(
resolveGhidraProject(
{ path: join(root, "missing", "app.gpr") },
{ path: join(root, "target") },
resources,
),
).rejects.toMatchObject({
_tag: "ConfigurationError",
settings: [{ setting: "REA_GHIDRA_PROJECT_PATH" }],
diagnostics: { system_code: "ENOENT" },
});
});
it.skipIf(process.platform === "win32")(
"follows native parent symlinks and preserves missing marker failures",
async () => {