feat(ghidra): admit soft-float MIPS32r2 ELF with integer-register float storage (#1949)

* 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.
This commit is contained in:
Aurelio Bernal
2026-10-12 04:54:38 +08:00
committed by GitHub
parent 5ba1642a7d
commit 5e219de441
17 changed files with 958 additions and 34 deletions
+5
View File
@@ -450,6 +450,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
String seeds = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
.getString("REA analysis seeds", null);
if (seeds != null) result.add("analysis_seeds", JsonParser.parseString(seeds));
// Written by ReaGhidraMipsSoftFloat after analysis; REA requires it
// exactly when the committed profile declares soft-float storage.
String softFloat = currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
.getString("REA MIPS soft-float storage", null);
if (softFloat != null) result.add("mips_soft_float_storage", JsonParser.parseString(softFloat));
return result;
}
+176
View File
@@ -0,0 +1,176 @@
import ghidra.app.script.GhidraScript;
import ghidra.program.database.SpecExtension;
import ghidra.program.model.data.AbstractFloatDataType;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.DoubleDataType;
import ghidra.program.model.data.FloatDataType;
import ghidra.program.model.data.FunctionDefinition;
import ghidra.program.model.data.IntegerDataType;
import ghidra.program.model.data.ParameterDefinition;
import ghidra.program.model.data.TypeDef;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.program.model.lang.PrototypeModel;
import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Parameter;
import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.VariableStorage;
import ghidra.program.model.pcode.XmlEncode;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* Soft-float o32 passes and returns float/double values exactly where same-size integers go.
* Ghidra's MIPS32 compiler specifications model only hard-float o32, so every typed float
* prototype (functions, externals, thunks via their targets, and function-pointer definitions)
* would otherwise read $f12/$f14/$f0. Derive a program-specific prototype model from the
* program's own default o32 model with its floating-point entries and rules removed, install it
* as a specification extension, and assign it to every callable whose formal types use floats.
*/
public final class ReaGhidraMipsSoftFloat extends GhidraScript {
static final String REPORT_KEY = "REA MIPS soft-float storage";
static final String POLICY = "o32-soft-float-prototype-v1";
static final String PROTOTYPE = "__rea_soft_float";
private static final Pattern FPU_REGISTER = Pattern.compile("f\\d+(_\\d+)?");
@Override
public void run() throws Exception {
String processor = currentProgram.getLanguage().getProcessor().toString();
if (!processor.equals("MIPS") || currentProgram.getDefaultPointerSize() != 4)
throw new IllegalStateException(
"REA soft-float storage requires a 32-bit MIPS program, not " + processor);
CompilerSpec spec = currentProgram.getCompilerSpec();
PrototypeModel base = spec.getDefaultCallingConvention();
new SpecExtension(currentProgram)
.addReplaceCompilerSpecExtension(softFloatPrototype(base, spec), monitor);
PrototypeModel soft = currentProgram.getCompilerSpec().getCallingConvention(PROTOTYPE);
if (soft == null)
throw new IllegalStateException("REA soft-float prototype was not installed");
requireIntegerStorage(
soft.getStorageLocations(
currentProgram,
new DataType[] {
FloatDataType.dataType, IntegerDataType.dataType, DoubleDataType.dataType,
FloatDataType.dataType, DoubleDataType.dataType
},
false),
"soft-float prototype self-check");
int functions = 0;
List<Function> callables = new ArrayList<>();
currentProgram.getFunctionManager().getFunctions(true).forEach(callables::add);
currentProgram.getFunctionManager().getExternalFunctions().forEach(callables::add);
for (Function function : callables) {
// A thunk's signature is its target's; the target is in this list.
if (function.isThunk() || !usesFloat(function)) continue;
if (function.hasCustomVariableStorage()) function.setCustomVariableStorage(false);
function.setCallingConvention(PROTOTYPE);
List<VariableStorage> storage = new ArrayList<>();
storage.add(function.getReturn().getVariableStorage());
for (Parameter parameter : function.getParameters()) storage.add(parameter.getVariableStorage());
requireIntegerStorage(storage.toArray(VariableStorage[]::new), function.getName(true));
functions += 1;
}
int definitions = 0;
Iterator<DataType> types = currentProgram.getDataTypeManager().getAllDataTypes();
while (types.hasNext()) {
if (!(types.next() instanceof FunctionDefinition definition) || !usesFloat(definition))
continue;
definition.setCallingConvention(PROTOTYPE);
definitions += 1;
}
// Written last: any failure above leaves no report, and REA refuses the session.
currentProgram.getOptions(Program.PROGRAM_INFO).setString(
REPORT_KEY,
"{\"policy\":\"" + POLICY + "\",\"functions\":" + functions +
",\"definitions\":" + definitions + "}");
println("REA MIPS soft-float prototype: functions=" + functions + " definitions=" + definitions);
}
/** The default o32 model without floating-point registers or float-consuming rules. */
private static String softFloatPrototype(PrototypeModel base, CompilerSpec spec) throws Exception {
XmlEncode encoder = new XmlEncode();
base.encode(encoder, spec.getPcodeInjectLibrary());
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document document =
factory.newDocumentBuilder().parse(new InputSource(new StringReader(encoder.toString())));
Element root = document.getDocumentElement();
if (!root.getTagName().equals("prototype"))
throw new IllegalStateException("Unexpected default prototype encoding " + root.getTagName());
root.setAttribute("name", PROTOTYPE);
List<Node> removed = new ArrayList<>();
collect(root.getElementsByTagName("pentry"), "metatype", "float", null, removed);
collect(root.getElementsByTagName("consume"), "storage", "float", "rule", removed);
if (removed.isEmpty())
throw new IllegalStateException("Default prototype has no floating-point entries to remove");
for (Node node : removed) node.getParentNode().removeChild(node);
StringWriter xml = new StringWriter();
var transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(document), new StreamResult(xml));
return xml.toString();
}
/** Select matching elements, or their nearest `ancestor` element when one is named. */
private static void collect(
NodeList nodes, String attribute, String value, String ancestor, List<Node> selected) {
for (int index = 0; index < nodes.getLength(); index += 1) {
Element element = (Element) nodes.item(index);
if (!value.equals(element.getAttribute(attribute))) continue;
Node target = element;
while (ancestor != null && target != null && !ancestor.equals(target.getNodeName()))
target = target.getParentNode();
if (target != null && !selected.contains(target)) selected.add(target);
}
}
private static void requireIntegerStorage(VariableStorage[] storage, String owner) {
for (VariableStorage item : storage) {
if (item.isBadStorage() || item.isUnassignedStorage())
throw new IllegalStateException("REA soft-float storage unresolved for " + owner);
List<Register> registers = item.getRegisters();
if (registers == null) continue; // stack-only storage
for (Register register : registers) {
if (FPU_REGISTER.matcher(register.getName()).matches())
throw new IllegalStateException(
"REA soft-float storage still uses " + register.getName() + " for " + owner);
}
}
}
private static boolean usesFloat(Function function) {
if (isFloat(function.getReturn().getFormalDataType())) return true;
for (Parameter parameter : function.getParameters()) {
if (isFloat(parameter.getFormalDataType())) return true;
}
return false;
}
private static boolean usesFloat(FunctionDefinition definition) {
if (isFloat(definition.getReturnType())) return true;
for (ParameterDefinition argument : definition.getArguments()) {
if (isFloat(argument.getDataType())) return true;
}
return false;
}
private static boolean isFloat(DataType type) {
DataType current = type;
while (current instanceof TypeDef typeDef) current = typeDef.getBaseDataType();
return current instanceof AbstractFloatDataType;
}
}
+49 -8
View File
@@ -39,12 +39,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 GPR/CPR1, no CPR2,
and a double-precision or FPXX ABI declaration with no ISA extension, ASE or
unknown general flags. Missing records remain unknown rather than defaulting
to a compatible ABI, and this initial lane refuses them. Unknown record values
are retained for diagnosis but do not gain provider support. These checks bind
the declared interpretation; the integer-only fixture does not establish full
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
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.
MIPS header and ABI record facts are committed in the analysis profile so
@@ -53,6 +55,37 @@ identity is versioned independently of file identity. Evidence, lifecycle
outputs and saved snapshots preserve the `mips` family. Hopper and IDA adapters
do not implicitly gain MIPS support. Windows Ghidra P0 remains PE x86/x86-64 only.
### Soft-float prototype
Ghidra's MIPS32 compiler specifications model hard-float o32 only: a typed
`float` or `double` parameter is bound to `$f12`/`$f14` and returned in `$f0`.
Soft-float o32 passes those values in integer registers and stack slots, so a
typed prototype (for example from DWARF) would make the decompiler read FPU
registers the code never writes and drop the real dataflow. That applies to
functions, external functions and indirect calls through typed function
pointers alike.
For soft-float targets REA commits
`mips_soft_float_storage: o32-soft-float-prototype-v1` in the analysis profile
and runs `ReaGhidraMipsSoftFloat.java` after auto-analysis. The script encodes
the program's own default o32 prototype, removes its floating-point parameter
and return entries and the rules that consume floating-point storage, and
installs the result as the program-specific `__rea_soft_float` prototype through
a Ghidra specification extension. Deriving it keeps the byte-order-specific
aggregate rules and the hidden struct-return pointer. The script then assigns
that prototype, with dynamic storage, to every non-thunk function and external
function, and to every function-pointer definition, whose declared types use
`float` or `double`. Thunks follow their targets. Types are kept.
The script fails, and writes no report, if the derived prototype still places a
float in an FPU register or any reassigned function does. It records the
function and definition counts last; REA requires that report exactly when the
profile commits the policy and refuses the session otherwise, before serving any
operation. Session Evidence carries the policy and counts as limitations.
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.
## Use
With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
@@ -70,7 +103,8 @@ REA's ELF parser.
## Verification
The source-owned freestanding fixture is `tests/conformance/c/mips.c`. The
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,
LLD, GNU `readelf`, Ghidra and its compatible JDK; ordinary host-native checks
do not acquire these tools. `REA_MIPS_CLANG` selects Clang and
@@ -81,7 +115,8 @@ These are test-time prerequisites, not REA runtime providers.
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:mips
```
The lane compiles both byte orders and never executes the targets. GNU readelf
The lane compiles both byte orders with hard-float and soft-float ABIs and never
executes the targets. GNU readelf
independently supplies header/ABI facts and symbol addresses, compared with the
production REA target resolver before provider startup. Its version, bounded
command output and selected tool path are retained in the verification report.
@@ -104,6 +139,12 @@ all program-header payloads. It checks the actual Ghidra language, entry-address
decompilation, distinct artifact digest and missing-section limitations through
CLI Evidence and an MCP-produced snapshot. Caller-selected Ghidra heap limits
are passed to these child workflows.
For soft-float fixtures the lane also checks, through `inspect_native_api`, the
exact parameter and return types and storage of float, double, mixed (pair
alignment and stack spill) and struct-returning functions; that a caller's
decompilation receives every source float constant without FPU inputs,
including an indirect call through a typed function pointer; and that the
snapshot profile and Evidence retain the policy and outcome counts.
Pseudocode presence is a liveness check, not proof of semantic equivalence.
Instruction decoding and reader agreement likewise do not establish runtime,
delay-slot execution or complete floating-point semantics. Assertion regressions
+1
View File
@@ -57,6 +57,7 @@
"bridge/ghidra/ReaGhidraApplySeeds.java",
"bridge/ghidra/ReaGhidraAnalysisOptions.java",
"bridge/ghidra/ReaGhidraNoReturnFix.java",
"bridge/ghidra/ReaGhidraMipsSoftFloat.java",
"bridge/ghidra/ReaGhidraPrepareRaw.java",
"bridge/android/ReaJadxBridge.java",
"bridge/mitmproxy/capture.py",
+83 -7
View File
@@ -15,6 +15,7 @@ import {
assertMipsLoadedImage,
assertMipsProbe,
assertMipsGlobal,
assertMipsSoftFloatStorage,
} from "../tests/conformance/ghidra/mips-checks.mjs";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
@@ -77,16 +78,64 @@ const env = {
for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
}
/**
* Typed float signatures must use soft-float o32 storage, and callers must see
* the source constants through it rather than FPU inputs.
*/
async function verifySoftFloat(call, symbols, byteOrder) {
const boundaries = {};
for (const name of [
"rea_mips_soft_pick",
"rea_mips_soft_wide",
"rea_mips_soft_mixed",
"rea_mips_soft_struct",
]) {
const api = await call("inspect_native_api", { procedure: symbols[name] });
boundaries[name] = api.boundary;
}
assertMipsSoftFloatStorage(boundaries, byteOrder);
const caller = functionDossierSchema.parse(
await call("analyze_function", {
procedure: symbols.rea_mips_soft_caller,
}),
);
// Includes the struct call and the indirect call through a typed pointer.
assert.doesNotMatch(caller.pseudocode, /\bin_f\d+\b/u);
for (const literal of [
"1.5",
"2.5",
"3.25",
"4.75",
"5.5",
"6.125",
"7.5",
"8.5",
"9.5",
])
assert.ok(
caller.pseudocode.includes(literal),
`Soft-float caller lost argument ${literal}`,
);
return { storage: boundaries };
}
const reports = [];
let primaryFailure;
try {
for (const byteOrder of psp ? ["little"] : ["little", "big"]) {
for (const [byteOrder, floatAbi] of psp
? [["little", "hard"]]
: [
["little", "hard"],
["big", "hard"],
["little", "soft"],
["big", "soft"],
]) {
const fixture = psp
? await buildPspFixture(workspace)
: await buildMipsFixture(workspace, byteOrder);
: await buildMipsFixture(workspace, byteOrder, floatAbi);
const independent = psp
? fixture
: await inspectMipsReadelf(fixture.path, byteOrder);
: await inspectMipsReadelf(fixture.path, byteOrder, floatAbi);
const resolved = await parseBinaryTarget(fixture.path);
if (!resolved.ok) throw resolved.error;
if (psp) {
@@ -104,7 +153,10 @@ try {
);
}
assert.equal(resolved.value.sha256, fixture.sha256);
const snapshotPath = join(workspace, `${byteOrder}.snapshot.json`);
const snapshotPath = join(
workspace,
`${byteOrder}-${floatAbi}.snapshot.json`,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
@@ -238,6 +290,10 @@ try {
callees.includes(leaf.address),
"Expected direct call not recovered",
);
const softFloat =
floatAbi === "soft"
? await verifySoftFloat(call, independent.symbols, byteOrder)
: undefined;
const assembly = await call("procedure_assembly", {
procedure: entry.address,
});
@@ -293,6 +349,19 @@ try {
},
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
);
assert.equal(
snapshot.binding.analysis_profile.parameters.mips_soft_float_storage,
floatAbi === "soft" ? "o32-soft-float-prototype-v1" : undefined,
);
assert.equal(
snapshot.evidence_bundle.records.some((record) =>
/Soft-float prototype outcome: 4 functions and [1-9]\d* function-pointer definitions use __rea_soft_float\./u.test(
JSON.stringify(record),
),
),
floatAbi === "soft",
"Soft-float prototype outcome missing from, or unexpected in, retained Evidence",
);
assert.ok(snapshot.evidence_bundle.records.length > 0);
if (psp) {
assert.equal(
@@ -419,7 +488,10 @@ try {
assert.ok(read32(offset + 4) + read32(offset + 16) <= cut);
}
const carvedBytes = sourceBytes.subarray(0, cut);
const carvedPath = join(workspace, `${byteOrder}-carved.elf`);
const carvedPath = join(
workspace,
`${byteOrder}-${floatAbi}-carved.elf`,
);
const carvedSha256 = createHash("sha256")
.update(carvedBytes)
.digest("hex");
@@ -460,7 +532,7 @@ try {
assert.ok(carvedDossier.pseudocode.trim().length > 0);
const carvedSnapshotPath = join(
workspace,
`${byteOrder}-carved.snapshot.json`,
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
);
await call("close_binary", { snapshot_path: carvedSnapshotPath });
const carvedSnapshot = parseAnalysisSnapshot(
@@ -523,7 +595,11 @@ try {
: "conditional branch destination",
"global word and marker bytes",
"CLI/MCP and snapshot identity",
...(floatAbi === "soft"
? ["soft-float o32 parameter/return storage and caller constants"]
: []),
],
...(softFloat === undefined ? {} : { soft_float: softFloat }),
procedures: procedures.length,
carved: carved ?? {
status: "not_run",
@@ -534,7 +610,7 @@ try {
});
} catch (cause) {
throw new Error(
`MIPS ${byteOrder} verification failed; MCP stderr: ${stderr}`,
`MIPS ${byteOrder} ${floatAbi}-float verification failed; MCP stderr: ${stderr}`,
{ cause },
);
} finally {
+23
View File
@@ -114,6 +114,8 @@ export interface GhidraHeadlessLauncherOptions {
readonly analysisSeeds?: GhidraSeedCommitment;
readonly analysisExtensions?: readonly GhidraExtension[];
readonly pspExtension?: GhidraPspExtension;
/** Remap typed float storage for the soft-float o32 ABI after analysis. */
readonly mipsSoftFloat?: true;
/** Spawn seam for provider-boundary lifecycle tests. */
readonly spawnProcess?: typeof spawnOwnedProviderProcess;
}
@@ -196,6 +198,9 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
const headlessArguments = ghidraHeadlessArguments({
platform,
...(psp === undefined ? {} : { psp: true as const }),
...(this.options.mipsSoftFloat === undefined
? {}
: { mipsSoftFloat: this.options.mipsSoftFloat }),
projectRoot: paths.projectRoot,
targetPath: session.targetPath,
bridgeScriptPath: this.options.bridgeScriptPath,
@@ -422,6 +427,7 @@ export interface GhidraHeadlessArgumentOptions {
readonly ghidraLogPath: string;
readonly scriptLogPath: string;
readonly psp?: true;
readonly mipsSoftFloat?: true;
readonly dosMz?: true;
readonly dosCom?: true;
readonly rawBinary?: {
@@ -460,6 +466,16 @@ export const ghidraHeadlessArguments = (
throw new Error(
"A PSP ELF import cannot be combined with a raw-binary import.",
);
if (
options.mipsSoftFloat === true &&
(options.psp === true ||
options.dosMz === true ||
options.dosCom === true ||
rawBinary !== undefined)
)
throw new Error(
"MIPS soft-float storage applies only to a generic MIPS ELF import.",
);
if (
override !== undefined &&
(options.dosMz === true || options.dosCom === true)
@@ -578,6 +594,13 @@ export const ghidraHeadlessArguments = (
"-postScript",
scriptPath.join(bridgeDirectory, "ReaGhidraNoReturnFix.java"),
]),
// After analysis and the no-return refit, before the bridge serves results.
...(options.mipsSoftFloat === true
? [
"-postScript",
scriptPath.join(bridgeDirectory, "ReaGhidraMipsSoftFloat.java"),
]
: []),
"-postScript",
// Ghidra checks the caller's cwd before scriptPath for a basename. Select
// the packaged source explicitly so unrelated entries cannot shadow it.
+153
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { ok } from "../domain/result.js";
import { parseConfig } from "../config/parseConfig.js";
import type {
BinaryTarget,
@@ -10,6 +11,8 @@ import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
import { IdaProvider } from "../ida/IdaProvider.js";
import { GhidraProvider } from "./GhidraProvider.js";
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
// Fake installation inspection tests admission/profile contracts, not Ghidra.
const host: GhidraInstallationHost = {
@@ -56,6 +59,18 @@ const target = (change: Partial<MipsElfMetadata> = {}): BinaryTarget => ({
...change,
},
});
const abi = (
change: Partial<NonNullable<MipsElfMetadata["abiFlags"]>>,
): Partial<MipsElfMetadata> => {
const base = target();
if (base.kind !== "executable" || base.architecture !== "mips")
throw new Error("expected a MIPS fixture target");
const flags = base.mips.abiFlags;
if (flags === undefined || flags === null)
throw new Error("expected fixture ABI flags");
return { abiFlags: { ...flags, ...change } };
};
const softFloat = abi({ cpr1Size: 0, fpAbi: 3 });
describe("bounded Ghidra MIPS admission", () => {
it("binds endian and flags without widening Windows P0", async () => {
@@ -118,6 +133,11 @@ describe("bounded Ghidra MIPS admission", () => {
[{ flags: 0x70001201 }, "EF_MIPS_FP64"],
[{ flags: 0x70001401 }, "EF_MIPS_NAN2008"],
[{ abiFlags: null }, "inspected ABI"],
[abi({ cpr1Size: 0, fpAbi: 1 }), "soft-float without CPR1"],
[abi({ cpr1Size: 1, fpAbi: 3 }), "soft-float without CPR1"],
[abi({ cpr1Size: 1, fpAbi: 2 }), "single-float"],
[abi({ cpr1Size: 1, fpAbi: 0 }), "unspecified"],
[abi({ cpr1Size: 2, fpAbi: 6 }), "FP64"],
] satisfies [Partial<MipsElfMetadata>, string][])(
"refuses an unverified MIPS interpretation %j",
async (change, reason) => {
@@ -140,3 +160,136 @@ describe("bounded Ghidra MIPS admission", () => {
expect(ida.inspectTargetSupport(target()).status).toBe("unsupported");
});
});
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),
};
};
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);
expect(ghidra.inspectTargetSupport(soft).status).toBe("supported");
const { profile } = await session(soft, report);
expect(profile.parameters.mips_soft_float_storage).toBe(
"o32-soft-float-prototype-v1",
);
const hard = await session(target(), undefined);
expect(hard.profile.parameters).not.toHaveProperty(
"mips_soft_float_storage",
);
});
it("admits the ODDSPREG default that soft-float toolchains emit, but no other flags", () => {
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
// GCC 5.2 kernel modules and LLVM/Rust firmware both record flags1 = 1.
expect(
ghidra.inspectTargetSupport(
target(abi({ cpr1Size: 0, fpAbi: 3, flags1: 1 })),
).status,
).toBe("supported");
expect(
ghidra.inspectTargetSupport(
target(abi({ cpr1Size: 0, fpAbi: 3, flags1: 2 })),
),
).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("general flags"),
});
});
it("reports the prototype outcome as session limitations", async () => {
const { client } = await session(target(softFloat), report);
const health = await client.execute("health", {});
if (!health.ok) throw health.error;
expect(health.value.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("model hard-float o32 only"),
expect.stringContaining(
"5 functions and 1 function-pointer definitions use __rea_soft_float.",
),
]),
);
});
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("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("refuses a profile whose storage policy disagrees with the target", async () => {
for (const [value, profileFor] of [
[target(softFloat), target()],
[target(), target(softFloat)],
] as const) {
const { counts, client } = await session(value, report, profileFor);
const failed = await client.execute("health", {});
expect(failed.ok).toBe(false);
expect(counts.starts).toBe(0);
}
});
});
+58 -4
View File
@@ -1,6 +1,8 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
const MIPS_FP_ABI_SOFT = 3;
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
export const ghidraMipsUnsupportedReason = (
target: BinaryTarget,
@@ -39,17 +41,66 @@ export const ghidraMipsUnsupportedReason = (
return "This Ghidra MIPS lane requires ABI flags version 0; newer record semantics are unsupported.";
if (abi.isaLevel !== 32 || abi.isaRevision !== 2)
return "MIPS ABI flags must agree with the declared MIPS32r2 ISA.";
if (abi.gprSize !== 1 || abi.cpr1Size !== 1 || abi.cpr2Size !== 0)
return "This Ghidra MIPS lane requires 32-bit GPR/CPR1 and no CPR2; other register modes need separate verification.";
if (abi.fpAbi !== 1 && abi.fpAbi !== 5)
return "This Ghidra MIPS lane admits double-precision or FPXX ABI declarations only; soft-float, FP64 and unspecified modes need separate verification.";
if (abi.gprSize !== 1 || abi.cpr2Size !== 0)
return "This Ghidra MIPS lane requires 32-bit GPRs and no CPR2; other register modes need separate verification.";
const hardFloat = abi.cpr1Size === 1 && (abi.fpAbi === 1 || abi.fpAbi === 5);
const softFloat = abi.cpr1Size === 0 && abi.fpAbi === MIPS_FP_ABI_SOFT;
if (!hardFloat && !softFloat)
return "This Ghidra MIPS lane admits double-precision or FPXX ABI declarations with 32-bit CPR1, or soft-float without CPR1; single-float, FP64 and unspecified modes need separate verification.";
if (abi.isaExtension !== 0 || abi.ases !== 0)
return "MIPS ABI flags declare an ISA extension or ASE outside this standard-instruction lane.";
// MIPS_AFL_FLAGS1_ODDSPREG (bit 0) is the GCC and LLVM default for MIPS32 o32
// whatever the FP ABI; soft-float objects from both carry it. With no CPR1 it
// selects nothing, so it is accepted for soft-float as well as hard-float.
if ((abi.flags1 & ~1) !== 0 || abi.flags2 !== 0)
return "MIPS ABI flags contain unsupported general flags.";
return null;
};
/** Soft-float prototype policy applied by ReaGhidraMipsSoftFloat.java after analysis. */
export const GHIDRA_MIPS_SOFT_FLOAT_STORAGE = "o32-soft-float-prototype-v1";
/** Whether the admitted target declares the soft-float o32 ABI. */
export const isGhidraMipsSoftFloatTarget = (target: BinaryTarget): boolean =>
target.kind === "executable" &&
target.architecture === "mips" &&
target.mips?.abiFlags?.cpr1Size === 0 &&
target.mips.abiFlags.fpAbi === MIPS_FP_ABI_SOFT;
/** Prototype outcome the post-analysis script records for the bridge handshake. */
export interface GhidraMipsSoftFloatReport {
readonly policy: typeof GHIDRA_MIPS_SOFT_FLOAT_STORAGE;
readonly functions: number;
readonly definitions: number;
}
/** Fail closed unless soft-float storage ran exactly for soft-float targets. */
export const ghidraMipsSoftFloatFailure = (
expected: boolean,
report: GhidraMipsSoftFloatReport | undefined,
): string | undefined =>
expected === (report !== undefined)
? undefined
: expected
? "The Ghidra session did not report the committed MIPS soft-float prototype."
: "The Ghidra session reported MIPS soft-float storage that the profile did not commit.";
/** Keep REA's soft-float prototype distinct from Ghidra's hard-float o32 model. */
export const ghidraMipsSoftFloatLimitations = (
expected: boolean,
report?: GhidraMipsSoftFloatReport,
): readonly string[] =>
expected
? [
"Soft-float o32: Ghidra's MIPS32 compiler specifications model hard-float o32 only. After analysis REA installs a program-specific `__rea_soft_float` prototype, derived from the default o32 model without its floating-point registers and rules, and assigns it to every function, external function and function-pointer definition whose declared types use float or double, so those values keep their types and use integer registers and stack slots.",
...(report === undefined
? []
: [
`Soft-float prototype outcome: ${report.functions} functions and ${report.definitions} function-pointer definitions use __rea_soft_float.`,
]),
]
: [];
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
export const ghidraMipsProfileParameters = (
target: BinaryTarget,
@@ -87,6 +138,9 @@ export const ghidraMipsProfileParameters = (
flags2: abi.flags2,
},
},
...(isGhidraMipsSoftFloatTarget(target)
? { mips_soft_float_storage: GHIDRA_MIPS_SOFT_FLOAT_STORAGE }
: {}),
mips_support_lane: "elf32-exec-o32-arch32r2-standard-v3",
};
};
+47 -2
View File
@@ -56,6 +56,12 @@ import {
GHIDRA_PSP_LIMITATIONS,
} from "./GhidraPspProfile.js";
import { ghidraPspExtensionSchema } from "./GhidraPspExtension.js";
import {
GHIDRA_MIPS_SOFT_FLOAT_STORAGE,
ghidraMipsSoftFloatFailure,
ghidraMipsSoftFloatLimitations,
isGhidraMipsSoftFloatTarget,
} from "./GhidraMipsProfile.js";
import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js";
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
@@ -386,6 +392,20 @@ export const createGhidraProviderClient = (input: {
},
}),
);
const softFloat = isGhidraMipsSoftFloatTarget(target);
if (
softFloat !==
(committedProfile.parameters.mips_soft_float_storage ===
GHIDRA_MIPS_SOFT_FLOAT_STORAGE)
)
return unavailableClient(
new ProviderAdapterError("ghidra", "open_binary", {
diagnostics: {
reason:
"Resolve the soft-float MIPS storage policy into the analysis profile before opening the session.",
},
}),
);
const extensionProfile = ghidraExtensionSchema
.array()
.safeParse(committedProfile.parameters.analysis_extensions ?? []);
@@ -465,6 +485,10 @@ export const createGhidraProviderClient = (input: {
let extensionFailure: AnalysisError | undefined;
// Replaced by the reported outcome once the session handshake is checked.
const seedLimitations: string[] = [...ghidraSeedLimitations(seeds)];
// Replaced by the reported storage outcome once the handshake is checked.
const softFloatLimitations: string[] = [
...ghidraMipsSoftFloatLimitations(softFloat),
];
const rawLanguage =
target.format === "raw-binary"
? ghidraRawBinaryLanguage(target.architecture)
@@ -508,6 +532,7 @@ export const createGhidraProviderClient = (input: {
...(pspExtension?.success === true
? { pspExtension: pspExtension.data }
: {}),
...(softFloat ? { mipsSoftFloat: true as const } : {}),
...(target.format === "dos-mz" ? { dosMz: true } : {}),
...(target.format === "dos-com" ? { dosCom: true } : {}),
...(rawLanguage === undefined || target.format !== "raw-binary"
@@ -569,18 +594,37 @@ export const createGhidraProviderClient = (input: {
operation: AnalysisOperation,
info: GhidraSessionInfo,
): Promise<AnalysisError | undefined> => {
const softFloatReason = ghidraMipsSoftFloatFailure(
softFloat,
info.mips_soft_float_storage,
);
extensionFailure =
ghidraExtensionFailure(
extensions,
info.analysis_extensions ?? [],
operation,
) ?? ghidraSeedFailure(seeds, info.analysis_seeds, operation);
) ??
ghidraSeedFailure(seeds, info.analysis_seeds, operation) ??
(softFloatReason === undefined
? undefined
: new ProviderAdapterError("ghidra", operation, {
diagnostics: { reason: softFloatReason },
}));
if (extensionFailure === undefined && info.analysis_seeds !== undefined)
seedLimitations.splice(
0,
seedLimitations.length,
...ghidraSeedLimitations(seeds, info.analysis_seeds),
);
if (extensionFailure === undefined)
softFloatLimitations.splice(
0,
softFloatLimitations.length,
...ghidraMipsSoftFloatLimitations(
softFloat,
info.mips_soft_float_storage,
),
);
if (extensionFailure === undefined) return undefined;
const closed = await client.close();
return closed.ok
@@ -598,6 +642,7 @@ export const createGhidraProviderClient = (input: {
...providerLimitations,
...targetLimitations,
...(psp ? GHIDRA_PSP_LIMITATIONS : []),
...softFloatLimitations,
...ghidraExtensionLimitations(extensions),
...seedLimitations,
...(releaseLimitation === undefined ? [] : [releaseLimitation]),
@@ -648,7 +693,7 @@ export const createGhidraProviderClient = (input: {
? parseGhidraFunctionInput(operation, parameters)
: parseGhidraInventoryInput(operation, parameters);
if (!input.ok) return input;
if (extensions.length > 0 || seeds !== undefined) {
if (extensions.length > 0 || seeds !== undefined || softFloat) {
const started = await client.start(options?.signal, options?.progress);
if (!started.ok)
return err(
+34
View File
@@ -52,6 +52,40 @@ describe("Ghidra DOS import commitment", () => {
});
});
describe("Ghidra MIPS soft-float storage report", () => {
const valid = {
policy: "o32-soft-float-prototype-v1",
functions: 2,
definitions: 1,
};
it("accepts the exact committed policy outcome", () => {
const parsed = parseGhidraSessionInfo(
{ ...session(), mips_soft_float_storage: valid },
expected,
);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.mips_soft_float_storage).toEqual(valid);
});
it.each([
[
"the superseded storage policy",
{ policy: "o32-integer-registers-v1", remapped: 2, retyped: 1 },
],
["a negative count", { ...valid, definitions: -1 }],
["a fractional count", { ...valid, functions: 1.5 }],
["a missing count", { policy: valid.policy, functions: 2 }],
["an undeclared field", { ...valid, skipped: 0 }],
["a non-object report", "functions"],
])("rejects %s", (_label, report) => {
expect(
parseGhidraSessionInfo(
{ ...session(), mips_soft_float_storage: report },
expected,
).ok,
).toBe(false);
});
});
describe("Ghidra mutation handshake", () => {
it("requires the exact mutation authority and rejects duplicate capabilities", () => {
const value = session();
+9
View File
@@ -9,6 +9,7 @@ import {
} from "./GhidraInventoryValues.js";
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js";
import { GHIDRA_MIPS_SOFT_FLOAT_STORAGE } from "./GhidraMipsProfile.js";
/** Exact methods proved by the bridge handshake after auto-analysis. */
export const GHIDRA_SESSION_CAPABILITIES = [
@@ -42,6 +43,14 @@ const sessionInfoSchema = z
analysis_timed_out: z.boolean(),
analysis_extensions: z.array(ghidraExtensionResultSchema).optional(),
analysis_seeds: ghidraSeedReportSchema.optional(),
mips_soft_float_storage: z
.object({
policy: z.literal(GHIDRA_MIPS_SOFT_FLOAT_STORAGE),
functions: z.number().int().nonnegative(),
definitions: z.number().int().nonnegative(),
})
.strict()
.optional(),
capabilities: z.array(capabilitySchema),
target: z
.object({
@@ -191,6 +191,45 @@ describe("Ghidra headless launcher", () => {
expect(arguments_).toContain("-readOnly");
expect(arguments_).toContain("-deleteProject");
});
it("remaps soft-float storage after the no-return fix and before the bridge", () => {
const base = {
platform: "linux" as const,
projectRoot: "/tmp/project",
targetPath: "/tmp/target",
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
descriptorPath: "/tmp/session.json",
ghidraLogPath: "/tmp/ghidra.log",
scriptLogPath: "/tmp/script.log",
};
const soft = "/package/bridge/ReaGhidraMipsSoftFloat.java";
expect(ghidraHeadlessArguments(base)).not.toContain(soft);
const arguments_ = ghidraHeadlessArguments({
...base,
mipsSoftFloat: true,
});
expect(arguments_[arguments_.indexOf(soft) - 1]).toBe("-postScript");
expect(
arguments_.indexOf("/package/bridge/ReaGhidraNoReturnFix.java"),
).toBeLessThan(arguments_.indexOf(soft));
expect(arguments_.indexOf(soft)).toBeLessThan(
arguments_.indexOf("/package/bridge/ReaGhidraBridge.java"),
);
for (const conflict of [
{ psp: true as const },
{ dosMz: true as const },
{ dosCom: true as const },
{
rawBinary: {
languageId: "x86:LE:32:default",
compilerSpecId: "default",
baseAddr: "0x1000",
},
},
])
expect(() =>
ghidraHeadlessArguments({ ...base, mipsSoftFloat: true, ...conflict }),
).toThrow(/generic MIPS ELF/u);
});
});
describe("Ghidra configured import language and seeds", () => {
+53
View File
@@ -0,0 +1,53 @@
/* Soft-float o32 storage fixture; linked with mips.c, never executed by REA.
* Without an FPU, float/double values travel in integer registers and stack
* slots: pick uses $a0/$a1/$a2 and returns in $v0, wide uses the $a0:$a1 pair,
* and mixed aligns d to $a2:$a3 so f and e spill to the stack at 16 and 24.
* structured returns through a hidden $a0 pointer, moving scale to $a1. The
* typed function pointer makes the caller's indirect call use a prototype that
* belongs to no function. No arithmetic, so no compiler-rt helpers are needed.
*/
volatile unsigned int rea_mips_soft_sink;
__attribute__((noinline)) float rea_mips_soft_pick(float a, float b, int sel)
{
return sel ? a : b;
}
__attribute__((noinline)) double rea_mips_soft_wide(double a, int sel)
{
return sel ? a : 0.5;
}
__attribute__((noinline)) double rea_mips_soft_mixed(int i, double d, float f, double e)
{
rea_mips_soft_sink = *(volatile unsigned int *)&f;
return i ? d : e;
}
struct rea_mips_soft_pair {
double first;
double second;
};
__attribute__((noinline)) struct rea_mips_soft_pair rea_mips_soft_struct(float scale, int sel)
{
struct rea_mips_soft_pair pair = { 0.25, 0.75 };
rea_mips_soft_sink = *(volatile unsigned int *)&scale;
if (sel)
pair.first = 1.25;
return pair;
}
/* Volatile so the indirect call through this typed pointer is not folded. */
float (*volatile rea_mips_soft_callback)(float, float, int) = rea_mips_soft_pick;
__attribute__((noinline)) unsigned int rea_mips_soft_caller(int sel)
{
float picked = rea_mips_soft_pick(1.5f, 2.5f, sel);
double wide = rea_mips_soft_wide(3.25, sel);
double mixed = rea_mips_soft_mixed(sel, 4.75, 5.5f, 6.125);
struct rea_mips_soft_pair pair = rea_mips_soft_struct(7.5f, sel);
float called = rea_mips_soft_callback(8.5f, 9.5f, sel);
return *(unsigned int *)&picked ^ *(unsigned int *)&wide ^ *(unsigned int *)&mixed ^
*(unsigned int *)&pair.first ^ *(unsigned int *)&called;
}
+8 -8
View File
@@ -1,23 +1,19 @@
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
export type MipsFloatAbi = "hard" | "soft";
export interface MipsReadelfFacts {
mips: MipsElfMetadata;
symbols: Record<
| "rea_mips_entry"
| "rea_mips_leaf"
| "rea_mips_probe"
| "rea_mips_global"
| "rea_mips_marker",
string
>;
symbols: Record<string, string>;
}
export function parseMipsReadelf(
text: string,
byteOrder: string,
floatAbi?: MipsFloatAbi,
): MipsReadelfFacts;
export function inspectMipsReadelf(
path: string,
byteOrder: string,
floatAbi?: MipsFloatAbi,
): Promise<
MipsReadelfFacts & { command: string; version: string; raw: string }
>;
@@ -33,3 +29,7 @@ export function assertMipsProbe(
address: string,
): void;
export function assertMipsGlobal(bytes: string, byteOrder: string): void;
export function assertMipsSoftFloatStorage(
boundaries: Record<string, unknown>,
byteOrder: string,
): void;
+83 -3
View File
@@ -15,8 +15,17 @@ const wordHex = (value, byteOrder) => {
return bytes.toString("hex");
};
const SOFT_FLOAT_SYMBOLS = [
"rea_mips_soft_pick",
"rea_mips_soft_wide",
"rea_mips_soft_mixed",
"rea_mips_soft_struct",
"rea_mips_soft_caller",
];
/** Parse only the documented GNU readelf fields needed by our tiny fixture. */
export function parseMipsReadelf(text, byteOrder) {
export function parseMipsReadelf(text, byteOrder, floatAbi = "hard") {
assert.ok(floatAbi === "hard" || floatAbi === "soft");
language(byteOrder);
const field = (expression, label) => {
const matches = [...text.matchAll(expression)];
@@ -44,8 +53,14 @@ export function parseMipsReadelf(text, byteOrder) {
const fpAbi = new Map([
["Hard float (double precision)", 1],
["Hard float (32-bit CPU, Any FPU)", 5],
["Soft float", 3],
]).get(fp);
assert.ok(fpAbi !== undefined, `Unrecognized fixture FP ABI: ${fp}`);
assert.equal(
fpAbi === 3,
floatAbi === "soft",
`Fixture FP ABI ${fp} differs from the ${floatAbi}-float build intent`,
);
assert.equal(field(/^ISA Extension:\s*(.+)$/gmu, "ISA extension"), "None");
assert.equal(field(/^ASEs:\s*\n[ \t]*(.+)$/gmu, "ASEs"), "None");
const abiFlags = {
@@ -76,6 +91,7 @@ export function parseMipsReadelf(text, byteOrder) {
"rea_mips_probe",
"rea_mips_global",
"rea_mips_marker",
...(floatAbi === "soft" ? SOFT_FLOAT_SYMBOLS : []),
]) {
const value = field(
new RegExp(
@@ -93,7 +109,7 @@ export function parseMipsReadelf(text, byteOrder) {
}
/** Independent reader is a bounded test prerequisite, never an REA provider. */
export async function inspectMipsReadelf(path, byteOrder) {
export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
const command = process.env.REA_MIPS_READELF ?? "readelf";
const options = {
env: { ...process.env, LC_ALL: "C" },
@@ -117,7 +133,7 @@ export async function inspectMipsReadelf(path, byteOrder) {
`readelf diagnostics: ${result.stderr}`,
);
return {
...parseMipsReadelf(result.stdout, byteOrder),
...parseMipsReadelf(result.stdout, byteOrder, floatAbi),
command,
version: version.stdout.split("\n")[0],
raw: result.stdout,
@@ -178,3 +194,67 @@ export function assertMipsProbe(move, branch, byteOrder, address) {
export function assertMipsGlobal(bytes, byteOrder) {
assert.equal(bytes, wordHex(7, byteOrder));
}
/**
* Soft-float o32 places float/double values exactly where a same-size integer
* goes: Ghidra joins register pairs most-significant word first. The struct
* return keeps its hidden $a0 pointer and its float in $a1.
*/
export function assertMipsSoftFloatStorage(boundaries, byteOrder) {
language(byteOrder);
const pair = (low, high) =>
byteOrder === "little" ? `${high}:4,${low}:4` : `${low}:4,${high}:4`;
const expected = {
rea_mips_soft_pick: {
return: ["float", "v0:4"],
parameters: [
["a", "float", "a0:4"],
["b", "float", "a1:4"],
["sel", "int", "a2:4"],
],
},
rea_mips_soft_wide: {
return: ["double", pair("v0", "v1")],
parameters: [
["a", "double", pair("a0", "a1")],
["sel", "int", "a2:4"],
],
},
rea_mips_soft_mixed: {
return: ["double", pair("v0", "v1")],
parameters: [
["i", "int", "a0:4"],
["d", "double", pair("a2", "a3")],
["f", "float", "Stack[0x10]:4"],
["e", "double", "Stack[0x18]:8"],
],
},
rea_mips_soft_struct: {
return: ["rea_mips_soft_pair *", "v0:4"],
parameters: [
["__return_storage_ptr__", "rea_mips_soft_pair *", "a0:4 (auto)"],
["scale", "float", "a1:4"],
["sel", "int", "a2:4"],
],
},
};
assert.deepEqual(
Object.keys(boundaries).sort(),
Object.keys(expected).sort(),
);
for (const [name, want] of Object.entries(expected)) {
const boundary = boundaries[name];
assert.deepEqual(
[boundary.return_type.data_type, boundary.return_type.storage],
want.return,
`${name} return storage`,
);
assert.deepEqual(
[...boundary.parameters]
.sort((left, right) => left.ordinal - right.ordinal)
.map((item) => [item.name, item.data_type, item.storage]),
want.parameters,
`${name} parameter storage`,
);
}
}
@@ -3,6 +3,7 @@ import {
assertMipsGlobal,
assertMipsLoadedImage,
assertMipsProbe,
assertMipsSoftFloatStorage,
parseMipsReadelf,
} from "./mips-checks.mjs";
@@ -227,3 +228,117 @@ describe("MIPS real-verifier independent expectations", () => {
expect(() => assertMipsGlobal("0700", "little")).toThrow();
});
});
describe("MIPS soft-float real-verifier expectations", () => {
const softReadelf = readelf
.replace("CPR1 size: 32", "CPR1 size: 0")
.replace("Hard float (32-bit CPU, Any FPU)", "Soft float")
.replace(
" 8: 00020240 32 FUNC GLOBAL DEFAULT 4 rea_mips_probe\n",
[
" 8: 00020240 32 FUNC GLOBAL DEFAULT 4 rea_mips_probe",
" 9: 00020300 64 FUNC GLOBAL DEFAULT 4 rea_mips_soft_pick",
" 10: 00020340 64 FUNC GLOBAL DEFAULT 4 rea_mips_soft_wide",
" 11: 00020380 96 FUNC GLOBAL DEFAULT 4 rea_mips_soft_mixed",
" 12: 000203e0 96 FUNC GLOBAL DEFAULT 4 rea_mips_soft_struct",
" 13: 00020440 160 FUNC GLOBAL DEFAULT 4 rea_mips_soft_caller",
"",
].join("\n"),
);
it("reads the soft-float ABI record and its fixture symbols", () => {
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
});
it("rejects a float ABI that differs from the build intent", () => {
expect(() => parseMipsReadelf(softReadelf, "little", "hard")).toThrow(
/build intent/u,
);
expect(() => parseMipsReadelf(readelf, "little", "soft")).toThrow(
/build intent/u,
);
});
const boundaries = (byteOrder: "little" | "big") => {
const pair = (low: string, high: string) =>
byteOrder === "little" ? `${high}:4,${low}:4` : `${low}:4,${high}:4`;
const item = (
ordinal: number,
name: string,
dataType: string,
storage: string,
) => ({ ordinal, name, data_type: dataType, storage });
return {
rea_mips_soft_pick: {
return_type: { data_type: "float", storage: "v0:4" },
parameters: [
item(2, "sel", "int", "a2:4"),
item(0, "a", "float", "a0:4"),
item(1, "b", "float", "a1:4"),
],
},
rea_mips_soft_wide: {
return_type: { data_type: "double", storage: pair("v0", "v1") },
parameters: [
item(0, "a", "double", pair("a0", "a1")),
item(1, "sel", "int", "a2:4"),
],
},
rea_mips_soft_mixed: {
return_type: { data_type: "double", storage: pair("v0", "v1") },
parameters: [
item(0, "i", "int", "a0:4"),
item(1, "d", "double", pair("a2", "a3")),
item(2, "f", "float", "Stack[0x10]:4"),
item(3, "e", "double", "Stack[0x18]:8"),
],
},
rea_mips_soft_struct: {
return_type: { data_type: "rea_mips_soft_pair *", storage: "v0:4" },
parameters: [
item(
0,
"__return_storage_ptr__",
"rea_mips_soft_pair *",
"a0:4 (auto)",
),
item(1, "scale", "float", "a1:4"),
item(2, "sel", "int", "a2:4"),
],
},
};
};
it("checks soft-float o32 storage, including pair order, alignment and spill", () => {
assertMipsSoftFloatStorage(boundaries("little"), "little");
assertMipsSoftFloatStorage(boundaries("big"), "big");
expect(() =>
assertMipsSoftFloatStorage(boundaries("big"), "little"),
).toThrow();
const hardFloat = boundaries("little");
const pickA = hardFloat.rea_mips_soft_pick.parameters.find(
(item) => item.ordinal === 0,
);
if (pickA === undefined) throw new Error("fixture lacks pick parameter a");
pickA.storage = "f12:4";
expect(() => assertMipsSoftFloatStorage(hardFloat, "little")).toThrow();
const unaligned = boundaries("little");
const mixedD = unaligned.rea_mips_soft_mixed.parameters.find(
(item) => item.ordinal === 1,
);
if (mixedD === undefined)
throw new Error("fixture lacks mixed parameter d");
mixedD.storage = "a2:4,a1:4";
expect(() => assertMipsSoftFloatStorage(unaligned, "little")).toThrow();
const lostHiddenReturn = boundaries("little");
lostHiddenReturn.rea_mips_soft_struct.parameters = [
{ ordinal: 0, name: "scale", data_type: "float", storage: "a0:4" },
{ ordinal: 1, name: "sel", data_type: "int", storage: "a1:4" },
];
expect(() =>
assertMipsSoftFloatStorage(lostHiddenReturn, "little"),
).toThrow();
});
});
+22 -2
View File
@@ -8,18 +8,29 @@ import { promisify } from "node:util";
const execute = promisify(execFile);
/** Build a source-owned freestanding ELF; never execute its target code. */
export async function buildMipsFixture(directory, byteOrder) {
export async function buildMipsFixture(
directory,
byteOrder,
floatAbi = "hard",
) {
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 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(
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}.elf`);
const path = join(directory, `${target}${soft ? "-soft-float" : ""}.elf`);
const args = [
`--target=${target}`,
"-march=mips32r2",
"-mabi=32",
...(soft ? ["-msoft-float"] : []),
"-mno-abicalls",
"-fno-pic",
"-fuse-ld=lld",
@@ -30,6 +41,7 @@ export async function buildMipsFixture(directory, byteOrder) {
"-Wl,-e,rea_mips_entry",
"-Wl,--build-id=none",
source,
...(soft ? [softSource] : []),
probe,
"-o",
path,
@@ -60,11 +72,19 @@ export async function buildMipsFixture(directory, byteOrder) {
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"),