diff --git a/docs/ghidra-provider.md b/docs/ghidra-provider.md index 8450cfbba4..88a450505b 100644 --- a/docs/ghidra-provider.md +++ b/docs/ghidra-provider.md @@ -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 diff --git a/src/ghidra/GhidraAnalysisProfile.ts b/src/ghidra/GhidraAnalysisProfile.ts index f21c5b4849..03a46f20f2 100644 --- a/src/ghidra/GhidraAnalysisProfile.ts +++ b/src/ghidra/GhidraAnalysisProfile.ts @@ -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.", ), ), ); diff --git a/src/ghidra/GhidraLanguageOverride.test.ts b/src/ghidra/GhidraLanguageOverride.test.ts index 29b46b7c62..b9fbe35485 100644 --- a/src/ghidra/GhidraLanguageOverride.test.ts +++ b/src/ghidra/GhidraLanguageOverride.test.ts @@ -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( diff --git a/src/ghidra/GhidraMipsProfile.test.ts b/src/ghidra/GhidraMipsProfile.test.ts index 1e64df9eb5..dbec5b2260 100644 --- a/src/ghidra/GhidraMipsProfile.test.ts +++ b/src/ghidra/GhidraMipsProfile.test.ts @@ -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" }], + }); + } }); }); diff --git a/src/ghidra/GhidraRiscvProfile.test.ts b/src/ghidra/GhidraRiscvProfile.test.ts index 3ada4d15b9..4ca069c1ce 100644 --- a/src/ghidra/GhidraRiscvProfile.test.ts +++ b/src/ghidra/GhidraRiscvProfile.test.ts @@ -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 () => {