mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
test(ghidra): align MIPS verifier ABI and snapshot expectations
Fixes #3415. Retain independently inspected GNU attribute facts and compare exact snapshot wire fields. Preserve strict metadata comparisons, specific incomplete-ABI refusals and source immutability checks. Original implementation by @AbdulSamad94. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e9a71efa31
commit
a40ee741c2
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import {
|
||||
inspectMipsReadelf,
|
||||
expectedMipsElfProfile,
|
||||
inspectLegacyMipsReadelf,
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
@@ -120,6 +121,17 @@ async function verifySoftFloat(call, symbols, byteOrder) {
|
||||
);
|
||||
return { storage: boundaries };
|
||||
}
|
||||
function assertIncompleteMipsInspection(error) {
|
||||
assert.equal(error.code, "capability_unavailable");
|
||||
assert.equal(error.details.selection_reason, "target_unsupported");
|
||||
const rejection = error.details.rejections.find(
|
||||
(candidate) => candidate.provider_id === "ghidra",
|
||||
);
|
||||
assert.equal(rejection?.code, "architecture_unsupported");
|
||||
assert.match(rejection.reason, /ABI declaration inspection is incomplete/u);
|
||||
assert.match(rejection.reason, /section header table/u);
|
||||
}
|
||||
|
||||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
@@ -475,28 +487,9 @@ try {
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(snapshot.target.architecture, "mips");
|
||||
const abi = independent.mips.abiFlags;
|
||||
assert.deepEqual(
|
||||
snapshot.binding.analysis_profile.parameters.mips_elf,
|
||||
{
|
||||
elf_class: independent.mips.elfClass,
|
||||
byte_order: independent.mips.byteOrder,
|
||||
type: independent.mips.type,
|
||||
flags: independent.mips.flags,
|
||||
abi_flags: {
|
||||
version: abi.version,
|
||||
isa_level: abi.isaLevel,
|
||||
isa_revision: abi.isaRevision,
|
||||
gpr_size: abi.gprSize,
|
||||
cpr1_size: abi.cpr1Size,
|
||||
cpr2_size: abi.cpr2Size,
|
||||
fp_abi: abi.fpAbi,
|
||||
isa_extension: abi.isaExtension,
|
||||
ases: abi.ases,
|
||||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
},
|
||||
expectedMipsElfProfile(independent.mips),
|
||||
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
|
||||
);
|
||||
assert.equal(
|
||||
@@ -617,7 +610,8 @@ try {
|
||||
// PSP keeps its independent Allegrex/A-B-A checks; carved PSP is not verified.
|
||||
let carved;
|
||||
if (!psp) {
|
||||
// Carved firmware keeps all program headers/load bytes but loses sections.
|
||||
// A program-header ABI record survives carving, but GNU declarations in
|
||||
// uncaptured section headers remain unknown: both adapters must refuse.
|
||||
const read16 = (offset) =>
|
||||
byteOrder === "little"
|
||||
? sourceBytes.readUInt16LE(offset)
|
||||
@@ -655,79 +649,50 @@ try {
|
||||
carvedResolved.value.mips.limitations.join(" "),
|
||||
/section header table/u,
|
||||
);
|
||||
const carvedTarget = await call("open_binary", {
|
||||
path: carvedPath,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(carvedTarget.sha256, carvedSha256);
|
||||
const carvedImage = await call("inspect_native_load_image");
|
||||
assertMipsLoadedImage(
|
||||
carvedImage.observations,
|
||||
byteOrder,
|
||||
carvedSha256,
|
||||
);
|
||||
const carvedProcedures = await call("list_procedures");
|
||||
assert.ok(
|
||||
carvedProcedures.some(
|
||||
(p) =>
|
||||
BigInt(p.address) === BigInt(independent.symbols.rea_mips_entry),
|
||||
const carvedError = requireMcpToolError(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: carvedPath, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
);
|
||||
const carvedDossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
procedure: independent.symbols.rea_mips_entry,
|
||||
}),
|
||||
);
|
||||
assert.ok(carvedDossier.pseudocode.trim().length > 0);
|
||||
const carvedSnapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
|
||||
);
|
||||
await call("close_binary", { snapshot_path: carvedSnapshotPath });
|
||||
const carvedSnapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(carvedSnapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(carvedSnapshot.target.sha256, carvedSha256);
|
||||
assertIncompleteMipsInspection(carvedError);
|
||||
let carvedCliError;
|
||||
try {
|
||||
await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
carvedPath,
|
||||
independent.symbols.rea_mips_entry,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 60000, maxBuffer: 1024 * 1024 },
|
||||
);
|
||||
} catch (cause) {
|
||||
carvedCliError = cause;
|
||||
}
|
||||
assert.ok(
|
||||
carvedSnapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
item.includes("section header table"),
|
||||
),
|
||||
),
|
||||
);
|
||||
const carvedCli = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
carvedPath,
|
||||
independent.symbols.rea_mips_entry,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const carvedEvidence = parseEvidence(JSON.parse(carvedCli.stdout));
|
||||
assert.equal(carvedEvidence.subject?.digest.sha256, carvedSha256);
|
||||
assert.ok(
|
||||
carvedEvidence.limitations.some((item) =>
|
||||
item.includes("section header table"),
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
functionDossierSchema.parse(carvedEvidence.normalized_result)
|
||||
.procedure.address,
|
||||
carvedDossier.procedure.address,
|
||||
carvedCliError &&
|
||||
Number.isInteger(carvedCliError.code) &&
|
||||
carvedCliError.code !== 0,
|
||||
"Expected an incomplete-ABI refusal, not a timeout or success",
|
||||
);
|
||||
assertIncompleteMipsInspection(JSON.parse(carvedCliError.stdout));
|
||||
assert.deepEqual(await readFile(carvedPath), carvedBytes);
|
||||
carved = {
|
||||
sha256: carvedSha256,
|
||||
source_bytes: carvedBytes.length,
|
||||
section_header_offset: sectionOffset,
|
||||
effective_language: carvedImage.observations.language_id,
|
||||
status: "expected-incomplete-abi-refusal",
|
||||
limitations: carvedResolved.value.mips.limitations,
|
||||
procedures: carvedProcedures.length,
|
||||
cli_exit: carvedCliError.code,
|
||||
mcp_error: carvedError,
|
||||
};
|
||||
}
|
||||
reports.push({
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
MipsAbiFlags,
|
||||
MipsElfMetadata,
|
||||
MipsGnuAttributes,
|
||||
} from "../../../src/domain/binaryTargetTypes.js";
|
||||
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
|
||||
export type MipsReadelfMetadata = MipsElfMetadata & {
|
||||
readonly abiFlags: MipsAbiFlags;
|
||||
readonly gnuAttributes: MipsGnuAttributes | null;
|
||||
};
|
||||
export function expectedMipsElfProfile(
|
||||
mips: MipsReadelfMetadata,
|
||||
): Readonly<Record<string, JsonValue>>;
|
||||
|
||||
export type MipsFloatAbi = "hard" | "soft";
|
||||
export interface MipsReadelfFacts {
|
||||
mips: MipsElfMetadata;
|
||||
mips: MipsReadelfMetadata & { readonly gnuAttributes: null };
|
||||
symbols: Record<string, string>;
|
||||
}
|
||||
export function parseMipsReadelfSectionTypes(text: string): readonly string[];
|
||||
export function parseMipsReadelf(
|
||||
text: string,
|
||||
byteOrder: string,
|
||||
|
||||
@@ -23,6 +23,65 @@ const SOFT_FLOAT_SYMBOLS = [
|
||||
"rea_mips_soft_caller",
|
||||
];
|
||||
|
||||
/** Complete indexed ELF32 section types shared by the independent fixture readers. */
|
||||
export function parseMipsReadelfSectionTypes(text) {
|
||||
const counts = [
|
||||
...text.matchAll(/^\s*Number of section headers:\s*(\d+)\s*$/gmu),
|
||||
];
|
||||
assert.equal(counts.length, 1, "Missing or ambiguous readelf section count");
|
||||
const sectionCount = Number(counts[0][1]);
|
||||
assert.ok(sectionCount > 0, "Fixture must have a section header table");
|
||||
const sections = [...text.matchAll(/^[ \t]*\[[ \t]*(\d+)\][ \t]+(.+)$/gmu)];
|
||||
assert.equal(
|
||||
sections.length,
|
||||
sectionCount,
|
||||
"Incomplete readelf section table",
|
||||
);
|
||||
return sections.map((section, index) => {
|
||||
assert.equal(Number(section[1]), index, "Ambiguous readelf section index");
|
||||
const columns =
|
||||
/^(?:\S+[ \t]+)?(\S+)[ \t]+[\da-f]{8}[ \t]+[\da-f]+[ \t]+[\da-f]+[ \t]+[\da-f]{2}[ \t]+(?:[A-Za-z]+[ \t]+)?\d+[ \t]+\d+[ \t]+\d+[ \t]*$/u.exec(
|
||||
section[2],
|
||||
);
|
||||
assert.ok(columns !== null, "Malformed readelf section header");
|
||||
return columns[1];
|
||||
});
|
||||
}
|
||||
|
||||
/** Independent wire-format expectation shared by generic and PSP snapshots. */
|
||||
export function expectedMipsElfProfile(mips) {
|
||||
const abi = mips.abiFlags;
|
||||
assert.ok(abi != null, "Fixture ABI declarations must be inspected");
|
||||
assert.notEqual(
|
||||
mips.gnuAttributes,
|
||||
undefined,
|
||||
"Fixture GNU attributes must be inspected",
|
||||
);
|
||||
return {
|
||||
elf_class: mips.elfClass,
|
||||
byte_order: mips.byteOrder,
|
||||
type: mips.type,
|
||||
flags: mips.flags,
|
||||
gnu_attributes:
|
||||
mips.gnuAttributes === null
|
||||
? null
|
||||
: { fp_abi: mips.gnuAttributes.fpAbi, msa: mips.gnuAttributes.msa },
|
||||
abi_flags: {
|
||||
version: abi.version,
|
||||
isa_level: abi.isaLevel,
|
||||
isa_revision: abi.isaRevision,
|
||||
gpr_size: abi.gprSize,
|
||||
cpr1_size: abi.cpr1Size,
|
||||
cpr2_size: abi.cpr2Size,
|
||||
fp_abi: abi.fpAbi,
|
||||
isa_extension: abi.isaExtension,
|
||||
ases: abi.ases,
|
||||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse only the documented GNU readelf fields needed by our tiny fixtures.
|
||||
* `rel` objects carry their own symbol checks in mips-relocatable-checks.mjs.
|
||||
@@ -51,6 +110,10 @@ export function parseMipsReadelf(
|
||||
objectType === "rel" ? /^REL\b/u : /^EXEC\b/u,
|
||||
);
|
||||
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
|
||||
assert.ok(
|
||||
!parseMipsReadelfSectionTypes(text).includes("GNU_ATTRIBUTES"),
|
||||
"Fixture unexpectedly declares GNU attributes",
|
||||
);
|
||||
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+),.*$/gmu, "flags"));
|
||||
assert.equal(flags, 0x70001001, "Fixture ELF flags differ from build intent");
|
||||
assert.equal(field(/^ISA:\s*(.+)$/gmu, "ISA"), "MIPS32r2");
|
||||
@@ -123,6 +186,7 @@ export function parseMipsReadelf(
|
||||
type: objectType === "rel" ? 1 : 2,
|
||||
flags,
|
||||
abiFlags,
|
||||
gnuAttributes: null,
|
||||
},
|
||||
symbols,
|
||||
};
|
||||
@@ -144,7 +208,7 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
|
||||
);
|
||||
const result = await execute(
|
||||
command,
|
||||
["-h", "-A", "-s", "-W", path],
|
||||
["-h", "-A", "-S", "-s", "-W", path],
|
||||
options,
|
||||
);
|
||||
assert.equal(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseMipsReadelf,
|
||||
} from "./mips-checks.mjs";
|
||||
|
||||
// GNU readelf 2.40 output for the source-owned Clang 17 / LLD fixture.
|
||||
// GNU readelf 2.44 output for the source-owned Clang 19 / LLD 19 fixture.
|
||||
// This tests the reader boundary, not Ghidra; the real lane rebuilds both orders.
|
||||
const readelf = `ELF Header:
|
||||
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
|
||||
@@ -21,7 +21,7 @@ const readelf = `ELF Header:
|
||||
Version: 0x1
|
||||
Entry point address: 0x201b0
|
||||
Start of program headers: 52 (bytes into file)
|
||||
Start of section headers: 2996 (bytes into file)
|
||||
Start of section headers: 2624 (bytes into file)
|
||||
Flags: 0x70001001, noreorder, o32, mips32r2
|
||||
Size of this header: 52 (bytes)
|
||||
Size of program headers: 32 (bytes)
|
||||
@@ -30,6 +30,38 @@ const readelf = `ELF Header:
|
||||
Number of section headers: 24
|
||||
Section header string table index: 22
|
||||
|
||||
Section Headers:
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 00010118 000118 000018 18 A 0 0 8
|
||||
[ 2] .reginfo MIPS_REGINFO 00010130 000130 000018 18 A 0 0 4
|
||||
[ 3] .rodata PROGBITS 00010148 000148 00001e 00 A 0 0 1
|
||||
[ 4] .text PROGBITS 00020170 000170 0000f0 00 AX 0 0 16
|
||||
[ 5] .data PROGBITS 00030260 000260 000000 00 WA 0 0 16
|
||||
[ 6] .got PROGBITS 00030260 000260 000008 00 WAp 0 0 16
|
||||
[ 7] .sdata PROGBITS 00030268 000268 000004 00 WAp 0 0 4
|
||||
[ 8] .bss NOBITS 00030270 00026c 000000 00 WA 0 0 16
|
||||
[ 9] .mdebug.abi32 PROGBITS 00000000 00026c 000000 00 0 0 1
|
||||
[10] .pdr PROGBITS 00000000 00026c 000060 00 0 0 4
|
||||
[11] .debug_abbrev MIPS_DWARF 00000000 0002cc 0000b3 00 0 0 1
|
||||
[12] .debug_info MIPS_DWARF 00000000 00037f 00015e 00 0 0 1
|
||||
[13] .debug_str_offsets MIPS_DWARF 00000000 0004dd 000040 00 0 0 1
|
||||
[14] .debug_str MIPS_DWARF 00000000 00051d 0000ef 01 MS 0 0 1
|
||||
[15] .debug_addr MIPS_DWARF 00000000 00060c 000018 00 0 0 1
|
||||
[16] .comment PROGBITS 00000000 000624 00003e 01 MS 0 0 1
|
||||
[17] .debug_frame MIPS_DWARF 00000000 000664 00004c 00 0 0 4
|
||||
[18] .debug_line MIPS_DWARF 00000000 0006b0 0000f8 00 0 0 1
|
||||
[19] .debug_line_str MIPS_DWARF 00000000 0007a8 000098 01 MS 0 0 1
|
||||
[20] .debug_aranges MIPS_DWARF 00000000 000840 000020 00 0 0 1
|
||||
[21] .symtab SYMTAB 00000000 000860 000090 10 23 4 4
|
||||
[22] .shstrtab STRTAB 00000000 0008f0 0000f0 00 0 0 1
|
||||
[23] .strtab STRTAB 00000000 0009e0 00005f 00 0 0 1
|
||||
Key to Flags:
|
||||
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
|
||||
L (link order), O (extra OS processing required), G (group), T (TLS),
|
||||
C (compressed), x (unknown), o (OS specific), E (exclude),
|
||||
D (mbind), p (processor specific)
|
||||
|
||||
Symbol table '.symtab' contains 9 entries:
|
||||
Num: Value Size Type Bind Vis Ndx Name
|
||||
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
|
||||
@@ -54,6 +86,15 @@ ASEs:
|
||||
None
|
||||
FLAGS 1: 00000000
|
||||
FLAGS 2: 00000000
|
||||
|
||||
Static GOT:
|
||||
Canonical gp value: 00038250
|
||||
|
||||
Reserved entries:
|
||||
Address Access Value
|
||||
00030260 -32752(gp) 00000000
|
||||
00030264 -32748(gp) 80000000
|
||||
|
||||
`;
|
||||
const sha256 = "a".repeat(64);
|
||||
const image = {
|
||||
@@ -94,6 +135,7 @@ describe("MIPS real-verifier independent expectations", () => {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
flags: 0x70001001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: {
|
||||
isaLevel: 32,
|
||||
isaRevision: 2,
|
||||
@@ -229,6 +271,67 @@ describe("MIPS real-verifier independent expectations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS real-verifier GNU attribute coverage", () => {
|
||||
it.each(["exec", "rel"] as const)(
|
||||
"reports verified GNU attribute absence for both byte orders of %s fixtures",
|
||||
(objectType) => {
|
||||
for (const byteOrder of ["little", "big"]) {
|
||||
const text = readelf
|
||||
.replace("little endian", `${byteOrder} endian`)
|
||||
.replace(
|
||||
"EXEC (Executable file)",
|
||||
objectType === "rel"
|
||||
? "REL (Relocatable file)"
|
||||
: "EXEC (Executable file)",
|
||||
);
|
||||
expect(
|
||||
parseMipsReadelf(text, byteOrder, "hard", objectType).mips
|
||||
.gnuAttributes,
|
||||
).toBeNull();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects GNU attribute sections even when their name is unrelated", () => {
|
||||
expect(() =>
|
||||
parseMipsReadelf(
|
||||
readelf.replace(
|
||||
".comment PROGBITS",
|
||||
".comment GNU_ATTRIBUTES",
|
||||
),
|
||||
"little",
|
||||
),
|
||||
).toThrow(/GNU attributes/u);
|
||||
});
|
||||
|
||||
it("uses the section type rather than its name to establish absence", () => {
|
||||
expect(
|
||||
parseMipsReadelf(
|
||||
readelf.replace(
|
||||
".comment PROGBITS",
|
||||
".gnu.attributes PROGBITS",
|
||||
),
|
||||
"little",
|
||||
).mips.gnuAttributes,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"missing table",
|
||||
readelf.replace(
|
||||
/Section Headers:[\s\S]*?Key to Flags:/u,
|
||||
"Key to Flags:",
|
||||
),
|
||||
],
|
||||
["missing entry", readelf.replace(/^.*\[ 5\].*\n/gmu, "")],
|
||||
["duplicate index", readelf.replace("[ 5]", "[ 4]")],
|
||||
["malformed entry", readelf.replace(/^.*\[ 5\].*$/gmu, " [ 5] malformed")],
|
||||
])("refuses GNU attribute absence from a %s", (_name, text) => {
|
||||
expect(() => parseMipsReadelf(text, "little")).toThrow(/section/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS soft-float real-verifier expectations", () => {
|
||||
const softReadelf = readelf
|
||||
.replace("CPR1 size: 32", "CPR1 size: 0")
|
||||
@@ -249,6 +352,7 @@ describe("MIPS soft-float real-verifier expectations", () => {
|
||||
it("reads the soft-float ABI record and its fixture symbols", () => {
|
||||
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
|
||||
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
|
||||
expect(parsed.mips.gnuAttributes).toBeNull();
|
||||
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { it } from "vitest";
|
||||
import { parsePspReadelfAbi } from "./psp-fixture.mjs";
|
||||
import { expectedMipsElfProfile } from "./mips-checks.mjs";
|
||||
import { ghidraMipsProfileParameters } from "../../../src/ghidra/GhidraMipsProfile.js";
|
||||
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { parsePspReadelfMips } from "./psp-fixture.mjs";
|
||||
|
||||
// GNU readelf 2.44 output from the maintained PSPDEV v20261001 fixture.
|
||||
const observed = `MIPS ABI Flags Version: 0
|
||||
@@ -16,19 +19,42 @@ FLAGS 1: 00000001
|
||||
FLAGS 2: 00000000
|
||||
`;
|
||||
|
||||
// Constructed section-table controls combined with the captured ABI declaration;
|
||||
// this does not establish a real PSPSDK or Allegrex-provider run.
|
||||
const metadata = `ELF Header:
|
||||
Class: ELF32
|
||||
Data: 2's complement, little endian
|
||||
Type: EXEC (Executable file)
|
||||
Machine: MIPS R3000
|
||||
Flags: 0x10a23001, noreorder, allegrex, eabi32, mips2
|
||||
Number of section headers: 3
|
||||
Section Headers:
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 08800118 000118 000018 18 A 0 0 8
|
||||
[ 2] .text PROGBITS 08800130 000130 000010 00 AX 0 0 16
|
||||
${observed}`;
|
||||
|
||||
it("reads the independent PSP producer's single-float declaration", () => {
|
||||
assert.deepEqual(parsePspReadelfAbi(observed), {
|
||||
version: 0,
|
||||
isaLevel: 2,
|
||||
isaRevision: 0,
|
||||
gprSize: 1,
|
||||
cpr1Size: 1,
|
||||
cpr2Size: 0,
|
||||
fpAbi: 2,
|
||||
isaExtension: 0,
|
||||
ases: 0,
|
||||
flags1: 1,
|
||||
flags2: 0,
|
||||
assert.deepEqual(parsePspReadelfMips(metadata), {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: {
|
||||
version: 0,
|
||||
isaLevel: 2,
|
||||
isaRevision: 0,
|
||||
gprSize: 1,
|
||||
cpr1Size: 1,
|
||||
cpr2Size: 0,
|
||||
fpAbi: 2,
|
||||
isaExtension: 0,
|
||||
ases: 0,
|
||||
flags1: 1,
|
||||
flags2: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +72,108 @@ it.each([
|
||||
])(
|
||||
"rejects unsupported or ambiguous independent ABI output: %s",
|
||||
(before, after) => {
|
||||
assert.throws(() => parsePspReadelfAbi(observed.replace(before, after)));
|
||||
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"missing table",
|
||||
metadata.replace(
|
||||
/Section Headers:[\s\S]*?MIPS ABI Flags/u,
|
||||
"MIPS ABI Flags",
|
||||
),
|
||||
],
|
||||
["incomplete table", metadata.replace(/^.*\[ 2\].*\n/gmu, "")],
|
||||
["duplicate index", metadata.replace("[ 2]", "[ 1]")],
|
||||
[
|
||||
"GNU section under another name",
|
||||
metadata.replace(
|
||||
".text PROGBITS",
|
||||
".text GNU_ATTRIBUTES",
|
||||
),
|
||||
],
|
||||
])("refuses PSP GNU attribute absence from a %s", (_name, text) => {
|
||||
assert.throws(() => parsePspReadelfMips(text), /section|GNU attributes/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Class: ELF32", "Class: ELF64"],
|
||||
["little endian", "big endian"],
|
||||
["Type: EXEC", "Type: REL"],
|
||||
["Flags: 0x10a23001", "Flags: 0x70001001"],
|
||||
["Class: ELF32", "Class: ELF32\n Class: ELF32"],
|
||||
])("refuses contradictory or ambiguous PSP headers: %s", (before, after) => {
|
||||
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
|
||||
});
|
||||
|
||||
// GNU readelf 2.40 independently decoded this declaration from a constructed
|
||||
// ELF32/PSP-shaped control, not a PSPSDK build or a real Allegrex import.
|
||||
const gnuDeclaration = `Attribute Section: gnu
|
||||
File Attributes
|
||||
Tag_GNU_MIPS_ABI_FP: Hard float (single precision)
|
||||
`;
|
||||
const gnuMetadata = metadata.replace(
|
||||
".text PROGBITS",
|
||||
".text GNU_ATTRIBUTES",
|
||||
);
|
||||
|
||||
it.each(["before", "after"])(
|
||||
"retains the independent single-float GNU declaration %s the ABI dump",
|
||||
(position) => {
|
||||
const text =
|
||||
position === "before"
|
||||
? gnuMetadata.replace(observed, `${gnuDeclaration}\n${observed}`)
|
||||
: `${gnuMetadata}\n${gnuDeclaration}`;
|
||||
assert.deepEqual(parsePspReadelfMips(text), {
|
||||
...parsePspReadelfMips(metadata),
|
||||
gnuAttributes: { fpAbi: 2, msa: null },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["unknown vendor", gnuDeclaration.replace("gnu", "aeabi")],
|
||||
["wrong float ABI", gnuDeclaration.replace("single", "double")],
|
||||
["unknown tag", `${gnuDeclaration} Tag_GNU_Unknown: 1\n`],
|
||||
[
|
||||
"unsupported scope",
|
||||
gnuDeclaration.replace("File Attributes", "Section Attributes: 1"),
|
||||
],
|
||||
["duplicate declaration", `${gnuDeclaration}${gnuDeclaration}`],
|
||||
])("refuses %s rather than inventing PSP GNU facts", (_name, declaration) => {
|
||||
assert.throws(
|
||||
() => parsePspReadelfMips(`${gnuMetadata}\n${declaration}`),
|
||||
/GNU attributes/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses undeclared GNU attributes rather than reporting absence", () => {
|
||||
assert.throws(
|
||||
() => parsePspReadelfMips(`${metadata}\n${gnuDeclaration}`),
|
||||
/GNU attributes/u,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["absent", metadata],
|
||||
["declared", `${gnuMetadata}\n${gnuDeclaration}`],
|
||||
])(
|
||||
"matches production snapshot serialization for %s GNU facts",
|
||||
(_name, text) => {
|
||||
const mips = parsePspReadelfMips(text);
|
||||
const target = {
|
||||
path: "/fixture/psp.elf",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "mips",
|
||||
availableArchitectures: ["mips"],
|
||||
mips,
|
||||
} satisfies BinaryTarget;
|
||||
assert.deepEqual(
|
||||
expectedMipsElfProfile(mips),
|
||||
ghidraMipsProfileParameters(target).mips_elf,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { MipsAbiFlags } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import type { MipsReadelfMetadata } from "./mips-checks.mjs";
|
||||
|
||||
export declare function parsePspReadelfAbi(text: string): MipsAbiFlags;
|
||||
export declare function parsePspReadelfMips(text: string): MipsReadelfMetadata;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
||||
import { copyFile, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { parseMipsReadelfSectionTypes } from "./mips-checks.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
@@ -42,15 +43,11 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
assert.equal(bytes.readUInt32LE(36), 0x10a23001);
|
||||
const readelf = await execute(
|
||||
"psp-readelf",
|
||||
["-h", "-A", "-sW", path],
|
||||
["-h", "-A", "-S", "-sW", path],
|
||||
options,
|
||||
);
|
||||
assert.equal(readelf.stderr.trim(), "");
|
||||
assert.match(readelf.stdout, /Class:\s+ELF32/u);
|
||||
assert.match(readelf.stdout, /Data:\s+2's complement, little endian/u);
|
||||
assert.match(readelf.stdout, /Type:\s+EXEC\b/u);
|
||||
assert.match(readelf.stdout, /Flags:\s+0x10a23001\b/u);
|
||||
const abiFlags = parsePspReadelfAbi(readelf.stdout);
|
||||
const mips = parsePspReadelfMips(readelf.stdout);
|
||||
const abiDump = await execute(
|
||||
"psp-readelf",
|
||||
["-x", ".MIPS.abiflags", path],
|
||||
@@ -99,13 +96,7 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
source_sha256: sourceDigests,
|
||||
symbols,
|
||||
flags: "0x10a23001",
|
||||
mips: {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
abiFlags,
|
||||
},
|
||||
mips,
|
||||
abi_record_hex_dump: abiDump.stdout,
|
||||
global_value: globalValue,
|
||||
versions,
|
||||
@@ -115,8 +106,8 @@ export async function buildPspFixture(directory, globalValue = 7) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Independent GNU fields for the pinned PSPSDK fixture, not REA's parser. */
|
||||
export function parsePspReadelfAbi(text) {
|
||||
/** Complete independent fields for the pinned PSPSDK fixture, not REA's parser. */
|
||||
export function parsePspReadelfMips(text) {
|
||||
const field = (expression, label) => {
|
||||
const matches = [...text.matchAll(expression)];
|
||||
assert.equal(
|
||||
@@ -126,6 +117,13 @@ export function parsePspReadelfAbi(text) {
|
||||
);
|
||||
return matches[0][1].trim();
|
||||
};
|
||||
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
|
||||
assert.equal(
|
||||
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
|
||||
"little",
|
||||
);
|
||||
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
|
||||
assert.match(field(/^\s*Flags:\s*(.+)$/gmu, "flags"), /^0x10a23001\b/u);
|
||||
const width = (name) => {
|
||||
const bits = Number(
|
||||
field(new RegExp(`^${name} size:\\s*(\\d+)\\s*$`, "gmu"), name),
|
||||
@@ -178,7 +176,49 @@ export function parsePspReadelfAbi(text) {
|
||||
},
|
||||
"PSPSDK ABI declaration differs from independently inspected build intent",
|
||||
);
|
||||
return abi;
|
||||
return {
|
||||
elfClass: 32,
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
abiFlags: abi,
|
||||
gnuAttributes: parsePspGnuAttributes(text),
|
||||
};
|
||||
}
|
||||
|
||||
// The maintained PSP fixture is single-float. Retain that independently decoded
|
||||
// GNU declaration when present, and refuse other tags/vendors/scopes rather
|
||||
// than confusing an uninspected or unsupported declaration with absence.
|
||||
function parsePspGnuAttributes(text) {
|
||||
const count = parseMipsReadelfSectionTypes(text).filter(
|
||||
(type) => type === "GNU_ATTRIBUTES",
|
||||
).length;
|
||||
const headers = [...text.matchAll(/^Attribute Section: (.+)$/gmu)];
|
||||
if (count === 0) {
|
||||
assert.equal(
|
||||
headers.length,
|
||||
0,
|
||||
"GNU attributes without a declared section",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
assert.equal(count, 1, "Ambiguous GNU attributes sections");
|
||||
assert.equal(headers.length, 1, "Missing or ambiguous GNU attributes output");
|
||||
const header = headers[0];
|
||||
assert.equal(header[1].trim(), "gnu", "Unsupported GNU attributes vendor");
|
||||
const tail = text.slice(header.index + header[0].length);
|
||||
// GNU attributes may precede or follow the architecture-specific ABI dump.
|
||||
const end = tail.search(/^MIPS ABI Flags Version:/mu);
|
||||
const lines = (end < 0 ? tail : tail.slice(0, end))
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
assert.deepEqual(
|
||||
lines,
|
||||
["File Attributes", "Tag_GNU_MIPS_ABI_FP: Hard float (single precision)"],
|
||||
"Unsupported or incomplete PSP GNU attributes declaration",
|
||||
);
|
||||
return { fpAbi: 2, msa: null };
|
||||
}
|
||||
|
||||
/** Observations from Ghidra, not an echo of the requested profile. */
|
||||
|
||||
Reference in New Issue
Block a user