mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
fix(verify): preserve Ghidra oracle state and coverage (#9994)
* fix(verify): check checkpoint state before restoring fixtures * fix(verify): require applicable Ghidra decoder controls * refactor(verify): require explicit instruction fixture facts
This commit is contained in:
1 parent
aece2e7e16
commit
ea00ca2d46
8 files changed
+307
-26
No files matched your search
@@ -0,0 +1,12 @@
|
||||
/** Require the CLI's explicit refusal instead of accepting an unrelated failure. */
|
||||
export function requireCheckpointCliRefusal(
|
||||
operation: () => Promise<unknown>,
|
||||
expectedCode: string,
|
||||
): Promise<{ readonly code: string; readonly [key: string]: unknown }>;
|
||||
|
||||
/** Verify refusal before the caller restores its deliberately invalid manifest. */
|
||||
export function assertCheckpointRefusalPreservesManifest(
|
||||
operation: () => Promise<unknown>,
|
||||
manifestPath: string,
|
||||
expectedBytes: Uint8Array,
|
||||
): Promise<{ readonly code: string; readonly [key: string]: unknown }>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
/** Require the CLI's explicit refusal instead of accepting an unrelated failure. */
|
||||
export async function requireCheckpointCliRefusal(operation, expectedCode) {
|
||||
let refusal;
|
||||
try {
|
||||
await operation();
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1, cause.stderr);
|
||||
refusal = JSON.parse(cause.stdout);
|
||||
}
|
||||
assert.ok(refusal, "CLI unexpectedly accepted unavailable checkpoint state");
|
||||
assert.equal(refusal.code, expectedCode, JSON.stringify(refusal));
|
||||
return refusal;
|
||||
}
|
||||
|
||||
/** Verify refusal before the caller restores its deliberately invalid manifest. */
|
||||
export async function assertCheckpointRefusalPreservesManifest(
|
||||
operation,
|
||||
manifestPath,
|
||||
expectedBytes,
|
||||
) {
|
||||
const refusal = await requireCheckpointCliRefusal(
|
||||
operation,
|
||||
"configuration_invalid",
|
||||
);
|
||||
assert.ok(
|
||||
(await readFile(manifestPath)).equals(expectedBytes),
|
||||
`Refused CLI command changed the checkpoint manifest: ${manifestPath}`,
|
||||
);
|
||||
return refusal;
|
||||
}
|
||||
@@ -212,7 +212,12 @@ export async function verifyGhidraBoundaries(
|
||||
);
|
||||
const names = await call("list_names");
|
||||
const batchIdentity = await verifyGhidraBatchIdentity(call, names);
|
||||
const instructionFlow = await verifyGhidraInstructionFlow(call, names, cli);
|
||||
const instructionFlow = await verifyGhidraInstructionFlow(
|
||||
call,
|
||||
names,
|
||||
cli,
|
||||
target,
|
||||
);
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { BinaryTarget } from "../../src/domain/binaryTargetTypes.js";
|
||||
|
||||
/** Check source-owned inventory controls using independently parsed target facts. */
|
||||
export function verifyGhidraInstructionFlow(
|
||||
call: (
|
||||
operation: string,
|
||||
parameters: Readonly<Record<string, string>>,
|
||||
) => Promise<unknown>,
|
||||
names: readonly { readonly address: string; readonly value: string }[],
|
||||
cli: (command: string, address: string) => Promise<unknown>,
|
||||
target: Pick<BinaryTarget, "format" | "architecture"> | null,
|
||||
): Promise<{
|
||||
readonly return_instruction: string;
|
||||
readonly decoded_pcode_return: true;
|
||||
readonly return_cli_mcp_parity: true;
|
||||
readonly explicit_x86_elf_controls: number;
|
||||
readonly explicit_control_scope: string;
|
||||
readonly generic_unknown_preserved: true;
|
||||
}>;
|
||||
@@ -1,7 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify decoded return evidence and distinct non-return flows on owned fixtures. */
|
||||
export async function verifyGhidraInstructionFlow(call, names, cli) {
|
||||
export async function verifyGhidraInstructionFlow(call, names, cli, target) {
|
||||
// inventory.c emits these controls only for an x86-64 ELF compilation.
|
||||
// Provider name omission cannot establish whether that source branch applied.
|
||||
const controlsRequired =
|
||||
target !== null &&
|
||||
target.format === "elf" &&
|
||||
target.architecture === "x86_64";
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
@@ -31,6 +37,11 @@ export async function verifyGhidraInstructionFlow(call, names, cli) {
|
||||
let checked = 0;
|
||||
for (const [symbol, mnemonic, bytes] of controls) {
|
||||
const procedure = names.find((item) => item.value === symbol);
|
||||
if (controlsRequired)
|
||||
assert.ok(
|
||||
procedure,
|
||||
`Source fixture lacks expected decoder control ${symbol}`,
|
||||
);
|
||||
if (procedure === undefined) continue;
|
||||
const before = await call("resolve_containing_procedure", {
|
||||
address: procedure.address,
|
||||
@@ -72,9 +83,11 @@ export async function verifyGhidraInstructionFlow(call, names, cli) {
|
||||
return_cli_mcp_parity: true,
|
||||
explicit_x86_elf_controls: checked,
|
||||
explicit_control_scope:
|
||||
checked === 0
|
||||
? "Not present on this host-native fixture format/architecture"
|
||||
: "x86-64 ELF",
|
||||
target === null
|
||||
? "Unknown: source fixture format/architecture was not established"
|
||||
: controlsRequired
|
||||
? "x86-64 ELF"
|
||||
: "Not compiled for this fixture format/architecture",
|
||||
generic_unknown_preserved: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import {
|
||||
requireMcpOperationResult,
|
||||
requireMcpToolError,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import {
|
||||
requireCheckpointCliRefusal,
|
||||
assertCheckpointRefusalPreservesManifest,
|
||||
} from "./lib/ghidra-checkpoint-refusal.mjs";
|
||||
import {
|
||||
createVerifierRun,
|
||||
completeVerifierRun,
|
||||
@@ -109,18 +113,6 @@ async function cli(procedure, flags = [], path = target) {
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
async function refuseCli(procedure, flags, expected) {
|
||||
let refusal;
|
||||
try {
|
||||
await cli(procedure, flags);
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1, cause.stderr);
|
||||
refusal = JSON.parse(cause.stdout);
|
||||
}
|
||||
assert.ok(refusal, "CLI unexpectedly accepted unavailable checkpoint state");
|
||||
assert.equal(refusal.code, expected, JSON.stringify(refusal));
|
||||
return refusal;
|
||||
}
|
||||
async function connect(environment = {}) {
|
||||
const owner = createVerifierMcpOwner();
|
||||
const client = new Client({
|
||||
@@ -346,9 +338,8 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
|
||||
await phase(
|
||||
"an older immutable REA snapshot refuses the changed checkpoint",
|
||||
async () => {
|
||||
const refusal = await refuseCli(
|
||||
"rea_checkpoint_leaf",
|
||||
["--snapshot", snapshotPath],
|
||||
const refusal = await requireCheckpointCliRefusal(
|
||||
() => cli("rea_checkpoint_leaf", ["--snapshot", snapshotPath]),
|
||||
"evidence_integrity_mismatch",
|
||||
);
|
||||
assert.ok(refusal.details.binding_mismatch.profile_digest);
|
||||
@@ -390,12 +381,16 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
|
||||
await phase(
|
||||
"target/profile mismatch is refused without replacing the saved state",
|
||||
async () => {
|
||||
const attemptedBytes = Buffer.from(
|
||||
JSON.stringify({ ...current, target_sha256: "0".repeat(64) }),
|
||||
);
|
||||
try {
|
||||
await writeFile(
|
||||
await writeFile(manifestPath, attemptedBytes);
|
||||
await assertCheckpointRefusalPreservesManifest(
|
||||
() => cli("rea_checkpoint_retained"),
|
||||
manifestPath,
|
||||
JSON.stringify({ ...current, target_sha256: "0".repeat(64) }),
|
||||
attemptedBytes,
|
||||
);
|
||||
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
|
||||
} finally {
|
||||
await writeFile(manifestPath, bytes);
|
||||
}
|
||||
@@ -405,9 +400,14 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
|
||||
await phase(
|
||||
"malformed manifest is refused without a fresh import",
|
||||
async () => {
|
||||
const attemptedBytes = Buffer.from("{");
|
||||
try {
|
||||
await writeFile(manifestPath, "{");
|
||||
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
|
||||
await writeFile(manifestPath, attemptedBytes);
|
||||
await assertCheckpointRefusalPreservesManifest(
|
||||
() => cli("rea_checkpoint_retained"),
|
||||
manifestPath,
|
||||
attemptedBytes,
|
||||
);
|
||||
} finally {
|
||||
await writeFile(manifestPath, bytes);
|
||||
}
|
||||
@@ -426,7 +426,10 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
|
||||
await copyFile(packed, backup);
|
||||
try {
|
||||
await writeFile(packed, "corrupted packed data");
|
||||
await refuseCli("rea_checkpoint_retained", [], "configuration_invalid");
|
||||
await requireCheckpointCliRefusal(
|
||||
() => cli("rea_checkpoint_retained"),
|
||||
"configuration_invalid",
|
||||
);
|
||||
} finally {
|
||||
await copyFile(backup, packed);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { assertCheckpointRefusalPreservesManifest } from "../../../../scripts/lib/ghidra-checkpoint-refusal.mjs";
|
||||
|
||||
const checkpointFile = async (bytes: Uint8Array) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-checkpoint-refusal-"));
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }));
|
||||
const path = join(root, "current.json");
|
||||
await writeFile(path, bytes);
|
||||
return path;
|
||||
};
|
||||
|
||||
const refusal = (code = "configuration_invalid", exitCode = 1) =>
|
||||
Object.assign(new Error("inert CLI refusal"), {
|
||||
code: exitCode,
|
||||
stdout: JSON.stringify({ code }),
|
||||
stderr: "controlled CLI diagnostic",
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "mismatched target identity",
|
||||
bytes: Buffer.from(JSON.stringify({ target_sha256: "0".repeat(64) })),
|
||||
},
|
||||
{ name: "malformed manifest", bytes: Buffer.from("{") },
|
||||
])("checks refusal-time manifest bytes for $name", async ({ bytes }) => {
|
||||
const path = await checkpointFile(bytes);
|
||||
await expect(
|
||||
assertCheckpointRefusalPreservesManifest(
|
||||
async () => {
|
||||
throw refusal();
|
||||
},
|
||||
path,
|
||||
bytes,
|
||||
),
|
||||
).resolves.toEqual({ code: "configuration_invalid" });
|
||||
expect((await readFile(path)).equals(bytes)).toBe(true);
|
||||
|
||||
await expect(
|
||||
assertCheckpointRefusalPreservesManifest(
|
||||
async () => {
|
||||
// The same expected refusal must not hide replacement of the owned state.
|
||||
await writeFile(path, JSON.stringify({ replaced: true }));
|
||||
throw refusal();
|
||||
},
|
||||
path,
|
||||
bytes,
|
||||
),
|
||||
).rejects.toThrow(/changed the checkpoint manifest/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "unexpected success", operation: async () => undefined },
|
||||
{
|
||||
name: "unrelated exit",
|
||||
operation: async () => {
|
||||
throw refusal("configuration_invalid", 2);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different refusal",
|
||||
operation: async () => {
|
||||
throw refusal("capability_unavailable");
|
||||
},
|
||||
},
|
||||
])(
|
||||
"rejects $name while checking manifest preservation",
|
||||
async ({ operation }) => {
|
||||
const bytes = Buffer.from("{");
|
||||
const path = await checkpointFile(bytes);
|
||||
await expect(
|
||||
assertCheckpointRefusalPreservesManifest(operation, path, bytes),
|
||||
).rejects.toThrow();
|
||||
expect((await readFile(path)).equals(bytes)).toBe(true);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { verifyGhidraInstructionFlow } from "../../../../scripts/lib/real-ghidra-instruction-flow.mjs";
|
||||
|
||||
const leaf = { address: "0x100", value: "rea_ghidra_inventory_leaf" };
|
||||
// Static decoder controls from inventory.c; no fixture bytes are executed.
|
||||
const controls = [
|
||||
["0x200", "rea_ghidra_flow_return", "RET", "c3"],
|
||||
["0x210", "rea_ghidra_flow_cleanup_return", "RET", "c21000"],
|
||||
["0x220", "rea_ghidra_flow_shared_tail", "JMP", "ebfe"],
|
||||
["0x230", "rea_ghidra_flow_trap", "UD2", "0f0b"],
|
||||
] as const;
|
||||
|
||||
const observedInstruction = (
|
||||
address: string,
|
||||
mnemonic = "RET",
|
||||
bytes = "c3",
|
||||
) => ({
|
||||
address,
|
||||
status: "decoded",
|
||||
mnemonic,
|
||||
bytes,
|
||||
operands: [{ raw: "0x10" }],
|
||||
flow: {
|
||||
kind: mnemonic === "RET" ? "return" : "branch",
|
||||
classification_evidence: [
|
||||
{ source: "ghidra-listing-flow-type" },
|
||||
...(mnemonic === "RET"
|
||||
? [{ source: "ghidra-instruction-pcode", value: "RETURN" }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
const instructions = new Map([
|
||||
[leaf.address, observedInstruction(leaf.address)],
|
||||
...controls.map(
|
||||
([address, , mnemonic, bytes]) =>
|
||||
[address, observedInstruction(address, mnemonic, bytes)] as const,
|
||||
),
|
||||
]);
|
||||
const call = async (
|
||||
operation: string,
|
||||
input: Readonly<Record<string, string>>,
|
||||
) => {
|
||||
if (operation === "read_function_instructions")
|
||||
return { instructions: [`${input.procedure}: fixture`] };
|
||||
if (operation === "inspect_native_instruction")
|
||||
return input.address === "0x0"
|
||||
? {
|
||||
status: "outside-memory",
|
||||
flow: { kind: "unavailable", classification_evidence: [] },
|
||||
}
|
||||
: instructions.get(input.address!);
|
||||
if (operation === "resolve_native_call_targets")
|
||||
return { status: "not-call" };
|
||||
if (operation === "resolve_containing_procedure")
|
||||
return { address: input.address };
|
||||
throw new Error(`Unexpected fixture operation ${operation}`);
|
||||
};
|
||||
const cli = async (_command: string, address: string) =>
|
||||
instructions.get(address);
|
||||
const names = controls.map(([address, value]) => ({ address, value }));
|
||||
|
||||
it("checks every retained inventory control for an independently parsed x86-64 ELF", async () => {
|
||||
const result = await verifyGhidraInstructionFlow(
|
||||
call,
|
||||
[leaf, ...names],
|
||||
cli,
|
||||
{
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
},
|
||||
);
|
||||
expect(result.explicit_x86_elf_controls).toBe(4);
|
||||
expect(result.decoded_pcode_return).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["all", "one"])(
|
||||
"refuses %s omitted source controls on x86-64 ELF",
|
||||
async (omission) => {
|
||||
await expect(
|
||||
verifyGhidraInstructionFlow(
|
||||
call,
|
||||
[leaf, ...(omission === "all" ? [] : names.slice(0, -1))],
|
||||
cli,
|
||||
{ format: "elf", architecture: "x86_64" },
|
||||
),
|
||||
).rejects.toThrow(/Source fixture lacks expected decoder control/u);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ format: "mach-o", architecture: "x86_64" },
|
||||
{ format: "elf", architecture: "arm64" },
|
||||
{ format: "pe", architecture: "x86_64" },
|
||||
] as const)(
|
||||
"keeps controls non-applicable for $format/$architecture",
|
||||
async (target) => {
|
||||
const result = await verifyGhidraInstructionFlow(call, [leaf], cli, target);
|
||||
expect(result.explicit_x86_elf_controls).toBe(0);
|
||||
expect(result.generic_unknown_preserved).toBe(true);
|
||||
expect(result.explicit_control_scope).toContain("Not compiled");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, 1, 4])(
|
||||
"keeps explicit unknown applicability with %i observed controls",
|
||||
async (observed) => {
|
||||
const result = await verifyGhidraInstructionFlow(
|
||||
call,
|
||||
[leaf, ...names.slice(0, observed)],
|
||||
cli,
|
||||
null,
|
||||
);
|
||||
expect(result.explicit_x86_elf_controls).toBe(observed);
|
||||
expect(result.explicit_control_scope).toContain("Unknown");
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user