fix(ghidra): refuse PSP targets with unexamined ABI declarations (#3904)

The generic Ghidra MIPS lane refuses a target whose ABI declaration
inspection is incomplete, such as a section-header table beyond the
captured file, because unexamined headers may hold declarations that
would refuse it. The PSP Allegrex lane skipped that check. It admitted
the target and resolved a supported profile while target inspection
said those declarations remained unknown.

The incomplete-declaration rule is now one shared helper,
ghidraMipsIncompleteDeclarationsReason, used by both the generic MIPS
and the PSP lanes.

Fixes #3329

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-14 23:30:12 -07:00
1 parent 57f30029b1
commit ab45bf641e
3 files changed
+80 -25

No files matched your search

+16 -5
View File
@@ -69,6 +69,20 @@ const hasLegacyMipsHeaderShape = (target: BinaryTarget): boolean => {
};
/** Return a refusal for GNU ABI attributes this existing MIPS lane cannot model. */
/**
* Every MIPS lane, including PSP, needs every ABI declaration source examined:
* headers outside the captured file may hold declarations that would refuse it.
*/
export const ghidraMipsIncompleteDeclarationsReason = (
metadata: MipsElfMetadata,
): string | null =>
(metadata.limitations?.length ?? 0) > 0
? [
"MIPS ABI declaration inspection is incomplete; unsupported attributes or conflicting declaration sources may remain unknown.",
...(metadata.limitations ?? []),
].join(" ")
: null;
export const ghidraMipsGnuAttributesUnsupportedReason = (
metadata: MipsElfMetadata,
): string | null => {
@@ -164,11 +178,8 @@ export const ghidraMipsUnsupportedReason = (
if (load.symbolRelativeCount !== 0)
return "Symbol-bearing R_MIPS_REL32 is unsupported: Ghidra normalizes MIPS16 function addresses and can lose the source function-pointer mode tag. Symbol-index-zero REL32 is verified separately.";
}
if ((metadata.limitations?.length ?? 0) > 0)
return [
"MIPS ABI declaration inspection is incomplete; unsupported attributes or conflicting declaration sources may remain unknown.",
...(metadata.limitations ?? []),
].join(" ");
const incomplete = ghidraMipsIncompleteDeclarationsReason(metadata);
if (incomplete !== null) return incomplete;
const abi = metadata.abiFlags;
if (shared && abi === null && (metadata.limitations?.length ?? 0) === 0)
return null; // Inspected absence leaves FP semantics unknown, never inferred.
+3
View File
@@ -1,6 +1,7 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
ghidraMipsGnuAttributesUnsupportedReason,
ghidraMipsIncompleteDeclarationsReason,
ghidraMipsUnsupportedReason,
} from "./GhidraMipsProfile.js";
import { ghidraPowerPcUnsupportedReason } from "./GhidraPowerPcProfile.js";
@@ -33,6 +34,8 @@ export const ghidraProcessorUnsupportedReason = (
const gnuAttributesReason =
ghidraMipsGnuAttributesUnsupportedReason(metadata);
if (gnuAttributesReason !== null) return gnuAttributesReason;
const incomplete = ghidraMipsIncompleteDeclarationsReason(metadata);
if (incomplete !== null) return incomplete;
if (metadata.elfClass !== 32 || metadata.byteOrder !== "little")
return "The PSP Allegrex profile requires little-endian ELF32.";
if (metadata.type !== 2)
@@ -326,31 +326,72 @@ describe.skipIf(process.platform === "win32")(
},
);
/**
* A PSPSDK-shaped ELF32 whose PT_MIPS_ABIFLAGS record matches the PSP profile.
* Optionally declare a section-header table beyond the captured file.
*/
const pspElfBytes = ({ sectionHeadersBeyondEof = false } = {}): Buffer => {
const abiOffset = 8192;
const bytes = Buffer.alloc(abiOffset + 24);
bytes.set([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]);
bytes.writeUInt16LE(2, 16);
bytes.writeUInt16LE(8, 18);
bytes.writeUInt32LE(1, 20);
bytes.writeUInt32LE(52, 28);
bytes.writeUInt32LE(0x10a23001, 36);
bytes.writeUInt16LE(52, 40);
bytes.writeUInt16LE(32, 42);
bytes.writeUInt16LE(1, 44);
if (sectionHeadersBeyondEof) {
bytes.writeUInt32LE(bytes.length + 64, 32); // e_shoff
bytes.writeUInt16LE(40, 46); // e_shentsize
bytes.writeUInt16LE(2, 48); // e_shnum
}
bytes.writeUInt32LE(0x70000003, 52);
bytes.writeUInt32LE(abiOffset, 56);
bytes.writeUInt32LE(24, 68);
bytes.writeUInt32LE(24, 72);
// GNU readelf -x .MIPS.abiflags, not bytes generated by REA.
bytes.set(
Buffer.from("000002000101000200000000000000000100000000000000", "hex"),
abiOffset,
);
return bytes;
};
describe.skipIf(process.platform === "win32")(
"PSP inspected ABI declaration boundary",
() => {
it("refuses a PSP image whose section headers lie beyond the captured file", async () => {
const { directory, provider } = await installation();
const path = join(directory, "psp-truncated-sections.elf");
await writeFile(path, pspElfBytes({ sectionHeadersBeyondEof: true }));
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
if (
parsed.value.kind !== "executable" ||
parsed.value.architecture !== "mips"
)
throw new Error("PSP ELF was not recognized as MIPS");
expect(parsed.value.mips.abiFlags).toEqual(pspAbiFlags);
expect(parsed.value.mips.limitations).toContainEqual(
expect.stringContaining("ABI declarations in unavailable headers"),
);
expect(provider.inspectTargetSupport(parsed.value).status).toBe(
"unsupported",
);
const result = await provider.resolveAnalysisProfile(parsed.value);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Incomplete ABI inspection was admitted");
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
expect(result.error.message).toContain(
"MIPS ABI declaration inspection is incomplete",
);
});
it("retains independently inspected PSP ABI bytes through the real target reader", async () => {
const { directory, provider } = await installation();
const abiOffset = 8192;
const bytes = Buffer.alloc(abiOffset + 24);
bytes.set([0x7f, 0x45, 0x4c, 0x46, 1, 1, 1]);
bytes.writeUInt16LE(2, 16);
bytes.writeUInt16LE(8, 18);
bytes.writeUInt32LE(1, 20);
bytes.writeUInt32LE(52, 28);
bytes.writeUInt32LE(0x10a23001, 36);
bytes.writeUInt16LE(52, 40);
bytes.writeUInt16LE(32, 42);
bytes.writeUInt16LE(1, 44);
bytes.writeUInt32LE(0x70000003, 52);
bytes.writeUInt32LE(abiOffset, 56);
bytes.writeUInt32LE(24, 68);
bytes.writeUInt32LE(24, 72);
// GNU readelf -x .MIPS.abiflags, not bytes generated by REA.
bytes.set(
Buffer.from("000002000101000200000000000000000100000000000000", "hex"),
abiOffset,
);
const bytes = pspElfBytes();
const path = join(directory, "psp-with-abi.elf");
await writeFile(path, bytes);
const parsed = await parseBinaryTarget(path);