feat(ghidra): admit relocatable MIPS32r2 ELF objects (#1950)
* feat(ghidra): admit soft-float MIPS32r2 ELF with integer-register float storage Soft-float o32 executables (Linux kernel builds, bare-metal firmware) were refused because their ABI flags record declares no CPR1 and FP ABI 3. Ghidra's MIPS32 compiler specifications model only hard-float o32, so typed float/double signatures (DWARF) would be bound to $f12/$f14/$f0 and the decompiler loses the real dataflow: `return sel ? a : b` decompiled to `return in_f0;`, and callers passed phantom FPU inputs. Admit ELF32 ET_EXEC o32 MIPS32r2 targets whose ABI record declares soft-float with no CPR1, and commit `mips_soft_float_storage: o32-integer-registers-v1` in their analysis profile only (hard-float profile digests are unchanged). ReaGhidraMipsSoftFloat.java runs after auto-analysis and, for each function with a float/double formal type, either keeps the types with the storage Ghidra's o32 model assigns to a same-size integer (pair alignment, stack spill), or, for auto-parameters such as hidden struct-return pointers, swaps float types for same-size integers under dynamic storage. No function is left on hard-float storage. The report is written last; the provider client requires it exactly when the profile commits the policy and refuses the session before any operation otherwise. Verification: real Ghidra 12.1.4 lane (verify:ghidra:mips) now builds hard- and soft-float fixtures in both byte orders and checks exact parameter/return storage through inspect_native_api, caller constants, snapshot profile and Evidence outcome; test:fast, verify:package and an installed-tarball soft-float run pass. * fix(ghidra): model soft-float MIPS as a prototype for every typed callable Review feedback on #1949: rewriting storage per Function left external functions and indirect calls through typed function pointers on Ghidra's hard-float o32 prototype, which still read $f12/$f14/$f0 while the session reported success. A negative control confirms it: without the change, `(*rea_mips_soft_callback)(8.5f, 9.5f, sel)` decompiles to `(*rea_mips_soft_callback)(in_f12, in_f14, sel)`. ReaGhidraMipsSoftFloat.java now derives a `__rea_soft_float` prototype from the program's own default o32 model (encoded, float pentries and float-consuming rules removed, so byte-order-specific aggregate rules and the hidden struct-return pointer are kept), installs it as a specification extension, and assigns it with dynamic storage to every non-thunk function, external function and function-pointer definition whose formal types use float or double. Thunks follow their targets. Float types are kept everywhere, so the integer-retype fallback is removed. The script fails without writing its report if the derived prototype or any reassigned function still uses an FPU register. The policy becomes `o32-soft-float-prototype-v1` with a {functions, definitions} report. The soft-float fixture adds an indirect call through a typed pointer; the real-Ghidra lane requires its constants with no FPU inputs and the struct return's `float scale` in $a1. * test(ghidra): pin ODDSPREG acceptance for soft-float MIPS ABI records Review on #1949 suggested requiring flags1 == 0 for soft-float, since MIPS_AFL_FLAGS1_ODDSPREG names CPR1 registers. GCC 5.2 kernel modules, LLVM/Rust firmware and Clang -msoft-float objects all record flags1 = 1 with FP ABI soft-float: it is the MIPS32 o32 toolchain default and selects nothing without CPR1. Keep accepting it, say why at the check, and test that soft-float with ODDSPREG is admitted while any other general flag is refused. * feat(ghidra): admit relocatable MIPS32r2 ELF objects Linux MIPS kernel modules (.ko) and other ELF32 o32 MIPS32r2 ET_REL objects were refused by the Ghidra MIPS lane. Ghidra's ELF loader already places each allocated section and applies REL relocations; this admits ET_REL alongside ET_EXEC (ET_DYN and PRX stay refused) under its own support lane, `elf32-rel-o32-arch32r2-standard-v1`, so executable targets keep theirs. Relocatable sessions carry limitations: addresses are analysis coordinates, undefined symbols live in Ghidra's EXTERNAL block, and HI16 relocations report PARTIAL because Ghidra applies them with their paired LO16. Verification: new `verify:ghidra:mips-relocatable` real-Ghidra lane builds -c -G0 objects in both byte orders plus a soft-float -mlong-calls module variant. GNU readelf supplies sections, symbols and REL entries; every relocation in an allocated section must encode S + A in the loaded word (S from Ghidra placement, A from original bytes), with HI16/LO16 checked as the effective address each LO16 forms, and coverage of a shared HI16, a LO16 carry, external code/data and R_MIPS_32 is asserted. The executable lane, unit suites pass, and on 30 real .ko files 54,972 relocations matched (93 reference empty sections and are not checkable). * fix(ghidra): verify every MIPS HI16 and archive-typed soft-float externals Review feedback on #1950: - The relocation oracle kept one pending HI16, so a second HI16 before the LO16 that resolves both overwrote the first, and unrelated relocations cleared it; the earlier site's high half and carry were never compared with S + A. Every HI16 now waits for the next LO16 of its symbol, which checks all of them; later LO16s reuse those HI16s, and a HI16 left unresolved at a section end fails. On 30 real soft-float kernel modules, 24 contain several HI16s per LO16; all 54,972 placed relocations match. - External callees with float prototypes: the relocatable fixture now calls powf, whose prototype Ghidra's C library archive applies by name. Every variant must decompile `powf(1.5,2.5)` without FPU inputs, and the soft-float module variant must report that __rea_soft_float covered it (with the external step removed it decompiles as `powf(param_1,param_2)`). `-fno-builtin` keeps the call from being folded. Also updates the module variant's expected soft-float policy to `o32-soft-float-prototype-v1`. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
5e219de441
commit
290d301ec7
13 files changed
+1343
-110
No files matched your search
+50
-7
@@ -13,13 +13,13 @@ retains class, byte order, `e_type`, raw `e_flags`, and inspected ABI declaratio
|
||||
Recognition does not mean every provider or MIPS variant is supported. Existing
|
||||
PE, managed and Mach-O architecture sets are unchanged.
|
||||
|
||||
The first Ghidra lane admits ELF32 `ET_EXEC`, little or big endian, with explicit
|
||||
o32 and `EF_MIPS_ARCH_32R2` flags and no machine-specific or ASE encoding flags.
|
||||
These are header declarations, not proof that every instruction conforms to
|
||||
that ISA. ELF64, n32/n64, unspecified ABI, MIPS16/microMIPS, machine-specific
|
||||
variants, shared/relocatable objects and PSP PRX are not covered by this lane.
|
||||
Their family may be identified, but provider selection returns the specific
|
||||
unsupported constraint rather than guessing a compatible profile.
|
||||
The Ghidra lane admits ELF32 `ET_EXEC` and `ET_REL`, little or big endian, with
|
||||
explicit o32 and `EF_MIPS_ARCH_32R2` flags and no machine-specific or ASE
|
||||
encoding flags. These are header declarations, not proof that every instruction
|
||||
conforms to that ISA. ELF64, n32/n64, unspecified ABI, MIPS16/microMIPS,
|
||||
machine-specific variants, shared objects and PSP PRX are not covered by this
|
||||
lane. Their family may be identified, but provider selection returns the
|
||||
specific unsupported constraint rather than guessing a compatible profile.
|
||||
|
||||
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
|
||||
@@ -86,6 +86,20 @@ Callables without typed float signatures keep Ghidra's default prototype, under
|
||||
which Ghidra already infers integer-register parameters. Hard-float profiles do
|
||||
not gain the policy, so their digests are unchanged.
|
||||
|
||||
### Relocatable objects
|
||||
|
||||
`ET_REL` objects, such as Linux kernel modules (`.ko`), are imported by Ghidra's
|
||||
ELF loader, which assigns each allocated section an analysis address and applies
|
||||
the object's REL relocations there. Undefined symbols are placed in Ghidra's
|
||||
`EXTERNAL` block: a thunk for code, a label for data. These addresses are
|
||||
analysis coordinates, not the addresses a kernel or linker would choose, and
|
||||
session Evidence says so. `inspect_native_load_image` reports each relocation
|
||||
with Ghidra's status; HI16 entries report `PARTIAL` because Ghidra applies them
|
||||
together with their paired LO16. Relocatable targets commit
|
||||
`mips_support_lane: elf32-rel-o32-arch32r2-standard-v1`, separate from the
|
||||
executable lane, whose identity executable targets keep. Kernel modules are
|
||||
usually soft-float and therefore also use the soft-float storage policy.
|
||||
|
||||
## Use
|
||||
|
||||
With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
|
||||
@@ -103,6 +117,8 @@ REA's ELF parser.
|
||||
|
||||
## Verification
|
||||
|
||||
### Executables
|
||||
|
||||
The source-owned freestanding fixture is `tests/conformance/c/mips.c`; the
|
||||
soft-float variant also links `tests/conformance/c/mips-soft-float.c`. The
|
||||
optional cross-target lane requires a caller-supplied Clang with MIPS targets,
|
||||
@@ -151,3 +167,30 @@ delay-slot execution or complete floating-point semantics. Assertion regressions
|
||||
reject incorrect byte order, constants, destinations and loaded identities.
|
||||
Parser/profile tests are narrower checks, not substitutes for this real-provider
|
||||
run. Report the actual lanes run and leave unexecuted verification pending.
|
||||
|
||||
### Relocatable objects
|
||||
|
||||
This lane has its own command and fixture:
|
||||
|
||||
```bash
|
||||
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:mips-relocatable
|
||||
```
|
||||
|
||||
It compiles `tests/conformance/c/mips-relocatable.c` with `-c -G0` in both byte
|
||||
orders, plus a little-endian soft-float `-mlong-calls` variant matching Linux
|
||||
module options. GNU readelf independently supplies sections, symbols and REL
|
||||
entries. For every relocation in an allocated section, the lane reads the
|
||||
loaded word through `read_bytes` and requires it to encode `S + A`: `S` from
|
||||
Ghidra's own section placement (`list_segments`) or `EXTERNAL` placement, `A`
|
||||
from the original file bytes. HI16/LO16 are checked as the effective address
|
||||
each LO16 forms with each HI16 it resolves, including several HI16s before one
|
||||
LO16, so a missing signed-low carry fails and a HI16 without a following LO16
|
||||
fails. The lane also requires matching Ghidra relocation entries and statuses,
|
||||
section file offsets and sizes, coverage of a shared HI16, a LO16 carry,
|
||||
external code and data and `R_MIPS_32` words, direct calls from the entry
|
||||
function, the relocatable support lane in the snapshot profile, the
|
||||
relocatable limitation in Evidence and an unchanged source object. The fixture
|
||||
also calls `powf`, an external whose float prototype comes from Ghidra's C
|
||||
library archive; every variant must decompile `powf(1.5,2.5)` without FPU
|
||||
inputs, and the soft-float variant must report that the soft-float prototype
|
||||
covered it.
|
||||
@@ -165,6 +165,7 @@
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:seeds": "npm run build:cached && node scripts/verify/ghidra/seeds.mjs",
|
||||
"verify:ghidra:mips": "npm run build:cached && node scripts/verify-real-ghidra-mips.mjs",
|
||||
"verify:ghidra:mips-relocatable": "npm run build:cached && node scripts/verify-real-ghidra-mips-relocatable.mjs",
|
||||
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
|
||||
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
|
||||
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { buildMipsRelocatableFixture } from "../tests/conformance/ghidra/mips-fixture.mjs";
|
||||
import {
|
||||
assertMipsRelocations,
|
||||
inspectMipsRelocatableReadelf,
|
||||
} from "../tests/conformance/ghidra/mips-relocatable-checks.mjs";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
// Optional cross-target lane for ET_REL objects. No tool acquisition, no
|
||||
// linking and no target execution.
|
||||
if (process.argv.length > 2)
|
||||
throw new Error(
|
||||
"Usage: node scripts/verify-real-ghidra-mips-relocatable.mjs",
|
||||
);
|
||||
if (!["linux", "darwin"].includes(process.platform))
|
||||
throw new Error(
|
||||
"MIPS verification requires a supported Linux/macOS Ghidra host",
|
||||
);
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR");
|
||||
const installation = inspectGhidraInstallation({
|
||||
environment: process.env,
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
if (installation.status !== "available")
|
||||
throw new Error(
|
||||
`Ghidra prerequisite unavailable: ${JSON.stringify(installation)}`,
|
||||
);
|
||||
const execute = promisify(execFile);
|
||||
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-mips-rel-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
GHIDRA_INSTALL_DIR: installation.installDir,
|
||||
JAVA_HOME: installation.javaHome,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
};
|
||||
for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
|
||||
}
|
||||
|
||||
/** Load every relocated word through the public read_bytes operation. */
|
||||
async function loadedWords(call, independent, segments, byteOrder) {
|
||||
const words = new Map();
|
||||
for (const relocation of independent.relocations) {
|
||||
const section = [...independent.sections.values()].find(
|
||||
(item) => item.name === relocation.section,
|
||||
);
|
||||
if (!section?.allocated) continue;
|
||||
const segment = segments.find((item) => item.name === section.name);
|
||||
assert.ok(segment, `Ghidra segment ${section.name}`);
|
||||
const address = BigInt(segment.start) + BigInt(relocation.offset);
|
||||
const read = await call("read_bytes", {
|
||||
address: `0x${address.toString(16)}`,
|
||||
length: 4,
|
||||
});
|
||||
assert.equal(read.returned_bytes, 4);
|
||||
const bytes = Buffer.from(read.bytes_hex, "hex");
|
||||
words.set(
|
||||
address,
|
||||
byteOrder === "little" ? bytes.readUInt32LE(0) : bytes.readUInt32BE(0),
|
||||
);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
async function verifyFixture(byteOrder, kernelModule) {
|
||||
const fixture = await buildMipsRelocatableFixture(
|
||||
workspace,
|
||||
byteOrder,
|
||||
kernelModule,
|
||||
);
|
||||
const independent = await inspectMipsRelocatableReadelf(
|
||||
fixture.path,
|
||||
byteOrder,
|
||||
fixture.float_abi,
|
||||
);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
assert.deepEqual(
|
||||
resolved.value.mips,
|
||||
independent.mips,
|
||||
"REA ELF/ABI interpretation disagrees with GNU readelf",
|
||||
);
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const label = `${byteOrder}${kernelModule ? "-module" : ""}`;
|
||||
const snapshotPath = join(workspace, `${label}.snapshot.json`);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-mips-rel-proof", version: "1" });
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
const call = async (name, args = {}) =>
|
||||
requireMcpOperationResult(
|
||||
await client.callTool({ name, arguments: args }, undefined, {
|
||||
timeout: 180000,
|
||||
}),
|
||||
name,
|
||||
);
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const target = await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(target.architecture, "mips");
|
||||
assert.equal(target.sha256, fixture.sha256);
|
||||
const image = (await call("inspect_native_load_image")).observations;
|
||||
assert.equal(
|
||||
image.language_id,
|
||||
`MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
);
|
||||
assert.equal(image.source_files.length, 1);
|
||||
assert.equal(image.source_files[0].original_sha256, fixture.sha256);
|
||||
assert.notEqual(
|
||||
image.source_files[0].modified_sha256,
|
||||
fixture.sha256,
|
||||
"Relocations must change the loaded bytes",
|
||||
);
|
||||
const names = await call("list_names");
|
||||
const segments = await call("list_segments");
|
||||
const coverage = assertMipsRelocations({
|
||||
facts: independent,
|
||||
fileBytes: await readFile(fixture.path),
|
||||
segments,
|
||||
mappings: image.mappings,
|
||||
ghidraRelocations: image.relocations,
|
||||
words: await loadedWords(call, independent, segments, byteOrder),
|
||||
names,
|
||||
byteOrder,
|
||||
});
|
||||
assert.equal(coverage.unplaced, 0, "Fixture references an empty section");
|
||||
assert.ok(coverage.shared_hi16, "No HI16 shared by several LO16s");
|
||||
assert.ok(coverage.lo16_carry, "No LO16 >= 0x8000 forced a HI16 carry");
|
||||
assert.ok(coverage.external_code, "No external code relocation");
|
||||
assert.ok(coverage.external_data, "No external data relocation");
|
||||
assert.ok(coverage.types.R_MIPS_32 > 0, "No R_MIPS_32 relocation");
|
||||
assert.equal(
|
||||
coverage.types.R_MIPS_26 === undefined,
|
||||
kernelModule,
|
||||
"Long calls must replace every R_MIPS_26 with HI16/LO16",
|
||||
);
|
||||
const procedures = await call("list_procedures");
|
||||
const named = (name) => {
|
||||
const found = procedures.filter((item) => item.value === name);
|
||||
assert.equal(found.length, 1, `Procedure ${name}`);
|
||||
return found[0].address;
|
||||
};
|
||||
const entry = named("rea_mips_rel_entry");
|
||||
const callees = await call("procedure_callees", { procedure: entry });
|
||||
for (const callee of [
|
||||
named("rea_mips_rel_leaf"),
|
||||
named("rea_mips_rel_far"),
|
||||
])
|
||||
assert.ok(callees.includes(callee), `Missing direct call to ${callee}`);
|
||||
// Ghidra lists the external function and its EXTERNAL-block thunk.
|
||||
assert.ok(
|
||||
procedures.some(
|
||||
(item) =>
|
||||
item.value === "<EXTERNAL>::rea_mips_rel_external" &&
|
||||
callees.includes(item.address),
|
||||
),
|
||||
"Missing call to the external function",
|
||||
);
|
||||
const dossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", { procedure: entry }),
|
||||
);
|
||||
for (const name of [
|
||||
"rea_mips_rel_leaf",
|
||||
"rea_mips_rel_far",
|
||||
"rea_mips_rel_external",
|
||||
])
|
||||
assert.ok(dossier.pseudocode.includes(name), `Pseudocode lost ${name}`);
|
||||
// powf's float prototype comes from Ghidra's C library archive; soft-float
|
||||
// builds pass 1.5f/2.5f in $a0/$a1, hard-float builds in $f12/$f14.
|
||||
const scaled = functionDossierSchema.parse(
|
||||
await call("analyze_function", {
|
||||
procedure: named("rea_mips_rel_scaled"),
|
||||
}),
|
||||
);
|
||||
assert.match(scaled.pseudocode, /\bpowf\(1\.5,2\.5\)/u);
|
||||
assert.doesNotMatch(scaled.pseudocode, /\bin_f\d+\b/u);
|
||||
await call("close_binary", { snapshot_path: snapshotPath });
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
const parameters = snapshot.binding.analysis_profile.parameters;
|
||||
assert.equal(parameters.mips_elf.type, 1);
|
||||
assert.equal(
|
||||
parameters.mips_support_lane,
|
||||
"elf32-rel-o32-arch32r2-standard-v1",
|
||||
);
|
||||
assert.equal(
|
||||
parameters.mips_soft_float_storage,
|
||||
kernelModule ? "o32-soft-float-prototype-v1" : undefined,
|
||||
);
|
||||
assert.ok(
|
||||
snapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
item.includes("Relocatable MIPS ELF"),
|
||||
),
|
||||
),
|
||||
"Relocatable limitation missing from retained Evidence",
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.evidence_bundle.records.some((record) =>
|
||||
parseEvidence(record).limitations.some((item) =>
|
||||
/Soft-float prototype outcome: [1-9]\d* functions/u.test(item),
|
||||
),
|
||||
),
|
||||
kernelModule,
|
||||
"Soft-float prototype must cover the archive-typed external in module builds",
|
||||
);
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.deepEqual(await call("list_procedures"), procedures);
|
||||
await call("close_binary");
|
||||
// Same public operation through CLI, not a private injected provider.
|
||||
const cli = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
fixture.path,
|
||||
"rea_mips_rel_entry",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(cli.stdout));
|
||||
assert.equal(evidence.subject?.architecture, "mips");
|
||||
assert.equal(evidence.subject?.digest.sha256, fixture.sha256);
|
||||
assert.equal(
|
||||
functionDossierSchema.parse(evidence.normalized_result).procedure.address,
|
||||
dossier.procedure.address,
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
.digest("hex"),
|
||||
fixture.sha256,
|
||||
"Analysis modified the source object",
|
||||
);
|
||||
return {
|
||||
...fixture,
|
||||
independent_reader: {
|
||||
command: independent.command,
|
||||
version: independent.version,
|
||||
},
|
||||
effective_language: image.language_id,
|
||||
relocation_coverage: coverage,
|
||||
procedures: procedures.length,
|
||||
status: "passed",
|
||||
};
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`MIPS ${label} relocatable verification failed; MCP stderr: ${stderr}`,
|
||||
{ cause },
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reports = [];
|
||||
let primaryFailure;
|
||||
try {
|
||||
for (const [byteOrder, kernelModule] of [
|
||||
["little", false],
|
||||
["big", false],
|
||||
["little", true],
|
||||
])
|
||||
reports.push(await verifyFixture(byteOrder, kernelModule));
|
||||
} catch (cause) {
|
||||
primaryFailure = cause;
|
||||
}
|
||||
const completed = await completeVerifierRun(run);
|
||||
if (
|
||||
completed.process_lineage.status !== "verified" ||
|
||||
completed.process_lineage.descendants.length !== 0
|
||||
)
|
||||
throw new Error(
|
||||
`Cleanup unverified; retained diagnostic workspace: ${workspace}`,
|
||||
{ cause: primaryFailure },
|
||||
);
|
||||
if (primaryFailure !== undefined)
|
||||
throw new Error(
|
||||
`MIPS relocatable verification failed; retained workspace: ${workspace}`,
|
||||
{ cause: primaryFailure },
|
||||
);
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
console.log(JSON.stringify({ ...completed, fixtures: reports }, null, 2));
|
||||
@@ -117,9 +117,8 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
|
||||
it.each([
|
||||
[{ elfClass: 64 }, "ELF32"],
|
||||
[{ type: 1 }, "ET_EXEC"],
|
||||
[{ type: 3 }, "ET_EXEC"],
|
||||
[{ type: 0xffa0 }, "ET_EXEC"],
|
||||
[{ type: 3 }, "ET_EXEC and ET_REL"],
|
||||
[{ type: 0xffa0 }, "ET_EXEC and ET_REL"],
|
||||
[{ flags: 0x10a23001 }, "separate MIPS ABI flags"],
|
||||
[{ flags: 0x70001021 }, "o32"],
|
||||
[{ flags: 0x70000001 }, "o32"],
|
||||
@@ -161,64 +160,64 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("soft-float MIPS storage", () => {
|
||||
const report: GhidraMipsSoftFloatReport = {
|
||||
policy: "o32-soft-float-prototype-v1",
|
||||
functions: 5,
|
||||
definitions: 1,
|
||||
};
|
||||
const session = async (
|
||||
value: BinaryTarget,
|
||||
reported: GhidraMipsSoftFloatReport | undefined,
|
||||
profileFor: BinaryTarget = value,
|
||||
) => {
|
||||
const counts = { starts: 0, calls: 0, closes: 0 };
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host, () => ({
|
||||
start: () => {
|
||||
counts.starts++;
|
||||
return Promise.resolve(
|
||||
ok({
|
||||
name: "REA Ghidra bridge" as const,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: "a".repeat(64),
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: false,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
target: {
|
||||
name: "fixture.elf",
|
||||
language_id: "MIPS:LE:32:default",
|
||||
compiler_spec_id: "default",
|
||||
image_base: "0x0",
|
||||
default_address_space: "ram",
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
...(reported === undefined
|
||||
? {}
|
||||
: { mips_soft_float_storage: reported }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
callTool: () => {
|
||||
counts.calls++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
close: () => {
|
||||
counts.closes++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
}));
|
||||
const resolved = await ghidra.resolveAnalysisProfile(profileFor);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a MIPS profile");
|
||||
return {
|
||||
counts,
|
||||
profile: resolved.value.profile,
|
||||
client: ghidra.createClient(value, resolved.value.profile),
|
||||
};
|
||||
const report: GhidraMipsSoftFloatReport = {
|
||||
policy: "o32-soft-float-prototype-v1",
|
||||
functions: 5,
|
||||
definitions: 1,
|
||||
};
|
||||
const session = async (
|
||||
value: BinaryTarget,
|
||||
reported: GhidraMipsSoftFloatReport | undefined,
|
||||
profileFor: BinaryTarget = value,
|
||||
) => {
|
||||
const counts = { starts: 0, calls: 0, closes: 0 };
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host, () => ({
|
||||
start: () => {
|
||||
counts.starts++;
|
||||
return Promise.resolve(
|
||||
ok({
|
||||
name: "REA Ghidra bridge" as const,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: "a".repeat(64),
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: false,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
target: {
|
||||
name: "fixture.elf",
|
||||
language_id: "MIPS:LE:32:default",
|
||||
compiler_spec_id: "default",
|
||||
image_base: "0x0",
|
||||
default_address_space: "ram",
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
...(reported === undefined
|
||||
? {}
|
||||
: { mips_soft_float_storage: reported }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
callTool: () => {
|
||||
counts.calls++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
close: () => {
|
||||
counts.closes++;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
}));
|
||||
const resolved = await ghidra.resolveAnalysisProfile(profileFor);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a MIPS profile");
|
||||
return {
|
||||
counts,
|
||||
profile: resolved.value.profile,
|
||||
client: ghidra.createClient(value, resolved.value.profile),
|
||||
};
|
||||
};
|
||||
|
||||
describe("soft-float MIPS storage", () => {
|
||||
it("admits soft-float o32 and commits its storage policy only for it", async () => {
|
||||
const soft = target(softFloat);
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
@@ -265,21 +264,55 @@ describe("soft-float MIPS storage", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed on a soft-float session without a storage report before any analysis call", async () => {
|
||||
const { counts, client } = await session(target(softFloat), undefined);
|
||||
const failed = await client.execute("list_procedures", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(counts).toEqual({ starts: 1, calls: 0, closes: 1 });
|
||||
it("admits relocatable objects under their own lane with coordinate limitations", async () => {
|
||||
const relocatable = target({ type: 1 });
|
||||
const { profile, client } = await session(relocatable, undefined);
|
||||
expect(profile.parameters.mips_support_lane).toBe(
|
||||
"elf32-rel-o32-arch32r2-standard-v1",
|
||||
);
|
||||
const executable = await session(target(), undefined);
|
||||
expect(executable.profile.parameters.mips_support_lane).toBe(
|
||||
"elf32-exec-o32-arch32r2-standard-v3",
|
||||
);
|
||||
const health = await client.execute("health", {});
|
||||
if (!health.ok) throw health.error;
|
||||
expect(health.value.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("analysis coordinates"),
|
||||
expect.stringContaining("EXTERNAL block"),
|
||||
]),
|
||||
);
|
||||
const executableHealth = await executable.client.execute("health", {});
|
||||
if (!executableHealth.ok) throw executableHealth.error;
|
||||
expect(executableHealth.value.limitations.join(" ")).not.toContain(
|
||||
"Relocatable MIPS ELF",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects soft-float storage reported by a hard-float session", async () => {
|
||||
const { counts, client } = await session(target(), report);
|
||||
const failed = await client.execute("health", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(counts).toEqual({ starts: 1, calls: 0, closes: 1 });
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
reason:
|
||||
"a soft-float session without a storage report before any analysis call",
|
||||
value: target(softFloat),
|
||||
storageReport: undefined,
|
||||
operation: "list_procedures",
|
||||
},
|
||||
{
|
||||
reason: "soft-float storage reported by a hard-float session",
|
||||
value: target(),
|
||||
storageReport: report,
|
||||
operation: "health",
|
||||
},
|
||||
] as const)(
|
||||
"rejects $reason",
|
||||
async ({ value, storageReport, operation }) => {
|
||||
const { counts, client } = await session(value, storageReport);
|
||||
const failed = await client.execute(operation, {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(counts).toEqual({ starts: 1, calls: 0, closes: 1 });
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a profile whose storage policy disagrees with the target", async () => {
|
||||
for (const [value, profileFor] of [
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
const MIPS_FP_ABI_SOFT = 3;
|
||||
const MIPS_ET_REL = 1;
|
||||
const MIPS_ET_EXEC = 2;
|
||||
|
||||
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
|
||||
export const ghidraMipsUnsupportedReason = (
|
||||
@@ -14,8 +16,8 @@ export const ghidraMipsUnsupportedReason = (
|
||||
return "MIPS analysis requires ELF header metadata; a family name alone does not select an ISA or ABI.";
|
||||
if (metadata.elfClass !== 32)
|
||||
return "This Ghidra MIPS lane supports ELF32 only; MIPS64 and n64 remain unsupported.";
|
||||
if (metadata.type !== 2)
|
||||
return "This Ghidra MIPS lane supports ET_EXEC only; relocatable objects, shared images and PSP PRX need separate load verification.";
|
||||
if (metadata.type !== MIPS_ET_EXEC && metadata.type !== MIPS_ET_REL)
|
||||
return "This Ghidra MIPS lane supports ET_EXEC and ET_REL only; shared images and PSP PRX need separate load verification.";
|
||||
if ((metadata.flags & 0x0f000000) !== 0)
|
||||
return "MIPS16, microMIPS and other ASE encodings are outside this standard-instruction MIPS lane.";
|
||||
if ((metadata.flags & 0x00ff0000) !== 0)
|
||||
@@ -101,6 +103,19 @@ export const ghidraMipsSoftFloatLimitations = (
|
||||
]
|
||||
: [];
|
||||
|
||||
/** Ghidra-placed relocatable objects need coordinates and relocation caveats. */
|
||||
export const ghidraMipsRelocatableLimitations = (
|
||||
target: BinaryTarget,
|
||||
): readonly string[] =>
|
||||
target.kind === "executable" &&
|
||||
target.architecture === "mips" &&
|
||||
target.mips?.type === MIPS_ET_REL
|
||||
? [
|
||||
"Relocatable MIPS ELF: Ghidra assigns each allocated section an analysis address and applies the object's relocations there. Returned addresses are analysis coordinates, not the addresses a kernel or linker would choose at load time.",
|
||||
"Undefined symbols (for example kernel exports used by a module) are placed in Ghidra's EXTERNAL block; their definitions are outside this artifact. inspect_native_load_image reports each relocation with Ghidra's status; HI16 entries report PARTIAL because Ghidra applies them together with their paired LO16.",
|
||||
]
|
||||
: [];
|
||||
|
||||
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
|
||||
export const ghidraMipsProfileParameters = (
|
||||
target: BinaryTarget,
|
||||
@@ -141,6 +156,9 @@ export const ghidraMipsProfileParameters = (
|
||||
...(isGhidraMipsSoftFloatTarget(target)
|
||||
? { mips_soft_float_storage: GHIDRA_MIPS_SOFT_FLOAT_STORAGE }
|
||||
: {}),
|
||||
mips_support_lane: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
mips_support_lane:
|
||||
target.mips.type === MIPS_ET_REL
|
||||
? "elf32-rel-o32-arch32r2-standard-v1"
|
||||
: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
};
|
||||
};
|
||||
@@ -59,6 +59,7 @@ import { ghidraPspExtensionSchema } from "./GhidraPspExtension.js";
|
||||
import {
|
||||
GHIDRA_MIPS_SOFT_FLOAT_STORAGE,
|
||||
ghidraMipsSoftFloatFailure,
|
||||
ghidraMipsRelocatableLimitations,
|
||||
ghidraMipsSoftFloatLimitations,
|
||||
isGhidraMipsSoftFloatTarget,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
@@ -510,6 +511,7 @@ export const createGhidraProviderClient = (input: {
|
||||
...(target.kind === "executable" && target.architecture === "mips"
|
||||
? (target.mips.limitations ?? [])
|
||||
: []),
|
||||
...ghidraMipsRelocatableLimitations(target),
|
||||
];
|
||||
const providerLimitations =
|
||||
installation.platform === "win32"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* Relocatable (ET_REL) static-analysis fixture, built with -c -G0 and never
|
||||
* executed or linked by REA. It exercises the relocations Linux MIPS kernel
|
||||
* modules use: R_MIPS_26 calls, HI16/LO16 address pairs (one HI16 shared by
|
||||
* several LO16s, and a LO16 >= 0x8000 that forces a HI16 carry), R_MIPS_32
|
||||
* data words, and undefined external code and data symbols. powf is an
|
||||
* external whose float prototype Ghidra's C library archive supplies by name,
|
||||
* so soft-float builds must give it the soft-float prototype too.
|
||||
*/
|
||||
extern int rea_mips_rel_external(int value);
|
||||
extern int rea_mips_rel_external_data;
|
||||
extern float powf(float base, float exponent);
|
||||
|
||||
struct rea_mips_rel_pair {
|
||||
int first;
|
||||
int second;
|
||||
};
|
||||
|
||||
struct rea_mips_rel_pair rea_mips_rel_pair = { 0x1111, 0x2222 };
|
||||
/* Places rea_mips_rel_carry past a signed 16-bit low-half boundary. */
|
||||
char rea_mips_rel_padding[0x8000] = { 1 };
|
||||
int rea_mips_rel_carry = 0x3333;
|
||||
static const char rea_mips_rel_marker[] = "rea-mips-relocatable-fixture";
|
||||
|
||||
__attribute__((noinline)) int rea_mips_rel_leaf(int value)
|
||||
{
|
||||
return value * 3 + rea_mips_rel_pair.first + rea_mips_rel_pair.second;
|
||||
}
|
||||
|
||||
__attribute__((noinline, long_call)) int rea_mips_rel_far(int value);
|
||||
|
||||
int rea_mips_rel_far(int value)
|
||||
{
|
||||
return value + rea_mips_rel_marker[value & 7] + rea_mips_rel_carry;
|
||||
}
|
||||
|
||||
int (*const rea_mips_rel_table[])(int) = {
|
||||
rea_mips_rel_leaf,
|
||||
rea_mips_rel_far,
|
||||
rea_mips_rel_external,
|
||||
};
|
||||
|
||||
__attribute__((noinline)) int rea_mips_rel_scaled(int value)
|
||||
{
|
||||
float scaled = powf(1.5f, 2.5f);
|
||||
return value ^ *(int *)&scaled;
|
||||
}
|
||||
|
||||
int rea_mips_rel_entry(int value)
|
||||
{
|
||||
return rea_mips_rel_leaf(value) + rea_mips_rel_far(value) +
|
||||
rea_mips_rel_external(value) + rea_mips_rel_external_data +
|
||||
rea_mips_rel_table[value & 1](value) + rea_mips_rel_scaled(value);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export function parseMipsReadelf(
|
||||
text: string,
|
||||
byteOrder: string,
|
||||
floatAbi?: MipsFloatAbi,
|
||||
objectType?: "exec" | "rel",
|
||||
): MipsReadelfFacts;
|
||||
export function inspectMipsReadelf(
|
||||
path: string,
|
||||
|
||||
@@ -23,9 +23,18 @@ const SOFT_FLOAT_SYMBOLS = [
|
||||
"rea_mips_soft_caller",
|
||||
];
|
||||
|
||||
/** Parse only the documented GNU readelf fields needed by our tiny fixture. */
|
||||
export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
|
||||
/**
|
||||
* Parse only the documented GNU readelf fields needed by our tiny fixtures.
|
||||
* `rel` objects carry their own symbol checks in mips-relocatable-checks.mjs.
|
||||
*/
|
||||
export function parseMipsReadelf(
|
||||
text,
|
||||
byteOrder,
|
||||
floatAbi = "hard",
|
||||
objectType = "exec",
|
||||
) {
|
||||
assert.ok(floatAbi === "hard" || floatAbi === "soft");
|
||||
assert.ok(objectType === "exec" || objectType === "rel");
|
||||
language(byteOrder);
|
||||
const field = (expression, label) => {
|
||||
const matches = [...text.matchAll(expression)];
|
||||
@@ -37,7 +46,10 @@ export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
|
||||
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*Type:\s*(.+)$/gmu, "type"),
|
||||
objectType === "rel" ? /^REL\b/u : /^EXEC\b/u,
|
||||
);
|
||||
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
|
||||
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+),.*$/gmu, "flags"));
|
||||
assert.equal(flags, 0x70001001, "Fixture ELF flags differ from build intent");
|
||||
@@ -85,14 +97,16 @@ export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
|
||||
),
|
||||
};
|
||||
const symbols = {};
|
||||
for (const name of [
|
||||
"rea_mips_entry",
|
||||
"rea_mips_leaf",
|
||||
"rea_mips_probe",
|
||||
"rea_mips_global",
|
||||
"rea_mips_marker",
|
||||
...(floatAbi === "soft" ? SOFT_FLOAT_SYMBOLS : []),
|
||||
]) {
|
||||
for (const name of objectType === "rel"
|
||||
? []
|
||||
: [
|
||||
"rea_mips_entry",
|
||||
"rea_mips_leaf",
|
||||
"rea_mips_probe",
|
||||
"rea_mips_global",
|
||||
"rea_mips_marker",
|
||||
...(floatAbi === "soft" ? SOFT_FLOAT_SYMBOLS : []),
|
||||
]) {
|
||||
const value = field(
|
||||
new RegExp(
|
||||
`^\\s*\\d+:\\s+([\\da-f]+)\\s+\\d+\\s+(?:FUNC|OBJECT)\\s+GLOBAL\\s+\\S+\\s+\\d+\\s+${name}\\s*$`,
|
||||
@@ -103,7 +117,13 @@ export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
|
||||
symbols[name] = `0x${BigInt(`0x${value}`).toString(16)}`;
|
||||
}
|
||||
return {
|
||||
mips: { elfClass: 32, byteOrder, type: 2, flags, abiFlags },
|
||||
mips: {
|
||||
elfClass: 32,
|
||||
byteOrder,
|
||||
type: objectType === "rel" ? 1 : 2,
|
||||
flags,
|
||||
abiFlags,
|
||||
},
|
||||
symbols,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export async function buildMipsFixture(
|
||||
if (floatAbi !== "hard" && floatAbi !== "soft")
|
||||
throw new Error("MIPS fixture float ABI must be hard or soft");
|
||||
const soft = floatAbi === "soft";
|
||||
const compiler = process.env.REA_MIPS_CLANG ?? "clang";
|
||||
const target = byteOrder === "little" ? "mipsel-linux-gnu" : "mips-linux-gnu";
|
||||
const source = fileURLToPath(new URL("../c/mips.c", import.meta.url));
|
||||
const softSource = fileURLToPath(
|
||||
@@ -46,6 +45,71 @@ export async function buildMipsFixture(
|
||||
"-o",
|
||||
path,
|
||||
];
|
||||
return {
|
||||
...(await compileMipsFixture(path, byteOrder, 2, args)),
|
||||
float_abi: floatAbi,
|
||||
source_sha256: createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
...(soft
|
||||
? {
|
||||
soft_source_sha256: createHash("sha256")
|
||||
.update(await readFile(softSource))
|
||||
.digest("hex"),
|
||||
}
|
||||
: {}),
|
||||
probe_sha256: createHash("sha256")
|
||||
.update(await readFile(probe))
|
||||
.digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the source-owned ET_REL object. `-G0` keeps gp-relative relocations
|
||||
* out, and `kernelModule` adds the soft-float long-call options Linux MIPS
|
||||
* modules use, so every call becomes a HI16/LO16 pair.
|
||||
*/
|
||||
export async function buildMipsRelocatableFixture(
|
||||
directory,
|
||||
byteOrder,
|
||||
kernelModule = false,
|
||||
) {
|
||||
if (byteOrder !== "little" && byteOrder !== "big")
|
||||
throw new Error("MIPS fixture byte order must be little or big");
|
||||
const target = byteOrder === "little" ? "mipsel-linux-gnu" : "mips-linux-gnu";
|
||||
const source = fileURLToPath(
|
||||
new URL("../c/mips-relocatable.c", import.meta.url),
|
||||
);
|
||||
const path = join(directory, `${target}${kernelModule ? "-module" : ""}.o`);
|
||||
const args = [
|
||||
`--target=${target}`,
|
||||
"-march=mips32r2",
|
||||
"-mabi=32",
|
||||
...(kernelModule ? ["-msoft-float", "-mlong-calls"] : []),
|
||||
"-mno-abicalls",
|
||||
"-fno-pic",
|
||||
"-G0",
|
||||
// Keeps powf(1.5f, 2.5f) a real external call instead of a folded constant.
|
||||
"-fno-builtin",
|
||||
"-O1",
|
||||
"-c",
|
||||
source,
|
||||
"-o",
|
||||
path,
|
||||
];
|
||||
return {
|
||||
...(await compileMipsFixture(path, byteOrder, 1, args)),
|
||||
float_abi: kernelModule ? "soft" : "hard",
|
||||
kernel_module_options: kernelModule,
|
||||
source_sha256: createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Compile and attest the shared ELF32/o32/MIPS32r2 fixture boundary. */
|
||||
async function compileMipsFixture(path, byteOrder, objectType, args) {
|
||||
const compiler = process.env.REA_MIPS_CLANG ?? "clang";
|
||||
const version = await execute(compiler, ["--version"], { timeout: 10000 });
|
||||
await execute(compiler, args, { timeout: 60000 });
|
||||
const bytes = await readFile(path);
|
||||
@@ -62,7 +126,7 @@ export async function buildMipsFixture(
|
||||
bytes.toString("hex", 0, 4) !== "7f454c46" ||
|
||||
bytes[4] !== 1 ||
|
||||
bytes[5] !== (byteOrder === "little" ? 1 : 2) ||
|
||||
read16(16) !== 2 ||
|
||||
read16(16) !== objectType ||
|
||||
read16(18) !== 8 ||
|
||||
read32(36) !== 0x70001001
|
||||
)
|
||||
@@ -72,22 +136,8 @@ export async function buildMipsFixture(
|
||||
return {
|
||||
path,
|
||||
byte_order: byteOrder,
|
||||
float_abi: floatAbi,
|
||||
flags: read32(36),
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
source_sha256: createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
...(soft
|
||||
? {
|
||||
soft_source_sha256: createHash("sha256")
|
||||
.update(await readFile(softSource))
|
||||
.digest("hex"),
|
||||
}
|
||||
: {}),
|
||||
probe_sha256: createHash("sha256")
|
||||
.update(await readFile(probe))
|
||||
.digest("hex"),
|
||||
compiler: version.stdout.trim(),
|
||||
arguments: args,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { MipsReadelfFacts, MipsFloatAbi } from "./mips-checks.mjs";
|
||||
|
||||
export interface MipsRelocatableSection {
|
||||
index: number;
|
||||
name: string;
|
||||
type: string;
|
||||
offset: number;
|
||||
size: number;
|
||||
allocated: boolean;
|
||||
}
|
||||
export interface MipsRelocatableSymbol {
|
||||
index: number;
|
||||
value: number;
|
||||
type: string;
|
||||
ndx: string;
|
||||
name: string;
|
||||
}
|
||||
export interface MipsRelocation {
|
||||
section: string;
|
||||
offset: number;
|
||||
type: number;
|
||||
typeName: string;
|
||||
symbolIndex: number;
|
||||
}
|
||||
export interface MipsRelocatableFacts {
|
||||
sections: Map<number, MipsRelocatableSection>;
|
||||
symbols: Map<number, MipsRelocatableSymbol>;
|
||||
relocations: MipsRelocation[];
|
||||
}
|
||||
export interface MipsRelocationCoverage {
|
||||
types: Record<string, number>;
|
||||
shared_hi16: boolean;
|
||||
multiple_hi16: boolean;
|
||||
lo16_carry: boolean;
|
||||
external_code: boolean;
|
||||
external_data: boolean;
|
||||
unplaced: number;
|
||||
}
|
||||
export function parseMipsRelocatableReadelf(text: string): MipsRelocatableFacts;
|
||||
export function inspectMipsRelocatableReadelf(
|
||||
path: string,
|
||||
byteOrder: string,
|
||||
floatAbi: MipsFloatAbi,
|
||||
): Promise<
|
||||
MipsReadelfFacts & MipsRelocatableFacts & { command: string; version: string }
|
||||
>;
|
||||
export function assertMipsRelocations(input: {
|
||||
facts: MipsRelocatableFacts;
|
||||
fileBytes: Buffer;
|
||||
segments: readonly { name: string; start: string; end: string }[];
|
||||
mappings: readonly {
|
||||
block: string;
|
||||
start: string;
|
||||
length: number;
|
||||
file_offset: number | null;
|
||||
}[];
|
||||
ghidraRelocations: readonly {
|
||||
address: string;
|
||||
type: number;
|
||||
status: string;
|
||||
}[];
|
||||
words: Map<bigint, number>;
|
||||
names: readonly {
|
||||
address: string;
|
||||
value: string;
|
||||
symbol?: { external: boolean; type: string };
|
||||
}[];
|
||||
byteOrder: string;
|
||||
}): MipsRelocationCoverage;
|
||||
@@ -0,0 +1,346 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { parseMipsReadelf } from "./mips-checks.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const RELOCATION_TYPES = new Map([
|
||||
[2, "R_MIPS_32"],
|
||||
[4, "R_MIPS_26"],
|
||||
[5, "R_MIPS_HI16"],
|
||||
[6, "R_MIPS_LO16"],
|
||||
]);
|
||||
const hex = (value) => `0x${value.toString(16)}`;
|
||||
const signed16 = (value) => ((value & 0xffff) ^ 0x8000) - 0x8000;
|
||||
|
||||
/** Parse GNU `readelf -S -s -r -W` sections, symbols and REL entries. */
|
||||
export function parseMipsRelocatableReadelf(text) {
|
||||
const sections = new Map();
|
||||
for (const match of text.matchAll(
|
||||
/^\s*\[\s*(\d+)\]\s+(\S+)\s+(\S+)\s+([\da-f]{8})\s+([\da-f]{6,})\s+([\da-f]{6,})\s+[\da-f]{2}\s+([A-Za-z]*)\s+\d+\s+\d+\s+\d+\s*$/gmu,
|
||||
)) {
|
||||
const index = Number(match[1]);
|
||||
assert.ok(!sections.has(index), `Duplicate readelf section ${index}`);
|
||||
sections.set(index, {
|
||||
index,
|
||||
name: match[2],
|
||||
type: match[3],
|
||||
offset: Number.parseInt(match[5], 16),
|
||||
size: Number.parseInt(match[6], 16),
|
||||
allocated: match[7].includes("A"),
|
||||
});
|
||||
}
|
||||
assert.ok(sections.size > 0, "Missing readelf section headers");
|
||||
const symbols = new Map();
|
||||
for (const match of text.matchAll(
|
||||
/^\s*(\d+):\s+([\da-f]{8})\s+\d+\s+(NOTYPE|OBJECT|FUNC|SECTION|FILE)\s+(LOCAL|GLOBAL|WEAK)\s+\S+\s+(UND|ABS|\d+)(?:[ \t]+(\S+))?\s*$/gmu,
|
||||
)) {
|
||||
const index = Number(match[1]);
|
||||
assert.ok(!symbols.has(index), `Duplicate readelf symbol ${index}`);
|
||||
symbols.set(index, {
|
||||
index,
|
||||
value: Number.parseInt(match[2], 16),
|
||||
type: match[3],
|
||||
ndx: match[5],
|
||||
name: match[6] ?? "",
|
||||
});
|
||||
}
|
||||
assert.ok(symbols.size > 0, "Missing readelf symbol table");
|
||||
const relocations = [];
|
||||
const headers = [
|
||||
...text.matchAll(
|
||||
/^Relocation section '\.rel(\S+)' at offset 0x[\da-f]+ contains (\d+) entr(?:y|ies):$/gmu,
|
||||
),
|
||||
];
|
||||
for (const [position, header] of headers.entries()) {
|
||||
const body = text.slice(
|
||||
(header.index ?? 0) + header[0].length,
|
||||
headers[position + 1]?.index ?? text.length,
|
||||
);
|
||||
const entries = [
|
||||
...body.matchAll(
|
||||
/^([\da-f]{8})\s+([\da-f]{8})\s+(R_MIPS_\S+)\s+[\da-f]{8}\s+\S.*$/gmu,
|
||||
),
|
||||
].map((match) => {
|
||||
const info = Number.parseInt(match[2], 16);
|
||||
const type = info & 0xff;
|
||||
assert.equal(
|
||||
match[3],
|
||||
RELOCATION_TYPES.get(type) ?? match[3],
|
||||
"readelf relocation name disagrees with r_info",
|
||||
);
|
||||
return {
|
||||
section: header[1],
|
||||
offset: Number.parseInt(match[1], 16),
|
||||
type,
|
||||
typeName: match[3],
|
||||
symbolIndex: info >>> 8,
|
||||
};
|
||||
});
|
||||
assert.equal(
|
||||
entries.length,
|
||||
Number(header[2]),
|
||||
`readelf .rel${header[1]} entry count`,
|
||||
);
|
||||
relocations.push(...entries);
|
||||
}
|
||||
assert.ok(relocations.length > 0, "Missing readelf relocations");
|
||||
return { sections, symbols, relocations };
|
||||
}
|
||||
|
||||
/** Independent reader is a bounded test prerequisite, never an REA provider. */
|
||||
export async function inspectMipsRelocatableReadelf(path, byteOrder, floatAbi) {
|
||||
const command = process.env.REA_MIPS_READELF ?? "readelf";
|
||||
const options = {
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
timeout: 10000,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
};
|
||||
const version = await execute(command, ["--version"], options);
|
||||
assert.match(
|
||||
version.stdout,
|
||||
/^GNU readelf\b/u,
|
||||
"This lane requires GNU readelf",
|
||||
);
|
||||
const result = await execute(
|
||||
command,
|
||||
["-h", "-A", "-S", "-s", "-r", "-W", path],
|
||||
options,
|
||||
);
|
||||
assert.equal(
|
||||
result.stderr.trim(),
|
||||
"",
|
||||
`readelf diagnostics: ${result.stderr}`,
|
||||
);
|
||||
return {
|
||||
...parseMipsReadelf(result.stdout, byteOrder, floatAbi, "rel"),
|
||||
...parseMipsRelocatableReadelf(result.stdout),
|
||||
command,
|
||||
version: version.stdout.split("\n")[0],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute every allocated-section relocation from the original file bytes
|
||||
* (REL addends are in place) and Ghidra's own section placement, then require
|
||||
* Ghidra's loaded words to encode exactly S + A. HI16/LO16 are checked as the
|
||||
* effective address each LO16 forms with its HI16, so a missing carry fails.
|
||||
* Placement comes from list_segments (uninitialized blocks included); file
|
||||
* mappings must agree with each file-backed section's offset and size. A
|
||||
* symbol in an empty section has no placement and is counted as unplaced.
|
||||
*/
|
||||
export function assertMipsRelocations({
|
||||
facts,
|
||||
fileBytes,
|
||||
segments,
|
||||
mappings,
|
||||
ghidraRelocations,
|
||||
words,
|
||||
names,
|
||||
byteOrder,
|
||||
}) {
|
||||
const read32 = (buffer, offset) =>
|
||||
byteOrder === "little"
|
||||
? buffer.readUInt32LE(offset)
|
||||
: buffer.readUInt32BE(offset);
|
||||
const segment = (name) => {
|
||||
const found = segments.filter((item) => item.name === name);
|
||||
assert.equal(found.length, 1, `Ghidra segment ${name}`);
|
||||
return {
|
||||
start: BigInt(found[0].start),
|
||||
size: BigInt(found[0].end) - BigInt(found[0].start),
|
||||
};
|
||||
};
|
||||
const sectionNamed = new Map(
|
||||
[...facts.sections.values()].map((section) => [section.name, section]),
|
||||
);
|
||||
for (const section of facts.sections.values()) {
|
||||
if (!section.allocated || section.size === 0) continue;
|
||||
assert.equal(
|
||||
segment(section.name).size,
|
||||
BigInt(section.size),
|
||||
`${section.name} size`,
|
||||
);
|
||||
if (section.type === "NOBITS") continue;
|
||||
const mapped = mappings.filter((item) => item.block === section.name);
|
||||
assert.equal(mapped.length, 1, `File mapping ${section.name}`);
|
||||
assert.equal(mapped[0].length, section.size, `${section.name} mapped size`);
|
||||
assert.equal(
|
||||
mapped[0].file_offset,
|
||||
section.offset,
|
||||
`${section.name} offset`,
|
||||
);
|
||||
}
|
||||
const external = segment("EXTERNAL");
|
||||
const symbolAddress = (index) => {
|
||||
const symbol = facts.symbols.get(index);
|
||||
assert.ok(symbol, `Relocation symbol ${index} missing from readelf`);
|
||||
if (symbol.ndx === "UND") {
|
||||
const found = names.filter(
|
||||
(item) =>
|
||||
item.symbol?.external === false &&
|
||||
item.value.replace(/^<EXTERNAL>::/u, "") === symbol.name &&
|
||||
BigInt(item.address) >= external.start &&
|
||||
BigInt(item.address) < external.start + external.size,
|
||||
);
|
||||
assert.equal(found.length, 1, `External placement of ${symbol.name}`);
|
||||
return {
|
||||
address: BigInt(found[0].address),
|
||||
external: found[0].symbol.type === "function" ? "code" : "data",
|
||||
};
|
||||
}
|
||||
const section = facts.sections.get(Number(symbol.ndx));
|
||||
assert.ok(section, `Section for symbol ${symbol.name}`);
|
||||
if (section.size === 0) return null;
|
||||
return {
|
||||
address: segment(section.name).start + BigInt(symbol.value),
|
||||
external: null,
|
||||
};
|
||||
};
|
||||
const site = (relocation) => {
|
||||
const section = sectionNamed.get(relocation.section);
|
||||
assert.ok(section, `Relocated section ${relocation.section}`);
|
||||
return section;
|
||||
};
|
||||
const checked = facts.relocations.filter((item) => site(item).allocated);
|
||||
// Non-allocated sections (.pdr, debug) live in Ghidra overlay spaces.
|
||||
const loaded = ghidraRelocations.filter((item) =>
|
||||
/^0x[\da-f]+$/u.test(item.address),
|
||||
);
|
||||
const ghidraByAddress = new Map(
|
||||
loaded.map((item) => [BigInt(item.address), item]),
|
||||
);
|
||||
const inside = loaded.filter((item) =>
|
||||
[...facts.sections.values()].some((section) => {
|
||||
if (!section.allocated || section.size === 0) return false;
|
||||
const { start } = segment(section.name);
|
||||
const address = BigInt(item.address);
|
||||
return address >= start && address < start + BigInt(section.size);
|
||||
}),
|
||||
);
|
||||
assert.equal(inside.length, checked.length, "Ghidra relocation count");
|
||||
const coverage = {
|
||||
types: {},
|
||||
shared_hi16: false,
|
||||
multiple_hi16: false,
|
||||
lo16_carry: false,
|
||||
external_code: false,
|
||||
external_data: false,
|
||||
unplaced: 0,
|
||||
};
|
||||
// HI16 entries wait for the next LO16 of their symbol, which resolves all of
|
||||
// them; later LO16s of that symbol reuse the HI16s it resolved.
|
||||
let outstanding = [];
|
||||
let resolved = new Map();
|
||||
let currentSection;
|
||||
const requireResolved = () =>
|
||||
assert.deepEqual(
|
||||
outstanding.map((high) => hex(high.address)),
|
||||
[],
|
||||
"HI16 relocations without a following LO16",
|
||||
);
|
||||
for (const relocation of checked) {
|
||||
const section = site(relocation);
|
||||
if (relocation.section !== currentSection) {
|
||||
requireResolved();
|
||||
resolved = new Map();
|
||||
currentSection = relocation.section;
|
||||
}
|
||||
const address = segment(section.name).start + BigInt(relocation.offset);
|
||||
const original = read32(fileBytes, section.offset + relocation.offset);
|
||||
const memory = words.get(address);
|
||||
assert.ok(memory !== undefined, `Loaded word at ${hex(address)}`);
|
||||
const reported = ghidraByAddress.get(address);
|
||||
assert.ok(reported, `Ghidra relocation at ${hex(address)}`);
|
||||
assert.equal(reported.type, relocation.type, `Type at ${hex(address)}`);
|
||||
assert.ok(
|
||||
reported.status === "APPLIED" ||
|
||||
(relocation.type === 5 && reported.status === "PARTIAL"),
|
||||
`Ghidra ${reported.status} at ${hex(address)}`,
|
||||
);
|
||||
const target = symbolAddress(relocation.symbolIndex);
|
||||
const name = RELOCATION_TYPES.get(relocation.type);
|
||||
assert.ok(name, `Unexpected relocation type ${relocation.type}`);
|
||||
coverage.types[name] = (coverage.types[name] ?? 0) + 1;
|
||||
if (target === null) {
|
||||
// Empty-section symbols have no address to recompute against.
|
||||
coverage.unplaced += 1;
|
||||
continue;
|
||||
}
|
||||
if (target.external === "code") coverage.external_code = true;
|
||||
if (target.external === "data") coverage.external_data = true;
|
||||
if (relocation.type === 2) {
|
||||
assert.equal(
|
||||
BigInt(memory),
|
||||
(target.address + BigInt(original)) & 0xffffffffn,
|
||||
`R_MIPS_32 at ${hex(address)}`,
|
||||
);
|
||||
} else if (relocation.type === 4) {
|
||||
const addend = BigInt((original & 0x3ffffff) << 2);
|
||||
assert.equal(
|
||||
memory >>> 26,
|
||||
original >>> 26,
|
||||
`jump opcode ${hex(address)}`,
|
||||
);
|
||||
assert.equal(
|
||||
BigInt(memory & 0x3ffffff),
|
||||
((target.address + addend) >> 2n) & 0x3ffffffn,
|
||||
`R_MIPS_26 at ${hex(address)}`,
|
||||
);
|
||||
} else if (relocation.type === 5) {
|
||||
assert.equal(
|
||||
memory >>> 16,
|
||||
original >>> 16,
|
||||
`HI16 opcode ${hex(address)}`,
|
||||
);
|
||||
outstanding.push({
|
||||
address,
|
||||
symbolIndex: relocation.symbolIndex,
|
||||
originalHigh: original & 0xffff,
|
||||
memoryHigh: memory & 0xffff,
|
||||
lows: 0,
|
||||
});
|
||||
} else {
|
||||
assert.equal(
|
||||
memory >>> 16,
|
||||
original >>> 16,
|
||||
`LO16 opcode ${hex(address)}`,
|
||||
);
|
||||
const low = signed16(original);
|
||||
const loaded = signed16(memory);
|
||||
const matched = outstanding.filter(
|
||||
(high) => high.symbolIndex === relocation.symbolIndex,
|
||||
);
|
||||
if (matched.length > 0) {
|
||||
outstanding = outstanding.filter((high) => !matched.includes(high));
|
||||
resolved.set(relocation.symbolIndex, matched);
|
||||
if (matched.length > 1) coverage.multiple_hi16 = true;
|
||||
}
|
||||
const highs = resolved.get(relocation.symbolIndex) ?? [];
|
||||
// The value each o32 lui/addiu (or load/store) pair actually forms.
|
||||
for (const high of highs) {
|
||||
const effective =
|
||||
(BigInt(high.memoryHigh << 16) + BigInt(loaded)) & 0xffffffffn;
|
||||
const expected =
|
||||
(target.address + BigInt(high.originalHigh << 16) + BigInt(low)) &
|
||||
0xffffffffn;
|
||||
assert.equal(
|
||||
effective,
|
||||
expected,
|
||||
`HI16 at ${hex(high.address)} with LO16 at ${hex(address)}`,
|
||||
);
|
||||
high.lows += 1;
|
||||
if (high.lows > 1) coverage.shared_hi16 = true;
|
||||
if ((memory & 0x8000) !== 0) coverage.lo16_carry = true;
|
||||
}
|
||||
if (highs.length === 0)
|
||||
assert.equal(
|
||||
BigInt(memory & 0xffff),
|
||||
(target.address + BigInt(low)) & 0xffffn,
|
||||
`Unpaired LO16 at ${hex(address)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
requireResolved();
|
||||
return coverage;
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertMipsRelocations,
|
||||
parseMipsRelocatableReadelf,
|
||||
} from "./mips-relocatable-checks.mjs";
|
||||
|
||||
// GNU readelf excerpt for a tiny ET_REL object; tests the reader boundary.
|
||||
const readelf = `Section Headers:
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
[ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
[ 1] .text PROGBITS 00000000 000040 000010 00 AX 0 0 16
|
||||
[ 2] .rel.text REL 00000000 000100 000018 08 I 5 1 4
|
||||
[ 3] .data PROGBITS 00000000 000050 008010 00 WA 0 0 16
|
||||
[ 4] .rel.data REL 00000000 000118 000010 08 I 5 3 4
|
||||
[ 5] .symtab SYMTAB 00000000 000128 000040 10 1 1 4
|
||||
|
||||
Relocation section '.rel.text' at offset 0x100 contains 3 entries:
|
||||
Offset Info Type Sym. Value Symbol's Name
|
||||
00000000 00000205 R_MIPS_HI16 00008000 far_data
|
||||
00000004 00000206 R_MIPS_LO16 00008000 far_data
|
||||
00000008 00000104 R_MIPS_26 00000000 func
|
||||
|
||||
Relocation section '.rel.data' at offset 0x118 contains 2 entries:
|
||||
Offset Info Type Sym. Value Symbol's Name
|
||||
00000000 00000102 R_MIPS_32 00000000 func
|
||||
00000004 00000302 R_MIPS_32 00000000 ext_data
|
||||
|
||||
Symbol table '.symtab' contains 4 entries:
|
||||
Num: Value Size Type Bind Vis Ndx Name
|
||||
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
|
||||
1: 00000000 16 FUNC GLOBAL DEFAULT 1 func
|
||||
2: 00008000 4 OBJECT GLOBAL DEFAULT 3 far_data
|
||||
3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data
|
||||
`;
|
||||
|
||||
// Ghidra placement: .text 0x10000, .data 0x10010, EXTERNAL 0x19000. far_data
|
||||
// is at 0x18010, so its LO16 is 0x8010 and the HI16 must carry to 2.
|
||||
const fileBytes = Buffer.alloc(0x50 + 0x8010);
|
||||
fileBytes.writeUInt32LE(0x3c010000, 0x40); // lui $1, 0
|
||||
fileBytes.writeUInt32LE(0x8c220000, 0x44); // lw $2, 0($1)
|
||||
fileBytes.writeUInt32LE(0x0c000000, 0x48); // jal 0
|
||||
const segments = [
|
||||
{ name: ".text", start: "0x10000", end: "0x10010" },
|
||||
{ name: ".data", start: "0x10010", end: "0x18020" },
|
||||
{ name: "EXTERNAL", start: "0x19000", end: "0x19004" },
|
||||
];
|
||||
const mappings = [
|
||||
{ block: ".text", start: "0x10000", length: 0x10, file_offset: 0x40 },
|
||||
{ block: ".data", start: "0x10010", length: 0x8010, file_offset: 0x50 },
|
||||
];
|
||||
const names = [
|
||||
{
|
||||
address: "0x19000",
|
||||
value: "ext_data",
|
||||
symbol: { external: false, type: "label" },
|
||||
},
|
||||
];
|
||||
const relocations = () => [
|
||||
{ address: "0x10000", type: 5, status: "PARTIAL" },
|
||||
{ address: "0x10004", type: 6, status: "APPLIED" },
|
||||
{ address: "0x10008", type: 4, status: "APPLIED" },
|
||||
{ address: "0x10010", type: 2, status: "APPLIED" },
|
||||
{ address: "0x10014", type: 2, status: "APPLIED" },
|
||||
{ address: ".pdr:0x0", type: 2, status: "APPLIED" },
|
||||
];
|
||||
const words = () =>
|
||||
new Map([
|
||||
[0x10000n, 0x3c010002],
|
||||
[0x10004n, 0x8c228010],
|
||||
[0x10008n, 0x0c004000],
|
||||
[0x10010n, 0x10000],
|
||||
[0x10014n, 0x19000],
|
||||
]);
|
||||
const check = (
|
||||
overrides: Partial<Parameters<typeof assertMipsRelocations>[0]> = {},
|
||||
) =>
|
||||
assertMipsRelocations({
|
||||
facts: parseMipsRelocatableReadelf(readelf),
|
||||
fileBytes,
|
||||
segments,
|
||||
mappings,
|
||||
ghidraRelocations: relocations(),
|
||||
words: words(),
|
||||
names,
|
||||
byteOrder: "little",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("MIPS relocatable real-verifier expectations", () => {
|
||||
it("reads sections, symbols and REL entries from r_info", () => {
|
||||
const facts = parseMipsRelocatableReadelf(readelf);
|
||||
expect(facts.sections.get(3)).toMatchObject({
|
||||
name: ".data",
|
||||
offset: 0x50,
|
||||
allocated: true,
|
||||
});
|
||||
expect(facts.symbols.get(3)).toMatchObject({
|
||||
ndx: "UND",
|
||||
name: "ext_data",
|
||||
});
|
||||
expect(facts.relocations).toEqual([
|
||||
expect.objectContaining({ section: ".text", type: 5, symbolIndex: 2 }),
|
||||
expect.objectContaining({ section: ".text", type: 6, symbolIndex: 2 }),
|
||||
expect.objectContaining({ section: ".text", type: 4, symbolIndex: 1 }),
|
||||
expect.objectContaining({ section: ".data", type: 2, symbolIndex: 1 }),
|
||||
expect.objectContaining({ section: ".data", type: 2, symbolIndex: 3 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a REL table whose entry count disagrees with its header", () => {
|
||||
expect(() =>
|
||||
parseMipsRelocatableReadelf(
|
||||
readelf.replace("contains 2 entries", "contains 3 entries"),
|
||||
),
|
||||
).toThrow(/entry count/u);
|
||||
});
|
||||
|
||||
it("accepts relocations that encode S + A, including a HI16 carry", () => {
|
||||
expect(check()).toMatchObject({
|
||||
lo16_carry: true,
|
||||
external_data: true,
|
||||
types: { R_MIPS_HI16: 1, R_MIPS_LO16: 1, R_MIPS_26: 1, R_MIPS_32: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a HI16 without the signed-low carry", 0x10000n, 0x3c010001],
|
||||
["a jump to the wrong target", 0x10008n, 0x0c004001],
|
||||
["a rewritten opcode", 0x10004n, 0x8c238010],
|
||||
["an unrelocated external word", 0x10014n, 0],
|
||||
])("rejects %s", (_label, address, value) => {
|
||||
const changed = words();
|
||||
changed.set(address, value);
|
||||
expect(() => check({ words: changed })).toThrow();
|
||||
});
|
||||
|
||||
it("rejects a failed or missing Ghidra relocation", () => {
|
||||
const failed = relocations().map((item, index) =>
|
||||
index === 2 ? { ...item, status: "FAILURE" } : item,
|
||||
);
|
||||
expect(() => check({ ghidraRelocations: failed })).toThrow(/FAILURE/u);
|
||||
expect(() => check({ ghidraRelocations: relocations().slice(1) })).toThrow(
|
||||
/relocation count/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a section Ghidra placed from different file bytes", () => {
|
||||
expect(() =>
|
||||
check({
|
||||
mappings: mappings.map((item) =>
|
||||
item.block === ".data" ? { ...item, file_offset: 0x60 } : item,
|
||||
),
|
||||
}),
|
||||
).toThrow(/\.data offset/u);
|
||||
});
|
||||
|
||||
it("counts a relocation against an empty section as unplaced, not verified", () => {
|
||||
const withEmpty = readelf
|
||||
.replace(
|
||||
" [ 5] .symtab",
|
||||
" [ 6] .sbss NOBITS 00000000 008060 000000 00 WAp 0 0 4\n [ 5] .symtab",
|
||||
)
|
||||
.replace("contains 2 entries", "contains 3 entries")
|
||||
.replace(
|
||||
"00000004 00000302 R_MIPS_32 00000000 ext_data\n",
|
||||
"00000004 00000302 R_MIPS_32 00000000 ext_data\n00000008 00000402 R_MIPS_32 00000000 .sbss\n",
|
||||
)
|
||||
.replace(
|
||||
" 3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data\n",
|
||||
" 3: 00000000 0 NOTYPE GLOBAL DEFAULT UND ext_data\n 4: 00000000 0 SECTION LOCAL DEFAULT 6 .sbss\n",
|
||||
);
|
||||
const loaded = words();
|
||||
loaded.set(0x10018n, 0x12345678);
|
||||
expect(
|
||||
check({
|
||||
facts: parseMipsRelocatableReadelf(withEmpty),
|
||||
ghidraRelocations: [
|
||||
...relocations(),
|
||||
{ address: "0x10018", type: 2, status: "APPLIED" },
|
||||
],
|
||||
words: loaded,
|
||||
}).unplaced,
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIPS relocatable HI16 entries resolved by one LO16", () => {
|
||||
// lui $1; lui $3; jal (unrelated R_MIPS_26); lw: both HI16s pair with the LO16.
|
||||
const multi = readelf.replace(
|
||||
/Relocation section '\.rel\.text'[^]*?\n\n/u,
|
||||
[
|
||||
"Relocation section '.rel.text' at offset 0x100 contains 4 entries:",
|
||||
" Offset Info Type Sym. Value Symbol's Name",
|
||||
"00000000 00000205 R_MIPS_HI16 00008000 far_data",
|
||||
"00000004 00000205 R_MIPS_HI16 00008000 far_data",
|
||||
"00000008 00000104 R_MIPS_26 00000000 func",
|
||||
"0000000c 00000206 R_MIPS_LO16 00008000 far_data",
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const bytes = Buffer.from(fileBytes);
|
||||
bytes.writeUInt32LE(0x3c010000, 0x40); // lui $1, 0
|
||||
bytes.writeUInt32LE(0x3c030000, 0x44); // lui $3, 0
|
||||
bytes.writeUInt32LE(0x0c000000, 0x48); // jal 0
|
||||
bytes.writeUInt32LE(0x8c220000, 0x4c); // lw $2, 0($1)
|
||||
const ghidra = () => [
|
||||
{ address: "0x10000", type: 5, status: "PARTIAL" },
|
||||
{ address: "0x10004", type: 5, status: "PARTIAL" },
|
||||
{ address: "0x10008", type: 4, status: "APPLIED" },
|
||||
{ address: "0x1000c", type: 6, status: "APPLIED" },
|
||||
{ address: "0x10010", type: 2, status: "APPLIED" },
|
||||
{ address: "0x10014", type: 2, status: "APPLIED" },
|
||||
];
|
||||
const loaded = () =>
|
||||
new Map([
|
||||
[0x10000n, 0x3c010002],
|
||||
[0x10004n, 0x3c030002],
|
||||
[0x10008n, 0x0c004000],
|
||||
[0x1000cn, 0x8c228010],
|
||||
[0x10010n, 0x10000],
|
||||
[0x10014n, 0x19000],
|
||||
]);
|
||||
const checkMulti = (
|
||||
overrides: Partial<Parameters<typeof assertMipsRelocations>[0]> = {},
|
||||
) =>
|
||||
check({
|
||||
facts: parseMipsRelocatableReadelf(multi),
|
||||
fileBytes: bytes,
|
||||
ghidraRelocations: ghidra(),
|
||||
words: loaded(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("checks every HI16 against the LO16, across unrelated relocations", () => {
|
||||
expect(checkMulti()).toMatchObject({
|
||||
multiple_hi16: true,
|
||||
lo16_carry: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a bad earlier HI16 that a later HI16 would have hidden", () => {
|
||||
const words = loaded();
|
||||
words.set(0x10000n, 0x3c010001);
|
||||
expect(() => checkMulti({ words })).toThrow(/HI16 at 0x10000 /u);
|
||||
});
|
||||
|
||||
it("rejects a HI16 that no LO16 resolves", () => {
|
||||
const unmatched = multi
|
||||
.replace("contains 4 entries", "contains 3 entries")
|
||||
.replace(
|
||||
"0000000c 00000206 R_MIPS_LO16 00008000 far_data\n",
|
||||
"",
|
||||
);
|
||||
expect(() =>
|
||||
checkMulti({
|
||||
facts: parseMipsRelocatableReadelf(unmatched),
|
||||
ghidraRelocations: ghidra().filter(
|
||||
(item) => item.address !== "0x1000c",
|
||||
),
|
||||
}),
|
||||
).toThrow(/without a following LO16/u);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user