Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/ghidra-mips.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ PSP/Allegrex has a [separate static-ELF profile](ghidra-psp.md) tracked in

## Admission boundary

After building, `node scripts/verify-real-ghidra-mips-boundaries.mjs` checks both
byte orders through production CLI/MCP. GNU readelf independently decodes the
soft-float attribute fixture; declared-o32 MIPS I and unsupported GNU soft-float
inputs are refused, while recordless MIPS I and MIPS32 controls produce real
Ghidra dossiers and exact loaded bytes. These observations do not establish
MIPS I instruction-subset or load-delay correctness. Supply existing Ghidra/Java
and GNU readelf (`REA_READELF`). Source hashes are checked after inspection,
and no target is executed. Cleanup checks both process lineage and an independent
run-token sweep, so reparenting cannot hide an owned survivor.

ELF `EM_MIPS` is retained as the provider-neutral `mips` family. The target also
retains class, byte order, `e_type`, raw `e_flags`, and inspected ABI declarations.
Recognition does not mean every provider or MIPS variant is supported. Existing
Expand Down
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"scripts/verify/fridaBindingsCompatibility.ts",
"scripts/verify/termux/browser.mjs",
"scripts/verify-real-ghidra-noreturn.mjs",
"scripts/verify-real-ghidra-mips-boundaries.mjs",
"scripts/verify-real-ghidra-decompiler-failure.mjs",
"scripts/verify-real-ghidra-decompile-timeout.mjs",
"tests/processOwnershipRunner.ts"
Expand Down
14 changes: 14 additions & 0 deletions scripts/lib/verifier-token-cleanup.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {
ProcessCleanupResult,
ProcessOwnershipHost,
} from "../../src/process/ProcessOwnership.js";
import type { VerifierRunStart } from "./verifier-run.mjs";

/** Capture the prelaunch identities used by the canonical, PPID-independent sweep. */
export function prepareVerifierTokenCleanup(
run: VerifierRunStart,
suppliedHost?: ProcessOwnershipHost,
): Promise<{
verify(): Promise<ProcessCleanupResult>;
close(): Promise<void>;
}>;
34 changes: 34 additions & 0 deletions scripts/lib/verifier-token-cleanup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { verifyNoTokenOwnedProcesses } from "../../dist/process/ProcessOwnership.js";
import { ProcessOwnershipEvidenceOwner } from "../../dist/process/ProcessOwnershipEvidence.js";
import { createSystemProcessOwnershipHost } from "../../dist/process/ProcessOwnershipObservation.js";

/** Capture the prelaunch identities used by the canonical, PPID-independent sweep. */
export async function prepareVerifierTokenCleanup(run, suppliedHost) {
const host = suppliedHost ?? createSystemProcessOwnershipHost();
try {
await host.prepare?.();
const baseline = await host.captureBaseline?.();
if (baseline === undefined)
throw new Error("Verifier process ownership baseline is unavailable");
const launcher = (await host.listProcesses()).find(
({ pid }) => pid === run.verifier_pid,
);
if (launcher === undefined)
throw new Error("Verifier launcher is absent from the process snapshot");
const relation = {
leaderPid: run.verifier_pid,
processGroupId: launcher.processGroupId,
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
};
return {
verify: () =>
verifyNoTokenOwnedProcesses(run.run_id, host, baseline, relation),
Comment thread
Bobtoshi marked this conversation as resolved.
Resolved
close: async () => {
await host.close?.();
},
};
} catch (cause) {
await host.close?.();
throw cause;
}
}
227 changes: 227 additions & 0 deletions scripts/verify-real-ghidra-mips-boundaries.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { buildMips1Fixture } from "../tests/conformance/ghidra/mips1-fixture.mjs";
import {
requireMcpOperationResult,
requireMcpToolError,
} from "./lib/mcp-verifier-results.mjs";
import { prepareVerifierTokenCleanup } from "./lib/verifier-token-cleanup.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";

