fix(ghidra)!: distinguish unlisted instruction addresses (#7823)

* fix(ghidra)!: distinguish unlisted instruction addresses

BREAKING CHANGE: Ghidra now returns not-in-listing for in-memory addresses without listed instructions or defined data. Consumers matching the former undecodable value must handle the new status.

* fix(ghidra): invalidate snapshots with old instruction status

* test(ghidra): update raw profile commitment goldens
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 04:59:04 +08:00
1 parent b0880d660f
commit bd687bbc40
8 files changed
+83 -16

No files matched your search

+50
View File
@@ -13,8 +13,10 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
import { nativeInstructionSchema } from "../dist/domain/native/nativeInstruction.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { buildDosComFixture } from "../tests/conformance/ghidra/dos-com-fixture.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
@@ -158,6 +160,54 @@ try {
instruction.bytes.replaceAll(" ", "").toLowerCase(),
fixture.entry_hex,
);
// This NOP follows the entry's terminating INT 20h and is not reached by
// either function. It remains valid instruction bytes outside the listing.
const unlistedAddress = "0x1013f";
const unlistedBytes = await call("read_bytes", {
address: unlistedAddress,
length: 1,
});
assert.equal(unlistedBytes.bytes_hex, "90");
const unlistedResponse = await client.callTool({
name: "inspect_native_instruction",
arguments: { address: unlistedAddress },
});
const unlistedEvidence = parseEvidence(
requireMcpOutput(unlistedResponse, "inspect_native_instruction"),
);
const unlisted = unlistedEvidence.normalized_result;
assert.equal(unlisted.status, "not-in-listing");
assert.equal(unlisted.bytes, null);
assert.ok(
unlisted.limitations.some((item) =>
item.includes("did not attempt a decode"),
),
);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_native_instruction",
);
assert.ok(advertised?.outputSchema);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
assert.equal(ajv.validateSchema(advertised.outputSchema), true);
assert.equal(
ajv.validate(advertised.outputSchema, unlistedResponse.structuredContent),
true,
JSON.stringify(ajv.errors),
);
assert.ok(
nativeInstructionSchema.safeParse({ ...unlisted, status: "undecodable" })
.success,
);
assert.equal(
(await call("resolve_native_call_targets", { address: unlistedAddress }))
.status,
"unavailable",
);
assert.deepEqual(
(await cli("inspect-native-instruction", unlistedAddress))
.normalized_result,
unlisted,
);
const cliImage = await cli("inspect-native-load-image");
const stable = (x) => ({
...x,