[GH-ISSUE #6031] [Bug] Ghidra override-policy conflicts blame the target instead of the configured setting #3676

Closed
opened 2026-10-31 00:42:58 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6031

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

An explicitly selected Ghidra open_binary request for an admitted raw ARM image fails as unsupported_target when REA_GHIDRA_LANGUAGE_ID is set. The error advises choosing another target or operation, and has no typed settings entry. The exact same image opens when the override is removed. Supplementary production-provider checks show the same classification for an RV32 language conflicting with the verified pin, and a matching language with a conflicting compiler spec. These are configuration-selection refusals, not target-format exclusions.

Reproduction

Save the following as tests/boundary/mcp/languageConflict.test.ts in the source checkout with locked dependencies and runtime artifacts. Run:

mkdir -p repro-data
REA_REPRO_ROOT="$PWD/repro-data" npx --no-install vitest run tests/boundary/mcp/languageConflict.test.ts --maxWorkers 1

This executes registered open_binary with the production provider, registry and session routing. Installation readiness uses the repository's simulated Ghidra 12.1.4 / Java 21 host; no engine starts. The supplementary RISC-V cases call the production provider's profile boundary directly. Assertions characterize the current misclassification. Remove the test and empty fixture directory afterward.

import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { expect, it } from 'vitest';
import { createServer } from '../../../src/server/createServer.js';
import { parseMcpToolError } from '../../fixtures/mcpToolError.js';
import { parseConfig } from '../../../src/config/parseConfig.js';
import { GhidraProvider } from '../../../src/ghidra/GhidraProvider.js';
import { ghidraProfileInstallationHost } from '../../../src/ghidra/GhidraInstallation.fixture.js';
import { silentLogger } from '../../../src/logger.js';
import { projectAnalysisError } from '../../../src/domain/analysisErrorProjection.js';
import { AnalysisProviderRegistry } from '../../../src/application/binary/AnalysisProviderRegistry.js';
import { composeBinarySession } from '../../../src/application/binary/BinarySessionComposition.js';
import { writeFile, unlink } from 'node:fs/promises';


it('Ghidra override refusal through MCP with no-override control', async () => {
  const path = `${process.env.REA_REPRO_ROOT}/image.bin`;
  await writeFile(path, Buffer.from([0,0,160,225]));
  try {
    for (const override of [true,false]) {
      const config = parseConfig({GHIDRA_INSTALL_DIR:'/opt/ghidra_12.1.4_PUBLIC',
        ...(override ? {REA_GHIDRA_LANGUAGE_ID:'ARM:LE:32:v7'} : {})});
      if (!config.ok) throw config.error;
      let clients = 0;
      const provider = new GhidraProvider(config.value, silentLogger, {}, ghidraProfileInstallationHost, () => {
        clients++; throw new Error('unexpected client startup');
      });
      const session = composeBinarySession(new AnalysisProviderRegistry([provider]));
      const server = createServer({kind:'session',session},{logger:silentLogger});
      const client = new Client({name:'override-state',version:'1'});
      const [ct,st] = InMemoryTransport.createLinkedPair();
      try {
        await server.connect(st); await client.connect(ct);
        const result = await client.callTool({name:'open_binary',arguments:{path,format:'raw-binary',architecture:'arm',entry_point:'0x8000000',provider_id:'ghidra'}});
        if (override) {
          const {error} = parseMcpToolError(result);
          expect(error).toMatchObject({code:'unsupported_target',category:'unsupported_target',details:{reason:'REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import.'}});
          expect(error.details).not.toHaveProperty('settings');
          console.log('GHIDRA', JSON.stringify({...error,details:{...error.details,path:'image.bin'}}));
        } else { expect(result.isError).not.toBe(true); console.log('GHIDRA_CONTROL', 'open succeeded'); }
        expect(clients).toBe(0);
      } finally { await client.close(); await server.close(); await session.close(); }
    }
  } finally { await unlink(path); }
});

it('Ghidra RISC-V language/compiler conflicts and admitted control', async () => {
  for (const env of [{}, {REA_GHIDRA_LANGUAGE_ID:'RISCV:LE:32:AndeStar_v5'},
    {REA_GHIDRA_LANGUAGE_ID:'RISCV:LE:32:default',REA_GHIDRA_COMPILER_SPEC_ID:'wrong'}]) {
    const config = parseConfig({GHIDRA_INSTALL_DIR:'/opt/ghidra_12.1.4_PUBLIC', ...env});
    if (!config.ok) throw config.error;
    const provider = new GhidraProvider(config.value,silentLogger,{},ghidraProfileInstallationHost);
    try {
      const result = await provider.resolveAnalysisProfile({path:'/fixture.elf',sha256:'a'.repeat(64),kind:'executable',format:'elf',architecture:'riscv',availableArchitectures:['riscv'],riscv:{elfClass:32,byteOrder:'little',type:2,flags:5}});
      if (Object.keys(env).length === 0) expect(result.ok).toBe(true);
      else {
        expect(result.ok).toBe(false); if(result.ok) throw new Error('expected refusal');
        const error = projectAnalysisError(result.error);
        expect(error.code).toBe('unsupported_target'); expect(error.details).not.toHaveProperty('settings');
        console.log('RISCV', JSON.stringify({env,error}));
      }
    } finally { await provider.closeOwnedResources(); }
  }
});

Expected behavior

Keep refusing the incompatible override and preserving the caller selection, but identify its named environment setting and constraint through configuration_invalid / invalid_input, with environment-correction advice. The unsupported-target type describes unsupported target formats. The RISC-V guide describes refusal of conflicting environment settings. Existing language/compiler admission already models rejected configured selections as ConfigurationError with setting names and constraints. Configured values need not be exposed.

Impact and scope

P2: clients receive the wrong typed failure category and recovery for a bounded optional-configuration conflict. A raw ARM input with an override failed, and its no-override control opened without starting a provider client. RISC-V no-override profile resolution succeeded, while language and compiler conflicts failed. Removing an unneeded override is a demonstrated admission workaround. No real Ghidra import, analysis, automatic-provider selection or resulting analysis quality was tested. PSP, DOS and nonlegacy MIPS conflicts use the same error constructor by source inspection; their runtime behavior was not exercised.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. Fetched main 6d356b84344659085f2b912857b5e27fe0160c9b has unchanged relevant code.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Vitest 5.0.3. Native providers were simulated. The repository-pinned Node 24.18.0 was not exercised.

Target and artifact context

Four-byte synthetic ARM raw image (00 00 a0 e1), architecture arm, entry point 0x8000000, explicit Ghidra selection. Additional synthetic ELF32 little-endian RISC-V ET_EXEC metadata with flags 0x5. No real Ghidra database or target execution.

Interface

MCP

Earliest failure stage

Setup or provider admission

Relevant output or evidence

The raw-image MCP error contained the following fields (fixture path omitted):

{"code":"unsupported_target","category":"unsupported_target","retryable":false,"remediation":{"action":"Select a target supported by this operation or choose an operation supporting the reported target format."},"details":{"operation":"resolve_analysis_profile","reason":"REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import."}}

details.settings was absent. The same open_binary request without the override succeeded, with zero provider client starts in either case. Each RISC-V conflict returned unsupported_target with reason The caller-selected processor or compiler specification is outside the verified generic little-endian RISC-V profile. The requested override has not been changed. and the same generic target-format remediation; both omitted settings. The no-override RISC-V profile succeeded. The final focused file passed all four cases, including the two separate Frida cases.

Typed outcome or recovery expected

configuration_invalid / invalid_input, with a setting constraint naming REA_GHIDRA_LANGUAGE_ID and/or REA_GHIDRA_COMPILER_SPEC_ID as applicable. Tell callers to remove or correct the conflicting environment selection in the environment starting REA. Preserve the refusal and avoid silently replacing their selection.

Analysis or suspected cause

Source-derived: the profile resolver and pinned-language/DOS guards construct AnalysisUnsupportedTargetError. The provider returns this before catalog validation, so the named-configuration error path is bypassed. Explicit provider selection returns the profile error unchanged.

Additional context

Related #5972 concerns catalog directory-read errors being swallowed and then blamed on valid configuration. This refusal occurs in the profile resolver before catalog enumeration; correcting directory-read handling does not repair it. #3930 and open PR #4103 address missing provider-selection diagnostics on the raw override route. Their fix wraps unsupported-target profile errors in a provider-selection error, preserving target-unsupported semantics; it does not classify the conflicting settings or provide configuration recovery. Closed #3178 changed legacy MIPS override acceptance; this report preserves the intended refusal for the pinned lanes. #1184 concerns a separate path-parser configuration diagnostic, and feature #5431 concerns shared MCP/schema/timeout recovery.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6031 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior An explicitly selected Ghidra `open_binary` request for an admitted raw ARM image fails as `unsupported_target` when `REA_GHIDRA_LANGUAGE_ID` is set. The error advises choosing another target or operation, and has no typed `settings` entry. The exact same image opens when the override is removed. Supplementary production-provider checks show the same classification for an RV32 language conflicting with the verified pin, and a matching language with a conflicting compiler spec. These are configuration-selection refusals, not target-format exclusions. ### Reproduction Save the following as `tests/boundary/mcp/languageConflict.test.ts` in the source checkout with locked dependencies and runtime artifacts. Run: ```sh mkdir -p repro-data REA_REPRO_ROOT="$PWD/repro-data" npx --no-install vitest run tests/boundary/mcp/languageConflict.test.ts --maxWorkers 1 ``` This executes registered `open_binary` with the production provider, registry and session routing. Installation readiness uses the repository's simulated Ghidra 12.1.4 / Java 21 host; no engine starts. The supplementary RISC-V cases call the production provider's profile boundary directly. Assertions characterize the current misclassification. Remove the test and empty fixture directory afterward. ```ts import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; import { expect, it } from 'vitest'; import { createServer } from '../../../src/server/createServer.js'; import { parseMcpToolError } from '../../fixtures/mcpToolError.js'; import { parseConfig } from '../../../src/config/parseConfig.js'; import { GhidraProvider } from '../../../src/ghidra/GhidraProvider.js'; import { ghidraProfileInstallationHost } from '../../../src/ghidra/GhidraInstallation.fixture.js'; import { silentLogger } from '../../../src/logger.js'; import { projectAnalysisError } from '../../../src/domain/analysisErrorProjection.js'; import { AnalysisProviderRegistry } from '../../../src/application/binary/AnalysisProviderRegistry.js'; import { composeBinarySession } from '../../../src/application/binary/BinarySessionComposition.js'; import { writeFile, unlink } from 'node:fs/promises'; it('Ghidra override refusal through MCP with no-override control', async () => { const path = `${process.env.REA_REPRO_ROOT}/image.bin`; await writeFile(path, Buffer.from([0,0,160,225])); try { for (const override of [true,false]) { const config = parseConfig({GHIDRA_INSTALL_DIR:'/opt/ghidra_12.1.4_PUBLIC', ...(override ? {REA_GHIDRA_LANGUAGE_ID:'ARM:LE:32:v7'} : {})}); if (!config.ok) throw config.error; let clients = 0; const provider = new GhidraProvider(config.value, silentLogger, {}, ghidraProfileInstallationHost, () => { clients++; throw new Error('unexpected client startup'); }); const session = composeBinarySession(new AnalysisProviderRegistry([provider])); const server = createServer({kind:'session',session},{logger:silentLogger}); const client = new Client({name:'override-state',version:'1'}); const [ct,st] = InMemoryTransport.createLinkedPair(); try { await server.connect(st); await client.connect(ct); const result = await client.callTool({name:'open_binary',arguments:{path,format:'raw-binary',architecture:'arm',entry_point:'0x8000000',provider_id:'ghidra'}}); if (override) { const {error} = parseMcpToolError(result); expect(error).toMatchObject({code:'unsupported_target',category:'unsupported_target',details:{reason:'REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import.'}}); expect(error.details).not.toHaveProperty('settings'); console.log('GHIDRA', JSON.stringify({...error,details:{...error.details,path:'image.bin'}})); } else { expect(result.isError).not.toBe(true); console.log('GHIDRA_CONTROL', 'open succeeded'); } expect(clients).toBe(0); } finally { await client.close(); await server.close(); await session.close(); } } } finally { await unlink(path); } }); it('Ghidra RISC-V language/compiler conflicts and admitted control', async () => { for (const env of [{}, {REA_GHIDRA_LANGUAGE_ID:'RISCV:LE:32:AndeStar_v5'}, {REA_GHIDRA_LANGUAGE_ID:'RISCV:LE:32:default',REA_GHIDRA_COMPILER_SPEC_ID:'wrong'}]) { const config = parseConfig({GHIDRA_INSTALL_DIR:'/opt/ghidra_12.1.4_PUBLIC', ...env}); if (!config.ok) throw config.error; const provider = new GhidraProvider(config.value,silentLogger,{},ghidraProfileInstallationHost); try { const result = await provider.resolveAnalysisProfile({path:'/fixture.elf',sha256:'a'.repeat(64),kind:'executable',format:'elf',architecture:'riscv',availableArchitectures:['riscv'],riscv:{elfClass:32,byteOrder:'little',type:2,flags:5}}); if (Object.keys(env).length === 0) expect(result.ok).toBe(true); else { expect(result.ok).toBe(false); if(result.ok) throw new Error('expected refusal'); const error = projectAnalysisError(result.error); expect(error.code).toBe('unsupported_target'); expect(error.details).not.toHaveProperty('settings'); console.log('RISCV', JSON.stringify({env,error})); } } finally { await provider.closeOwnedResources(); } } }); ``` ### Expected behavior Keep refusing the incompatible override and preserving the caller selection, but identify its named environment setting and constraint through `configuration_invalid` / `invalid_input`, with environment-correction advice. The [unsupported-target type](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L7-L23) describes unsupported target formats. The [RISC-V guide](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/ghidra-riscv.md#L53-L59) describes refusal of conflicting environment settings. Existing [language/compiler admission](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/ghidra/GhidraLanguageCatalog.ts#L181-L211) already models rejected configured selections as `ConfigurationError` with setting names and constraints. Configured values need not be exposed. ### Impact and scope P2: clients receive the wrong typed failure category and recovery for a bounded optional-configuration conflict. A raw ARM input with an override failed, and its no-override control opened without starting a provider client. RISC-V no-override profile resolution succeeded, while language and compiler conflicts failed. Removing an unneeded override is a demonstrated admission workaround. No real Ghidra import, analysis, automatic-provider selection or resulting analysis quality was tested. PSP, DOS and nonlegacy MIPS conflicts use the same error constructor by source inspection; their runtime behavior was not exercised. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. Fetched main `6d356b84344659085f2b912857b5e27fe0160c9b` has unchanged relevant code. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Vitest 5.0.3. Native providers were simulated. The repository-pinned Node 24.18.0 was not exercised. ### Target and artifact context Four-byte synthetic ARM raw image (00 00 a0 e1), architecture arm, entry point 0x8000000, explicit Ghidra selection. Additional synthetic ELF32 little-endian RISC-V ET_EXEC metadata with flags 0x5. No real Ghidra database or target execution. ### Interface MCP ### Earliest failure stage Setup or provider admission ### Relevant output or evidence The raw-image MCP error contained the following fields (fixture path omitted): ```json {"code":"unsupported_target","category":"unsupported_target","retryable":false,"remediation":{"action":"Select a target supported by this operation or choose an operation supporting the reported target format."},"details":{"operation":"resolve_analysis_profile","reason":"REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import."}} ``` `details.settings` was absent. The same `open_binary` request without the override succeeded, with zero provider client starts in either case. Each RISC-V conflict returned `unsupported_target` with reason `The caller-selected processor or compiler specification is outside the verified generic little-endian RISC-V profile. The requested override has not been changed.` and the same generic target-format remediation; both omitted settings. The no-override RISC-V profile succeeded. The final focused file passed all four cases, including the two separate Frida cases. ### Typed outcome or recovery expected `configuration_invalid` / `invalid_input`, with a setting constraint naming `REA_GHIDRA_LANGUAGE_ID` and/or `REA_GHIDRA_COMPILER_SPEC_ID` as applicable. Tell callers to remove or correct the conflicting environment selection in the environment starting REA. Preserve the refusal and avoid silently replacing their selection. ### Analysis or suspected cause Source-derived: the [profile resolver](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/ghidra/GhidraAnalysisProfile.ts#L94-L124) and [pinned-language/DOS guards](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/ghidra/GhidraAnalysisProfile.ts#L176-L207) construct `AnalysisUnsupportedTargetError`. The [provider](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/ghidra/GhidraProvider.ts#L385-L398) returns this before catalog validation, so the named-configuration error path is bypassed. [Explicit provider selection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/binary/AnalysisProviderRegistry.ts#L590-L597) returns the profile error unchanged. ### Additional context Related #5972 concerns catalog directory-read errors being swallowed and then blamed on valid configuration. This refusal occurs in the profile resolver before catalog enumeration; correcting directory-read handling does not repair it. #3930 and open PR #4103 address missing provider-selection diagnostics on the raw override route. Their fix wraps unsupported-target profile errors in a provider-selection error, preserving target-unsupported semantics; it does not classify the conflicting settings or provide configuration recovery. Closed #3178 changed legacy MIPS override acceptance; this report preserves the intended refusal for the pinned lanes. #1184 concerns a separate path-parser configuration diagnostic, and feature #5431 concerns shared MCP/schema/timeout recovery.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Raw-binary and pinned RISC-V override conflicts now use a shared languageOverrideConflict returning ConfigurationError with the conflicting setting names. A matching language with incompatible compiler names only the compiler setting. Generic target unsupported decisions remain separate.

Implementation: GhidraAnalysisProfile.ts, GhidraAnalysisProfile.ts.

The relevant regression checks in GhidraRiscvProfile.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380729184 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Raw-binary and pinned RISC-V override conflicts now use a shared languageOverrideConflict returning ConfigurationError with the conflicting setting names. A matching language with incompatible compiler names only the compiler setting. Generic target unsupported decisions remain separate. Implementation: [GhidraAnalysisProfile.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ghidra/GhidraAnalysisProfile.ts#L78), [GhidraAnalysisProfile.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ghidra/GhidraAnalysisProfile.ts#L149). The relevant regression checks in [GhidraRiscvProfile.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/ghidra/GhidraRiscvProfile.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3676
No description provided.