// Static source-owned controls. Never execute an ELF or acquire a toolchain.
if (!["darwin", "linux"].includes(process.platform))
throw new Error(
"MIPS boundary verification requires a Linux/macOS Ghidra host with verified process lineage.",
);
if (!process.env.GHIDRA_INSTALL_DIR || !process.env.JAVA_HOME)
throw new Error(
"Supply GHIDRA_INSTALL_DIR and JAVA_HOME for the MIPS32 positive control.",
);
const execute = promisify(execFile);
const run = createVerifierRun();
const root = await mkdtemp(join(tmpdir(), "rea-mips-boundaries-"));
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const cli = fileURLToPath(new URL("./rea.mjs", import.meta.url));
// Only declared tool prerequisites and resource settings reach REA children.
// Ambient REA seeds, analyzer controls and session settings cannot alter controls.
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
TMPDIR: root,
REA_ANALYSIS_PROVIDER: "ghidra",
REA_LOG_LEVEL: "silent",
REA_PROCESS_RUN_ID: run.run_id,
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
JAVA_HOME: process.env.JAVA_HOME,
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;
const cleanup = await prepareVerifierTokenCleanup(run);
try {
for (const byteOrder of ["little", "big"]) {
const original = await buildMips1Fixture(root, byteOrder);
const initial = await readFile(original.path);
const legacy = Buffer.from(initial);
const u32 = (bytes, offset, value) =>
byteOrder === "little"
? bytes.writeUInt32LE(value, offset)
: bytes.writeUInt32BE(value, offset);
const u16 = (bytes, offset, value) =>
byteOrder === "little"
? bytes.writeUInt16LE(value, offset)
: bytes.writeUInt16BE(value, offset);
u32(legacy, 36, 0x50000001); // Existing recordless MIPS32 lane.
const legacyPath = join(root, `legacy-${byteOrder}.elf`);
await writeFile(legacyPath, legacy);
const recordlessMips1 = Buffer.from(initial);
u32(recordlessMips1, 36, 0x1);
const recordlessMips1Path = join(root, `recordless-mips1-${byteOrder}.elf`);
await writeFile(recordlessMips1Path, recordlessMips1);
const gnu = Buffer.alloc(0x1200);
legacy.copy(gnu);
u32(gnu, 68, gnu.length);
u32(gnu, 72, gnu.length);
u16(gnu, 48, 6);
u32(gnu, 0x1188 + 4, 0x6ffffff5);
u32(gnu, 0x1188 + 16, 0x11d0);
u32(gnu, 0x1188 + 20, 16);
gnu[0x11d0] = 0x41;
u32(gnu, 0x11d1, 15);
gnu.write("gnu\0", 0x11d5, "ascii");
gnu[0x11d9] = 1;
u32(gnu, 0x11da, 7);
gnu[0x11de] = 4; // Tag_GNU_MIPS_ABI_FP
gnu[0x11df] = 3; // Soft float.
const gnuPath = join(root, `gnu-fp-${byteOrder}.elf`);
await writeFile(gnuPath, gnu);
const { stdout: attributes } = await execute(
process.env.REA_READELF ?? "readelf",
["-A", gnuPath],
{ env: { PATH: env.PATH, LC_ALL: "C" } },
);
Comment thread
Bobtoshi marked this conversation as resolved.
Resolved
assert.match(attributes, /Tag_GNU_MIPS_ABI_FP.*Soft float/iu);
const transport = new StdioClientTransport({
command: process.execPath,
args: [cli, "mcp"],
env,
stderr: "inherit",
});
const client = new Client({ name: "rea-mips-boundaries", version: "1" });
const invoke = (name, args = {}) =>
client.callTool({ name, arguments: args }, { timeout: 240_000 });
const call = async (name, args = {}) =>
requireMcpOperationResult(await invoke(name, args), name);
try {
await client.connect(transport);
for (const [path, pattern] of [
[
original.path,
/requires EF_MIPS_ARCH_32R2 for headers that declare an ABI/u,
],
[gnuPath, /GNU floating-point ABI attribute/u],
]) {
const error = requireMcpToolError(
await invoke("open_binary", { path, provider_id: "ghidra" }),
);
assert.match(JSON.stringify(error), pattern);
let stdout;
try {
await execute(
process.execPath,
[
cli,
"function",
path,
original.entry,
"--provider",
"ghidra",
"--full-output",
"--json",
],
{ env, timeout: 240_000 },
);
assert.fail("CLI admitted an unsupported MIPS declaration");
} catch (cause) {
assert.equal(cause.code, 1);
stdout = cause.stdout;
}
const reply = JSON.parse(stdout);
assert.equal(reply.ok, false);
assert.match(JSON.stringify(reply), pattern);
}
for (const [path, expected] of [
[legacyPath, legacy],
[recordlessMips1Path, recordlessMips1],
]) {
await call("open_binary", { path, provider_id: "ghidra" });
const dossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: original.entry,
detail: "complete",
}),
);
assert.ok(dossier.pseudocode.length > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the MIPS-I language pin

When the recordless MIPS-I override is missing or wrong, Ghidra still imports this fixture but auto-selects a 64-bit MIPS language (as documented in docs/providers/ghidra-mips.md:46-49), so nonempty pseudocode and exact read_bytes can both pass without exercising the pinned MIPS:<LE|BE>:32:default lane. The sibling real-MIPS verifier explicitly checks inspect_native_load_image().observations.language_id; add that assertion for this MIPS-I control before reporting its admission as exercised.

AGENTS.md reference: AGENTS.md:L31-L33

Useful? React with 👍 / 👎.

const loaded = await call("read_bytes", {
address: original.entry,
length: 24,
});
assert.equal(
loaded.bytes_hex,
expected.subarray(0x1000, 0x1018).toString("hex"),
);
await call("close_binary");
}
assert.deepEqual(await readFile(original.path), initial);
assert.deepEqual(await readFile(gnuPath), gnu);
assert.deepEqual(await readFile(legacyPath), legacy);
assert.deepEqual(await readFile(recordlessMips1Path), recordlessMips1);
reports.push({
byte_order: byteOrder,
declared_o32_mips1: "refused by CLI and MCP",
recordless_mips1:
"real Ghidra dossier and exact loaded bytes; instruction subset/load-delay semantics unverified",
gnu_fp_attributes:
"independently decoded soft-float declaration; refused by CLI and MCP",
existing_mips32_control: "real Ghidra dossier and exact loaded bytes",
hashes: {
mips1: digest(initial),
gnu: digest(gnu),
legacy: digest(legacy),
recordless_mips1: digest(recordlessMips1),
},
source_unchanged: true,
target_executed: false,
});
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
}
} catch (cause) {
primaryFailure = cause;
}
let tokenCleanup;
try {
tokenCleanup = await cleanup.verify();
} finally {
await cleanup.close();
}
const completed = await completeVerifierRun(run);
if (
!tokenCleanup.cleaned ||
completed.process_lineage.status !== "verified" ||
Comment thread
Bobtoshi marked this conversation as resolved.
Resolved
completed.process_lineage.descendants.length !== 0
Comment thread
Bobtoshi marked this conversation as resolved.
Resolved
)
throw new Error(
`Cleanup unverified (${JSON.stringify(tokenCleanup)}); retained diagnostic workspace: ${root}`,
{ cause: primaryFailure },
);
if (primaryFailure !== undefined)
throw new Error(
`MIPS boundary verification failed; retained workspace: ${root}`,
{ cause: primaryFailure },
);
await rm(root, { recursive: true, force: true });
console.log(
JSON.stringify(
{ ...completed, token_cleanup: tokenCleanup, status: "passed", reports },
null,
2,
),
);
Loading
Loading