mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* refactor(verify): give the remaining verifiers' roots to VerifierFixture * fix(verify): let the loader-relocation supervisor dispose itself so a retried stop can observe its exit * test(verify): give the isolated bridge-sources and packaged-wasm runs the fixture owner they now import * test(verify): observe the Bun and Go verifier roots where VerifierFixture loads them
243 lines
7.8 KiB
JavaScript
243 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
|
|
import {
|
|
ProviderProcessSupervisor,
|
|
spawnOwnedProviderProcess,
|
|
} from "../dist/process/ProviderProcess.js";
|
|
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
|
|
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
|
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
|
import {
|
|
VerifierFixture,
|
|
withVerifierFixture,
|
|
} from "./lib/verifier-fixture.mjs";
|
|
|
|
const exec = promisify(execFile);
|
|
const expectedChecks = [
|
|
"bridge_source_bound",
|
|
"relocation_protection_precedes_decompiler",
|
|
"fixture_is_elf_shared_object",
|
|
"dynamic_elf_relocation_table_is_relocatable",
|
|
"importer_relocation_present",
|
|
"relocation_not_applied_by_importer",
|
|
"reader_references_relocation",
|
|
"writer_references_relocation",
|
|
"got_function_relocation_present",
|
|
"got_data_relocation_present",
|
|
"got_caller_references_loader_relocation",
|
|
"destination_is_loaded_initialized",
|
|
"destination_is_file_backed",
|
|
"destination_is_non_writable",
|
|
"destination_not_volatile_before_protection",
|
|
"destination_block_is_volatile",
|
|
"destination_permissions_preserved",
|
|
"source_filebytes_unchanged",
|
|
"evidence_limitation_represents_loader_uncertainty",
|
|
"non_read_relocation_reference_not_reported_as_read",
|
|
"relocation_lookup_covers_last_affected_byte",
|
|
"applied_got_relocations_not_volatile",
|
|
"native_decompiler_available",
|
|
"baseline_got_pseudocode_resolves_external_symbols",
|
|
"protected_got_pseudocode_keeps_external_symbols",
|
|
"full_relro_got_caller_pseudocode_is_stable",
|
|
];
|
|
|
|
if (process.argv.length !== 2)
|
|
throw new Error("Usage: node scripts/verify-ghidra-loader-relocation.mjs");
|
|
if (process.platform !== "linux")
|
|
throw new Error(
|
|
"Loader-relocation verification requires a Linux Ghidra host with an ELF linker.",
|
|
);
|
|
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
|
throw new Error(
|
|
"Set GHIDRA_INSTALL_DIR to a bring-your-own Ghidra installation.",
|
|
);
|
|
|
|
const installation = await inspectVerifierGhidraInstallation({
|
|
environment: process.env,
|
|
installDir: process.env.GHIDRA_INSTALL_DIR,
|
|
...(process.env.JAVA_HOME === undefined
|
|
? {}
|
|
: { javaHome: process.env.JAVA_HOME }),
|
|
});
|
|
assert.ok(
|
|
installation.analyzeHeadlessPath !== null &&
|
|
(installation.status === "available" ||
|
|
installation.rejection?.code === "executable_missing"),
|
|
JSON.stringify(installation),
|
|
);
|
|
|
|
const run = createVerifierRun();
|
|
const workspaceFixture = await VerifierFixture.open({
|
|
prefix: "rea-ghidra-loader-relocation-",
|
|
private: false,
|
|
});
|
|
const workspace = workspaceFixture.path;
|
|
const project = join(workspace, "project");
|
|
const target = join(workspace, "rea-loader-relocation.elf");
|
|
const provider = join(workspace, "librea-loader-provider.so");
|
|
const fixture = fileURLToPath(
|
|
new URL("../tests/conformance/ghidra/loader-relocation.c", import.meta.url),
|
|
);
|
|
const providerFixture = fileURLToPath(
|
|
new URL(
|
|
"../tests/conformance/ghidra/loader-relocation-provider.c",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
const bridgeDirectory = fileURLToPath(
|
|
new URL("../bridge/ghidra", import.meta.url),
|
|
);
|
|
const fixtureDirectory = fileURLToPath(
|
|
new URL("../tests/conformance/ghidra", import.meta.url),
|
|
);
|
|
const bridge = join(bridgeDirectory, "ReaGhidraBridge.java");
|
|
const compiler = process.env.REA_CC ?? "cc";
|
|
let supervisor;
|
|
let report;
|
|
// Owned Ghidra processes may still use the workspace until cleanup proves
|
|
// they are gone, so the fixture removes it only after that proof.
|
|
workspaceFixture.releaseAfter(async () => {
|
|
if (supervisor === undefined) return;
|
|
const stopped = await supervisor.stop();
|
|
assert.ok(
|
|
stopped.status !== "incomplete",
|
|
`Owned Ghidra cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`,
|
|
);
|
|
});
|
|
|
|
await withVerifierFixture(workspaceFixture, async () => {
|
|
await mkdir(project);
|
|
await buildProvider(compiler, providerFixture, provider);
|
|
await buildFixture(compiler, fixture, provider, target);
|
|
const targetDigest = digest(await readFile(target));
|
|
const launch = await spawnOwnedProviderProcess({
|
|
command: installation.analyzeHeadlessPath,
|
|
arguments: [
|
|
project,
|
|
"LoaderRelocation",
|
|
"-import",
|
|
target,
|
|
"-readOnly",
|
|
"-deleteProject",
|
|
"-scriptPath",
|
|
`${bridgeDirectory};${fixtureDirectory}`,
|
|
"-postScript",
|
|
"ReaLoaderRelocationProbe.java",
|
|
bridge,
|
|
],
|
|
runId: run.run_id,
|
|
expectedCommand: null,
|
|
});
|
|
supervisor = new ProviderProcessSupervisor({
|
|
...launch,
|
|
ownsProcessLifetime: true,
|
|
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
|
|
});
|
|
assert.ok(
|
|
await supervisor.waitForExit(180_000),
|
|
"Ghidra loader-relocation probe exceeded its 180-second deadline",
|
|
);
|
|
assert.ok(
|
|
await supervisor.waitForOutputClose(1000),
|
|
"Ghidra loader-relocation probe output did not close",
|
|
);
|
|
const snapshot = supervisor.snapshot();
|
|
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
|
assert.equal(snapshot.exitCode, 0, output);
|
|
const matches = [
|
|
...output.matchAll(/REA_LOADER_RELOCATION_PROBE_JSON (\{[^\r\n]*\})/gu),
|
|
];
|
|
assert.equal(matches.length, 1, output);
|
|
report = JSON.parse(matches[0][1]);
|
|
assert.equal(report.status, "passed");
|
|
assert.equal(report.ghidra_version, installation.providerVersion);
|
|
assert.deepEqual(
|
|
expectedChecks.every((check) => report.checks.includes(check)),
|
|
true,
|
|
);
|
|
assert.equal(report.source_filebytes_unchanged, true);
|
|
assert.equal(report.permissions, "read=true,write=false,execute=false");
|
|
assert.ok(
|
|
report.limitations.some(
|
|
(value) =>
|
|
value.includes("non-writable block") &&
|
|
value.includes("file-backed value may differ") &&
|
|
value.includes("not authoritative"),
|
|
),
|
|
);
|
|
assert.equal(report.decompiler_verified, true);
|
|
assert.ok(report.checks.includes("pseudocode_does_not_use_file_constant"));
|
|
// Ghidra 12.1.4 shows this x86-64 copy-relocated read by name even
|
|
// unprotected, so the before/after difference is checked only when the
|
|
// baseline folds the file-backed zero.
|
|
assert.equal(typeof report.baseline_folds_file_constant, "boolean");
|
|
if (report.baseline_folds_file_constant)
|
|
assert.ok(
|
|
report.checks.includes("protected_pseudocode_is_not_baseline_constant"),
|
|
);
|
|
assert.ok(
|
|
report.checks.includes("full_relro_got_caller_pseudocode_is_stable"),
|
|
);
|
|
assert.equal(digest(await readFile(target)), targetDigest);
|
|
report = {
|
|
...report,
|
|
target_sha256: targetDigest,
|
|
decompiler_scope: report.baseline_folds_file_constant
|
|
? "native Ghidra baseline folded the file constant; protected pseudocode does not"
|
|
: "native Ghidra baseline did not fold the file constant on this toolchain; protected pseudocode does not use it and GOT callers are unchanged",
|
|
};
|
|
});
|
|
|
|
assert.ok(report);
|
|
console.log(
|
|
JSON.stringify({
|
|
...report,
|
|
cleanup: "complete",
|
|
verifier_run: await completeVerifierRun(run),
|
|
}),
|
|
);
|
|
|
|
async function buildProvider(compiler, source, output) {
|
|
const arguments_ = [
|
|
"-O0",
|
|
"-fPIC",
|
|
"-shared",
|
|
"-nostdlib",
|
|
source,
|
|
"-o",
|
|
output,
|
|
];
|
|
await exec(compiler, arguments_, { timeout: 60_000 });
|
|
}
|
|
|
|
async function buildFixture(compiler, source, provider, output) {
|
|
const arguments_ = [
|
|
"-O0",
|
|
"-fPIE",
|
|
"-fno-plt",
|
|
"-pie",
|
|
"-nostdlib",
|
|
"-Wl,-z,relro,-z,now",
|
|
"-Wl,-e,rea_loader_start",
|
|
"-Wl,-rpath,$ORIGIN",
|
|
source,
|
|
provider,
|
|
"-o",
|
|
output,
|
|
];
|
|
await exec(compiler, arguments_, { timeout: 60_000 });
|
|
}
|
|
|
|
function digest(bytes) {
|
|
return createHash("sha256").update(bytes).digest("hex");
|
|
}
|