[GH-ISSUE #3329] [Bug] PSP admission accepts unexamined ABI declaration headers #1200

Closed
opened 2026-10-15 18:31:56 +00:00 by bsakaguchi · 0 comments
Owner

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

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

MCP open_binary with provider ghidra accepts a PSP ELF as supported when its program-header ABI record matches the PSP profile but its declared section-header table is outside the captured file. Target inspection explicitly reports that ABI declarations in those unavailable headers remain unknown. The PSP admission path still resolves a supported profile and opens the target, contrary to the documented refusal of incomplete ABI declaration inspection.

Both public MCP opens succeeded. The complete-declaration control had no inspection limitations. The incomplete case retained the explicit unknown-declarations limitation but was still supported and admitted. The reproduction completed with exit status 0.

Reproduction

  1. Prepare a clean checkout at the reported baseline and build the runtime.
git clone https://github.com/morluto/rea.git
cd rea
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. From the checkout root, run the command below. It creates a 256-byte little-endian ELF32 ET_EXEC with the required PSP flags and complete 24-byte program-header ABI record. The negative case differs by declaring two 40-byte section headers at file offset 512, beyond EOF. The placeholder extension satisfies the existing fingerprint and metadata checks; its contents are never executed. Every input file is created inline, and the command removes all created files afterward.
node --input-type=module <<'JS'
import { mkdtemp, writeFile, rm, mkdir, realpath } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { composeBinarySession } from './dist/application/binary/BinarySessionComposition.js';
import { AnalysisProviderRegistry } from './dist/application/binary/AnalysisProviderRegistry.js';
import { parseBinaryTarget } from './dist/application/BinaryTargetResolver.js';
import { parseConfig } from './dist/config/parseConfig.js';
import { GhidraProvider } from './dist/ghidra/GhidraProvider.js';
import { silentLogger } from './dist/logger.js';
import { createServer } from './dist/server/createServer.js';
const root = await realpath(await mkdtemp(join(tmpdir(), 'rea-psp-repro-')));
const extension = join(root, 'Ghidra/Extensions/ghidra-allegrex');
for (const [name, content] of Object.entries({
  'Module.manifest': '', 'extension.properties': 'name=ghidra-allegrex\nversion=12.1.4\n',
  'data/languages/allegrex.ldefs': 'fixture', 'data/languages/allegrex.sla': 'fixture',
  'data/languages/allegrex.pspec': 'fixture', 'data/languages/allegrex.cspec': 'fixture',
  'lib/extension.jar': 'fixture',
})) {
  const path = join(extension, name); await mkdir(dirname(path), { recursive: true });
  await writeFile(path, content);
}
const config = parseConfig({ GHIDRA_INSTALL_DIR: root });
if (!config.ok) throw config.error;
const host = {
  platform: 'linux', architecture: 'x64',
  readText: () => 'application.version=12.1.4\napplication.java.min=21\n',
  executable: () => true,
  probeJava: () => ({ osArch: 'amd64', osName: 'Linux', version: '21.0.11', major: 21, home: root, bits: 64, runtime: 'jdk' }),
};
const provider = new GhidraProvider(config.value, silentLogger, {}, host);
const session = composeBinarySession(new AnalysisProviderRegistry([provider]));
const server = createServer({ kind: 'session', session }, { logger: silentLogger });
const mcp = new Client({ name: 'psp-repro', version: '1.0.0' });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
  await server.connect(serverTransport); await mcp.connect(clientTransport);
  for (const truncated of [false, true]) {
    const b = Buffer.alloc(256);
    b.set([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]);
    b.writeUInt16LE(2,16); b.writeUInt16LE(8,18); b.writeUInt32LE(1,20);
    b.writeUInt32LE(52,28); b.writeUInt32LE(0x10a23001,36);
    b.writeUInt32LE(truncated ? 512 : 0,32);
    b.writeUInt16LE(truncated ? 40 : 0,46); b.writeUInt16LE(truncated ? 2 : 0,48);
    b.writeUInt16LE(52,40); b.writeUInt16LE(32,42); b.writeUInt16LE(1,44);
    b.writeUInt32LE(0x70000003,52); b.writeUInt32LE(128,56);
    b.writeUInt32LE(24,68); b.writeUInt32LE(24,72);
    b.set(Buffer.from('000002000101000200000000000000000100000000000000','hex'),128);
    const path = join(root, `sample-${truncated ? 'truncated' : 'complete'}.elf`);
    await writeFile(path,b);
    const target = await parseBinaryTarget(path); if (!target.ok) throw target.error;
    const reply = await mcp.callTool({ name: 'open_binary', arguments: { path, provider_id: 'ghidra' } });
    console.log(JSON.stringify({ truncated, limitations: target.value.mips.limitations ?? [], support: provider.inspectTargetSupport(target.value).status, open_error: reply.isError === true }));
  }
} finally {
  await Promise.allSettled([mcp.close(), server.close()]); await session.close();
  await rm(root, { recursive: true, force: true });
}
JS
  1. Compare the two printed results. The truncated: true case reports that zero of two section headers could be examined, yet both cases return support: "supported" and open_error: false.

