fix(ghidra): admit carved MIPS ELF with incomplete section tables (#1629)

* fix(ghidra): inspect carved MIPS ABI declarations without missing sections

* test: share filesystem fixtures and cleanup assertions
This commit is contained in:
Ysopwnable
2026-10-11 04:33:05 +08:00
committed by GitHub
parent bbd01923e9
commit d6f698d8af
9 changed files with 313 additions and 116 deletions
+12
View File
@@ -29,6 +29,13 @@ section names or the record being inside the initial 4 KiB header probe.
Program and section declarations must agree; duplicate, conflicting, truncated
or out-of-file records are rejected. Stripped section tables and section-only
records are supported. Extended table numbering is explicitly unsupported.
For carved ELFs whose section table extends beyond the captured file, REA
examines every complete captured section header and records the unavailable
headers as a limitation. A valid program-header ABI record can still establish
the supported declaration. Available section declarations must agree with it;
their duplicate, conflicting or unreadable records remain errors. A missing
program-header record does not make ABI declarations in uncaptured sections
absent or supply a default ABI.
The field layout follows
[`Elf_MIPS_ABIFlags_v0`](https://github.com/bminor/glibc/blob/master/elf/elf.h).
@@ -92,6 +99,11 @@ observations and does not relabel the upstream result as verified.
CLI/MCP discovery and decompilation, direct calls, target/profile/Evidence
identity, snapshot round-trip and final process cleanup remain covered.
For both byte orders, the lane also carves away the section table while retaining
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.
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
+109 -1
View File
@@ -2,7 +2,7 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -60,6 +60,9 @@ const env = {
JAVA_HOME: installation.javaHome,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
};
for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
}
const reports = [];
let primaryFailure;
try {
@@ -227,6 +230,103 @@ try {
fixture.sha256,
"Analysis modified the source executable",
);
// Carved firmware keeps all program headers/load bytes but loses sections.
const read16 = (offset) =>
byteOrder === "little"
? sourceBytes.readUInt16LE(offset)
: sourceBytes.readUInt16BE(offset);
const read32 = (offset) =>
byteOrder === "little"
? sourceBytes.readUInt32LE(offset)
: sourceBytes.readUInt32BE(offset);
const sectionOffset = read32(32);
const cut = sectionOffset - 1;
const programOffset = read32(28);
const programCount = read16(44);
assert.equal(read16(42), 32);
assert.ok(cut >= programOffset + programCount * 32);
for (let i = 0; i < programCount; i += 1) {
const offset = programOffset + i * 32;
assert.ok(read32(offset + 4) + read32(offset + 16) <= cut);
}
const carvedBytes = sourceBytes.subarray(0, cut);
const carvedPath = join(workspace, `${byteOrder}-carved.elf`);
const carvedSha256 = createHash("sha256")
.update(carvedBytes)
.digest("hex");
await writeFile(carvedPath, carvedBytes);
const carvedResolved = await parseBinaryTarget(carvedPath);
if (!carvedResolved.ok) throw carvedResolved.error;
assert.deepEqual(
carvedResolved.value.mips.abiFlags,
independent.mips.abiFlags,
);
assert.match(
carvedResolved.value.mips.limitations.join(" "),
/section header table/u,
);
const carvedTarget = await call("open_binary", {
path: carvedPath,
provider_id: "ghidra",
});
assert.equal(carvedTarget.sha256, carvedSha256);
const carvedImage = await call("inspect_native_load_image");
assertMipsLoadedImage(carvedImage.observations, byteOrder, carvedSha256);
const carvedProcedures = await call("list_procedures");
assert.ok(
carvedProcedures.some(
(p) =>
BigInt(p.address) === BigInt(independent.symbols.rea_mips_entry),
),
);
const carvedDossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: independent.symbols.rea_mips_entry,
}),
);
assert.ok(carvedDossier.pseudocode.trim().length > 0);
const carvedSnapshotPath = join(
workspace,
`${byteOrder}-carved.snapshot.json`,
);
await call("close_binary", { snapshot_path: carvedSnapshotPath });
const carvedSnapshot = parseAnalysisSnapshot(
JSON.parse(await readFile(carvedSnapshotPath, "utf8")),
);
assert.equal(carvedSnapshot.target.sha256, carvedSha256);
assert.ok(
carvedSnapshot.evidence_bundle.records.some((record) =>
parseEvidence(record).limitations.some((item) =>
item.includes("section header table"),
),
),
);
const carvedCli = await execute(
process.execPath,
[
entrypoint,
"function",
carvedPath,
independent.symbols.rea_mips_entry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const carvedEvidence = parseEvidence(JSON.parse(carvedCli.stdout));
assert.equal(carvedEvidence.subject?.digest.sha256, carvedSha256);
assert.ok(
carvedEvidence.limitations.some((item) =>
item.includes("section header table"),
),
);
assert.equal(
functionDossierSchema.parse(carvedEvidence.normalized_result).procedure
.address,
carvedDossier.procedure.address,
);
assert.deepEqual(await readFile(carvedPath), carvedBytes);
reports.push({
...fixture,
independent_reader: independent,
@@ -241,6 +341,14 @@ try {
"CLI/MCP and snapshot identity",
],
procedures: procedures.length,
carved: {
sha256: carvedSha256,
source_bytes: carvedBytes.length,
section_header_offset: sectionOffset,
effective_language: carvedImage.observations.language_id,
limitations: carvedResolved.value.mips.limitations,
procedures: carvedProcedures.length,
},
status: "passed",
});
} catch (cause) {
+7 -1
View File
@@ -373,7 +373,13 @@ const readExecutableMetadata = async (
if (!abiFlags.ok) return abiFlags;
return ok({
...parsed.value,
mips: { ...parsed.value.mips, abiFlags: abiFlags.value },
mips: {
...parsed.value.mips,
abiFlags: abiFlags.value.abiFlags,
...(abiFlags.value.limitations.length === 0
? {}
: { limitations: abiFlags.value.limitations }),
},
});
};
+27 -6
View File
@@ -5,6 +5,12 @@ import type {
} from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
/** Observed ABI declarations and coverage of their source tables. */
export interface MipsAbiFlagsInspection {
readonly abiFlags: MipsAbiFlags | null;
readonly limitations: readonly string[];
}
/**
* Read ELF32 MIPS ABI declarations from the caller's already-open file.
* Inspect both typed tables (not section names), including records beyond the
@@ -15,7 +21,7 @@ export const readMipsElfAbiFlags = async (
handle: FileHandle,
metadata: MipsElfMetadata,
checkCancelled: () => void,
): Promise<Result<MipsAbiFlags | null, string>> => {
): Promise<Result<MipsAbiFlagsInspection, string>> => {
checkCancelled();
if (metadata.elfClass !== 32)
return err("MIPS ABI record inspection currently supports ELF32 only");
@@ -81,6 +87,7 @@ export const readMipsElfAbiFlags = async (
dataOffset: 4,
sizeOffset: 16,
recordType: 0x70000003, // PT_MIPS_ABIFLAGS
optional: false,
},
{
label: "section header table",
@@ -92,6 +99,7 @@ export const readMipsElfAbiFlags = async (
dataOffset: 16,
sizeOffset: 20,
recordType: 0x7000002a, // SHT_MIPS_ABIFLAGS
optional: true,
},
];
// Extended numbering needs a separate section-zero reader. Refuse it rather
@@ -99,6 +107,7 @@ 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;
const limitations: string[] = [];
for (const table of tables) {
if (table.count === 0) {
if (table.offset !== 0)
@@ -107,16 +116,27 @@ export const readMipsElfAbiFlags = async (
}
if (table.offset < 52 || table.stride !== table.entrySize)
return err(`invalid MIPS ${table.label} offset or entry size`);
const count = table.optional
? Math.min(
table.count,
Math.max(0, Math.floor((fileSize - table.offset) / table.entrySize)),
)
: table.count;
if (count < table.count)
limitations.push(
`MIPS ${table.label} at file offset ${table.offset} extends beyond the captured file: examined ${count} of ${table.count} complete headers. ABI declarations in unavailable headers remain unknown.`,
);
if (count === 0) continue;
// Counts are bounded by the ELF16 fields and strides by their exact ELF32
// structure sizes. Never allocate according to an untrusted section size.
const contents = await read(
table.offset,
table.count * table.entrySize,
count * table.entrySize,
table.label,
);
if (!contents.ok) return contents;
let found = false;
for (let i = 0; i < table.count; i += 1) {
for (let i = 0; i < count; i += 1) {
checkCancelled();
const position = i * table.entrySize;
if (u32(contents.value, position + table.typeOffset) !== table.recordType)
@@ -134,10 +154,10 @@ export const readMipsElfAbiFlags = async (
record = candidate.value;
}
}
if (record === undefined) return ok(null);
if (record === undefined) return ok({ abiFlags: null, limitations });
// Preserve declarations verbatim, including unknown versions/values. Provider
// admission, not parsing, decides which interpretations have been verified.
return ok({
const abiFlags: MipsAbiFlags = {
version: u16(record, 0),
isaLevel: record.readUInt8(2),
isaRevision: record.readUInt8(3),
@@ -149,5 +169,6 @@ export const readMipsElfAbiFlags = async (
ases: u32(record, 12),
flags1: u32(record, 16),
flags2: u32(record, 20),
});
};
return ok({ abiFlags, limitations });
};
+3 -1
View File
@@ -14,8 +14,10 @@ export interface MipsElfMetadata {
readonly byteOrder: "little" | "big";
readonly type: number;
readonly flags: number;
/** Undefined until inspected; null means neither typed table has a record. */
/** Undefined until inspected; null means no ABI record was observed. */
readonly abiFlags?: MipsAbiFlags | null;
/** Unavailable declaration sources; missing records there remain unknown. */
readonly limitations?: readonly string[];
}
/** Declared fields of the 24-byte MIPS ABI flags record, not inferred semantics. */
+7 -1
View File
@@ -31,7 +31,10 @@ export const ghidraMipsUnsupportedReason = (
return "EF_MIPS_NAN2008 requires separate verification of NaN encoding semantics in this Ghidra lane.";
const abi = metadata.abiFlags;
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.";
return [
"This Ghidra MIPS lane requires an inspected ABI flags record; header flags alone do not establish the floating-point ABI.",
...(metadata.limitations ?? []),
].join(" ");
if (abi.version !== 0)
return "This Ghidra MIPS lane requires ABI flags version 0; newer record semantics are unsupported.";
if (abi.isaLevel !== 32 || abi.isaRevision !== 2)
@@ -64,6 +67,9 @@ export const ghidraMipsProfileParameters = (
byte_order: target.mips.byteOrder,
type: target.mips.type,
flags: target.mips.flags,
...(target.mips.limitations === undefined
? {}
: { abi_flags_limitations: [...target.mips.limitations] }),
abi_flags:
abi === undefined || abi === null
? null
+7 -3
View File
@@ -117,13 +117,17 @@ export const createGhidraProviderClient = (input: {
}),
);
let extensionFailure: AnalysisError | undefined;
const targetLimitations =
target.format === "dos-mz"
const targetLimitations = [
...(target.format === "dos-mz"
? [
"DOS MZ uses 16-bit x86 real mode with the Ghidra load segment 0x1000. Returned addresses are linear byte coordinates; they do not identify a unique segment:offset alias.",
"Static DOS analysis does not emulate BIOS, DOS interrupts, device ports, or self-modifying unpacking code. Packed targets require a separately identified unpacked artifact for original-program analysis; appended overlays are not the initialized load module.",
]
: [];
: []),
...(target.kind === "executable" && target.architecture === "mips"
? (target.mips.limitations ?? [])
: []),
];
const providerLimitations =
installation.platform === "win32"
? windowsP0Limitations
@@ -56,6 +56,41 @@ const waitForNoMatchingDescriptor = async (identity: Stats): Promise<void> => {
expect(await matchingDescriptorCount(identity)).toBe(0);
};
const createUnpackedFixture = async () => {
const root = await createTestTempDirectory("rea-asar-unpacked-");
const source = join(root, "source");
const archive = join(root, "fixture.asar");
await mkdir(source);
await writeFile(join(source, "small.js"), "module.exports = 1;\n");
await createPackageWithOptions(source, archive, { unpack: "small.js" });
const unpackedPath = join(`${archive}.unpacked`, "small.js");
return {
archive,
unpackedPath,
identity: await lstat(unpackedPath),
};
};
const unpackedMember = async (reader: AsarArtifactReader) => {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const member = entries.find(({ path }) => path === "small.js");
if (member === undefined) throw new Error("Expected unpacked ASAR member");
return member;
};
const expectRetainedHandleRecovery = async (
reader: AsarArtifactReader,
identity: Stats,
closeCount: () => number,
): Promise<void> => {
expect(closeCount()).toBe(1);
expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0);
await reader.close();
expect(closeCount()).toBe(2);
await waitForNoMatchingDescriptor(identity);
};
describe("ASAR entry streaming", () => {
it.skipIf(process.platform === "win32")(
"closes a packed member handle when its unopened output stream is destroyed",
@@ -292,14 +327,7 @@ describe("ASAR read failures", () => {
it.skipIf(process.platform === "win32")(
"closes the opened unpacked file and retains ASAR context when handle stat fails",
async () => {
const root = await createTestTempDirectory("rea-asar-unpacked-stat-");
const source = join(root, "source");
const archive = join(root, "fixture.asar");
await mkdir(source);
await writeFile(join(source, "small.js"), "module.exports = 1;\n");
await createPackageWithOptions(source, archive, { unpack: "small.js" });
const unpackedPath = join(`${archive}.unpacked`, "small.js");
const identity = await lstat(unpackedPath);
const { archive, unpackedPath, identity } = await createUnpackedFixture();
const ioError = Object.assign(new Error("device stat failed"), {
code: "EIO",
errno: -5,
@@ -328,11 +356,7 @@ describe("ASAR read failures", () => {
});
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries.find(({ path }) => path === "small.js");
if (entry === undefined)
throw new Error("Expected unpacked ASAR member");
const entry = await unpackedMember(reader);
await expect(reader.open(entry)).rejects.toMatchObject({
name: "ArtifactReaderFailure",
@@ -349,11 +373,7 @@ describe("ASAR read failures", () => {
reason: "device close failed",
},
});
expect(closeCount).toBe(1);
expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0);
await reader.close();
expect(closeCount).toBe(2);
await waitForNoMatchingDescriptor(identity);
await expectRetainedHandleRecovery(reader, identity, () => closeCount);
} finally {
await reader.close();
}
@@ -364,14 +384,7 @@ describe("ASAR read failures", () => {
it.skipIf(process.platform === "win32")(
"retries cleanup when cancellation arrives immediately after unpacked stat",
async () => {
const root = await createTestTempDirectory("rea-asar-unpacked-abort-stat-");
const source = join(root, "source");
const archive = join(root, "fixture.asar");
await mkdir(source);
await writeFile(join(source, "small.js"), "module.exports = 1;\n");
await createPackageWithOptions(source, archive, { unpack: "small.js" });
const unpackedPath = join(`${archive}.unpacked`, "small.js");
const identity = await lstat(unpackedPath);
const { archive, unpackedPath, identity } = await createUnpackedFixture();
const controller = new AbortController();
let closeCount = 0;
const reader = new AsarArtifactReader(archive, async (path, flags) => {
@@ -392,10 +405,7 @@ it.skipIf(process.platform === "win32")(
});
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries.find(({ path }) => path === "small.js");
if (entry === undefined) throw new Error("Expected unpacked ASAR member");
const entry = await unpackedMember(reader);
await expect(reader.open(entry, controller.signal)).rejects.toMatchObject(
{
@@ -406,11 +416,7 @@ it.skipIf(process.platform === "win32")(
},
},
);
expect(closeCount).toBe(1);
expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0);
await reader.close();
expect(closeCount).toBe(2);
await waitForNoMatchingDescriptor(identity);
await expectRetainedHandleRecovery(reader, identity, () => closeCount);
} finally {
await reader.close();
}
@@ -420,14 +426,7 @@ it.skipIf(process.platform === "win32")(
it.skipIf(process.platform === "win32")(
"preserves an unpacked identity failure when closing its handle fails",
async () => {
const root = await createTestTempDirectory("rea-asar-unpacked-identity-");
const source = join(root, "source");
const archive = join(root, "fixture.asar");
await mkdir(source);
await writeFile(join(source, "small.js"), "module.exports = 1;\n");
await createPackageWithOptions(source, archive, { unpack: "small.js" });
const unpackedPath = join(`${archive}.unpacked`, "small.js");
const identity = await lstat(unpackedPath);
const { archive, unpackedPath, identity } = await createUnpackedFixture();
const closeError = new Error("first close failed");
let closeCount = 0;
const reader = new AsarArtifactReader(archive, async (path, flags) => {
@@ -449,10 +448,7 @@ it.skipIf(process.platform === "win32")(
});
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries.find(({ path }) => path === "small.js");
if (entry === undefined) throw new Error("Expected unpacked ASAR member");
const entry = await unpackedMember(reader);
await expect(reader.open(entry)).rejects.toMatchObject({
name: "ArtifactReaderFailure",
@@ -465,11 +461,7 @@ it.skipIf(process.platform === "win32")(
reason: "first close failed",
},
});
expect(closeCount).toBe(1);
expect(await matchingDescriptorCount(identity)).toBeGreaterThan(0);
await reader.close();
expect(closeCount).toBe(2);
await waitForNoMatchingDescriptor(identity);
await expectRetainedHandleRecovery(reader, identity, () => closeCount);
} finally {
await reader.close();
}
@@ -479,14 +471,7 @@ it.skipIf(process.platform === "win32")(
it.skipIf(process.platform === "win32")(
"retains unpacked handles and snapshots across combined cleanup failures",
async () => {
const root = await createTestTempDirectory("rea-asar-combined-cleanup-");
const source = join(root, "source");
const archive = join(root, "fixture.asar");
await mkdir(source);
await writeFile(join(source, "small.js"), "module.exports = 1;\n");
await createPackageWithOptions(source, archive, { unpack: "small.js" });
const unpackedPath = join(`${archive}.unpacked`, "small.js");
const identity = await lstat(unpackedPath);
const { archive, unpackedPath, identity } = await createUnpackedFixture();
const closeError = new Error("unpacked close denied");
const snapshotError = new Error("snapshot removal denied");
let closeCount = 0;
@@ -519,10 +504,7 @@ it.skipIf(process.platform === "win32")(
);
try {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const entry = entries.find(({ path }) => path === "small.js");
if (entry === undefined) throw new Error("Expected unpacked ASAR member");
const entry = await unpackedMember(reader);
await expect(reader.open(entry)).rejects.toMatchObject({
reason: "io",
+95 -39
View File
@@ -8,6 +8,7 @@ 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 { ok } from "../../../src/domain/result.js";
import {
ghidraMipsProfileParameters,
ghidraMipsUnsupportedReason,
@@ -52,17 +53,44 @@ const fixture = (little = true): Buffer => {
bytes.set([0, 0, 32, 2, 1, 1, 0, 5], abiOffset);
return bytes;
};
const writeFixture = async (bytes: Buffer): Promise<string> => {
const directory = await createTestTempDirectory("rea-mips-abi-test-");
const path = join(directory, "fixture.elf");
await writeFile(path, bytes);
return path;
};
const resolveFixture = async (bytes: Buffer) => {
const path = await writeFixture(bytes);
const result = await parseBinaryTarget(path);
if (!result.ok) throw result.error;
if (
result.value.kind !== "executable" ||
result.value.architecture !== "mips"
)
throw new Error("Expected a resolved MIPS executable");
assert.equal(result.value.format, "elf");
assert.equal(result.value.path, path);
assert.equal(
result.value.sha256,
createHash("sha256").update(bytes).digest("hex"),
);
assert.deepEqual(await readFile(path), bytes);
return result.value;
};
const inspect = async (
bytes: Buffer,
little = true,
checkCancelled: () => void = () => {},
) => {
const directory = await createTestTempDirectory("rea-mips-abi-test-");
const path = join(directory, "fixture.elf");
await writeFile(path, bytes);
const path = await writeFixture(bytes);
const handle = await open(path, "r");
try {
return await readMipsElfAbiFlags(handle, metadata(little), checkCancelled);
const result = await readMipsElfAbiFlags(
handle,
metadata(little),
checkCancelled,
);
return result.ok ? ok(result.value.abiFlags) : result;
} finally {
await handle.close();
}
@@ -116,6 +144,62 @@ it("supports stripped section tables using PT_MIPS_ABIFLAGS", async () => {
assert.ok(result.ok);
assert.equal(result.value?.fpAbi, 5);
});
for (const little of [true, false]) {
for (const availableBytes of [0, 7, 47]) {
it(`resolves a carved ${little ? "LE" : "BE"} ELF with ${availableBytes} captured section-table bytes`, async () => {
const original = fixture(little);
const bytes = Buffer.concat([original, Buffer.alloc(availableBytes)]);
const sectionOffset = original.length + (availableBytes === 0 ? 1 : 0);
if (little) bytes.writeUInt32LE(sectionOffset, 32);
else bytes.writeUInt32BE(sectionOffset, 32);
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.abiFlags?.fpAbi, 5);
assert.equal(ghidraMipsUnsupportedReason(resolved), null);
assert.match(
resolved.mips.limitations?.join(" ") ?? "",
/section header table/u,
);
});
}
}
it("keeps declarations in uncaptured section headers unknown when program ABI data is absent", async () => {
const bytes = fixture();
bytes.writeUInt32LE(bytes.length + 1, 32);
bytes.writeUInt32LE(0, 52);
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.abiFlags, null);
assert.match(
ghidraMipsUnsupportedReason(resolved) ?? "",
/inspected ABI.*unavailable headers remain unknown/u,
);
});
for (const failure of ["conflicting", "outside", "duplicate"]) {
it(`retains ${failure} ABI errors in a partly captured section table`, async () => {
const original = fixture();
const count = failure === "duplicate" ? 2 : 1;
const bytes = Buffer.concat([original, Buffer.alloc(count * 40 + 7)]);
bytes.writeUInt32LE(original.length, 32);
bytes.writeUInt16LE(count + 1, 48);
original.copy(bytes, abiOffset + 24, abiOffset, abiOffset + 24);
bytes[abiOffset + 24 + 7] = 6;
for (let i = 0; i < count; i += 1) {
const position = original.length + i * 40;
bytes.writeUInt32LE(0x7000002a, position + 4);
bytes.writeUInt32LE(
failure === "outside"
? 0xfffffff0
: failure === "conflicting"
? abiOffset + 24
: abiOffset,
position + 16,
);
bytes.writeUInt32LE(24, position + 20);
}
const result = await inspect(bytes);
assert.ok(!result.ok);
assert.match(result.error, new RegExp(failure, "u"));
});
}
it("supports a section-only ABI record without assuming its name", async () => {
const bytes = fixture();
bytes.writeUInt32LE(0, 28);
@@ -239,49 +323,21 @@ it("preserves the caller's cancellation error during table inspection", async ()
for (const little of [true, false]) {
it(`resolves full-file MIPS ABI identity before provider selection (${little ? "LE" : "BE"})`, async () => {
const directory = await createTestTempDirectory("rea-mips-resolution-");
const path = join(directory, "fixture.elf");
const bytes = fixture(little);
await writeFile(path, bytes);
const result = await parseBinaryTarget(path);
if (!result.ok) throw result.error;
assert.equal(result.value.architecture, "mips");
assert.equal(result.value.format, "elf");
assert.equal(
result.value.sha256,
createHash("sha256").update(bytes).digest("hex"),
);
if (
result.value.kind !== "executable" ||
result.value.architecture !== "mips"
)
throw new Error("Expected a resolved MIPS executable");
assert.equal(result.value.mips.byteOrder, little ? "little" : "big");
assert.equal(result.value.mips.abiFlags?.fpAbi, 5);
assert.equal(ghidraMipsUnsupportedReason(result.value), null);
assert.deepEqual(await readFile(path), bytes);
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.byteOrder, little ? "little" : "big");
assert.equal(resolved.mips.abiFlags?.fpAbi, 5);
assert.equal(ghidraMipsUnsupportedReason(resolved), null);
});
}
it("keeps missing ABI data unknown through the public target resolver", async () => {
const directory = await createTestTempDirectory("rea-mips-resolution-");
const path = join(directory, "fixture.elf");
const bytes = fixture();
bytes.writeUInt32LE(0, 52);
bytes.writeUInt32LE(0, 172);
await writeFile(path, bytes);
const result = await parseBinaryTarget(path);
if (!result.ok) throw result.error;
if (
result.value.kind !== "executable" ||
result.value.architecture !== "mips"
)
throw new Error("Expected a resolved MIPS executable");
assert.equal(result.value.mips.abiFlags, null);
assert.match(
ghidraMipsUnsupportedReason(result.value) ?? "",
/inspected ABI/u,
);
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.abiFlags, null);
assert.match(ghidraMipsUnsupportedReason(resolved) ?? "", /inspected ABI/u);
});
it("projects a malformed out-of-probe ABI record as a target error", async () => {