fix(ghidra): attribute import policy conflicts to settings

This commit is contained in:
morluto
2026-10-24 04:15:11 +08:00
parent c6ac60acfe
commit 84c327ee65
5 changed files with 83 additions and 17 deletions
+4 -1
View File
@@ -15,7 +15,10 @@ snapshot parser and a separate optional annotation extension. Check the
[release boundary](installation.md#released-package-and-main) when using npm.
See [import language and analysis seeds](installation.md#ghidra-import-language-and-analysis-seeds)
to override the header-detected language or seed analysis. An inaccessible language
to override the header-detected language or seed analysis. An override that conflicts with a fixed raw, DOS, PSP, MIPS or RISC-V import
is a configuration error naming the conflicting settings. It does not make an
otherwise admitted target unsupported; REA preserves the requested settings.
An inaccessible language
catalog remains a read failure; it does not prove that a configured language is
undefined. Installation `application.properties` has a fixed 1 MiB read allowance
with no runtime override. The failure retains that allowance and directs recovery
+31 -15
View File
@@ -12,6 +12,7 @@ import {
AnalysisCancelledError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -74,6 +75,24 @@ export const ghidraPinnedLanguage = (
ghidraRiscvLanguageOverride(target) ??
ghidraPowerPcLanguage(target);
/** Policy conflicts belong to the selected settings, not to the target's support. */
const languageOverrideConflict = (
override: GhidraLanguageOverride,
constraint: string,
pinned?: GhidraLanguageOverride,
): ConfigurationError =>
new ConfigurationError(constraint, {
settings: [
...(pinned?.languageId === override.languageId
? []
: [{ setting: "REA_GHIDRA_LANGUAGE_ID", constraint }]),
...(override.compilerSpecId === undefined ||
pinned?.compilerSpecId === override.compilerSpecId
? []
: [{ setting: "REA_GHIDRA_COMPILER_SPEC_ID", constraint }]),
],
});
/** Resolve version-bound, deterministic semantics before Ghidra imports a target. */
export const resolveGhidraAnalysisProfile = async (
target: BinaryTarget,
@@ -108,10 +127,9 @@ export const resolveGhidraAnalysisProfile = async (
// A caller-selected generic processor cannot override this verified PSP loader.
if (isGhidraPspTarget(target) && languageOverride !== undefined)
return err(
new AnalysisUnsupportedTargetError(
"resolve_analysis_profile",
target.path,
"The bounded PSP Allegrex profile does not accept REA_GHIDRA_LANGUAGE_ID overrides.",
languageOverrideConflict(
languageOverride,
"The bounded PSP Allegrex profile does not accept processor or compiler specification overrides.",
),
);
const pspExtension = isGhidraPspTarget(target)
@@ -132,10 +150,9 @@ export const resolveGhidraAnalysisProfile = async (
if (languageOverride !== undefined)
return Promise.resolve(
err(
new AnalysisUnsupportedTargetError(
"resolve_analysis_profile",
target.path,
"REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import.",
languageOverrideConflict(
languageOverride,
"Processor or compiler specification overrides cannot replace the admitted raw-binary import.",
),
),
);
@@ -187,14 +204,14 @@ export const resolveGhidraAnalysisProfile = async (
languageOverride.compilerSpecId !== pinnedLanguage.compilerSpecId))
)
return err(
new AnalysisUnsupportedTargetError(
"resolve_analysis_profile",
target.path,
languageOverrideConflict(
languageOverride,
`The caller-selected processor or compiler specification is outside the verified ${
target.architecture === "riscv"
? "generic little-endian RISC-V"
: "endian-matched MIPS32"
} profile. The requested override has not been changed.`,
pinnedLanguage,
),
);
const selectedLanguage = languageOverride ?? pinnedLanguage;
@@ -202,10 +219,9 @@ export const resolveGhidraAnalysisProfile = async (
if (dos && languageOverride !== undefined)
return Promise.resolve(
err(
new AnalysisUnsupportedTargetError(
"resolve_analysis_profile",
target.path,
"REA_GHIDRA_LANGUAGE_ID cannot override the admitted DOS real-mode import.",
languageOverrideConflict(
languageOverride,
"Processor or compiler specification overrides cannot replace the admitted DOS real-mode import.",
),
),
);
+26
View File
@@ -399,6 +399,32 @@ describe("Ghidra language override profile", () => {
});
});
it.each(["raw-binary", "dos-mz", "dos-com"] as const)(
"attributes a fixed %s import override conflict to the named settings",
async (format) => {
const target: BinaryTarget =
format === "raw-binary"
? { ...pe, format, entryPoint: "0x0", entryPointSource: "0x0" }
: { ...pe, format };
const resolved = await provider({
REA_GHIDRA_LANGUAGE_ID: "x86:LE:32:default",
REA_GHIDRA_COMPILER_SPEC_ID: "borlandcpp",
}).resolveAnalysisProfile(target);
if (resolved.ok) throw new Error("Expected an override policy conflict");
expect(projectAnalysisError(resolved.error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: [
{ setting: "REA_GHIDRA_LANGUAGE_ID" },
{ setting: "REA_GHIDRA_COMPILER_SPEC_ID" },
],
},
});
const automatic = await provider({}).resolveAnalysisProfile(target);
expect(automatic.ok).toBe(true);
},
);
it("compares committed and configured selections exactly", () => {
expect(sameGhidraLanguageOverride(undefined, undefined)).toBe(true);
expect(
+6 -1
View File
@@ -514,10 +514,15 @@ describe("MIPS16 shared-image profile", () => {
const ghidra = new GhidraProvider(parsed.value, silentLogger, {}, host);
const resolved = await ghidra.resolveAnalysisProfile(shared());
expect(resolved.ok).toBe(false);
if (!resolved.ok)
if (!resolved.ok) {
expect(resolved.error.message).toContain(
"requested override has not been changed",
);
expect(resolved.error).toMatchObject({
_tag: "ConfigurationError",
settings: [{ setting: "REA_GHIDRA_LANGUAGE_ID" }],
});
}
});
});
+16
View File
@@ -192,6 +192,22 @@ describe("bounded Ghidra RISC-V admission", () => {
expect(resolved.ok).toBe(false);
if (resolved.ok) throw new Error("expected a refusal");
expect(resolved.error.message).toContain("generic little-endian RISC-V");
expect(resolved.error).toMatchObject({
_tag: "ConfigurationError",
settings: [{ setting: "REA_GHIDRA_LANGUAGE_ID" }],
});
});
it("attributes a compiler-only pin conflict to its setting", async () => {
const resolved = await ghidra({
REA_GHIDRA_LANGUAGE_ID: "RISCV:LE:64:default",
REA_GHIDRA_COMPILER_SPEC_ID: "windows",
}).resolveAnalysisProfile(target());
if (resolved.ok) throw new Error("expected a configuration conflict");
expect(resolved.error).toMatchObject({
_tag: "ConfigurationError",
settings: [{ setting: "REA_GHIDRA_COMPILER_SPEC_ID" }],
});
});
it("keeps a caller-selected language that matches the pin as configured", async () => {