mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
fix(ghidra): commit analysis profiles to the bridge script sources (#10562)
* fix(ghidra): commit analysis profiles to the bridge script sources Snapshot reuse for Ghidra depends on the analysis profile digest, but the profile committed only to hand-bumped semantic markers. A bridge change that forgot to bump its marker left earlier snapshots admissible (#3664). Add ghidraBridgeSourcesDigest, a SHA-256 over every packaged bridge source by name, and commit it as bridge_sources_sha256 in every Ghidra profile, for new imports and existing projects alike. A changed script now changes the profile digest without anyone remembering a marker. Existing snapshots are not reused once, after upgrading. GhidraRawProfileDigest.test.ts pinned literal digests that every bridge or marker change had to re-record; it now checks the committed parameters and that the digest is the profile owner's digest of them. A new test checks that the source list covers every packaged .java file. * fix(ghidra): frame bridge digest metadata as a tuple
This commit is contained in:
1 parent
e68e548a3d
commit
3fbdf0057b
4 files changed
+78
-18
No files matched your search
@@ -17,6 +17,7 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { ghidraDisabledAnalyzerPreset } from "./GhidraDisabledAnalyzers.js";
|
||||
import { ghidraBridgeSourcesDigest } from "./GhidraBridgeSources.js";
|
||||
import type { GhidraInstallationInspection } from "./GhidraInstallation.js";
|
||||
import {
|
||||
isLegacyMipsAdmission,
|
||||
@@ -54,7 +55,7 @@ import {
|
||||
} from "../domain/native/nativeAotPe.js";
|
||||
|
||||
/** Shared analysis commitments for raw and header-selected imports. */
|
||||
const sharedAnalysisSemantics = {
|
||||
const sharedAnalysisSemanticMarkers = {
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
byte_search_boundary: "contiguous-eligible-initialized-blocks-v1",
|
||||
load_image_observations: "source-mappings-entry-context-relocation-symbol-v3",
|
||||
@@ -77,6 +78,11 @@ const sharedAnalysisSemantics = {
|
||||
byte_search_coverage: "separate-loaded-overlays-v1",
|
||||
function_boundary_observations: "ghidra-function-boundary-limitations-v5",
|
||||
} as const;
|
||||
/** Marker revisions plus the bridge sources that produce every Ghidra result. */
|
||||
const sharedAnalysisSemantics = () => ({
|
||||
...sharedAnalysisSemanticMarkers,
|
||||
bridge_sources_sha256: ghidraBridgeSourcesDigest(),
|
||||
});
|
||||
const loaderRelocationSemantics = "volatile-unapplied-relocation-blocks-v1";
|
||||
// Read-only sessions record relocation limitations without marking blocks volatile.
|
||||
const readOnlyLoaderRelocationSemantics = "read-only-relocation-limitations-v1";
|
||||
@@ -124,7 +130,7 @@ const binaryLoaderPolicy = (
|
||||
architecture: target.architecture,
|
||||
available_architectures: [...target.availableArchitectures].sort(),
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
...sharedAnalysisSemantics,
|
||||
...sharedAnalysisSemantics(),
|
||||
no_return_repair:
|
||||
installation.platform === "win32"
|
||||
? "disabled-windows-p0"
|
||||
@@ -329,7 +335,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
windows_arm_pe_machine: target.peMachine ?? null,
|
||||
}
|
||||
: {}),
|
||||
...sharedAnalysisSemantics,
|
||||
...sharedAnalysisSemantics(),
|
||||
// Windows P0 has no database-mutation authority: limitations only.
|
||||
loader_relocation_semantics:
|
||||
installation.platform === "win32"
|
||||
@@ -499,7 +505,7 @@ export const existingGhidraAnalysisProfile = (
|
||||
architecture: target.architecture ?? null,
|
||||
...(mipsElf === undefined ? {} : { mips_elf: mipsElf }),
|
||||
import_mode: "existing-project-private-copy-v1",
|
||||
...sharedAnalysisSemantics,
|
||||
...sharedAnalysisSemantics(),
|
||||
selected_project_relationship_coverage: "prior-analysis-unknown-v1",
|
||||
annotation_policy: "read-only-existing-project-v1",
|
||||
loader_relocation_semantics: readOnlyLoaderRelocationSemantics,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
GHIDRA_BRIDGE_SOURCES,
|
||||
ghidraBridgeSourcesDigest,
|
||||
} from "./GhidraBridgeSources.js";
|
||||
|
||||
const directory = new URL("../../bridge/ghidra/", import.meta.url);
|
||||
|
||||
it("lists every packaged Ghidra bridge source", async () => {
|
||||
const java = (await readdir(directory)).filter((name) =>
|
||||
name.endsWith(".java"),
|
||||
);
|
||||
expect(Object.values(GHIDRA_BRIDGE_SOURCES).sort()).toEqual(java.sort());
|
||||
});
|
||||
|
||||
it("commits to the name and bytes of each bridge source", async () => {
|
||||
const hash = createHash("sha256");
|
||||
for (const name of Object.values(GHIDRA_BRIDGE_SOURCES).sort()) {
|
||||
const bytes = await readFile(new URL(name, directory));
|
||||
hash.update(JSON.stringify([name, bytes.byteLength])).update(bytes);
|
||||
}
|
||||
expect(ghidraBridgeSourcesDigest()).toBe(hash.digest("hex"));
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/** Required packaged sources, selected by their runtime role rather than order. */
|
||||
export const GHIDRA_BRIDGE_SOURCES = Object.freeze({
|
||||
bridge: "ReaGhidraBridge.java",
|
||||
@@ -12,3 +15,23 @@ export const GHIDRA_BRIDGE_SOURCES = Object.freeze({
|
||||
mipsSoftFloat: "ReaGhidraMipsSoftFloat.java",
|
||||
riscvCallingConvention: "ReaGhidraRiscvCallingConvention.java",
|
||||
} as const);
|
||||
|
||||
let bridgeSourcesDigest: string | undefined;
|
||||
|
||||
/**
|
||||
* SHA-256 over the packaged bridge sources, keyed by file name. Ghidra results
|
||||
* depend on these scripts, so the analysis profile commits to them: a changed
|
||||
* script yields a new profile digest and earlier snapshots are not reused,
|
||||
* without a hand-bumped semantics marker.
|
||||
*/
|
||||
export const ghidraBridgeSourcesDigest = (): string => {
|
||||
if (bridgeSourcesDigest !== undefined) return bridgeSourcesDigest;
|
||||
const directory = new URL("../../bridge/ghidra/", import.meta.url);
|
||||
const hash = createHash("sha256");
|
||||
for (const name of Object.values(GHIDRA_BRIDGE_SOURCES).sort()) {
|
||||
const bytes = readFileSync(new URL(name, directory));
|
||||
hash.update(JSON.stringify([name, bytes.byteLength])).update(bytes);
|
||||
}
|
||||
bridgeSourcesDigest = hash.digest("hex");
|
||||
return bridgeSourcesDigest;
|
||||
};
|
||||
@@ -5,16 +5,23 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import { ghidraProfileInstallationHost as host } from "./GhidraInstallation.fixture.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import { ghidraBridgeSourcesDigest } from "./GhidraBridgeSources.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
|
||||
// Digests commit to the current shared policy, including the instruction
|
||||
// status, load-image relocation-symbol, procedure search, byte-search and
|
||||
// bit-field base-type, runtime value-flow budgets, function-boundary and inventory-search revisions that prevent reuse of snapshots with earlier
|
||||
// results.
|
||||
const RAW_DIGESTS = {
|
||||
"6502": "6c4e88435526e8f0907b072600e3b121577962a36554e023dde4cf85113dd9f6",
|
||||
"x86-disabled-analyzer":
|
||||
"b2dc33d1932cbe71d72819428634927f3a2d8a2bbd54b6893f823ccb6b795d0b",
|
||||
} as const;
|
||||
// A profile digest is the digest of its committed parameters; these tests pin
|
||||
// the parameters (semantic revisions and the bridge sources) rather than a
|
||||
// literal hash that every bridge or marker change would have to re-record.
|
||||
const expectCommittedProfile = (
|
||||
profile: ReturnType<typeof createAnalysisProfile> | null | undefined,
|
||||
): void => {
|
||||
if (profile == null) throw new Error("Expected an analysis profile");
|
||||
expect(profile.parameters).toMatchObject({
|
||||
bridge_sources_sha256: ghidraBridgeSourcesDigest(),
|
||||
});
|
||||
expect(profile.digest).toBe(
|
||||
createAnalysisProfile(profile.provider, profile.parameters).digest,
|
||||
);
|
||||
};
|
||||
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra", ...environment });
|
||||
@@ -34,12 +41,12 @@ const raw = (architecture: "6502" | "x86", entryPoint: string): BinaryTarget =>
|
||||
entryPointSource: entryPoint,
|
||||
}) as unknown as BinaryTarget;
|
||||
|
||||
describe("raw-binary analysis profile digest", () => {
|
||||
describe("raw-binary analysis profile commitments", () => {
|
||||
it("commits the 6502 raw profile to the current shared policy", async () => {
|
||||
const ghidra = new GhidraProvider(configFor({}), silentLogger, {}, host);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(raw("6502", "0x8001"));
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
expect(resolved.value.profile?.digest).toBe(RAW_DIGESTS["6502"]);
|
||||
expectCommittedProfile(resolved.value.profile);
|
||||
expect(resolved.value.profile?.parameters).toMatchObject({
|
||||
target_format: "raw-binary",
|
||||
loader: "BinaryLoader",
|
||||
@@ -72,9 +79,7 @@ describe("raw-binary analysis profile digest", () => {
|
||||
);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(raw("x86", "0x1000"));
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
expect(resolved.value.profile?.digest).toBe(
|
||||
RAW_DIGESTS["x86-disabled-analyzer"],
|
||||
);
|
||||
expectCommittedProfile(resolved.value.profile);
|
||||
expect(resolved.value.profile?.parameters).toMatchObject({
|
||||
inventory_search_projection: "provider-inventory-facts-v1",
|
||||
value_flow_effect_scope: "pcode-load-store-explicit-v1",
|
||||
|
||||
Reference in new issue
Block a user