feat(ghidra): admit verified MIPS16 shared ELF images
Admit the verified ELF32 o32 MIPS32r2 MIPS16 shared-image profile after complete source load and relocation inspection. Preserve caller-selected bytes, pin the endian-matched language, and report per-address decoder context. Refuse unverified relocation shapes with explicit reasons. Verified on Linux x64 with Ghidra 12.1.4: eight source-owned public CLI/MCP controls, original-artifact instruction/pseudocode/export replay, snapshot restore and owned cleanup. 152 integrated focused tests, static checks and documentation checks passed. Fixes #1851 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
parent
6395e8d300
commit
e9fe6437a7
|
|
@ -908,6 +908,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint())));
|
||||
result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString());
|
||||
result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant());
|
||||
if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")) {
|
||||
JsonArray decoderContext = new JsonArray();
|
||||
var modeRegister = currentProgram.getRegister("ISA_MODE");
|
||||
if (modeRegister != null) {
|
||||
var modeValue = currentProgram.getProgramContext().getValue(modeRegister, address, false);
|
||||
JsonObject context = new JsonObject();
|
||||
context.addProperty("register", modeRegister.getName());
|
||||
context.add("value", modeValue == null ? JsonNull.INSTANCE : GSON.toJsonTree(modeValue.toString()));
|
||||
context.addProperty("source", "ghidra-program-context");
|
||||
decoderContext.add(context);
|
||||
}
|
||||
result.add("decoder_context", decoderContext);
|
||||
}
|
||||
JsonArray operands = new JsonArray();
|
||||
JsonArray references = new JsonArray();
|
||||
JsonArray destinations = new JsonArray();
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ PE, managed and Mach-O architecture sets are unchanged.
|
|||
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
|
||||
conforms to that ISA. ELF64, n32/n64, unspecified ABI, microMIPS,
|
||||
machine-specific variants and PSP PRX are not covered by this standard-instruction
|
||||
lane. MIPS16 shared images have the separate bounded profile below; other shared
|
||||
or compressed images remain unsupported. Their family may be identified, but provider selection returns the
|
||||
specific unsupported constraint rather than guessing a compatible profile.
|
||||
|
||||
Explicit `EF_MIPS_FP64` and `EF_MIPS_NAN2008` declarations are refused. Their
|
||||
|
|
@ -49,6 +50,63 @@ 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.
|
||||
|
||||
### MIPS16 shared images
|
||||
|
||||
A separate profile admits ELF32 `ET_DYN`, either byte order, declaring explicit
|
||||
o32, MIPS32r2 and exactly the MIPS16 ASE. Machine variants, n32, FP64 and NAN2008
|
||||
remain refused. Source inspection must examine the complete dynamic relocation
|
||||
declarations through uniquely mapped file-backed `PT_LOAD` ranges. This profile
|
||||
verifies `R_MIPS_NONE` and symbol-index-zero `R_MIPS_REL32`; section declarations
|
||||
must describe that same dynamic table. Unavailable section headers and additional
|
||||
section relocation tables (including `--emit-relocs` output) remain unsupported.
|
||||
Symbol-bearing REL32,
|
||||
other relocation types, RELA and PLT declarations remain unsupported. A real
|
||||
negative control shows that Ghidra can normalize a symbol-bearing MIPS16 function
|
||||
pointer to an even address, losing the source mode tag. REA refuses that shape
|
||||
rather than rewriting the source or silently repairing the analysis result.
|
||||
|
||||
The profile pins the endian-matched `MIPS:<LE|BE>:32:default` processor and default
|
||||
compiler specification, and checks the bridge-reported language after import.
|
||||
The generic processor preserves mixed MIPS32/MIPS16 code through per-address
|
||||
`ISA_MODE` context; a global `16e` processor would impose one mode across the
|
||||
image. A caller-selected incompatible processor or compiler specification is
|
||||
explicitly refused without replacing the requested choice.
|
||||
|
||||
An observed ABI flags record must agree with the header and MIPS16 ASE, and
|
||||
must declare the same verified floating-point combinations as the standard
|
||||
lane. With no observed record, every ABI declaration table must have been
|
||||
completely examined. The header still declares o32, but floating-point ABI
|
||||
semantics remain unknown and are stated in Evidence; selecting the default
|
||||
compiler specification does not establish floating-point storage. Partial
|
||||
section tables do not prove record absence. The existing soft-float prototype
|
||||
policy applies when the source explicitly declares soft-float.
|
||||
|
||||
Returned addresses are analysis coordinates, not runtime load addresses.
|
||||
`inspect_native_load_image` preserves original relocation bytes, current loaded
|
||||
memory bytes and provider status. External GOT bindings remain unverified
|
||||
without their defining images. The provider's `modified_sha256` describes its
|
||||
ephemeral relocated image and can differ from `original_sha256`; it does not
|
||||
mean that the caller's artifact was written. `inspect_native_instruction` retains the program
|
||||
language variant in `mode` and reports actual per-address `ISA_MODE` separately
|
||||
in `decoder_context`, including null when unavailable. Artifact bytes and digests
|
||||
remain unchanged; this workflow does not execute the shared library.
|
||||
|
||||
The optional real-provider check builds eight source-owned GNU controls, covering
|
||||
both byte orders, mixed MIPS32/MIPS16 code, declared and absent ABI records, and
|
||||
the rejected symbol-bearing and additional section relocations. It compares source instruction bytes,
|
||||
function symbols and the local function-pointer tag with actual CLI/MCP results,
|
||||
checks context through a snapshot round trip, and verifies process cleanup.
|
||||
It requires caller-supplied GNU MIPS cross-binutils and Ghidra; no fixture target
|
||||
is executed or patched after linking. Floating-point behavior of compressed
|
||||
functions is not established by these integer controls.
|
||||
|
||||
```bash
|
||||
REA_MIPS_AS=/absolute/path/to/mips-linux-gnu-as \
|
||||
REA_MIPS_LD=/absolute/path/to/mips-linux-gnu-ld \
|
||||
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra \
|
||||
npm run verify:ghidra:mips-shared
|
||||
```
|
||||
|
||||
MIPS header and ABI record facts are committed in the analysis profile so
|
||||
interpretation changes invalidate profile-bound snapshots. The support-lane
|
||||
identity is versioned independently of file identity. Evidence, lifecycle
|
||||
|
|
|
|||
|
|
@ -202,8 +202,14 @@ rea inspect-native-data-type <binary> --address <typed-data-address> --provider
|
|||
Instruction facts include bytes, length, decoder text, ordered register/scalar/
|
||||
address tokens, typed references and flow destinations. Mid-instruction, data,
|
||||
outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
memory base/index/displacement roles remain unavailable. Ghidra reports its program
|
||||
language variant in `mode`; other providers retain their own mode label. Ghidra
|
||||
MIPS results additionally expose `decoder_context`
|
||||
with the observed `ISA_MODE` register at the queried address and its provider
|
||||
source. A null value is unavailable, not mode zero; an empty array reports no
|
||||
available context register, and omission preserves older captures or providers
|
||||
that do not expose this facet. The context is an analysis observation, not proof
|
||||
of a runtime processor state.
|
||||
|
||||
`inspect_native_instruction` also returns `incoming_references` to the exact
|
||||
queried address, including data, interior-instruction and undecodable addresses.
|
||||
|
|
|
|||
|
|
@ -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-shared": "npm run build:cached && node scripts/verify-real-ghidra-mips-shared.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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,338 @@
|
|||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
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 { buildMips16SharedFixture } from "../tests/conformance/ghidra/mips16-shared-fixture.mjs";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
requireMcpToolError,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
if (
|
||||
process.env.GHIDRA_INSTALL_DIR === undefined ||
|
||||
!["linux", "darwin"].includes(process.platform)
|
||||
)
|
||||
throw new Error(
|
||||
"This optional lane requires BYO Ghidra on Linux/macOS and GNU MIPS cross-binutils.",
|
||||
);
|
||||
const execute = promisify(execFile);
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-mips-shared-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: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
||||
};
|
||||
const reports = [];
|
||||
let failure;
|
||||
try {
|
||||
for (const byteOrder of ["big", "little"]) {
|
||||
for (const variant of [
|
||||
"declared",
|
||||
"recordless",
|
||||
"symbol-relative",
|
||||
"section-relocations",
|
||||
]) {
|
||||
const fixture = await buildMips16SharedFixture(
|
||||
workspace,
|
||||
byteOrder,
|
||||
variant,
|
||||
);
|
||||
const resolved = await parseBinaryTarget(fixture.path);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
assert.equal(resolved.value.architecture, "mips");
|
||||
if (variant === "section-relocations") {
|
||||
assert.equal(resolved.value.mips.sharedLoad.status, "unavailable");
|
||||
assert.match(
|
||||
resolved.value.mips.sharedLoad.reason,
|
||||
/section relocation declarations/u,
|
||||
);
|
||||
} else {
|
||||
assert.equal(resolved.value.mips.sharedLoad.status, "complete");
|
||||
assert.deepEqual(
|
||||
resolved.value.mips.sharedLoad.relocationTypes,
|
||||
[0, 3],
|
||||
);
|
||||
assert.equal(
|
||||
resolved.value.mips.sharedLoad.symbolRelativeCount,
|
||||
variant === "symbol-relative" ? 1 : 0,
|
||||
);
|
||||
}
|
||||
if (variant === "recordless")
|
||||
assert.equal(resolved.value.mips.abiFlags, null);
|
||||
else {
|
||||
assert.equal(resolved.value.mips.abiFlags.fpAbi, 5);
|
||||
assert.equal(resolved.value.mips.abiFlags.ases, 0x400);
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["scripts/rea.mjs", "mcp"],
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "rea-mips16-shared-conformance",
|
||||
version: "1",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-262144);
|
||||
});
|
||||
let calls = 0;
|
||||
const reply = async (name, args = {}) => {
|
||||
const response = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
await writeFile(
|
||||
join(workspace, `${byteOrder}-${variant}-${++calls}-${name}.json`),
|
||||
JSON.stringify(response, null, 2),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
const call = async (name, args) =>
|
||||
requireMcpOperationResult(await reply(name, args), name);
|
||||
const original = await readFile(fixture.path);
|
||||
try {
|
||||
await client.connect(transport);
|
||||
await client.listTools();
|
||||
if (
|
||||
variant === "symbol-relative" ||
|
||||
variant === "section-relocations"
|
||||
) {
|
||||
const refusal = requireMcpToolError(
|
||||
await reply("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
}),
|
||||
);
|
||||
assert.equal(refusal.category, "unsupported_provider");
|
||||
assert.match(
|
||||
JSON.stringify(refusal),
|
||||
variant === "symbol-relative"
|
||||
? /Symbol-bearing R_MIPS_REL32/u
|
||||
: /section relocation declarations/u,
|
||||
);
|
||||
assert.deepEqual(await readFile(fixture.path), original);
|
||||
await client.ping();
|
||||
reports.push({ ...fixture, status: "refused", ping: true });
|
||||
continue;
|
||||
}
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
const loadedReply = await reply("inspect_native_load_image");
|
||||
const loaded = requireMcpOperationResult(
|
||||
loadedReply,
|
||||
"inspect_native_load_image",
|
||||
);
|
||||
const evidence = parseEvidence(
|
||||
JSON.parse(
|
||||
loadedReply.content.find((item) => item.type === "text").text,
|
||||
),
|
||||
);
|
||||
const language = `MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`;
|
||||
assert.equal(loaded.observations.language_id, language);
|
||||
assert.equal(loaded.observations.compiler_spec_id, "default");
|
||||
assert(
|
||||
loaded.observations.source_files.some(
|
||||
(file) => file.original_sha256 === fixture.sha256,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
evidence.limitations.some((item) =>
|
||||
item.includes("External GOT bindings"),
|
||||
),
|
||||
);
|
||||
if (variant === "recordless")
|
||||
assert(
|
||||
evidence.limitations.some((item) =>
|
||||
item.includes("does not establish the floating-point ABI"),
|
||||
),
|
||||
);
|
||||
const base = BigInt(loaded.observations.image_base);
|
||||
const address = (source, clearTag = false) =>
|
||||
`0x${(base + (clearTag ? BigInt(source) & ~1n : BigInt(source))).toString(16)}`;
|
||||
const compressed = address(fixture.symbols.rea_mips16_probe, true);
|
||||
const standard = address(fixture.symbols.rea_mips32_probe);
|
||||
const procedures = await call("list_procedures");
|
||||
assert(
|
||||
procedures.some(
|
||||
(item) =>
|
||||
item.address === compressed && item.value === "rea_mips16_probe",
|
||||
),
|
||||
);
|
||||
assert(
|
||||
procedures.some(
|
||||
(item) =>
|
||||
item.address === standard && item.value === "rea_mips32_probe",
|
||||
),
|
||||
);
|
||||
let firstInstruction;
|
||||
const expected =
|
||||
byteOrder === "little"
|
||||
? ["5a6a", "8eea", "074a", "a0e8"]
|
||||
: ["6a5a", "ea8e", "4a07", "e8a0"];
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: `0x${(BigInt(compressed) + BigInt(2 * i)).toString(16)}`,
|
||||
});
|
||||
assert.equal(instruction.status, "decoded");
|
||||
assert.equal(instruction.mode, "default");
|
||||
assert.equal(instruction.length, 2);
|
||||
assert.equal(instruction.bytes, expected[i]);
|
||||
assert.deepEqual(instruction.decoder_context, [
|
||||
{
|
||||
register: "ISA_MODE",
|
||||
value: "1",
|
||||
source: "ghidra-program-context",
|
||||
},
|
||||
]);
|
||||
if (i === 0) firstInstruction = instruction;
|
||||
}
|
||||
const standardInstruction = await call("inspect_native_instruction", {
|
||||
address: standard,
|
||||
});
|
||||
assert.equal(standardInstruction.length, 4);
|
||||
assert.equal(
|
||||
standardInstruction.bytes,
|
||||
byteOrder === "little" ? "03008224" : "24820003",
|
||||
);
|
||||
assert.equal(
|
||||
standardInstruction.decoder_context[0].register,
|
||||
"ISA_MODE",
|
||||
);
|
||||
assert(
|
||||
[null, "0"].includes(standardInstruction.decoder_context[0].value),
|
||||
);
|
||||
const pointer = await call("read_bytes", {
|
||||
address: address(fixture.symbols.rea_mips16_pointer),
|
||||
length: 4,
|
||||
});
|
||||
const pointerBytes = Buffer.from(pointer.bytes_hex, "hex");
|
||||
const pointerValue =
|
||||
byteOrder === "little"
|
||||
? pointerBytes.readUInt32LE(0)
|
||||
: pointerBytes.readUInt32BE(0);
|
||||
assert.equal(
|
||||
BigInt(pointerValue),
|
||||
base + BigInt(fixture.symbols.rea_mips16_probe),
|
||||
);
|
||||
const dossier = await call("analyze_function", {
|
||||
procedure: compressed,
|
||||
});
|
||||
assert.match(dossier.pseudocode, /\^\s*0x5a/u);
|
||||
assert.match(dossier.pseudocode, /\+\s*(?:7|0x7)\b/u);
|
||||
const standardDossier = await call("analyze_function", {
|
||||
procedure: standard,
|
||||
});
|
||||
assert.match(standardDossier.pseudocode, /\+\s*(?:3|0x3)\b/u);
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${variant}.snapshot.json`,
|
||||
);
|
||||
await call("close_binary", { snapshot_path: snapshotPath });
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.binding.analysis_profile.parameters.language_selection,
|
||||
"provider-pinned-v1",
|
||||
);
|
||||
assert.equal(
|
||||
snapshot.binding.analysis_profile.parameters.language_id,
|
||||
language,
|
||||
);
|
||||
await call("open_binary", {
|
||||
path: fixture.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.deepEqual(
|
||||
await call("inspect_native_instruction", { address: compressed }),
|
||||
firstInstruction,
|
||||
);
|
||||
await call("close_binary");
|
||||
await client.ping();
|
||||
const cli = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
"scripts/rea.mjs",
|
||||
"inspect-native-instruction",
|
||||
fixture.path,
|
||||
compressed,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{ env, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
parseEvidence(JSON.parse(cli.stdout)).normalized_result,
|
||||
firstInstruction,
|
||||
);
|
||||
assert.deepEqual(await readFile(fixture.path), original);
|
||||
reports.push({
|
||||
...fixture,
|
||||
status: "passed",
|
||||
language_id: language,
|
||||
mixed_mode: true,
|
||||
standard_decoder_context: standardInstruction.decoder_context,
|
||||
pointer_tag_preserved: true,
|
||||
snapshot_context_preserved: true,
|
||||
cli_mcp_parity: true,
|
||||
source_unchanged: true,
|
||||
ping: true,
|
||||
});
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await writeFile(
|
||||
join(workspace, `${byteOrder}-${variant}.stderr`),
|
||||
stderr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
}
|
||||
const completed = await completeVerifierRun(run);
|
||||
if (
|
||||
completed.process_lineage.status !== "verified" ||
|
||||
completed.process_lineage.descendants.length !== 0
|
||||
)
|
||||
throw new Error(`Cleanup unverified; retained workspace: ${workspace}`, {
|
||||
cause: failure,
|
||||
});
|
||||
if (failure !== undefined)
|
||||
throw new Error(
|
||||
`MIPS16 shared verification failed; retained workspace: ${workspace}`,
|
||||
{ cause: failure },
|
||||
);
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
console.log(JSON.stringify({ ...completed, fixtures: reports }, null, 2));
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { readMipsElfAbiFlags } from "./MipsElfAbiFlags.js";
|
||||
import { readMipsElfMetadata } from "./MipsElfMetadata.js";
|
||||
import {
|
||||
resolveAppBundleExecutable,
|
||||
type ResolvedAppBundle,
|
||||
|
|
@ -430,18 +430,23 @@ const readExecutableMetadata = async (
|
|||
parsed.value.mips.elfClass !== 32
|
||||
)
|
||||
return parsed;
|
||||
const abiFlags = await readMipsElfAbiFlags(handle, parsed.value.mips, () =>
|
||||
throwIfTargetResolutionCancelled(signal),
|
||||
const declarations = await readMipsElfMetadata(
|
||||
handle,
|
||||
parsed.value.mips,
|
||||
() => throwIfTargetResolutionCancelled(signal),
|
||||
);
|
||||
if (!abiFlags.ok) return abiFlags;
|
||||
if (!declarations.ok) return declarations;
|
||||
return ok({
|
||||
...parsed.value,
|
||||
mips: {
|
||||
...parsed.value.mips,
|
||||
abiFlags: abiFlags.value.abiFlags,
|
||||
...(abiFlags.value.limitations.length === 0
|
||||
abiFlags: declarations.value.abiFlags,
|
||||
...(declarations.value.sharedLoad === undefined
|
||||
? {}
|
||||
: { limitations: abiFlags.value.limitations }),
|
||||
: { sharedLoad: declarations.value.sharedLoad }),
|
||||
...(declarations.value.limitations.length === 0
|
||||
? {}
|
||||
: { limitations: declarations.value.limitations }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,24 +4,29 @@ import type {
|
|||
MipsElfMetadata,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
readMipsElfSharedLoad,
|
||||
type MipsElfRelocationSection,
|
||||
} from "./MipsElfSharedLoad.js";
|
||||
|
||||
/** Observed ABI declarations and coverage of their source tables. */
|
||||
export interface MipsAbiFlagsInspection {
|
||||
/** Observed ABI and load declarations, retaining unavailable source coverage. */
|
||||
export interface MipsElfInspection {
|
||||
readonly abiFlags: MipsAbiFlags | null;
|
||||
readonly limitations: readonly string[];
|
||||
readonly sharedLoad?: MipsElfMetadata["sharedLoad"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read ELF32 MIPS ABI declarations from the caller's already-open file.
|
||||
* Read ELF32 MIPS ABI and shared-load declarations from the caller's already-open file.
|
||||
* Inspect both typed tables (not section names), including records beyond the
|
||||
* initial header probe. A missing record is unknown, not a default FP ABI.
|
||||
* The caller supplies its cancellation check so its tagged error is preserved.
|
||||
*/
|
||||
export const readMipsElfAbiFlags = async (
|
||||
export const readMipsElfMetadata = async (
|
||||
handle: FileHandle,
|
||||
metadata: MipsElfMetadata,
|
||||
checkCancelled: () => void,
|
||||
): Promise<Result<MipsAbiFlagsInspection, string>> => {
|
||||
): Promise<Result<MipsElfInspection, string>> => {
|
||||
checkCancelled();
|
||||
if (metadata.elfClass !== 32)
|
||||
return err("MIPS ABI record inspection currently supports ELF32 only");
|
||||
|
|
@ -107,6 +112,9 @@ export const readMipsElfAbiFlags = async (
|
|||
if (u16(bytes, 44) === 0xffff || u16(bytes, 48) >= 0xff00)
|
||||
return err("extended or reserved MIPS ELF table numbering is unsupported");
|
||||
let record: Buffer | undefined;
|
||||
let sharedLoad: MipsElfMetadata["sharedLoad"];
|
||||
let programHeaders: Buffer | undefined;
|
||||
const relocationSections: MipsElfRelocationSection[] = [];
|
||||
const limitations: string[] = [];
|
||||
for (const table of tables) {
|
||||
if (table.count === 0) {
|
||||
|
|
@ -135,12 +143,20 @@ export const readMipsElfAbiFlags = async (
|
|||
table.label,
|
||||
);
|
||||
if (!contents.ok) return contents;
|
||||
if (!table.optional && metadata.type === 3) programHeaders = contents.value;
|
||||
let found = false;
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
checkCancelled();
|
||||
const position = i * table.entrySize;
|
||||
if (u32(contents.value, position + table.typeOffset) !== table.recordType)
|
||||
continue;
|
||||
const type = u32(contents.value, position + table.typeOffset);
|
||||
if (metadata.type === 3 && table.optional && (type === 4 || type === 9))
|
||||
relocationSections.push({
|
||||
type,
|
||||
offset: u32(contents.value, position + 16),
|
||||
size: u32(contents.value, position + 20),
|
||||
entrySize: u32(contents.value, position + 36),
|
||||
});
|
||||
if (type !== table.recordType) continue;
|
||||
if (found) return err(`duplicate MIPS ABI records in ${table.label}`);
|
||||
found = true;
|
||||
const offset = u32(contents.value, position + table.dataOffset);
|
||||
|
|
@ -154,7 +170,37 @@ export const readMipsElfAbiFlags = async (
|
|||
record = candidate.value;
|
||||
}
|
||||
}
|
||||
if (record === undefined) return ok({ abiFlags: null, limitations });
|
||||
if (metadata.type === 3 && programHeaders !== undefined) {
|
||||
if (limitations.length > 0)
|
||||
sharedLoad = {
|
||||
status: "unavailable",
|
||||
reason: `MIPS shared-image relocation declaration sources are incomplete. ${limitations.join(" ")}`,
|
||||
};
|
||||
else {
|
||||
const shared = await readMipsElfSharedLoad(
|
||||
programHeaders,
|
||||
little,
|
||||
fileSize,
|
||||
read,
|
||||
checkCancelled,
|
||||
relocationSections,
|
||||
);
|
||||
sharedLoad = shared.ok
|
||||
? shared.value
|
||||
: { status: "unavailable", reason: shared.error };
|
||||
}
|
||||
}
|
||||
const shared =
|
||||
metadata.type === 3
|
||||
? {
|
||||
sharedLoad: sharedLoad ?? {
|
||||
status: "unavailable" as const,
|
||||
reason: "MIPS shared image has no inspected program header table",
|
||||
},
|
||||
}
|
||||
: {};
|
||||
if (record === undefined)
|
||||
return ok({ abiFlags: null, limitations, ...shared });
|
||||
// Preserve declarations verbatim, including unknown versions/values. Provider
|
||||
// admission, not parsing, decides which interpretations have been verified.
|
||||
const abiFlags: MipsAbiFlags = {
|
||||
|
|
@ -170,5 +216,5 @@ export const readMipsElfAbiFlags = async (
|
|||
flags1: u32(record, 16),
|
||||
flags2: u32(record, 20),
|
||||
};
|
||||
return ok({ abiFlags, limitations });
|
||||
return ok({ abiFlags, limitations, ...shared });
|
||||
};
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
import type { MipsElfMetadata } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
type ReadRange = (
|
||||
offset: number,
|
||||
length: number,
|
||||
label: string,
|
||||
) => Promise<Result<Buffer, string>>;
|
||||
|
||||
export interface MipsElfRelocationSection {
|
||||
readonly type: number;
|
||||
readonly offset: number;
|
||||
readonly size: number;
|
||||
readonly entrySize: number;
|
||||
}
|
||||
type SharedLoadFacts = Extract<
|
||||
NonNullable<MipsElfMetadata["sharedLoad"]>,
|
||||
{ status: "complete" }
|
||||
>;
|
||||
|
||||
/** Source ELF facts only; the provider owns interpretation and admission. */
|
||||
export const readMipsElfSharedLoad = async (
|
||||
programHeaders: Buffer,
|
||||
little: boolean,
|
||||
fileSize: number,
|
||||
read: ReadRange,
|
||||
checkCancelled: () => void,
|
||||
sections: readonly MipsElfRelocationSection[],
|
||||
): Promise<Result<SharedLoadFacts, string>> => {
|
||||
const u32 = (bytes: Buffer, offset: number): number =>
|
||||
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
||||
const loads: {
|
||||
offset: number;
|
||||
address: number;
|
||||
size: number;
|
||||
memorySize: number;
|
||||
}[] = [];
|
||||
let dynamic: { offset: number; address: number; size: number } | undefined;
|
||||
for (let position = 0; position < programHeaders.length; position += 32) {
|
||||
checkCancelled();
|
||||
const type = u32(programHeaders, position);
|
||||
const offset = u32(programHeaders, position + 4);
|
||||
const size = u32(programHeaders, position + 16);
|
||||
if (type === 1) {
|
||||
const address = u32(programHeaders, position + 8);
|
||||
const memorySize = u32(programHeaders, position + 20);
|
||||
if (
|
||||
offset > fileSize - size ||
|
||||
size > memorySize ||
|
||||
address + memorySize > 0x100000000
|
||||
)
|
||||
return err("invalid or incomplete MIPS PT_LOAD range");
|
||||
loads.push({ offset, address, size, memorySize });
|
||||
}
|
||||
if (type === 2) {
|
||||
if (dynamic !== undefined) return err("duplicate MIPS PT_DYNAMIC tables");
|
||||
dynamic = { offset, address: u32(programHeaders, position + 8), size };
|
||||
}
|
||||
}
|
||||
if (dynamic === undefined)
|
||||
return err("MIPS shared image has no PT_DYNAMIC table");
|
||||
if (dynamic.size === 0 || dynamic.size % 8 !== 0)
|
||||
return err("invalid MIPS ELF32 dynamic table size");
|
||||
if (dynamic.offset > fileSize - dynamic.size)
|
||||
return err("MIPS PT_DYNAMIC range extends beyond the file");
|
||||
const ranges = loads
|
||||
.filter((load) => load.memorySize > 0)
|
||||
.sort((left, right) => left.address - right.address);
|
||||
for (let i = 1; i < ranges.length; i += 1) {
|
||||
checkCancelled();
|
||||
const previous = ranges[i - 1]!;
|
||||
if (ranges[i]!.address < previous.address + previous.memorySize)
|
||||
return err(
|
||||
"overlapping MIPS PT_LOAD memory ranges require separate mapping verification",
|
||||
);
|
||||
}
|
||||
// One normalized index prevents relocation count * program-header count work.
|
||||
const mapped = (address: number, length: number, fileBacked: boolean) => {
|
||||
let low = 0;
|
||||
let high = ranges.length;
|
||||
while (low < high) {
|
||||
const middle = Math.floor((low + high) / 2);
|
||||
if (ranges[middle]!.address <= address) low = middle + 1;
|
||||
else high = middle;
|
||||
}
|
||||
const range = ranges[low - 1];
|
||||
return range !== undefined &&
|
||||
address - range.address <=
|
||||
(fileBacked ? range.size : range.memorySize) - length
|
||||
? range
|
||||
: undefined;
|
||||
};
|
||||
const dynamicMapping = mapped(dynamic.address, dynamic.size, true);
|
||||
if (
|
||||
dynamicMapping === undefined ||
|
||||
dynamicMapping.offset + dynamic.address - dynamicMapping.address !==
|
||||
dynamic.offset
|
||||
)
|
||||
return err(
|
||||
"MIPS PT_DYNAMIC has no unique matching file-backed PT_LOAD range",
|
||||
);
|
||||
|
||||
// Bound temporary storage, not admitted artifact size. ELF32 records are
|
||||
// exact eight-byte structures; every declared record is examined or refused.
|
||||
const chunkBytes = 64 * 1024;
|
||||
const relevant = new Set([2, 7, 8, 9, 17, 18, 19, 20, 23]);
|
||||
const tags = new Map<number, number>();
|
||||
let terminated = false;
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < dynamic.size && !terminated;
|
||||
offset += chunkBytes
|
||||
) {
|
||||
const chunk = await read(
|
||||
dynamic.offset + offset,
|
||||
Math.min(chunkBytes, dynamic.size - offset),
|
||||
"dynamic table",
|
||||
);
|
||||
if (!chunk.ok) return chunk;
|
||||
for (let position = 0; position < chunk.value.length; position += 8) {
|
||||
checkCancelled();
|
||||
const tag = u32(chunk.value, position);
|
||||
if (tag === 0) {
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
if (!relevant.has(tag)) continue;
|
||||
if (tags.has(tag)) return err(`duplicate MIPS dynamic tag ${tag}`);
|
||||
tags.set(tag, u32(chunk.value, position + 4));
|
||||
}
|
||||
}
|
||||
if (!terminated) return err("MIPS dynamic table has no DT_NULL terminator");
|
||||
if ([2, 7, 8, 9, 23].some((tag) => (tags.get(tag) ?? 0) !== 0))
|
||||
return err(
|
||||
"MIPS shared RELA or PLT relocation declarations require separate verification",
|
||||
);
|
||||
const address = tags.get(17);
|
||||
const size = tags.get(18);
|
||||
const stride = tags.get(19);
|
||||
const checkSections = (
|
||||
offset: number,
|
||||
length: number,
|
||||
): string | undefined => {
|
||||
let observed = 0;
|
||||
for (const section of sections) {
|
||||
checkCancelled();
|
||||
if (section.size === 0) continue;
|
||||
if (
|
||||
section.type !== 9 ||
|
||||
section.entrySize !== 8 ||
|
||||
section.offset !== offset ||
|
||||
section.size !== length
|
||||
)
|
||||
return "MIPS section relocation declarations outside the dynamic REL table require separate verification";
|
||||
observed += 1;
|
||||
}
|
||||
return observed > 1
|
||||
? "duplicate MIPS section relocation declarations require separate verification"
|
||||
: undefined;
|
||||
};
|
||||
const emptyRelocations = (): Result<SharedLoadFacts, string> => {
|
||||
const reason = checkSections(0, 0);
|
||||
return reason === undefined
|
||||
? ok({
|
||||
status: "complete",
|
||||
relocationCount: 0,
|
||||
relocationTypes: [],
|
||||
symbolRelativeCount: 0,
|
||||
})
|
||||
: err(reason);
|
||||
};
|
||||
if (address === undefined && size === undefined && stride === undefined)
|
||||
return emptyRelocations();
|
||||
if (
|
||||
address === undefined ||
|
||||
size === undefined ||
|
||||
stride !== 8 ||
|
||||
size % 8 !== 0
|
||||
)
|
||||
return err(
|
||||
"incomplete or invalid MIPS DT_REL/DT_RELSZ/DT_RELENT declaration",
|
||||
);
|
||||
if (size === 0) return emptyRelocations();
|
||||
const mapping = mapped(address, size, true);
|
||||
if (mapping === undefined)
|
||||
return err(
|
||||
"MIPS DT_REL range has no unique complete file-backed PT_LOAD mapping",
|
||||
);
|
||||
const fileOffset = mapping.offset + address - mapping.address;
|
||||
const sectionReason = checkSections(fileOffset, size);
|
||||
if (sectionReason !== undefined) return err(sectionReason);
|
||||
let symbolRelativeCount = 0;
|
||||
const types = new Set<number>();
|
||||
for (let offset = 0; offset < size; offset += chunkBytes) {
|
||||
const chunk = await read(
|
||||
fileOffset + offset,
|
||||
Math.min(chunkBytes, size - offset),
|
||||
"dynamic REL records",
|
||||
);
|
||||
if (!chunk.ok) return chunk;
|
||||
for (let position = 0; position < chunk.value.length; position += 8) {
|
||||
checkCancelled();
|
||||
const info = u32(chunk.value, position + 4);
|
||||
const type = info & 0xff;
|
||||
const relocationAddress = u32(chunk.value, position);
|
||||
if (type === 3 && mapped(relocationAddress, 4, false) === undefined)
|
||||
return err(
|
||||
"MIPS REL32 destination has no unique complete PT_LOAD memory range",
|
||||
);
|
||||
types.add(type);
|
||||
if (type === 3 && info >>> 8 !== 0) symbolRelativeCount += 1;
|
||||
}
|
||||
}
|
||||
return ok({
|
||||
status: "complete",
|
||||
relocationCount: size / 8,
|
||||
relocationTypes: [...types].sort((left, right) => left - right),
|
||||
symbolRelativeCount,
|
||||
});
|
||||
};
|
||||
|
|
@ -18,6 +18,15 @@ export interface MipsElfMetadata {
|
|||
readonly abiFlags?: MipsAbiFlags | null;
|
||||
/** Unavailable declaration sources; missing records there remain unknown. */
|
||||
readonly limitations?: readonly string[];
|
||||
/** Complete dynamic REL declarations, or the reason they could not be inspected. */
|
||||
readonly sharedLoad?:
|
||||
| {
|
||||
readonly status: "complete";
|
||||
readonly relocationCount: number;
|
||||
readonly relocationTypes: readonly number[];
|
||||
readonly symbolRelativeCount: number;
|
||||
}
|
||||
| { readonly status: "unavailable"; readonly reason: string };
|
||||
}
|
||||
|
||||
/** Declared fields of the 24-byte MIPS ABI flags record, not inferred semantics. */
|
||||
|
|
|
|||
|
|
@ -53,7 +53,23 @@ export const nativeInstructionSchema = z.strictObject({
|
|||
]),
|
||||
procedure: z.string().nullable(),
|
||||
architecture: z.string(),
|
||||
mode: z.string(),
|
||||
mode: z
|
||||
.string()
|
||||
.describe(
|
||||
"Provider-reported mode label. Per-address decoder context, when available, is reported separately; a program-wide label does not establish runtime encoding mode.",
|
||||
),
|
||||
decoder_context: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
register: z.string().min(1),
|
||||
value: z.string().min(1).nullable(),
|
||||
source: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"Observed per-address decoder context. Null means no value was available; omission means the provider or older capture does not expose this facet. The separate mode field retains the program language variant.",
|
||||
),
|
||||
bytes: z.string().nullable(),
|
||||
length: z.number().int().positive().nullable(),
|
||||
mnemonic: z.string().nullable(),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { GhidraInstallationInspection } from "./GhidraInstallation.js";
|
||||
import { ghidraMipsProfileParameters } from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
ghidraMipsProfileParameters,
|
||||
ghidraMipsSharedLanguageOverride,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import { ghidraRawBinaryLanguage } from "./GhidraRawBinaryLanguage.js";
|
||||
|
||||
import {
|
||||
|
|
@ -132,6 +135,22 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
const dosMz = target.format === "dos-mz";
|
||||
const dosCom = target.format === "dos-com";
|
||||
const dos = dosMz || dosCom;
|
||||
const mipsLanguage = ghidraMipsSharedLanguageOverride(target);
|
||||
if (
|
||||
mipsLanguage !== undefined &&
|
||||
languageOverride !== undefined &&
|
||||
(languageOverride.languageId !== mipsLanguage.languageId ||
|
||||
(languageOverride.compilerSpecId !== undefined &&
|
||||
languageOverride.compilerSpecId !== mipsLanguage.compilerSpecId))
|
||||
)
|
||||
return err(
|
||||
new AnalysisUnsupportedTargetError(
|
||||
"resolve_analysis_profile",
|
||||
target.path,
|
||||
"The caller-selected processor or compiler specification is outside the verified endian-matched MIPS32 shared-image profile. The requested override has not been changed.",
|
||||
),
|
||||
);
|
||||
const selectedLanguage = languageOverride ?? mipsLanguage;
|
||||
// DOS imports already force an admitted loader, language, and context.
|
||||
if (dos && languageOverride !== undefined)
|
||||
return Promise.resolve(
|
||||
|
|
@ -199,14 +218,16 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
: "auto-from-header",
|
||||
language_id: dos
|
||||
? "x86:LE:16:Real Mode"
|
||||
: (languageOverride?.languageId ?? "auto-from-header"),
|
||||
: (selectedLanguage?.languageId ?? "auto-from-header"),
|
||||
compiler_spec_id: dos
|
||||
? "default"
|
||||
: languageOverride === undefined
|
||||
: selectedLanguage === undefined
|
||||
? "auto-default"
|
||||
: (languageOverride.compilerSpecId ?? "language-default"),
|
||||
: (selectedLanguage.compilerSpecId ?? "language-default"),
|
||||
...(languageOverride === undefined
|
||||
? {}
|
||||
? mipsLanguage === undefined
|
||||
? {}
|
||||
: { language_selection: "provider-pinned-v1" }
|
||||
: { language_selection: "configured-v1" }),
|
||||
...(dos
|
||||
? {
|
||||
|
|
@ -246,7 +267,11 @@ export const resolveGhidraAnalysisProfile = async (
|
|||
export const ghidraProfileLanguageOverride = (
|
||||
parameters: Readonly<Record<string, unknown>>,
|
||||
): GhidraLanguageOverride | undefined => {
|
||||
if (parameters.language_selection !== "configured-v1") return undefined;
|
||||
if (
|
||||
parameters.language_selection !== "configured-v1" &&
|
||||
parameters.language_selection !== "provider-pinned-v1"
|
||||
)
|
||||
return undefined;
|
||||
const languageId = parameters.language_id;
|
||||
const compilerSpecId = parameters.compiler_spec_id;
|
||||
if (typeof languageId !== "string" || typeof compilerSpecId !== "string")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { GhidraProvider } from "./GhidraProvider.js";
|
|||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import type { GhidraMipsSoftFloatReport } from "./GhidraMipsProfile.js";
|
||||
import { ghidraProfileLanguageOverride } from "./GhidraAnalysisProfile.js";
|
||||
|
||||
// Fake installation inspection tests admission/profile contracts, not Ghidra.
|
||||
const host: GhidraInstallationHost = {
|
||||
|
|
@ -217,6 +218,130 @@ const session = async (
|
|||
};
|
||||
};
|
||||
|
||||
describe("MIPS16 shared-image profile", () => {
|
||||
const shared = (change: Partial<MipsElfMetadata> = {}) =>
|
||||
target({
|
||||
type: 3,
|
||||
flags: 0x74001005,
|
||||
abiFlags: null,
|
||||
sharedLoad: {
|
||||
status: "complete",
|
||||
relocationCount: 14,
|
||||
relocationTypes: [0, 3],
|
||||
symbolRelativeCount: 0,
|
||||
},
|
||||
...change,
|
||||
});
|
||||
it.each(["little", "big"] as const)(
|
||||
"pins a generic 32-bit %s language and retains the source ABI uncertainty",
|
||||
async (byteOrder) => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
const value = shared({ byteOrder });
|
||||
expect(ghidra.inspectTargetSupport(value).status).toBe("supported");
|
||||
const resolved = await ghidra.resolveAnalysisProfile(value);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected shared profile");
|
||||
const parameters = resolved.value.profile.parameters;
|
||||
expect(parameters).toMatchObject({
|
||||
language_id: `MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
compiler_spec_id: "default",
|
||||
language_selection: "provider-pinned-v1",
|
||||
mips_support_lane: "elf32-dyn-o32-arch32r2-mips16-local-rel32-v1",
|
||||
mips_elf: { abi_flags: null },
|
||||
});
|
||||
expect(ghidraProfileLanguageOverride(parameters)).toEqual({
|
||||
languageId: parameters.language_id,
|
||||
compilerSpecId: "default",
|
||||
});
|
||||
},
|
||||
);
|
||||
it("retains shared-image and unknown floating-point limitations on the session", async () => {
|
||||
const { client } = await session(shared(), undefined);
|
||||
const health = await client.execute("health", {});
|
||||
if (!health.ok) throw health.error;
|
||||
expect(health.value.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("External GOT bindings"),
|
||||
expect.stringContaining("does not establish the floating-point ABI"),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("continues to admit a declared FPXX record only when its MIPS16 ASE agrees", () => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(
|
||||
ghidra.inspectTargetSupport(shared(abi({ ases: 0x400 }))).status,
|
||||
).toBe("supported");
|
||||
expect(ghidra.inspectTargetSupport(shared(abi({ ases: 0 }))).status).toBe(
|
||||
"unsupported",
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
{ flags: 0x74001045 },
|
||||
{ limitations: ["Unexamined section declarations"] },
|
||||
{
|
||||
sharedLoad: {
|
||||
status: "complete",
|
||||
relocationCount: 1,
|
||||
relocationTypes: [3],
|
||||
symbolRelativeCount: 1,
|
||||
},
|
||||
},
|
||||
] satisfies Partial<MipsElfMetadata>[])(
|
||||
"refuses incomplete or unverified source declarations %j",
|
||||
(change) => {
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(ghidra.inspectTargetSupport(shared(change)).status).toBe(
|
||||
"unsupported",
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each(["sharedLoad", "abiFlags"] as const)(
|
||||
"refuses an uninspected %s declaration",
|
||||
(declaration) => {
|
||||
const value = shared();
|
||||
if (value.kind !== "executable" || value.architecture !== "mips")
|
||||
throw new Error("expected MIPS target");
|
||||
const facts = { ...value.mips };
|
||||
delete facts[declaration];
|
||||
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
|
||||
expect(
|
||||
ghidra.inspectTargetSupport({ ...value, mips: facts }),
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining(
|
||||
declaration === "abiFlags"
|
||||
? "inspected ABI flags"
|
||||
: "have not been inspected",
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
it("refuses an endian pin committed for a different target", async () => {
|
||||
const { counts, client } = await session(
|
||||
shared({ byteOrder: "big" }),
|
||||
undefined,
|
||||
shared(),
|
||||
);
|
||||
const health = await client.execute("health", {});
|
||||
expect(health.ok).toBe(false);
|
||||
expect(counts.starts).toBe(0);
|
||||
});
|
||||
it("rejects a caller-selected global MIPS16 processor without replacing the request", async () => {
|
||||
const parsed = parseConfig({
|
||||
GHIDRA_INSTALL_DIR: "/ghidra",
|
||||
REA_GHIDRA_LANGUAGE_ID: "MIPS:LE:32:16e",
|
||||
});
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const ghidra = new GhidraProvider(parsed.value, silentLogger, {}, host);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(shared());
|
||||
expect(resolved.ok).toBe(false);
|
||||
if (!resolved.ok)
|
||||
expect(resolved.error.message).toContain(
|
||||
"requested override has not been changed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("soft-float MIPS storage", () => {
|
||||
it("admits soft-float o32 and commits its storage policy only for it", async () => {
|
||||
const soft = target(softFloat);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,30 @@
|
|||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
|
||||
const MIPS_FP_ABI_SOFT = 3;
|
||||
const MIPS_ET_REL = 1;
|
||||
const MIPS_ET_EXEC = 2;
|
||||
const MIPS_ET_DYN = 3;
|
||||
const MIPS16_HEADER_ASE = 0x04000000;
|
||||
|
||||
/** A distinct compressed shared-image shape; admission still checks every declaration. */
|
||||
const isGhidraMips16SharedTarget = (target: BinaryTarget): boolean =>
|
||||
target.kind === "executable" &&
|
||||
target.architecture === "mips" &&
|
||||
target.mips.type === MIPS_ET_DYN &&
|
||||
(target.mips.flags & 0x0f000000) === MIPS16_HEADER_ASE;
|
||||
|
||||
/** Generic MIPS32 permits per-function ISA_MODE; a global 16e processor does not. */
|
||||
export const ghidraMipsSharedLanguageOverride = (
|
||||
target: BinaryTarget,
|
||||
): GhidraLanguageOverride | undefined =>
|
||||
isGhidraMips16SharedTarget(target) && target.architecture === "mips"
|
||||
? {
|
||||
languageId: `MIPS:${target.mips.byteOrder === "little" ? "LE" : "BE"}:32:default`,
|
||||
compilerSpecId: "default",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
|
||||
export const ghidraMipsUnsupportedReason = (
|
||||
|
|
@ -14,11 +35,16 @@ export const ghidraMipsUnsupportedReason = (
|
|||
const metadata = target.mips;
|
||||
if (target.format !== "elf" || metadata === undefined)
|
||||
return "MIPS analysis requires ELF header metadata; a family name alone does not select an ISA or ABI.";
|
||||
const shared = isGhidraMips16SharedTarget(target);
|
||||
if (metadata.elfClass !== 32)
|
||||
return "This Ghidra MIPS lane supports ELF32 only; MIPS64 and n64 remain unsupported.";
|
||||
if (metadata.type !== MIPS_ET_EXEC && metadata.type !== MIPS_ET_REL)
|
||||
if (
|
||||
metadata.type !== MIPS_ET_EXEC &&
|
||||
metadata.type !== MIPS_ET_REL &&
|
||||
!shared
|
||||
)
|
||||
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)
|
||||
if ((metadata.flags & 0x0f000000) !== 0 && !shared)
|
||||
return "MIPS16, microMIPS and other ASE encodings are outside this standard-instruction MIPS lane.";
|
||||
if ((metadata.flags & 0x00ff0000) !== 0)
|
||||
return "Machine-specific MIPS variants, including PSP Allegrex, need a separately supported processor profile.";
|
||||
|
|
@ -33,7 +59,20 @@ export const ghidraMipsUnsupportedReason = (
|
|||
return "EF_MIPS_FP64 requires separate verification of 64-bit floating-point register semantics in this Ghidra lane.";
|
||||
if ((metadata.flags & 0x00000400) !== 0)
|
||||
return "EF_MIPS_NAN2008 requires separate verification of NaN encoding semantics in this Ghidra lane.";
|
||||
if (shared) {
|
||||
if ((metadata.flags & 0x40) !== 0)
|
||||
return "EF_MIPS_ABI_ON32 declares an unverified ABI for the MIPS16 shared-image lane.";
|
||||
const load = metadata.sharedLoad;
|
||||
if (load === undefined || load.status !== "complete")
|
||||
return `MIPS16 shared images require complete dynamic relocation inspection. ${load?.reason ?? "The source declarations have not been inspected."}`;
|
||||
if (load.relocationTypes.some((type) => type !== 0 && type !== 3))
|
||||
return "This MIPS16 shared-image lane verifies R_MIPS_NONE and R_MIPS_REL32 only; other dynamic relocation types require separate verification.";
|
||||
if (load.symbolRelativeCount !== 0)
|
||||
return "Symbol-bearing R_MIPS_REL32 is unsupported: Ghidra normalizes MIPS16 function addresses and can lose the source function-pointer mode tag. Symbol-index-zero REL32 is verified separately.";
|
||||
}
|
||||
const abi = metadata.abiFlags;
|
||||
if (shared && abi === null && (metadata.limitations?.length ?? 0) === 0)
|
||||
return null; // Inspected absence leaves FP semantics unknown, never inferred.
|
||||
if (abi === undefined || abi === null)
|
||||
return [
|
||||
"This Ghidra MIPS lane requires an inspected ABI flags record; header flags alone do not establish the floating-point ABI.",
|
||||
|
|
@ -49,8 +88,8 @@ export const ghidraMipsUnsupportedReason = (
|
|||
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.";
|
||||
if (abi.isaExtension !== 0 || abi.ases !== (shared ? 0x400 : 0))
|
||||
return "MIPS ABI flags declare an ISA extension or ASE outside this profile's verified instruction declarations.";
|
||||
// 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.
|
||||
|
|
@ -116,6 +155,22 @@ export const ghidraMipsRelocatableLimitations = (
|
|||
]
|
||||
: [];
|
||||
|
||||
/** Shared-image results are analysis observations, not dynamic-loader execution. */
|
||||
export const ghidraMipsSharedLimitations = (
|
||||
target: BinaryTarget,
|
||||
): readonly string[] =>
|
||||
isGhidraMips16SharedTarget(target) && target.architecture === "mips"
|
||||
? [
|
||||
"MIPS16 shared ELF: the endian-matched generic MIPS32 language uses the provider's per-address ISA_MODE context. The program language variant is not the instruction's encoding mode.",
|
||||
"Shared-image addresses are Ghidra analysis coordinates, not runtime addresses. The source bytes remain unchanged; inspect_native_load_image retains original relocation bytes, loaded memory bytes and provider relocation status. External GOT bindings remain unverified without their defining images.",
|
||||
...(target.mips.abiFlags === null
|
||||
? [
|
||||
"No MIPS ABI flags record was found in the completely examined declaration tables. The header declares o32 but does not establish the floating-point ABI; the default compiler specification does not verify floating-point argument or return storage.",
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: [];
|
||||
|
||||
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
|
||||
export const ghidraMipsProfileParameters = (
|
||||
target: BinaryTarget,
|
||||
|
|
@ -152,12 +207,33 @@ export const ghidraMipsProfileParameters = (
|
|||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
...(target.mips.sharedLoad === undefined
|
||||
? {}
|
||||
: {
|
||||
shared_load:
|
||||
target.mips.sharedLoad.status === "complete"
|
||||
? {
|
||||
status: "complete",
|
||||
relocation_count: target.mips.sharedLoad.relocationCount,
|
||||
relocation_types: [
|
||||
...target.mips.sharedLoad.relocationTypes,
|
||||
],
|
||||
symbol_relative_count:
|
||||
target.mips.sharedLoad.symbolRelativeCount,
|
||||
}
|
||||
: {
|
||||
status: "unavailable",
|
||||
reason: target.mips.sharedLoad.reason,
|
||||
},
|
||||
}),
|
||||
},
|
||||
...(isGhidraMipsSoftFloatTarget(target)
|
||||
? { mips_soft_float_storage: GHIDRA_MIPS_SOFT_FLOAT_STORAGE }
|
||||
: {}),
|
||||
mips_support_lane:
|
||||
target.mips.type === MIPS_ET_REL
|
||||
instruction_context_evidence: "per-address-isa-mode-v1",
|
||||
mips_support_lane: isGhidraMips16SharedTarget(target)
|
||||
? "elf32-dyn-o32-arch32r2-mips16-local-rel32-v1"
|
||||
: target.mips.type === MIPS_ET_REL
|
||||
? "elf32-rel-o32-arch32r2-standard-v1"
|
||||
: "elf32-exec-o32-arch32r2-standard-v3",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ import {
|
|||
GHIDRA_MIPS_SOFT_FLOAT_STORAGE,
|
||||
ghidraMipsSoftFloatFailure,
|
||||
ghidraMipsRelocatableLimitations,
|
||||
ghidraMipsSharedLimitations,
|
||||
ghidraMipsSharedLanguageOverride,
|
||||
ghidraMipsSoftFloatLimitations,
|
||||
isGhidraMipsSoftFloatTarget,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
|
|
@ -444,7 +446,10 @@ export const createGhidraProviderClient = (input: {
|
|||
committedProfile.parameters,
|
||||
);
|
||||
if (
|
||||
!sameGhidraLanguageOverride(languageOverride, config.ghidraLanguageOverride)
|
||||
!sameGhidraLanguageOverride(
|
||||
languageOverride,
|
||||
config.ghidraLanguageOverride ?? ghidraMipsSharedLanguageOverride(target),
|
||||
)
|
||||
)
|
||||
return unavailableClient(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
|
|
@ -512,6 +517,7 @@ export const createGhidraProviderClient = (input: {
|
|||
? (target.mips.limitations ?? [])
|
||||
: []),
|
||||
...ghidraMipsRelocatableLimitations(target),
|
||||
...ghidraMipsSharedLimitations(target),
|
||||
];
|
||||
const providerLimitations =
|
||||
installation.platform === "win32"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver
|
|||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { readMipsElfAbiFlags } from "../../../src/application/MipsElfAbiFlags.js";
|
||||
import { readMipsElfMetadata } from "../../../src/application/MipsElfMetadata.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import {
|
||||
ghidraMipsProfileParameters,
|
||||
|
|
@ -85,7 +85,7 @@ const inspect = async (
|
|||
const path = await writeFixture(bytes);
|
||||
const handle = await open(path, "r");
|
||||
try {
|
||||
const result = await readMipsElfAbiFlags(
|
||||
const result = await readMipsElfMetadata(
|
||||
handle,
|
||||
metadata(little),
|
||||
checkCancelled,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,260 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { open, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { it } from "vitest";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { readMipsElfMetadata } from "../../../src/application/MipsElfMetadata.js";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import {
|
||||
ghidraMipsProfileParameters,
|
||||
ghidraMipsUnsupportedReason,
|
||||
} from "../../../src/ghidra/GhidraMipsProfile.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const relOffset = 4096;
|
||||
const fixture = (little: boolean, count = 2): Buffer => {
|
||||
const bytes = Buffer.alloc(relOffset + count * 8 + 16);
|
||||
bytes.set([0x7f, 0x45, 0x4c, 0x46, 1, little ? 1 : 2, 1]);
|
||||
const u16 = (value: number, offset: number) =>
|
||||
little
|
||||
? bytes.writeUInt16LE(value, offset)
|
||||
: bytes.writeUInt16BE(value, offset);
|
||||
const u32 = (value: number, offset: number) =>
|
||||
little
|
||||
? bytes.writeUInt32LE(value, offset)
|
||||
: bytes.writeUInt32BE(value, offset);
|
||||
u16(3, 16);
|
||||
u16(8, 18);
|
||||
u32(1, 20);
|
||||
u32(52, 28);
|
||||
u32(0x74001005, 36);
|
||||
u16(52, 40);
|
||||
u16(32, 42);
|
||||
u16(2, 44);
|
||||
u32(1, 52);
|
||||
u32(bytes.length, 68);
|
||||
u32(bytes.length, 72);
|
||||
u32(2, 84);
|
||||
u32(256, 88);
|
||||
u32(256, 92);
|
||||
u32(32, 100);
|
||||
u32(32, 104);
|
||||
u32(17, 256);
|
||||
u32(relOffset, 260);
|
||||
u32(18, 264);
|
||||
u32(count * 8, 268);
|
||||
u32(19, 272);
|
||||
u32(8, 276);
|
||||
for (let i = 1; i < count; i += 1) {
|
||||
u32(512, relOffset + 8 * i);
|
||||
u32(3, relOffset + 8 * i + 4);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
const resolve = async (bytes: Buffer) => {
|
||||
const directory = await createTestTempDirectory("rea-mips-shared-test-");
|
||||
const path = join(directory, "shared.elf");
|
||||
await writeFile(path, bytes);
|
||||
const result = await parseBinaryTarget(path);
|
||||
assert.ok(result.ok);
|
||||
assert.equal(result.value.kind, "executable");
|
||||
assert.equal(result.value.architecture, "mips");
|
||||
if (
|
||||
result.value.kind !== "executable" ||
|
||||
result.value.architecture !== "mips"
|
||||
)
|
||||
throw new Error("expected MIPS ELF");
|
||||
assert.equal(
|
||||
result.value.sha256,
|
||||
createHash("sha256").update(bytes).digest("hex"),
|
||||
);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
return result.value;
|
||||
};
|
||||
|
||||
for (const little of [true, false]) {
|
||||
it(`retains complete dynamic declarations beyond the initial probe (${little ? "LE" : "BE"})`, async () => {
|
||||
const target = await resolve(fixture(little));
|
||||
assert.equal(target.mips.abiFlags, null);
|
||||
assert.deepEqual(target.mips.sharedLoad, {
|
||||
status: "complete",
|
||||
relocationCount: 2,
|
||||
relocationTypes: [0, 3],
|
||||
symbolRelativeCount: 0,
|
||||
});
|
||||
assert.equal(ghidraMipsUnsupportedReason(target), null);
|
||||
assert.deepEqual(ghidraMipsProfileParameters(target).mips_elf, {
|
||||
elf_class: 32,
|
||||
byte_order: little ? "little" : "big",
|
||||
type: 3,
|
||||
flags: 0x74001005,
|
||||
abi_flags: null,
|
||||
shared_load: {
|
||||
status: "complete",
|
||||
relocation_count: 2,
|
||||
relocation_types: [0, 3],
|
||||
symbol_relative_count: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
it(`retains and refuses symbol-bearing REL32 without repairing the mode tag (${little ? "LE" : "BE"})`, async () => {
|
||||
const bytes = fixture(little);
|
||||
if (little) bytes.writeUInt32LE(0x103, relOffset + 12);
|
||||
else bytes.writeUInt32BE(0x103, relOffset + 12);
|
||||
const target = await resolve(bytes);
|
||||
assert.equal(target.mips.sharedLoad?.status, "complete");
|
||||
assert.match(
|
||||
ghidraMipsUnsupportedReason(target) ?? "",
|
||||
/Symbol-bearing R_MIPS_REL32/u,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("examines relocation records beyond a read chunk before deciding completeness", async () => {
|
||||
const count = 8194;
|
||||
const bytes = fixture(true, count);
|
||||
bytes.writeUInt32LE(0x103, relOffset + (count - 1) * 8 + 4);
|
||||
const target = await resolve(bytes);
|
||||
assert.deepEqual(target.mips.sharedLoad, {
|
||||
status: "complete",
|
||||
relocationCount: count,
|
||||
relocationTypes: [0, 3],
|
||||
symbolRelativeCount: 1,
|
||||
});
|
||||
assert.match(ghidraMipsUnsupportedReason(target) ?? "", /mode tag/u);
|
||||
});
|
||||
|
||||
it("does not treat uncaptured ABI declaration headers as an inspected absence", async () => {
|
||||
const bytes = fixture(true);
|
||||
bytes.writeUInt32LE(bytes.length + 1, 32);
|
||||
bytes.writeUInt16LE(40, 46);
|
||||
bytes.writeUInt16LE(1, 48);
|
||||
const target = await resolve(bytes);
|
||||
assert.equal(target.mips.abiFlags, null);
|
||||
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
||||
assert.match(
|
||||
ghidraMipsUnsupportedReason(target) ?? "",
|
||||
/unavailable headers remain unknown/u,
|
||||
);
|
||||
});
|
||||
|
||||
const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
|
||||
[
|
||||
"overlapping load ranges",
|
||||
(b) => {
|
||||
b.writeUInt16LE(3, 44);
|
||||
b.writeUInt32LE(1, 116);
|
||||
b.writeUInt32LE(256, 124);
|
||||
b.writeUInt32LE(16, 132);
|
||||
b.writeUInt32LE(16, 136);
|
||||
},
|
||||
/overlapping MIPS PT_LOAD/u,
|
||||
],
|
||||
[
|
||||
"inconsistent dynamic address",
|
||||
(b) => b.writeUInt32LE(0x10000, 92),
|
||||
/matching file-backed PT_LOAD/u,
|
||||
],
|
||||
["missing dynamic table", (b) => b.writeUInt32LE(0, 84), /no PT_DYNAMIC/u],
|
||||
["repeated dynamic tables", (b) => b.writeUInt32LE(2, 52), /duplicate/u],
|
||||
[
|
||||
"declared dynamic tail outside file",
|
||||
(b) => b.writeUInt32LE(b.length, 100),
|
||||
/beyond the file/u,
|
||||
],
|
||||
[
|
||||
"unavailable load bytes",
|
||||
(b) => b.writeUInt32LE(b.length + 1, 68),
|
||||
/PT_LOAD range/u,
|
||||
],
|
||||
["missing terminator", (b) => b.writeUInt32LE(1, 280), /DT_NULL/u],
|
||||
[
|
||||
"repeated scalar tags",
|
||||
(b) => b.writeUInt32LE(17, 272),
|
||||
/duplicate MIPS dynamic tag/u,
|
||||
],
|
||||
["invalid REL entry size", (b) => b.writeUInt32LE(16, 276), /DT_RELENT/u],
|
||||
["truncated REL record", (b) => b.writeUInt32LE(17, 268), /DT_RELENT/u],
|
||||
[
|
||||
"unmapped REL table",
|
||||
(b) => b.writeUInt32LE(0x10000, 260),
|
||||
/PT_LOAD mapping/u,
|
||||
],
|
||||
[
|
||||
"unmapped REL destination",
|
||||
(b) => b.writeUInt32LE(0x10000, relOffset + 8),
|
||||
/PT_LOAD memory range/u,
|
||||
],
|
||||
[
|
||||
"RELA declaration",
|
||||
(b) => {
|
||||
b.writeUInt32LE(7, 272);
|
||||
b.writeUInt32LE(4096, 276);
|
||||
},
|
||||
/RELA or PLT/u,
|
||||
],
|
||||
];
|
||||
it.each(malformed)(
|
||||
"preserves useful partial header facts for %s",
|
||||
async (_name, mutate, reason) => {
|
||||
const bytes = fixture(true);
|
||||
mutate(bytes);
|
||||
const target = await resolve(bytes);
|
||||
assert.equal(target.mips.flags, 0x74001005);
|
||||
assert.equal(target.mips.abiFlags, null);
|
||||
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
||||
assert.match(ghidraMipsUnsupportedReason(target) ?? "", reason);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains unverified relocation type instead of claiming every examined record is supported", async () => {
|
||||
const bytes = fixture(true);
|
||||
bytes.writeUInt32LE(5, relOffset + 12);
|
||||
const target = await resolve(bytes);
|
||||
assert.deepEqual(target.mips.sharedLoad, {
|
||||
status: "complete",
|
||||
relocationCount: 2,
|
||||
relocationTypes: [0, 5],
|
||||
symbolRelativeCount: 0,
|
||||
});
|
||||
assert.match(
|
||||
ghidraMipsUnsupportedReason(target) ?? "",
|
||||
/other dynamic relocation types/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not equate complete dynamic REL coverage with coverage of another section relocation table", async () => {
|
||||
const bytes = fixture(true);
|
||||
bytes.writeUInt32LE(1024, 32);
|
||||
bytes.writeUInt16LE(40, 46);
|
||||
bytes.writeUInt16LE(1, 48);
|
||||
bytes.writeUInt32LE(9, 1028);
|
||||
bytes.writeUInt32LE(2048, 1040);
|
||||
bytes.writeUInt32LE(8, 1044);
|
||||
bytes.writeUInt32LE(8, 1060);
|
||||
const target = await resolve(bytes);
|
||||
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
||||
assert.match(
|
||||
ghidraMipsUnsupportedReason(target) ?? "",
|
||||
/section relocation declarations/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the caller cancellation error during a bounded relocation scan", async () => {
|
||||
const target = await resolve(fixture(true, 8194));
|
||||
const handle = await open(target.path, "r");
|
||||
const cancellation = new AnalysisCancelledError("open_binary");
|
||||
let checks = 0;
|
||||
try {
|
||||
await assert.rejects(
|
||||
readMipsElfMetadata(handle, target.mips, () => {
|
||||
checks += 1;
|
||||
if (checks === 100) throw cancellation;
|
||||
}),
|
||||
(error) => error === cancellation,
|
||||
);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* Build an older-style shared image from source without ABI declarations. */
|
||||
PHDRS {
|
||||
headers PT_PHDR PHDRS;
|
||||
text PT_LOAD FILEHDR PHDRS FLAGS(5);
|
||||
data PT_LOAD FLAGS(6);
|
||||
dynamic PT_DYNAMIC FLAGS(6);
|
||||
}
|
||||
SECTIONS {
|
||||
. = SIZEOF_HEADERS;
|
||||
.hash : { *(.hash) } :text
|
||||
.gnu.hash : { *(.gnu.hash) } :text
|
||||
.dynsym : { *(.dynsym) } :text
|
||||
.dynstr : { *(.dynstr) } :text
|
||||
.rel.dyn : { *(.rel.*) } :text
|
||||
.text : { *(.text*) } :text
|
||||
. = ALIGN(0x10000);
|
||||
.dynamic : { *(.dynamic) } :data :dynamic
|
||||
.got : { *(.got*) } :data
|
||||
.data : { *(.data*) } :data
|
||||
.bss : { *(.bss*) *(COMMON) } :data
|
||||
/DISCARD/ : { *(.MIPS.abiflags) *(.reginfo) *(.pdr) *(.mdebug.*) *(.comment) *(.note.*) }
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const source = fileURLToPath(new URL("./mips16-shared.S", import.meta.url));
|
||||
const linkerScript = fileURLToPath(
|
||||
new URL("./mips16-recordless.ld", import.meta.url),
|
||||
);
|
||||
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** BYO GNU cross-binutils; generated controls are never executed or byte-patched. */
|
||||
export async function buildMips16SharedFixture(directory, byteOrder, variant) {
|
||||
assert(["little", "big"].includes(byteOrder));
|
||||
assert(
|
||||
[
|
||||
"declared",
|
||||
"recordless",
|
||||
"symbol-relative",
|
||||
"section-relocations",
|
||||
].includes(variant),
|
||||
);
|
||||
const assembler = process.env.REA_MIPS_AS ?? "mips-linux-gnu-as";
|
||||
const linker = process.env.REA_MIPS_LD ?? "mips-linux-gnu-ld";
|
||||
const endian = byteOrder === "little" ? "-EL" : "-EB";
|
||||
const prefix = join(directory, `${byteOrder}-${variant}`);
|
||||
const object = `${prefix}.o`;
|
||||
const path = `${prefix}.so`;
|
||||
const assemblerVersion = (
|
||||
await execute(assembler, ["--version"], { timeout: 10000 })
|
||||
).stdout;
|
||||
const linkerVersion = (
|
||||
await execute(linker, ["--version"], { timeout: 10000 })
|
||||
).stdout;
|
||||
assert(
|
||||
assemblerVersion.includes("GNU assembler"),
|
||||
"Use GNU's MIPS assembler: other assemblers have different compressed-instruction behavior.",
|
||||
);
|
||||
assert(linkerVersion.includes("GNU ld"));
|
||||
await execute(
|
||||
assembler,
|
||||
[
|
||||
"-32",
|
||||
"-mips32r2",
|
||||
endian,
|
||||
...(variant === "symbol-relative"
|
||||
? ["--defsym", "REA_MIPS_SYMBOL_RELATIVE=1"]
|
||||
: []),
|
||||
source,
|
||||
"-o",
|
||||
object,
|
||||
],
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await execute(
|
||||
linker,
|
||||
[
|
||||
endian,
|
||||
"-shared",
|
||||
...(variant === "section-relocations" ? ["--emit-relocs"] : []),
|
||||
...(variant === "recordless" ? ["-T", linkerScript] : []),
|
||||
object,
|
||||
"-o",
|
||||
path,
|
||||
],
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
const little = byteOrder === "little";
|
||||
const u16 = (offset) =>
|
||||
little ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset);
|
||||
const u32 = (offset) =>
|
||||
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
||||
assert.equal(
|
||||
bytes.subarray(0, 6).toString("hex"),
|
||||
little ? "7f454c460101" : "7f454c460102",
|
||||
);
|
||||
assert.equal(u16(16), 3);
|
||||
assert.equal(u16(18), 8);
|
||||
assert.equal(u32(36), 0x74001007); // noreorder/PIC/CPIC plus o32/MIPS16/MIPS32r2.
|
||||
const headers = Array.from(
|
||||
{ length: u16(44) },
|
||||
(_, i) => u32(28) + i * u16(42),
|
||||
);
|
||||
const fileOffset = (address, size) => {
|
||||
const mappings = headers.filter(
|
||||
(offset) =>
|
||||
u32(offset) === 1 &&
|
||||
address >= u32(offset + 8) &&
|
||||
address - u32(offset + 8) <= u32(offset + 16) - size,
|
||||
);
|
||||
assert.equal(mappings.length, 1);
|
||||
return u32(mappings[0] + 4) + address - u32(mappings[0] + 8);
|
||||
};
|
||||
const readelf = await execute(
|
||||
process.env.REA_MIPS_READELF ?? "readelf",
|
||||
["-W", "-h", "-l", "-s", "-r", path],
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const symbols = {};
|
||||
for (const name of [
|
||||
"rea_mips16_probe",
|
||||
"rea_mips32_probe",
|
||||
"rea_mips16_pointer",
|
||||
]) {
|
||||
const addresses = new Set(
|
||||
readelf.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim().split(/\s+/u))
|
||||
.filter(
|
||||
(fields) =>
|
||||
fields[0]?.endsWith(":") === true &&
|
||||
["FUNC", "OBJECT"].includes(fields[3]) &&
|
||||
fields.at(-1) === name,
|
||||
)
|
||||
.map((fields) => Number.parseInt(fields[1], 16)),
|
||||
);
|
||||
assert.equal(addresses.size, 1, `Missing or conflicting ${name}`);
|
||||
symbols[name] = [...addresses][0];
|
||||
}
|
||||
assert.equal(symbols.rea_mips16_probe & 1, 1);
|
||||
assert.equal(symbols.rea_mips32_probe & 1, 0);
|
||||
const compressedOffset = fileOffset(symbols.rea_mips16_probe - 1, 8);
|
||||
assert.equal(
|
||||
bytes.subarray(compressedOffset, compressedOffset + 8).toString("hex"),
|
||||
little ? "5a6a8eea074aa0e8" : "6a5aea8e4a07e8a0",
|
||||
);
|
||||
const standardOffset = fileOffset(symbols.rea_mips32_probe, 12);
|
||||
assert.equal(
|
||||
bytes.subarray(standardOffset, standardOffset + 12).toString("hex"),
|
||||
little ? "030082240800e00300000000" : "2482000303e0000800000000",
|
||||
);
|
||||
const pointerOffset = fileOffset(symbols.rea_mips16_pointer, 4);
|
||||
const rawPointer = u32(pointerOffset);
|
||||
if (variant !== "symbol-relative")
|
||||
assert.equal(rawPointer, symbols.rea_mips16_probe);
|
||||
return {
|
||||
path,
|
||||
sha256: hash(bytes),
|
||||
byte_order: byteOrder,
|
||||
variant,
|
||||
elf_flags: u32(36),
|
||||
symbols,
|
||||
raw_pointer: rawPointer,
|
||||
source_sha256: hash(await readFile(source)),
|
||||
linker_script_sha256:
|
||||
variant === "recordless" ? hash(await readFile(linkerScript)) : null,
|
||||
assembler_version: assemblerVersion.split("\n")[0],
|
||||
linker_version: linkerVersion.split("\n")[0],
|
||||
readelf_output: readelf.stdout,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/* Source-owned mixed-instruction shared-image control. */
|
||||
.abicalls
|
||||
.module fp=xx
|
||||
.module nooddspreg
|
||||
.text
|
||||
.set noreorder
|
||||
.set nomicromips
|
||||
.set mips16
|
||||
.globl rea_mips16_probe
|
||||
.type rea_mips16_probe, @function
|
||||
.ent rea_mips16_probe
|
||||
rea_mips16_probe:
|
||||
li $2, 90
|
||||
xor $2, $4
|
||||
addiu $2, 7
|
||||
jrc $31
|
||||
.end rea_mips16_probe
|
||||
.size rea_mips16_probe, . - rea_mips16_probe
|
||||
.ifndef REA_MIPS_SYMBOL_RELATIVE
|
||||
.hidden rea_mips16_probe
|
||||
.endif
|
||||
|
||||
.set nomips16
|
||||
.globl rea_mips32_probe
|
||||
.type rea_mips32_probe, @function
|
||||
.ent rea_mips32_probe
|
||||
rea_mips32_probe:
|
||||
addiu $2, $4, 3
|
||||
jr $31
|
||||
nop
|
||||
.end rea_mips32_probe
|
||||
.size rea_mips32_probe, . - rea_mips32_probe
|
||||
|
||||
.data
|
||||
.balign 4
|
||||
.globl rea_mips16_pointer
|
||||
.type rea_mips16_pointer, @object
|
||||
.size rea_mips16_pointer, 4
|
||||
rea_mips16_pointer:
|
||||
.word rea_mips16_probe
|
||||
.section .note.GNU-stack, "", @progbits
|
||||
Loading…
Reference in New Issue