Expected behavior

The complete-declaration control should open successfully. The case with unexamined section headers should be refused before provider launch or supported profile admission, with the incomplete declaration inspection preserved in its diagnostic. A valid program-header ABI record cannot establish the absence of conflicting or unsupported declarations in uncaptured section headers.

docs/ghidra-mips.md:71 says the provider refuses incomplete ABI declaration inspection even when a program-header ABI record is valid. docs/ghidra-psp.md:11 defines the PSP declaration boundary and explicitly refuses missing, uninspected and contradictory producer declarations.

Impact and scope

Impact level: P2. The assessment is supported. A declared unsupported input shape crosses the PSP admission gate and receives a supported profile despite known incomplete ABI coverage. The observed consequence is incorrect admission; downstream wrong analysis or a real-engine import failure has not been established.

Workaround status: unverified. No way to obtain a complete declaration inspection from this captured file was verified. A separately acquired complete ELF could establish the missing declarations, but that source is not available in this reproduction.

File parsing, support classification and MCP admission were observed with real fixture-file parsing and the built runtime. This tests pre-launch target and profile admission. Real Ghidra startup, Allegrex import, decompilation, imported analysis and any effect on recovered code were not executed.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Observed on macOS 27.0.1 with Node.js v26.10.0. The host architecture was not recorded. Synthetic installation observations report Linux x64, Ghidra 12.1.4 and JDK 21.0.11 with osArch: "amd64" and 64-bit JDK runtime identity. No real Ghidra or JDK process starts.

Target and artifact context

Two synthetic 256-byte little-endian ELF32 EM_MIPS ET_EXEC files declare PSP flags 0x10a23001 and a complete 24-byte PT_MIPS_ABIFLAGS record at offset 128. The control declares no section table; the negative case declares two 40-byte section headers starting at offset 512, outside the captured bytes. The fixture extension includes Module.manifest, extension.properties declaring name=ghidra-allegrex and version=12.1.4, Allegrex language files and a placeholder lib/extension.jar.

Interface

MCP

Earliest failure stage

Setup or provider admission

Relevant output or evidence

{"truncated":false,"limitations":[],"support":"supported","open_error":false}
{"truncated":true,"limitations":["MIPS section header table at file offset 512 extends beyond the captured file: examined 0 of 2 complete headers. ABI declarations in unavailable headers remain unknown."],"support":"supported","open_error":false}

Typed outcome or recovery expected

Rerun both fixtures through public MCP open_binary. The complete-declaration PSP control must remain admitted; the otherwise equivalent fixture with uncaptured section headers must fail with the incomplete-inspection reason. Preserve the target reader's distinction between inspected absence and declarations that could not be examined, including when a valid program-header ABI record exists.

Analysis or suspected cause

The following reasoning is source-derived. src/application/MipsElfMetadata.ts:144 records the unavailable-header limitation. src/ghidra/GhidraPspProfile.ts:25 selects the PSP-specific gate and returns success after checking the observed record without rejecting metadata.limitations. src/ghidra/GhidraProvider.ts:184 uses that gate for support, and src/ghidra/GhidraAnalysisProfile.ts:74 uses it again before resolving the PSP profile.

