feat(ghidra): admit recordless legacy MIPS32 ELFs with a pinned 32-bit language (#1898)
* feat(ghidra): admit recordless legacy MIPS32 ELFs with a pinned 32-bit language Firmware built with pre-ABI-flags GNU toolchains declares EF_MIPS_ARCH_32 with an unspecified ABI field and no PT_MIPS_ABIFLAGS record. The lane refused them before provider selection, and Ghidra's own header auto-selection picks a 64-bit R6 language for exactly these files. Admit ELF32 ET_EXEC headers with EF_MIPS_ARCH_32 or EF_MIPS_ARCH_32R2, an unspecified ABI field, EF_MIPS_ABI2 clear and no observed ABI flags record. o32 is recorded as an inferred analysis assumption (mips_abi_basis), floating-point ABI semantics stay unverified, and the existing refusals (FP64, NAN2008, ASEs, machine variants, ELF64) are unchanged. The profile pins -processor MIPS:<LE|BE>:32:default through a provider-pinned-v1 selection the bridge-reported language_id is checked against after import; a caller-selected REA_GHIDRA_LANGUAGE_ID still wins. The lane identity is versioned elf32-exec-arch32-legacy-pinned-v1. verify:ghidra:mips gains a recordless legacy round for both byte orders that strips the ABI record declarations from a real mips32 fixture, checks REA's interpretation against GNU readelf, asserts the pinned language after import and the retained assumption limitations through a snapshot round-trip. Fixes #1620 * fix(ghidra): require an inspected absence for the recordless MIPS lane Address review: undefined ABI flags means the record was not inspected, not that none exists. Only an inspected absence (null) establishes the recordless legacy admission; a merely unobserved record keeps its specific refusal. Also document the legacy admission and its pinned language in the MIPS provider guide. * fix(ghidra): tighten the recordless MIPS admission boundaries Address review: a recordless-looking header whose ABI-record inspection left declaration sources unexamined (carved ELF with a partial section table and no program-header record) no longer establishes the recordless admission; it is refused with the inspection limitation retained. An EF_MIPS_ARCH_32 header that declares an ABI is refused rather than relabeled through the 32R2 declared-ABI lane. * fix(ghidra): reject EF_MIPS_ABI_ON32 from the recordless MIPS lane Address review: bit 0x40 declares the old n32 ABI. A recordless header carrying it was admitted with the inferred-o32 assumption even though the header itself declares n32; the recordless shape now requires the bit clear and refuses it with a specific constraint. * fix(ghidra): keep recordless relocatable MIPS objects off the legacy lane The recordless admission now requires ET_EXEC: the lane and its verification cover executable firmware images, and a recordless ET_REL object must stay on the declared-ABI path instead of inheriting the inferred-o32 assumption under an executable lane id. The ABI-record requirement paragraph is scoped to the declared-ABI lane so it no longer contradicts the recordless admission documented above it. * fix(ghidra): name the ET_EXEC scope for recordless objects and probe the legacy lane A recordless relocatable object is not an incomplete inspection, so its refusal now names the ET_EXEC scope of the legacy admission instead of claiming unavailable ABI headers remain unknown. The recordless legacy verifier round reads the probe pair and applies the full assertMipsProbe oracle like the standard round, and its report keeps the independent GNU readelf receipt beside the fixture facts. * fix(ghidra): retain the concrete ELF32 legacy compiler pin * fix(ghidra): bind legacy profiles to fully admitted targets --------- Co-authored-by: BaaRai <baarai@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
c6503edfc4
commit
a4b8e9ccee
|
|
@ -22,6 +22,28 @@ lane. MIPS16 shared images have the separate bounded profile below; other shared
|
|||
or compressed images remain unsupported. Their family may be identified, but provider selection returns the
|
||||
specific unsupported constraint rather than guessing a compatible profile.
|
||||
|
||||
A second admission covers recordless legacy firmware images
|
||||
([#1620](https://github.com/morluto/rea/issues/1620)): ELF32 `ET_EXEC` headers
|
||||
declaring `EF_MIPS_ARCH_32` or `EF_MIPS_ARCH_32R2` with an unspecified ABI
|
||||
field, `EF_MIPS_ABI2` clear, and an _inspected_ absence of any ABI flags
|
||||
record (a target whose record was merely not observed stays refused). For
|
||||
these images o32 is an analysis assumption derived from the ELF32/ET_EXEC
|
||||
structure, recorded as `mips_abi_basis` in the profile and stated as a
|
||||
limitation in Evidence, never a declared fact; floating-point ABI semantics
|
||||
stay unverified. Ghidra's own header auto-selection picks a 64-bit language
|
||||
for exactly these files, so the lane pins `-processor MIPS:<LE|BE>:32:default`
|
||||
through a `provider-pinned-v1` profile selection and checks the
|
||||
bridge-reported `language_id` after import. A caller-selected
|
||||
`REA_GHIDRA_LANGUAGE_ID`/`REA_GHIDRA_COMPILER_SPEC_ID` still wins over the
|
||||
pin; combining the pinned profile with an environment override is a typed
|
||||
refusal. A header that declares an ABI field but carries no record is still
|
||||
refused. Two further boundaries apply: an inspection that could not examine
|
||||
every ABI declaration source (for example a carved ELF whose section table
|
||||
extends past the captured bytes without a program-header record) does not
|
||||
establish the recordless admission, and an `EF_MIPS_ARCH_32` header that
|
||||
_declares_ an ABI is refused rather than relabeled, because the declared-ABI
|
||||
lane is verified for `EF_MIPS_ARCH_32R2` headers only.
|
||||
|
||||
Explicit `EF_MIPS_FP64` and `EF_MIPS_NAN2008` declarations are refused. Their
|
||||
absence is insufficient: Clang can emit identical `e_flags` for FP32, FP64 and
|
||||
soft-float targets. Target resolution also reads the 24-byte ABI flags record
|
||||
|
|
@ -40,12 +62,14 @@ absent or supply a default ABI.
|
|||
The field layout follows
|
||||
[`Elf_MIPS_ABIFlags_v0`](https://github.com/bminor/glibc/blob/master/elf/elf.h).
|
||||
|
||||
Provider admission requires record version 0, MIPS32r2, 32-bit GPRs, no CPR2,
|
||||
no ISA extension, ASE or unknown general flags, and one of two floating-point
|
||||
declarations: 32-bit CPR1 with a double-precision or FPXX ABI, or no CPR1 with
|
||||
the soft-float ABI. Other combinations, including single-float, FP64 and
|
||||
unspecified ABIs, are refused. Missing records remain unknown rather than
|
||||
defaulting to a compatible ABI, and this lane refuses them. Unknown record
|
||||
Provider admission on the declared-ABI lane above requires record version 0,
|
||||
MIPS32r2, 32-bit GPRs, no CPR2, no ISA extension, ASE or unknown general
|
||||
flags, and one of two floating-point declarations: 32-bit CPR1 with a
|
||||
double-precision or FPXX ABI, or no CPR1 with the soft-float ABI. Other
|
||||
combinations, including single-float, FP64 and unspecified ABIs, are refused.
|
||||
Missing records remain unknown rather than defaulting to a compatible ABI, and
|
||||
the declared-ABI lane refuses them; the recordless legacy admission above is
|
||||
the only path that admits a recordless image. Unknown record
|
||||
values are retained for diagnosis but do not gain provider support. These checks
|
||||
bind the declared interpretation; the fixtures do not establish full
|
||||
floating-point behavioral conformance.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import {
|
||||
inspectMipsReadelf,
|
||||
inspectLegacyMipsReadelf,
|
||||
assertMipsLoadedImage,
|
||||
assertMipsProbe,
|
||||
assertMipsGlobal,
|
||||
|
|
@ -122,6 +123,157 @@ async function verifySoftFloat(call, symbols, byteOrder) {
|
|||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
if (!psp) {
|
||||
// Recordless legacy admission: ABI field 0, no ABI flags record, pinned
|
||||
// 32-bit language. Independent GNU readelf checks the header interpretation.
|
||||
for (const byteOrder of ["little", "big"]) {
|
||||
const fixture = await buildMipsFixture(workspace, byteOrder, "hard", {
|
||||
legacy: true,
|
||||
});
|
||||
const legacy = await inspectLegacyMipsReadelf(fixture.path, byteOrder);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
assert.equal(resolved.value.mips.flags, 0x50000001);
|
||||
assert.equal(resolved.value.mips.abiFlags, null);
|
||||
assert.deepEqual(
|
||||
resolved.value.mips.limitations ?? [],
|
||||
legacy.limitations,
|
||||
"REA legacy ABI-record absence interpretation disagrees with GNU readelf",
|
||||
);
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const snapshotPath = join(workspace, `${byteOrder}-legacy.snapshot.json`);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "rea-real-mips-legacy-proof",
|
||||
version: "1",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
undefined,
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
return requireMcpOperationResult(reply, name);
|
||||
};
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const selected = await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(selected.architecture, "mips");
|
||||
assert.equal(selected.sha256, fixture.sha256);
|
||||
const image = await call("inspect_native_load_image");
|
||||
assertMipsLoadedImage(image.observations, byteOrder, fixture.sha256);
|
||||
// The pinned import language is the checked one, not an auto-selection.
|
||||
assert.equal(
|
||||
image.observations.language_id,
|
||||
`MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
);
|
||||
const procedures = await call("list_procedures");
|
||||
const entry = procedures.find((p) => p.value === "rea_mips_entry");
|
||||
assert.ok(entry, "Entry function missing from legacy inventory");
|
||||
assert.equal(
|
||||
BigInt(entry.address),
|
||||
BigInt(legacy.symbols.rea_mips_entry),
|
||||
);
|
||||
const probe = await call("inspect_native_instruction", {
|
||||
address: legacy.symbols.rea_mips_probe,
|
||||
});
|
||||
const probeBranch = await call("inspect_native_instruction", {
|
||||
address: `0x${(BigInt(legacy.symbols.rea_mips_probe) + 4n).toString(16)}`,
|
||||
});
|
||||
assertMipsProbe(
|
||||
probe,
|
||||
probeBranch,
|
||||
byteOrder,
|
||||
legacy.symbols.rea_mips_probe,
|
||||
);
|
||||
const dossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", { procedure: entry.address }),
|
||||
);
|
||||
assert.ok(dossier.pseudocode.trim().length > 0);
|
||||
const saved = await call("close_binary", {
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.ok(saved.evidence_records > 0);
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(snapshot.target.architecture, "mips");
|
||||
const parameters = snapshot.binding.analysis_profile.parameters;
|
||||
assert.equal(
|
||||
parameters.language_id,
|
||||
`MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
);
|
||||
assert.equal(parameters.language_selection, "provider-pinned-v1");
|
||||
assert.equal(
|
||||
parameters.mips_support_lane,
|
||||
"elf32-exec-arch32-legacy-pinned-v1",
|
||||
);
|
||||
assert.equal(parameters.mips_abi_basis, "inferred-o32-assumption-v1");
|
||||
assert.ok(
|
||||
snapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
item.includes("analysis assumption"),
|
||||
),
|
||||
),
|
||||
"Legacy o32 assumption limitation missing from retained Evidence",
|
||||
);
|
||||
assert.ok(
|
||||
snapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
item.includes("Floating-point ABI semantics are unverified"),
|
||||
),
|
||||
),
|
||||
"Legacy floating-point uncertainty missing from retained Evidence",
|
||||
);
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.deepEqual(await call("list_procedures"), procedures);
|
||||
await call("close_binary");
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`MIPS legacy ${byteOrder} verification failed; MCP stderr: ${stderr}`,
|
||||
{ cause },
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
reports.push({
|
||||
...fixture,
|
||||
independent_reader: legacy,
|
||||
variant: "recordless-legacy-pinned",
|
||||
effective_language: `MIPS:${
|
||||
byteOrder === "little" ? "LE" : "BE"
|
||||
}:32:default`,
|
||||
procedures: "verified-through-snapshot",
|
||||
checks: [
|
||||
"recordless header admitted with inferred o32 assumption",
|
||||
"pinned 32-bit language checked after import",
|
||||
"legacy limitations retained in Evidence",
|
||||
"snapshot round-trip and profile parameters",
|
||||
],
|
||||
status: "passed",
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [byteOrder, floatAbi] of psp
|
||||
? [["little", "hard"]]
|
||||
: [
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { err, ok, type Result } from "../domain/result.js";
|
|||
import type { GhidraInstallationInspection } from "./GhidraInstallation.js";
|
||||
import {
|
||||
ghidraMipsProfileParameters,
|
||||
ghidraMipsSharedLanguageOverride,
|
||||
ghidraMipsLanguageOverride,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
ghidraRiscvLanguageOverride,
|
||||
|
|
@ -44,8 +44,7 @@ import {
|
|||
export const ghidraPinnedLanguage = (
|
||||
target: BinaryTarget,
|
||||
): GhidraLanguageOverride | undefined =>
|
||||
ghidraMipsSharedLanguageOverride(target) ??
|
||||
ghidraRiscvLanguageOverride(target);
|
||||
ghidraMipsLanguageOverride(target) ?? ghidraRiscvLanguageOverride(target);
|
||||
|
||||
/** Resolve version-bound, deterministic semantics before Ghidra imports a target. */
|
||||
export const resolveGhidraAnalysisProfile = async (
|
||||
|
|
@ -168,7 +167,7 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
`The caller-selected processor or compiler specification is outside the verified ${
|
||||
target.architecture === "riscv"
|
||||
? "generic little-endian RISC-V"
|
||||
: "endian-matched MIPS32 shared-image"
|
||||
: "endian-matched MIPS32"
|
||||
} profile. The requested override has not been changed.`,
|
||||
),
|
||||
);
|
||||
|
|
@ -273,7 +272,7 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
}
|
||||
: {}),
|
||||
...analyzerParameters,
|
||||
...ghidraMipsProfileParameters(target),
|
||||
...ghidraMipsProfileParameters(target, languageOverride),
|
||||
...ghidraRiscvProfileParameters(target),
|
||||
...(pspExtension?.ok === true
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,17 @@ import { HopperProvider } from "../hopper/HopperProvider.js";
|
|||
import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import {
|
||||
ghidraMipsProfileParameters,
|
||||
ghidraPinnedMipsLanguage,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
|
||||
import { ghidraProfileLanguageOverride } from "./GhidraAnalysisProfile.js";
|
||||
import {
|
||||
ghidraPinnedLanguage,
|
||||
ghidraProfileLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
|
|
@ -161,6 +168,126 @@ describe("bounded Ghidra MIPS admission", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("recordless legacy MIPS32 admission", () => {
|
||||
it("keeps recordless relocatable objects off the inferred-o32 lane", async () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
const object = target({
|
||||
flags: 0x50000001,
|
||||
abiFlags: null,
|
||||
type: 1,
|
||||
});
|
||||
expect(ghidra.inspectTargetSupport(object)).toMatchObject({
|
||||
status: "unsupported",
|
||||
});
|
||||
const resolved = await ghidra.resolveAnalysisProfile(object);
|
||||
expect(resolved.ok).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ flags: 0x50000001 }, "EF_MIPS_ARCH_32"],
|
||||
[{ flags: 0x70000001 }, "EF_MIPS_ARCH_32R2"],
|
||||
] satisfies [Partial<MipsElfMetadata>, string][])(
|
||||
"admits a recordless legacy header (%s) with a pinned 32-bit language",
|
||||
async (change) => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
const little = target({ ...change, abiFlags: null });
|
||||
const big = target({ ...change, abiFlags: null, byteOrder: "big" });
|
||||
expect(ghidra.inspectTargetSupport(little).status).toBe("supported");
|
||||
expect(ghidra.inspectTargetSupport(big).status).toBe("supported");
|
||||
const resolved = await ghidra.resolveAnalysisProfile(little);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
expect(resolved.value.profile?.parameters).toMatchObject({
|
||||
mips_support_lane: "elf32-exec-arch32-legacy-pinned-v1",
|
||||
mips_abi_basis: "inferred-o32-assumption-v1",
|
||||
language_id: "MIPS:LE:32:default",
|
||||
compiler_spec_id: "default",
|
||||
language_selection: "provider-pinned-v1",
|
||||
});
|
||||
const resolvedBig = await ghidra.resolveAnalysisProfile(big);
|
||||
if (!resolvedBig.ok) throw resolvedBig.error;
|
||||
expect(resolvedBig.value.profile?.parameters).toMatchObject({
|
||||
language_id: "MIPS:BE:32:default",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("requires an inspected absence; a merely unobserved record stays refused", () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
const withRecord = target({ flags: 0x50000001 }) as BinaryTarget & {
|
||||
mips: MipsElfMetadata;
|
||||
};
|
||||
const { abiFlags: _record, ...unobservedMips } = withRecord.mips;
|
||||
void _record;
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({
|
||||
...withRecord,
|
||||
mips: unobservedMips,
|
||||
} as BinaryTarget),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining("recordless legacy header"),
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ flags: 0x50000021 }, "n32"],
|
||||
[{ flags: 0x50000041 }, "EF_MIPS_ABI_ON32"],
|
||||
[{ flags: 0x50000101 }, "EF_MIPS_32BITMODE"],
|
||||
[{ flags: 0x70000101 }, "EF_MIPS_32BITMODE"],
|
||||
[
|
||||
{ flags: 0x50001001 },
|
||||
"EF_MIPS_ARCH_32R2 for headers that declare an ABI",
|
||||
],
|
||||
[{ flags: 0x50000201 }, "EF_MIPS_FP64"],
|
||||
[{ flags: 0x50000401 }, "EF_MIPS_NAN2008"],
|
||||
[{ flags: 0x60000001 }, "32R2"],
|
||||
[
|
||||
{
|
||||
flags: 0x50000001,
|
||||
limitations: [
|
||||
"MIPS section header table at file offset 96 extends beyond the captured file: examined 2 of 5 complete headers. ABI declarations in unavailable headers remain unknown.",
|
||||
],
|
||||
},
|
||||
"complete ABI-record inspection",
|
||||
],
|
||||
] satisfies [Partial<MipsElfMetadata>, string][])(
|
||||
"keeps refusing unverified recordless variants %j",
|
||||
(change, reason) => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(target({ ...change, abiFlags: null })),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining(reason),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("a caller-selected language wins over the provider pin", () => {
|
||||
const parameters = ghidraMipsProfileParameters(
|
||||
target({ flags: 0x50000001, abiFlags: null }),
|
||||
{ languageId: "MIPS:LE:32:default", compilerSpecId: "default" },
|
||||
);
|
||||
expect(parameters.language_selection).toBeUndefined();
|
||||
expect(parameters.language_id).toBeUndefined();
|
||||
expect(parameters).toMatchObject({
|
||||
mips_support_lane: "elf32-exec-arch32-legacy-pinned-v1",
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the pinned import language back only from pinned profiles", () => {
|
||||
const pinned = ghidraMipsProfileParameters(
|
||||
target({ flags: 0x50000001, abiFlags: null }),
|
||||
);
|
||||
expect(ghidraPinnedMipsLanguage(pinned)).toEqual({
|
||||
languageId: "MIPS:LE:32:default",
|
||||
compilerSpecId: "default",
|
||||
});
|
||||
const standard = ghidraMipsProfileParameters(target());
|
||||
expect(ghidraPinnedMipsLanguage(standard)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const report: GhidraMipsSoftFloatReport = {
|
||||
policy: "o32-soft-float-prototype-v1",
|
||||
functions: 5,
|
||||
|
|
@ -218,6 +345,56 @@ const session = async (
|
|||
};
|
||||
};
|
||||
|
||||
describe("legacy profile bindings", () => {
|
||||
it("refuses a legacy endian pin committed for a different target", async () => {
|
||||
const { counts, client } = await session(
|
||||
target({ flags: 0x50000001, abiFlags: null, byteOrder: "big" }),
|
||||
undefined,
|
||||
target({ flags: 0x50000001, abiFlags: null, byteOrder: "little" }),
|
||||
);
|
||||
expect((await client.execute("health", {})).ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
});
|
||||
it.each([0x50000201, 0x50000401, 0x54000001, 0x50910001])(
|
||||
"refuses unsupported same-endian legacy flags %i before startup",
|
||||
async (flags) => {
|
||||
const value = target({ flags, abiFlags: null });
|
||||
expect(ghidraPinnedLanguage(value)).toBeUndefined();
|
||||
const { counts, client } = await session(
|
||||
value,
|
||||
undefined,
|
||||
target({ flags: 0x50000001, abiFlags: null }),
|
||||
);
|
||||
expect((await client.execute("health", {})).ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
},
|
||||
);
|
||||
const unpinnedTargets: BinaryTarget[] = [
|
||||
target(),
|
||||
{
|
||||
path: "/fixture.elf",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
},
|
||||
];
|
||||
it.each(unpinnedTargets)(
|
||||
"refuses a pinned profile for an unpinned target $architecture",
|
||||
async (value) => {
|
||||
expect(ghidraPinnedLanguage(value)).toBeUndefined();
|
||||
const { counts, client } = await session(
|
||||
value,
|
||||
undefined,
|
||||
target({ flags: 0x50000001, abiFlags: null }),
|
||||
);
|
||||
expect((await client.execute("health", {})).ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("MIPS16 shared-image profile", () => {
|
||||
const shared = (change: Partial<MipsElfMetadata> = {}) =>
|
||||
target({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
|
||||
const MIPS_FP_ABI_SOFT = 3;
|
||||
const MIPS_ET_REL = 1;
|
||||
|
|
@ -15,17 +15,56 @@ const isGhidraMips16SharedTarget = (target: BinaryTarget): boolean =>
|
|||
target.mips.type === MIPS_ET_DYN &&
|
||||
(target.mips.flags & 0x0f000000) === MIPS16_HEADER_ASE;
|
||||
|
||||
/** Generic MIPS32 permits per-function ISA_MODE; a global 16e processor does not. */
|
||||
export const ghidraMipsSharedLanguageOverride = (
|
||||
/** Derive the verified MIPS32 import pin from this target's observed byte order. */
|
||||
export const ghidraMipsLanguageOverride = (
|
||||
target: BinaryTarget,
|
||||
): GhidraLanguageOverride | undefined =>
|
||||
isGhidraMips16SharedTarget(target) && target.architecture === "mips"
|
||||
(isGhidraMips16SharedTarget(target) || hasLegacyMipsHeaderShape(target)) &&
|
||||
ghidraMipsUnsupportedReason(target) === null &&
|
||||
target.architecture === "mips"
|
||||
? {
|
||||
languageId: `MIPS:${target.mips.byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
compilerSpecId: "default",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* Recordless legacy ELF32 MIPS admission: the header declares no ABI field and
|
||||
* carries no ABI flags record, so o32 is an analysis assumption derived from
|
||||
* ELF32/ET_EXEC structure, never a declared fact. Ghidra's header
|
||||
* auto-selection picks a 64-bit language for exactly these files, so the lane
|
||||
* pins the 32-bit language instead.
|
||||
*/
|
||||
const hasLegacyMipsHeaderShape = (target: BinaryTarget): boolean => {
|
||||
if (
|
||||
target.kind !== "executable" ||
|
||||
target.architecture !== "mips" ||
|
||||
target.format !== "elf" ||
|
||||
target.mips === undefined
|
||||
)
|
||||
return false;
|
||||
const { flags, abiFlags, limitations } = target.mips;
|
||||
const arch = flags & 0xf0000000;
|
||||
return (
|
||||
// The lane and its verification cover executable firmware images only;
|
||||
// recordless relocatable objects stay outside the inferred-o32 assumption.
|
||||
target.mips.elfClass === 32 &&
|
||||
target.mips.type === MIPS_ET_EXEC &&
|
||||
(arch === 0x50000000 || arch === 0x70000000) &&
|
||||
(flags & 0x0000f000) === 0 &&
|
||||
(flags & 0x20) === 0 &&
|
||||
// EF_MIPS_ABI_ON32 (bit 0x40) declares the old n32 ABI; it is not an
|
||||
// unspecified-ABI header and must not inherit the o32 assumption.
|
||||
(flags & 0x40) === 0 &&
|
||||
(flags & 0x100) === 0 &&
|
||||
// Undefined means the record was not inspected; only an inspected
|
||||
// absence establishes the recordless legacy admission. An inspection
|
||||
// that could not examine every declaration source stays unknown.
|
||||
abiFlags === null &&
|
||||
(limitations === undefined || limitations.length === 0)
|
||||
);
|
||||
};
|
||||
|
||||
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
|
||||
export const ghidraMipsUnsupportedReason = (
|
||||
target: BinaryTarget,
|
||||
|
|
@ -48,13 +87,41 @@ export const ghidraMipsUnsupportedReason = (
|
|||
return "MIPS16, microMIPS and other ASE encodings are outside this standard-instruction MIPS lane.";
|
||||
if ((metadata.flags & 0x00ff0000) !== 0)
|
||||
return "Machine-specific MIPS variants, including PSP Allegrex, need a separately supported processor profile.";
|
||||
if ((metadata.flags & 0xf0000000) !== 0x70000000)
|
||||
return "This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2; other ISA revisions need separate verification.";
|
||||
const arch = metadata.flags & 0xf0000000;
|
||||
if (arch !== 0x70000000 && arch !== 0x50000000)
|
||||
return "This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2 or a recordless legacy EF_MIPS_ARCH_32 header; other ISA revisions need separate verification.";
|
||||
const recordlessShape =
|
||||
(metadata.flags & 0x0000f000) === 0 &&
|
||||
(metadata.flags & 0x20) === 0 &&
|
||||
metadata.abiFlags === null;
|
||||
if (recordlessShape) {
|
||||
if ((metadata.flags & 0x100) !== 0)
|
||||
return "EF_MIPS_32BITMODE declares code for a 64-bit processor in 32-bit mode; it is outside the recordless MIPS32 admission.";
|
||||
if ((metadata.flags & 0x40) !== 0)
|
||||
return "EF_MIPS_ABI_ON32 declares the n32 ABI; recordless headers carrying it are outside the recordless legacy admission.";
|
||||
if ((metadata.flags & 0x00000200) !== 0)
|
||||
return "EF_MIPS_FP64 requires separate verification of 64-bit floating-point register semantics in this Ghidra lane.";
|
||||
if ((metadata.flags & 0x00000400) !== 0)
|
||||
return "EF_MIPS_NAN2008 requires separate verification of NaN encoding semantics in this Ghidra lane.";
|
||||
if (hasLegacyMipsHeaderShape(target)) return null;
|
||||
// A recordless relocatable object is not an incomplete inspection; the
|
||||
// lane is scoped to executable firmware images by design.
|
||||
if (metadata.type !== MIPS_ET_EXEC)
|
||||
return "The recordless legacy admission covers ET_EXEC firmware images only; recordless relocatable objects need their own verified build, and the declared-ABI lane requires an ABI flags record.";
|
||||
// The inspection could not examine every ABI declaration source, so the
|
||||
// recordless admission is not established for this header.
|
||||
return [
|
||||
"This recordless legacy admission requires a complete ABI-record inspection; ABI declarations in unavailable headers remain unknown.",
|
||||
...(metadata.limitations ?? []),
|
||||
].join(" ");
|
||||
}
|
||||
if (
|
||||
(metadata.flags & 0x0000f000) !== 0x00001000 ||
|
||||
(metadata.flags & 0x20) !== 0
|
||||
)
|
||||
return "This Ghidra MIPS lane requires an explicit o32 ABI; unspecified, n32 and EABI targets remain unsupported.";
|
||||
return "This Ghidra MIPS lane requires an explicit o32 ABI or a recordless legacy header with an unspecified ABI; n32, EABI and mixed declarations remain unsupported.";
|
||||
if (arch !== 0x70000000)
|
||||
return "This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2 for headers that declare an ABI; EF_MIPS_ARCH_32 headers are admitted only through the recordless legacy admission.";
|
||||
if ((metadata.flags & 0x00000200) !== 0)
|
||||
return "EF_MIPS_FP64 requires separate verification of 64-bit floating-point register semantics in this Ghidra lane.";
|
||||
if ((metadata.flags & 0x00000400) !== 0)
|
||||
|
|
@ -98,6 +165,11 @@ export const ghidraMipsUnsupportedReason = (
|
|||
return null;
|
||||
};
|
||||
|
||||
/** A legacy lane is established only after every MIPS declaration is admitted. */
|
||||
export const isLegacyMipsAdmission = (target: BinaryTarget): boolean =>
|
||||
hasLegacyMipsHeaderShape(target) &&
|
||||
ghidraMipsUnsupportedReason(target) === null;
|
||||
|
||||
/** Soft-float prototype policy applied by ReaGhidraMipsSoftFloat.java after analysis. */
|
||||
export const GHIDRA_MIPS_SOFT_FLOAT_STORAGE = "o32-soft-float-prototype-v1";
|
||||
|
||||
|
|
@ -174,6 +246,7 @@ export const ghidraMipsSharedLimitations = (
|
|||
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
|
||||
export const ghidraMipsProfileParameters = (
|
||||
target: BinaryTarget,
|
||||
languageOverride?: GhidraLanguageOverride,
|
||||
): Readonly<Record<string, JsonValue>> => {
|
||||
if (
|
||||
target.kind !== "executable" ||
|
||||
|
|
@ -182,6 +255,18 @@ export const ghidraMipsProfileParameters = (
|
|||
)
|
||||
return {};
|
||||
const abi = target.mips.abiFlags;
|
||||
// A deliberate caller-selected language wins over the legacy pin; the pin
|
||||
// exists because header auto-selection misselects a 64-bit language.
|
||||
const legacy = isLegacyMipsAdmission(target);
|
||||
const language = ghidraMipsLanguageOverride(target);
|
||||
const pinnedLanguage =
|
||||
legacy && languageOverride === undefined && language !== undefined
|
||||
? {
|
||||
language_id: language.languageId,
|
||||
compiler_spec_id: language.compilerSpecId ?? "default",
|
||||
language_selection: "provider-pinned-v1",
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
mips_elf: {
|
||||
elf_class: target.mips.elfClass,
|
||||
|
|
@ -231,10 +316,28 @@ export const ghidraMipsProfileParameters = (
|
|||
? { mips_soft_float_storage: GHIDRA_MIPS_SOFT_FLOAT_STORAGE }
|
||||
: {}),
|
||||
instruction_context_evidence: "per-address-isa-mode-v1",
|
||||
mips_support_lane: isGhidraMips16SharedTarget(target)
|
||||
? "elf32-dyn-o32-arch32r2-mips16-local-rel32-v1"
|
||||
: target.mips.type === MIPS_ET_REL
|
||||
? "elf32-rel-o32-arch32r2-standard-v1"
|
||||
: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
mips_support_lane: legacy
|
||||
? "elf32-exec-arch32-legacy-pinned-v1"
|
||||
: isGhidraMips16SharedTarget(target)
|
||||
? "elf32-dyn-o32-arch32r2-mips16-local-rel32-v1"
|
||||
: target.mips.type === MIPS_ET_REL
|
||||
? "elf32-rel-o32-arch32r2-standard-v1"
|
||||
: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
...(legacy ? { mips_abi_basis: "inferred-o32-assumption-v1" } : {}),
|
||||
...pinnedLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
/** Read the provider-pinned import language back from a resolved legacy profile. */
|
||||
export const ghidraPinnedMipsLanguage = (
|
||||
parameters: Readonly<Record<string, unknown>>,
|
||||
): GhidraLanguageOverride | undefined => {
|
||||
if (parameters.language_selection !== "provider-pinned-v1") return undefined;
|
||||
const languageId = parameters.language_id;
|
||||
const compilerSpecId = parameters.compiler_spec_id;
|
||||
if (typeof languageId !== "string" || typeof compilerSpecId !== "string")
|
||||
return undefined;
|
||||
return compilerSpecId === "language-default"
|
||||
? { languageId }
|
||||
: { languageId, compilerSpecId };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
sameGhidraDisabledAnalyzers,
|
||||
sameGhidraLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
import { ghidraPinnedMipsLanguage } from "./GhidraMipsProfile.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
|
|
@ -449,13 +450,39 @@ export const createGhidraProviderClient = (input: {
|
|||
}),
|
||||
);
|
||||
const seeds = seedProfile.data;
|
||||
const languageOverride = ghidraProfileLanguageOverride(
|
||||
const parameterLanguage = ghidraProfileLanguageOverride(
|
||||
committedProfile.parameters,
|
||||
);
|
||||
const pinnedLanguage = ghidraPinnedMipsLanguage(committedProfile.parameters);
|
||||
const targetLanguage = ghidraPinnedLanguage(target);
|
||||
if (pinnedLanguage !== undefined && targetLanguage === undefined)
|
||||
return unavailableClient(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
diagnostics: {
|
||||
reason:
|
||||
"The committed profile pins a MIPS language that the current target does not establish; reopen an admitted target to resolve a new profile.",
|
||||
},
|
||||
}),
|
||||
);
|
||||
// Provider-pinned profiles declare their language through the same
|
||||
// parameter slot caller-configured profiles use, so only an environment
|
||||
// override conflicts with the legacy pin; the slot itself carries it.
|
||||
if (
|
||||
pinnedLanguage !== undefined &&
|
||||
config.ghidraLanguageOverride !== undefined
|
||||
)
|
||||
return unavailableClient(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
diagnostics: {
|
||||
reason:
|
||||
"The committed legacy MIPS profile pins its import language; REA_GHIDRA_LANGUAGE_ID/REA_GHIDRA_COMPILER_SPEC_ID cannot be combined with it. Reopen the target without the override.",
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (
|
||||
!sameGhidraLanguageOverride(
|
||||
languageOverride,
|
||||
config.ghidraLanguageOverride ?? ghidraPinnedLanguage(target),
|
||||
parameterLanguage,
|
||||
config.ghidraLanguageOverride ?? targetLanguage,
|
||||
)
|
||||
)
|
||||
return unavailableClient(
|
||||
|
|
@ -466,6 +493,9 @@ export const createGhidraProviderClient = (input: {
|
|||
},
|
||||
}),
|
||||
);
|
||||
// Both caller-selected and provider-pinned languages use the validated
|
||||
// committed slot; the profile cannot supply its own applicability proof.
|
||||
const importLanguage = parameterLanguage;
|
||||
const disabledAnalyzers = ghidraProfileDisabledAnalyzers(
|
||||
committedProfile.parameters,
|
||||
);
|
||||
|
|
@ -483,6 +513,7 @@ export const createGhidraProviderClient = (input: {
|
|||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const invalidProfile = validateGhidraExtensionProfile(
|
||||
extensions,
|
||||
config,
|
||||
|
|
@ -523,6 +554,13 @@ export const createGhidraProviderClient = (input: {
|
|||
...(target.kind === "executable" && target.architecture === "mips"
|
||||
? (target.mips.limitations ?? [])
|
||||
: []),
|
||||
...(committedProfile.parameters.mips_abi_basis ===
|
||||
"inferred-o32-assumption-v1"
|
||||
? [
|
||||
"The ELF header declares no ABI field and no ABI flags record was observed; o32 calling conventions are an analysis assumption of this lane, not a declared fact.",
|
||||
"Floating-point ABI semantics are unverified for this recordless legacy admission.",
|
||||
]
|
||||
: []),
|
||||
...ghidraMipsRelocatableLimitations(target),
|
||||
...ghidraMipsSharedLimitations(target),
|
||||
...ghidraRiscvLimitations(target),
|
||||
|
|
@ -563,7 +601,9 @@ export const createGhidraProviderClient = (input: {
|
|||
baseAddr: target.entryPoint,
|
||||
},
|
||||
}),
|
||||
...(languageOverride === undefined ? {} : { languageOverride }),
|
||||
...(importLanguage === undefined
|
||||
? {}
|
||||
: { languageOverride: importLanguage }),
|
||||
...(config.ghidraDisabledAnalyzers === undefined
|
||||
? {}
|
||||
: { disabledAnalyzers: config.ghidraDisabledAnalyzers }),
|
||||
|
|
@ -588,13 +628,13 @@ export const createGhidraProviderClient = (input: {
|
|||
expectedCompilerSpecId: "default",
|
||||
}
|
||||
: rawLanguage === undefined
|
||||
? languageOverride === undefined
|
||||
? importLanguage === undefined
|
||||
? {}
|
||||
: {
|
||||
expectedLanguageId: languageOverride.languageId,
|
||||
...(languageOverride.compilerSpecId === undefined
|
||||
expectedLanguageId: importLanguage.languageId,
|
||||
...(importLanguage.compilerSpecId === undefined
|
||||
? {}
|
||||
: { expectedCompilerSpecId: languageOverride.compilerSpecId }),
|
||||
: { expectedCompilerSpecId: importLanguage.compilerSpecId }),
|
||||
}
|
||||
: {
|
||||
expectedLanguageId: rawLanguage.languageId,
|
||||
|
|
|
|||
|
|
@ -160,7 +160,89 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
|
|||
};
|
||||
}
|
||||
|
||||
/** Check effective Ghidra observations, not the requested analysis profile. */
|
||||
/** Independent GNU readelf interpretation for the recordless legacy fixture. */
|
||||
export async function inspectLegacyMipsReadelf(path, byteOrder) {
|
||||
const command = process.env.REA_MIPS_READELF ?? "readelf";
|
||||
const options = {
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
timeout: 10000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
};
|
||||
const version = await execute(command, ["--version"], options);
|
||||
assert.match(
|
||||
version.stdout,
|
||||
/^GNU readelf\b/u,
|
||||
"This lane requires GNU readelf",
|
||||
);
|
||||
const header = await execute(command, ["-h", "-W", path], options);
|
||||
assert.equal(
|
||||
header.stderr.trim(),
|
||||
"",
|
||||
`readelf diagnostics: ${header.stderr}`,
|
||||
);
|
||||
const field = (expression, label) => {
|
||||
const matches = [...header.stdout.matchAll(expression)];
|
||||
assert.equal(matches.length, 1, `Missing or ambiguous readelf ${label}`);
|
||||
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"),
|
||||
byteOrder,
|
||||
);
|
||||
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
|
||||
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
|
||||
const flags = field(/^\s*Flags:\s*(0x[\da-f]+)\b.*$/gmu, "flags");
|
||||
assert.equal(
|
||||
Number(flags),
|
||||
0x50000001,
|
||||
"Legacy fixture must carry EF_MIPS_ARCH_32 with an unspecified ABI field",
|
||||
);
|
||||
const abi = await execute(command, ["-A", path], options);
|
||||
assert.equal(abi.stderr.trim(), "", `readelf diagnostics: ${abi.stderr}`);
|
||||
// -A also prints unrelated MIPS facts such as the static GOT; only the ABI
|
||||
// flags declaration itself must be gone from the recordless fixture.
|
||||
assert.doesNotMatch(
|
||||
abi.stdout,
|
||||
/MIPS ABI Flags Version/u,
|
||||
"GNU readelf still observes an ABI flags declaration in the legacy fixture",
|
||||
);
|
||||
const symbols = await execute(command, ["-s", "-W", path], options);
|
||||
assert.equal(
|
||||
symbols.stderr.trim(),
|
||||
"",
|
||||
`readelf diagnostics: ${symbols.stderr}`,
|
||||
);
|
||||
const parsed = {};
|
||||
for (const name of ["rea_mips_entry", "rea_mips_probe"]) {
|
||||
const matches = [
|
||||
...symbols.stdout.matchAll(
|
||||
new RegExp(
|
||||
`^\\s*\\d+:\\s+([\\da-f]+)\\s+\\d+\\s+FUNC\\s+GLOBAL\\s+\\S+\\s+\\d+\\s+${name}\\s*$`,
|
||||
"gmu",
|
||||
),
|
||||
),
|
||||
];
|
||||
assert.equal(
|
||||
matches.length,
|
||||
1,
|
||||
`Missing or ambiguous readelf symbol ${name}`,
|
||||
);
|
||||
parsed[name] = `0x${BigInt(`0x${matches[0][1]}`).toString(16)}`;
|
||||
}
|
||||
return {
|
||||
flags: Number(flags),
|
||||
limitations: [],
|
||||
symbols: parsed,
|
||||
command,
|
||||
version: version.stdout.split("\n")[0],
|
||||
raw: {
|
||||
header: header.stdout,
|
||||
abi: abi.stdout,
|
||||
symbols: symbols.stdout,
|
||||
},
|
||||
};
|
||||
}
|
||||
export function assertMipsLoadedImage(observations, byteOrder, sha256) {
|
||||
assert.equal(observations.language_id, language(byteOrder));
|
||||
assert.equal(observations.compiler_spec_id, "default");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
|
@ -12,11 +12,13 @@ export async function buildMipsFixture(
|
|||
directory,
|
||||
byteOrder,
|
||||
floatAbi = "hard",
|
||||
options = {},
|
||||
) {
|
||||
if (byteOrder !== "little" && byteOrder !== "big")
|
||||
throw new Error("MIPS fixture byte order must be little or big");
|
||||
if (floatAbi !== "hard" && floatAbi !== "soft")
|
||||
throw new Error("MIPS fixture float ABI must be hard or soft");
|
||||
const legacy = options.legacy === true;
|
||||
const soft = floatAbi === "soft";
|
||||
const target = byteOrder === "little" ? "mipsel-linux-gnu" : "mips-linux-gnu";
|
||||
const source = fileURLToPath(new URL("../c/mips.c", import.meta.url));
|
||||
|
|
@ -24,10 +26,13 @@ export async function buildMipsFixture(
|
|||
new URL("../c/mips-soft-float.c", import.meta.url),
|
||||
);
|
||||
const probe = fileURLToPath(new URL("./mips-probe.S", import.meta.url));
|
||||
const path = join(directory, `${target}${soft ? "-soft-float" : ""}.elf`);
|
||||
const path = join(
|
||||
directory,
|
||||
`${target}${soft ? "-soft-float" : ""}${legacy ? "-legacy" : ""}.elf`,
|
||||
);
|
||||
const args = [
|
||||
`--target=${target}`,
|
||||
"-march=mips32r2",
|
||||
`-march=${legacy ? "mips32" : "mips32r2"}`,
|
||||
"-mabi=32",
|
||||
...(soft ? ["-msoft-float"] : []),
|
||||
"-mno-abicalls",
|
||||
|
|
@ -45,8 +50,88 @@ export async function buildMipsFixture(
|
|||
"-o",
|
||||
path,
|
||||
];
|
||||
const compiled = await compileMipsFixture(path, byteOrder, 2, args, {
|
||||
expectedFlags: legacy ? 0x50001001 : 0x70001001,
|
||||
});
|
||||
// Legacy variant: retype the ABI record declarations out of the image and
|
||||
// zero the header ABI field, matching pre-ABI-flags 2000s firmware toolchains
|
||||
// (their compilers predate the record; the header leaves the ABI unspecified).
|
||||
let attested = compiled;
|
||||
if (legacy) {
|
||||
const raw = await readFile(path);
|
||||
const write16 = (offset, value) =>
|
||||
byteOrder === "little"
|
||||
? raw.writeUInt16LE(value, offset)
|
||||
: raw.writeUInt16BE(value, offset);
|
||||
const write32 = (offset, value) =>
|
||||
byteOrder === "little"
|
||||
? raw.writeUInt32LE(value, offset)
|
||||
: raw.writeUInt32BE(value, offset);
|
||||
const read16 = (offset) =>
|
||||
byteOrder === "little"
|
||||
? raw.readUInt16LE(offset)
|
||||
: raw.readUInt16BE(offset);
|
||||
const read32 = (offset) =>
|
||||
byteOrder === "little"
|
||||
? raw.readUInt32LE(offset)
|
||||
: raw.readUInt32BE(offset);
|
||||
const flags = read32(36);
|
||||
if ((flags & 0xf0000000) !== 0x50000000)
|
||||
throw new Error("Legacy fixture did not build as EF_MIPS_ARCH_32");
|
||||
write32(36, flags & ~0x0000f000);
|
||||
const programOffset = read32(28);
|
||||
const programCount = read16(44);
|
||||
if (read16(42) !== 32)
|
||||
throw new Error("Legacy fixture has non-standard program headers");
|
||||
for (let index = 0; index < programCount; index += 1) {
|
||||
const offset = programOffset + index * 32;
|
||||
if (read32(offset) === 0x70000003) write32(offset, 0);
|
||||
}
|
||||
const sectionOffset = read32(32);
|
||||
const sectionCount = read16(48);
|
||||
if (read16(46) !== 40)
|
||||
throw new Error("Legacy fixture has non-standard section headers");
|
||||
for (let index = 0; index < sectionCount; index += 1) {
|
||||
const offset = sectionOffset + index * 40;
|
||||
if (read32(offset + 4) === 0x7000002a) write32(offset + 4, 1);
|
||||
}
|
||||
// GNU readelf resolves the record by section name, not type, so a retyped
|
||||
// header alone keeps a named declaration. Replace the name in the string
|
||||
// table in place, preserving every remaining name and offset.
|
||||
const stringTableIndex = read16(50);
|
||||
const stringTableOffset =
|
||||
stringTableIndex < sectionCount
|
||||
? read32(sectionOffset + stringTableIndex * 40 + 16)
|
||||
: -1;
|
||||
const named =
|
||||
stringTableOffset === -1
|
||||
? -1
|
||||
: raw.indexOf(
|
||||
Buffer.from(".MIPS.abiflags", "ascii"),
|
||||
stringTableOffset,
|
||||
);
|
||||
if (named === -1)
|
||||
throw new Error("Legacy fixture lacks the expected ABI section name");
|
||||
raw.write("x.rea.no.flags", named, "ascii");
|
||||
await writeFile(path, raw);
|
||||
const patched = await readFile(path);
|
||||
const patchedFlags =
|
||||
byteOrder === "little"
|
||||
? patched.readUInt32LE(36)
|
||||
: patched.readUInt32BE(36);
|
||||
if (patchedFlags !== 0x50000001)
|
||||
throw new Error(
|
||||
"Legacy fixture did not end with the recordless o32 declaration",
|
||||
);
|
||||
attested = {
|
||||
...compiled,
|
||||
flags: patchedFlags,
|
||||
sha256: createHash("sha256").update(patched).digest("hex"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...(await compileMipsFixture(path, byteOrder, 2, args)),
|
||||
...attested,
|
||||
legacy,
|
||||
float_abi: floatAbi,
|
||||
source_sha256: createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
|
|
@ -108,7 +193,13 @@ export async function buildMipsRelocatableFixture(
|
|||
}
|
||||
|
||||
/** Compile and attest the shared ELF32/o32/MIPS32r2 fixture boundary. */
|
||||
async function compileMipsFixture(path, byteOrder, objectType, args) {
|
||||
async function compileMipsFixture(
|
||||
path,
|
||||
byteOrder,
|
||||
objectType,
|
||||
args,
|
||||
options = {},
|
||||
) {
|
||||
const compiler = process.env.REA_MIPS_CLANG ?? "clang";
|
||||
const version = await execute(compiler, ["--version"], { timeout: 10000 });
|
||||
await execute(compiler, args, { timeout: 60000 });
|
||||
|
|
@ -128,7 +219,7 @@ async function compileMipsFixture(path, byteOrder, objectType, args) {
|
|||
bytes[5] !== (byteOrder === "little" ? 1 : 2) ||
|
||||
read16(16) !== objectType ||
|
||||
read16(18) !== 8 ||
|
||||
read32(36) !== 0x70001001
|
||||
read32(36) !== (options.expectedFlags ?? 0x70001001)
|
||||
)
|
||||
throw new Error(
|
||||
"Compiler did not produce the declared ELF32/o32/MIPS32r2 fixture",
|
||||
|
|
|
|||
Loading…
Reference in New Issue