Additional context

  • #3155 requests admitting recordless carved legacy MIPS32 files with explicit limitations; this report concerns a PSP file with an inspected program-header ABI record that is admitted despite the current incomplete-inspection refusal contract.
  • #1619, closed as completed, reported generic MIPS32r2 files being refused because section headers lay past EOF; this report concerns the opposite failure in PSP admission, so it does not reproduce that completed issue's failed contract.
  • #3096, closed as completed, concerns parsing and reconciling captured GNU ABI attributes; this report concerns declarations in uncaptured headers, which cannot be inspected.
  • #1330 requests the overall PSP/Allegrex integration; this report concerns incorrect admission in the implemented PSP profile.
  • #1620, closed as completed, introduced recordless legacy MIPS admission and a language pin while explicitly excluding PSP; this report concerns PSP admission with a separate ABI flags record.
  • #2338 requests a plain MIPS32 PIC shared-image lane and correct link-base placement; this report concerns static PSP ET_EXEC declaration inspection.
  • #3178 concerns rejection of a caller-selected language for an admitted legacy MIPS target; this report concerns admitting PSP targets whose declaration inspection is incomplete.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3329 ### 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 MCP `open_binary` with provider `ghidra` accepts a PSP ELF as supported when its program-header ABI record matches the PSP profile but its declared section-header table is outside the captured file. Target inspection explicitly reports that ABI declarations in those unavailable headers remain unknown. The PSP admission path still resolves a supported profile and opens the target, contrary to the documented refusal of incomplete ABI declaration inspection. Both public MCP opens succeeded. The complete-declaration control had no inspection limitations. The incomplete case retained the explicit unknown-declarations limitation but was still supported and admitted. The reproduction completed with exit status 0. ### Reproduction 1. Prepare a clean checkout at the reported baseline and build the runtime. ```sh git clone https://github.com/morluto/rea.git cd rea git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. From the checkout root, run the command below. It creates a 256-byte little-endian ELF32 `ET_EXEC` with the required PSP flags and complete 24-byte program-header ABI record. The negative case differs by declaring two 40-byte section headers at file offset 512, beyond EOF. The placeholder extension satisfies the existing fingerprint and metadata checks; its contents are never executed. Every input file is created inline, and the command removes all created files afterward. ```sh node --input-type=module <<'JS' import { mkdtemp, writeFile, rm, mkdir, realpath } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; import { composeBinarySession } from './dist/application/binary/BinarySessionComposition.js'; import { AnalysisProviderRegistry } from './dist/application/binary/AnalysisProviderRegistry.js'; import { parseBinaryTarget } from './dist/application/BinaryTargetResolver.js'; import { parseConfig } from './dist/config/parseConfig.js'; import { GhidraProvider } from './dist/ghidra/GhidraProvider.js'; import { silentLogger } from './dist/logger.js'; import { createServer } from './dist/server/createServer.js'; const root = await realpath(await mkdtemp(join(tmpdir(), 'rea-psp-repro-'))); const extension = join(root, 'Ghidra/Extensions/ghidra-allegrex'); for (const [name, content] of Object.entries({ 'Module.manifest': '', 'extension.properties': 'name=ghidra-allegrex\nversion=12.1.4\n', 'data/languages/allegrex.ldefs': 'fixture', 'data/languages/allegrex.sla': 'fixture', 'data/languages/allegrex.pspec': 'fixture', 'data/languages/allegrex.cspec': 'fixture', 'lib/extension.jar': 'fixture', })) { const path = join(extension, name); await mkdir(dirname(path), { recursive: true }); await writeFile(path, content); } const config = parseConfig({ GHIDRA_INSTALL_DIR: root }); if (!config.ok) throw config.error; const host = { platform: 'linux', architecture: 'x64', readText: () => 'application.version=12.1.4\napplication.java.min=21\n', executable: () => true, probeJava: () => ({ osArch: 'amd64', osName: 'Linux', version: '21.0.11', major: 21, home: root, bits: 64, runtime: 'jdk' }), }; const provider = new GhidraProvider(config.value, silentLogger, {}, host); const session = composeBinarySession(new AnalysisProviderRegistry([provider])); const server = createServer({ kind: 'session', session }, { logger: silentLogger }); const mcp = new Client({ name: 'psp-repro', version: '1.0.0' }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await mcp.connect(clientTransport); for (const truncated of [false, true]) { const b = Buffer.alloc(256); b.set([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]); b.writeUInt16LE(2,16); b.writeUInt16LE(8,18); b.writeUInt32LE(1,20); b.writeUInt32LE(52,28); b.writeUInt32LE(0x10a23001,36); b.writeUInt32LE(truncated ? 512 : 0,32); b.writeUInt16LE(truncated ? 40 : 0,46); b.writeUInt16LE(truncated ? 2 : 0,48); b.writeUInt16LE(52,40); b.writeUInt16LE(32,42); b.writeUInt16LE(1,44); b.writeUInt32LE(0x70000003,52); b.writeUInt32LE(128,56); b.writeUInt32LE(24,68); b.writeUInt32LE(24,72); b.set(Buffer.from('000002000101000200000000000000000100000000000000','hex'),128); const path = join(root, `sample-${truncated ? 'truncated' : 'complete'}.elf`); await writeFile(path,b); const target = await parseBinaryTarget(path); if (!target.ok) throw target.error; const reply = await mcp.callTool({ name: 'open_binary', arguments: { path, provider_id: 'ghidra' } }); console.log(JSON.stringify({ truncated, limitations: target.value.mips.limitations ?? [], support: provider.inspectTargetSupport(target.value).status, open_error: reply.isError === true })); } } finally { await Promise.allSettled([mcp.close(), server.close()]); await session.close(); await rm(root, { recursive: true, force: true }); } JS ``` 3. Compare the two printed results. The `truncated: true` case reports that zero of two section headers could be examined, yet both cases return `support: "supported"` and `open_error: false`. ### Expected behavior The complete-declaration control should open successfully. The case with unexamined section headers should be refused before provider launch or supported profile admission, with the incomplete declaration inspection preserved in its diagnostic. A valid program-header ABI record cannot establish the absence of conflicting or unsupported declarations in uncaptured section headers. `docs/ghidra-mips.md:71` says the provider refuses incomplete ABI declaration inspection even when a program-header ABI record is valid. `docs/ghidra-psp.md:11` defines the PSP declaration boundary and explicitly refuses missing, uninspected and contradictory producer declarations. ### Impact and scope Impact level: P2. The assessment is supported. A declared unsupported input shape crosses the PSP admission gate and receives a supported profile despite known incomplete ABI coverage. The observed consequence is incorrect admission; downstream wrong analysis or a real-engine import failure has not been established. Workaround status: unverified. No way to obtain a complete declaration inspection from this captured file was verified. A separately acquired complete ELF could establish the missing declarations, but that source is not available in this reproduction. File parsing, support classification and MCP admission were observed with real fixture-file parsing and the built runtime. This tests pre-launch target and profile admission. Real Ghidra startup, Allegrex import, decompilation, imported analysis and any effect on recovered code were not executed. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Observed on macOS 27.0.1 with Node.js v26.10.0. The host architecture was not recorded. Synthetic installation observations report Linux x64, Ghidra 12.1.4 and JDK 21.0.11 with `osArch: "amd64"` and 64-bit JDK runtime identity. No real Ghidra or JDK process starts. ### Target and artifact context Two synthetic 256-byte little-endian ELF32 `EM_MIPS` `ET_EXEC` files declare PSP flags `0x10a23001` and a complete 24-byte `PT_MIPS_ABIFLAGS` record at offset 128. The control declares no section table; the negative case declares two 40-byte section headers starting at offset 512, outside the captured bytes. The fixture extension includes `Module.manifest`, `extension.properties` declaring `name=ghidra-allegrex` and `version=12.1.4`, Allegrex language files and a placeholder `lib/extension.jar`. ### Interface MCP ### Earliest failure stage Setup or provider admission ### Relevant output or evidence ```text {"truncated":false,"limitations":[],"support":"supported","open_error":false} {"truncated":true,"limitations":["MIPS section header table at file offset 512 extends beyond the captured file: examined 0 of 2 complete headers. ABI declarations in unavailable headers remain unknown."],"support":"supported","open_error":false} ``` ### Typed outcome or recovery expected Rerun both fixtures through public MCP `open_binary`. The complete-declaration PSP control must remain admitted; the otherwise equivalent fixture with uncaptured section headers must fail with the incomplete-inspection reason. Preserve the target reader's distinction between inspected absence and declarations that could not be examined, including when a valid program-header ABI record exists. ### Analysis or suspected cause The following reasoning is source-derived. `src/application/MipsElfMetadata.ts:144` records the unavailable-header limitation. `src/ghidra/GhidraPspProfile.ts:25` selects the PSP-specific gate and returns success after checking the observed record without rejecting `metadata.limitations`. `src/ghidra/GhidraProvider.ts:184` uses that gate for support, and `src/ghidra/GhidraAnalysisProfile.ts:74` uses it again before resolving the PSP profile. ### Additional context - [#3155](https://github.com/morluto/rea/issues/3155) requests admitting recordless carved legacy MIPS32 files with explicit limitations; this report concerns a PSP file with an inspected program-header ABI record that is admitted despite the current incomplete-inspection refusal contract. - [#1619](https://github.com/morluto/rea/issues/1619), closed as completed, reported generic MIPS32r2 files being refused because section headers lay past EOF; this report concerns the opposite failure in PSP admission, so it does not reproduce that completed issue's failed contract. - [#3096](https://github.com/morluto/rea/issues/3096), closed as completed, concerns parsing and reconciling captured GNU ABI attributes; this report concerns declarations in uncaptured headers, which cannot be inspected. - [#1330](https://github.com/morluto/rea/issues/1330) requests the overall PSP/Allegrex integration; this report concerns incorrect admission in the implemented PSP profile. - [#1620](https://github.com/morluto/rea/issues/1620), closed as completed, introduced recordless legacy MIPS admission and a language pin while explicitly excluding PSP; this report concerns PSP admission with a separate ABI flags record. - [#2338](https://github.com/morluto/rea/issues/2338) requests a plain MIPS32 PIC shared-image lane and correct link-base placement; this report concerns static PSP `ET_EXEC` declaration inspection. - [#3178](https://github.com/morluto/rea/issues/3178) concerns rejection of a caller-selected language for an admitted legacy MIPS target; this report concerns admitting PSP targets whose declaration inspection is incomplete.
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#1200
No description provided.