fix(ghidra): supervise Swift probes and cover retained text capacity

This commit is contained in:
morluto committed 2026-10-13 22:57:42 +08:00
commit 1e96613e9e
236 files changed
+12284 -2008

No files matched your search

+5
View File
@@ -55,6 +55,9 @@ export const SCOPES = {
"tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts",
"tests/boundary/filesystem/setupUnavailableProvider.test.ts",
"tests/boundary/filesystem/windowsNpxRegistration.test.ts",
"tests/boundary/android/workspaceGuardianRetry.test.ts",
"src/android/JadxWorkspaceRoot.ts",
"scripts/rea-android-workspace-guardian.mjs",
"tests/acceptance/setup/homeDirectoryFallback.test.ts",
"tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts",
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
@@ -218,6 +221,7 @@ export const SCOPES = {
"src/artifacts/readStableArtifact*.ts",
"src/artifacts/ArtifactReader.ts",
"scripts/verify-evm-interface.mjs",
"scripts/verify-solidity-metadata.mjs",
"scripts/fixtures/evm-interface.sol",
"third_party/evmole/**",
],
@@ -359,6 +363,7 @@ export const SCOPES = {
"website/**",
"scripts/*website*.py",
"README*.md",
"docs/readme/README*.md",
".github/workflows/website-check.yml",
".github/workflows/website-pages.yml",
],
@@ -8,7 +8,10 @@ import { spawnOwnedProviderProcess } from "../../dist/process/ProviderProcess.js
import { createSystemProcessOwnershipHost } from "../../dist/process/ProcessOwnershipObservation.js";
import { liveProcesses } from "../../dist/process/ProcessOwnershipProcessTree.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js";
import {
BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
waitForBrowserDevtoolsPort,
} from "../../dist/browser/BrowserProcessStartup.js";
/** Launch the synthetic fixture browser and await CDP in its own verified process group. */
export async function startRuntimeFixtureBrowser(executable, origin) {
@@ -50,7 +53,7 @@ export async function startRuntimeFixtureBrowser(executable, origin) {
stderr: () => stderr,
// Match the broader real-browser verifier's allowance for cold Chrome
// startup on shared runners; early process failures still reject at once.
timeoutMs: 60_000,
timeoutMs: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
});
return {
port,
+58 -11
View File
@@ -1,13 +1,41 @@
import { readFile, readdir, stat } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
const readmeLinkTargets = (content, language = "en") =>
[...content.matchAll(/\]\(([^)\s]+)\)/gu)]
.map((match) => match[1] ?? "")
const repoRelative = (root, from, target) => {
const index = target.search(/[#?]/u);
const pathPart = index === -1 ? target : target.slice(0, index);
const suffix = index === -1 ? "" : target.slice(index);
if (pathPart.length === 0 || /^[a-z][a-z0-9+.-]*:/iu.test(pathPart))
return target;
const absolute = resolve(root, dirname(from), pathPart);
const withinRepo = relative(root, absolute);
if (
withinRepo === ".." ||
withinRepo.startsWith(`..${sep}`) ||
isAbsolute(withinRepo)
)
return target;
return `${withinRepo.split(sep).join("/")}${suffix}`;
};
const isLanguageNavigation = (target) => {
const path = target.split(/[#?]/u)[0];
return (
path === "README.md" ||
/^docs\/readme\/README_[A-Za-z0-9-]+\.md$/u.test(path)
);
};
const readmeDestinations = (content) => [
...content.matchAll(/\]\(([^)\s]+)\)/gu),
...content.matchAll(/\b(?:src|href)="([^"]+)"/gu),
];
const readmeLinkTargets = (root, from, content, language = "en") =>
readmeDestinations(content)
.map((match) => repoRelative(root, from, match[1] ?? ""))
.filter(
(target) =>
!target.startsWith("#") &&
!/^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(target),
(target) => !target.startsWith("#") && !isLanguageNavigation(target),
)
.map((target) => {
if (language === "en" || !URL.canParse(target)) return target;
@@ -70,9 +98,19 @@ export const documentationFactIssues = async (root, catalog) => {
const issues = await skillReferenceIssues(
join(root, "skills/reverse-engineer-anything"),
);
const readmePaths = (await readdir(root))
.filter((path) => /^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(path))
.sort();
const translatedFolder = join(root, "docs/readme");
let translatedNames = [];
try {
translatedNames = await readdir(translatedFolder);
} catch (cause) {
if (cause?.code !== "ENOENT") throw cause;
}
const readmePaths = [
"README.md",
...translatedNames
.filter((name) => /^README_[A-Za-z0-9-]+\.md$/u.test(name))
.map((name) => `docs/readme/${name}`),
].sort();
const readmes = new Map();
for (const path of readmePaths) {
const content = await readFile(join(root, path), "utf8");
@@ -94,7 +132,11 @@ export const documentationFactIssues = async (root, catalog) => {
);
}
}
const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
const canonicalLinks = readmeLinkTargets(
root,
"README.md",
readmes.get("README.md") ?? "",
);
const languageInventory = JSON.parse(
await readFile(join(root, "website/locales/languages.json"), "utf8"),
);
@@ -104,7 +146,12 @@ export const documentationFactIssues = async (root, catalog) => {
for (const path of readmePaths.filter((path) => path !== "README.md"))
if (
JSON.stringify(
readmeLinkTargets(readmes.get(path) ?? "", readmeLanguages.get(path)),
readmeLinkTargets(
root,
path,
readmes.get(path) ?? "",
readmeLanguages.get(path),
),
) !== JSON.stringify(canonicalLinks)
)
issues.push(`${path}: documentation links differ from README.md`);
@@ -0,0 +1,46 @@
#!/usr/bin/env node
import { setTimeout as delay } from "node:timers/promises";
import { WindowsPrivateRuntime } from "../dist/windows/WindowsPrivateRuntime.js";
if (process.platform !== "win32" || process.argv.length !== 3)
throw new Error(
"Android workspace guardian requires Windows and one parent directory",
);
// This process owns the native directory handle. Detached process creation
// keeps it alive when the REA owner is terminated without running JS cleanup.
const runtime = WindowsPrivateRuntime.create(process.argv[2], "rea-android-");
const ready = await new Promise((resolve) => {
process.stdout.once("error", () => resolve(false));
process.stdout.write(
JSON.stringify({
path: runtime.observation.path,
volumeSerial: runtime.observation.volumeSerial,
fileId: runtime.observation.fileId,
privateDacl: runtime.observation.privateDacl,
}) + "\n",
(error) => resolve(error == null),
);
});
// The owner holds the write end until close/death. A failed readiness pipe
// also grants cleanup authority, including owner death during startup.
if (ready) {
for await (const _ of process.stdin) {
// No commands are accepted; only EOF grants cleanup authority.
}
} else {
process.stdin.destroy();
}
// A failed native walk retains the directory identity lease for a later retry.
// Keep this process alive so a transient sharing violation cannot orphan it.
for (;;) {
try {
await runtime.close();
process.exit(0);
} catch {
await delay(1000);
}
}
+3 -1
View File
@@ -261,7 +261,9 @@ class WebsiteTranslationChecks(unittest.TestCase):
inventory = [item for item in languages(ROOT) if item["code"] in {"en", "zh-CN", "ar"}]
(root / "website/locales/languages.json").write_text(json.dumps(inventory))
for item in inventory:
(root / item["readme"]).write_text("# REA\n")
destination = root / item["readme"]
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text("# REA\n")
WEBSITE.sync_pages(root)
extract_catalogs(root)
yield root
+20
View File
@@ -225,6 +225,26 @@ try {
for (const mode of ["cli", "mcp"]) {
const report = await inspect(mode, path, encoding);
assert.equal(report.bytecode.hex, hex);
assert.equal(report.bytecode.metadata.status, "decoded");
assert.deepEqual(report.bytecode.metadata.compiler_version, {
representation: "release-bytes",
value: "0.8.30",
});
assert.equal(
report.bytecode.metadata.bytecode_offset +
report.bytecode.metadata.cbor_bytes +
2,
Buffer.from(hex, "hex").length,
);
assert.ok(
report.bytecode.metadata.entries.some(
({ key, value }) =>
key.kind === "text" &&
key.value === "solc" &&
value.kind === "bytes" &&
value.value === "00081e",
),
);
assert.deepEqual(
report.functions
.filter(({ dispatch }) => dispatch === "abi")
+16 -14
View File
@@ -1,12 +1,12 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { readdir, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { hashStableFile } from "../dist/filesystem/StableFileHash.js";
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
@@ -27,17 +27,16 @@ const expectations =
options.expectations === null
? null
: JSON.parse(await readFile(options.expectations, "utf8"));
const root = await PrivateRuntimeRoot.create({
prefix: "rea-flutter-verifier-",
});
// This is a shared test parent, not a leased provider-owned Windows runtime.
const root = await mkdtemp(join(tmpdir(), "rea-flutter-verifier-"));
const entrypoint = resolve("scripts/rea.mjs");
const environment = Object.fromEntries(
Object.entries(process.env).filter(([, value]) => typeof value === "string"),
);
Object.assign(environment, {
TMPDIR: root.path,
TMP: root.path,
TEMP: root.path,
TMPDIR: root,
TMP: root,
TEMP: root,
});
const client = new Client({ name: "flutter-verifier", version: "1" });
const transport = new StdioClientTransport({
@@ -190,15 +189,18 @@ try {
} finally {
// Server shutdown retries retained provider cleanup and must settle before success.
try {
await client.close();
await transport.close();
try {
await client.close();
} finally {
await transport.close();
}
assert.deepEqual(
await readdir(root.path),
await readdir(root),
[],
"Provider snapshots must be released before verification succeeds",
`Provider snapshots remain under ${root}`,
);
} finally {
await root.close();
await rm(root, { recursive: true, force: true });
}
}
process.stdout.write(
+1
View File
@@ -76,6 +76,7 @@ export async function verifyPackageEnvironment({
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: codexDir,
COPILOT_HOME: join(home, ".copilot"),
+1
View File
@@ -98,6 +98,7 @@ export async function verifyPackageUpdate({
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
+1
View File
@@ -118,6 +118,7 @@ async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
+5 -3
View File
@@ -9,7 +9,10 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { CdpBrowserProvider } from "../dist/browser/CdpBrowserProvider.js";
import { waitForBrowserDevtoolsPort } from "../dist/browser/BrowserProcessStartup.js";
import {
BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
waitForBrowserDevtoolsPort,
} from "../dist/browser/BrowserProcessStartup.js";
import { createBrowserScenarioProvider } from "../dist/composition/browserScenario.js";
import {
inspectWebPageInputSchema,
@@ -47,7 +50,6 @@ import { artifactCliEvidence, artifactMcpResult } from "./lib/artifact-e2e.mjs";
import { verifyScenarioEnvironment } from "./lib/browser-scenario-environment-e2e.mjs";
import { verifyScenarioStorage } from "./lib/browser-scenario-storage-e2e.mjs";
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret";
process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
@@ -92,7 +94,7 @@ try {
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: REAL_BROWSER_STARTUP_TIMEOUT_MS,
timeoutMs: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
+152
View File
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import {
inspectMipsReadelf,
inspectLegacyMipsReadelf,
assertMipsLoadedImage,
assertMipsProbe,
assertMipsGlobal,
@@ -122,6 +123,157 @@ async function verifySoftFloat(call, symbols, byteOrder) {
const reports = [];
let primaryFailure;
try {
if (!psp) {
// Recordless legacy admission: ABI field 0, no ABI flags record, pinned
// 32-bit language. Independent GNU readelf checks the header interpretation.
for (const byteOrder of ["little", "big"]) {
const fixture = await buildMipsFixture(workspace, byteOrder, "hard", {
legacy: true,
});
const legacy = await inspectLegacyMipsReadelf(fixture.path, byteOrder);
const resolved = await parseBinaryTarget(fixture.path);
if (!resolved.ok) throw resolved.error;
assert.equal(resolved.value.mips.flags, 0x50000001);
assert.equal(resolved.value.mips.abiFlags, null);
assert.deepEqual(
resolved.value.mips.limitations ?? [],
legacy.limitations,
"REA legacy ABI-record absence interpretation disagrees with GNU readelf",
);
assert.equal(resolved.value.sha256, fixture.sha256);
const snapshotPath = join(workspace, `${byteOrder}-legacy.snapshot.json`);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({
name: "rea-real-mips-legacy-proof",
version: "1",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const call = async (name, args = {}) => {
const reply = await client.callTool(
{ name, arguments: args },
undefined,
{ timeout: 180000 },
);
return requireMcpOperationResult(reply, name);
};
try {
await client.connect(transport);
const selected = await call("open_binary", {
path: fixture.path,
provider_id: "ghidra",
});
assert.equal(selected.architecture, "mips");
assert.equal(selected.sha256, fixture.sha256);
const image = await call("inspect_native_load_image");
assertMipsLoadedImage(image.observations, byteOrder, fixture.sha256);
// The pinned import language is the checked one, not an auto-selection.
assert.equal(
image.observations.language_id,
`MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
);
const procedures = await call("list_procedures");
const entry = procedures.find((p) => p.value === "rea_mips_entry");
assert.ok(entry, "Entry function missing from legacy inventory");
assert.equal(
BigInt(entry.address),
BigInt(legacy.symbols.rea_mips_entry),
);
const probe = await call("inspect_native_instruction", {
address: legacy.symbols.rea_mips_probe,
});
const probeBranch = await call("inspect_native_instruction", {
address: `0x${(BigInt(legacy.symbols.rea_mips_probe) + 4n).toString(16)}`,
});
assertMipsProbe(
probe,
probeBranch,
byteOrder,
legacy.symbols.rea_mips_probe,
);
const dossier = functionDossierSchema.parse(
await call("analyze_function", { procedure: entry.address }),
);
assert.ok(dossier.pseudocode.trim().length > 0);
const saved = await call("close_binary", {
snapshot_path: snapshotPath,
});
assert.ok(saved.evidence_records > 0);
const snapshot = parseAnalysisSnapshot(
JSON.parse(await readFile(snapshotPath, "utf8")),
);
assert.equal(snapshot.target.architecture, "mips");
const parameters = snapshot.binding.analysis_profile.parameters;
assert.equal(
parameters.language_id,
`MIPS:${byteOrder === "little" ? "LE" : "BE"}:32:default`,
);
assert.equal(parameters.language_selection, "provider-pinned-v1");
assert.equal(
parameters.mips_support_lane,
"elf32-exec-arch32-legacy-pinned-v1",
);
assert.equal(parameters.mips_abi_basis, "inferred-o32-assumption-v1");
assert.ok(
snapshot.evidence_bundle.records.some((record) =>
parseEvidence(record).limitations.some((item) =>
item.includes("analysis assumption"),
),
),
"Legacy o32 assumption limitation missing from retained Evidence",
);
assert.ok(
snapshot.evidence_bundle.records.some((record) =>
parseEvidence(record).limitations.some((item) =>
item.includes("Floating-point ABI semantics are unverified"),
),
),
"Legacy floating-point uncertainty missing from retained Evidence",
);
await call("open_binary", {
path: fixture.path,
provider_id: "ghidra",
snapshot_path: snapshotPath,
});
assert.deepEqual(await call("list_procedures"), procedures);
await call("close_binary");
} catch (cause) {
throw new Error(
`MIPS legacy ${byteOrder} verification failed; MCP stderr: ${stderr}`,
{ cause },
);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
reports.push({
...fixture,
independent_reader: legacy,
variant: "recordless-legacy-pinned",
effective_language: `MIPS:${
byteOrder === "little" ? "LE" : "BE"
}:32:default`,
procedures: "verified-through-snapshot",
checks: [
"recordless header admitted with inferred o32 assumption",
"pinned 32-bit language checked after import",
"legacy limitations retained in Evidence",
"snapshot round-trip and profile parameters",
],
status: "passed",
});
}
}
for (const [byteOrder, floatAbi] of psp
? [["little", "hard"]]
: [
+88 -23
View File
@@ -16,6 +16,11 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../dist/process/ProviderProcess.js";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
@@ -63,6 +68,7 @@ const literalAssembly = fileURLToPath(
const literalObject = join(root, "decoder-safeguards.o");
const reports = [];
let literalReport;
let decoderCleaned = true;
try {
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
timeout: 60_000,
@@ -180,7 +186,7 @@ try {
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
if (decoderCleaned) await rm(root, { recursive: true, force: true });
}
console.log(
JSON.stringify(
@@ -277,30 +283,89 @@ async function verifyDecoderSafeguards() {
join(scripts, "ReaSwiftDecoderRegressions.java"),
);
const report = join(root, "decoder-safeguards.txt");
await execute(
installation.analyzeHeadlessPath,
[
root,
"decoder-regressions",
"-import",
literalBinary,
"-scriptPath",
scripts,
"-postScript",
"ReaSwiftDecoderRegressions.java",
report,
"-deleteProject",
"-max-cpu",
"2",
],
{
env: { ...process.env, JAVA_HOME: installation.javaHome },
timeout: 120_000,
maxBuffer: 8 * 1024 * 1024,
},
);
await runDecoderProbe(scripts, report);
assert.equal(
await readFile(report, "utf8"),
"REA_SWIFT_DECODER_SAFEGUARDS_OK\n",
);
}
async function runDecoderProbe(scripts, report) {
const controller = new AbortController();
let interrupt;
const interrupted = new Promise((resolve) => {
interrupt = () => {
controller.abort(new Error("Swift decoder probe interrupted"));
resolve(false);
};
});
process.on("SIGINT", interrupt);
process.on("SIGTERM", interrupt);
let supervisor;
try {
const launch = await spawnOwnedProviderProcess({
command: installation.analyzeHeadlessPath,
arguments: [
root,
"decoder-regressions",
"-import",
literalBinary,
"-scriptPath",
scripts,
"-postScript",
"ReaSwiftDecoderRegressions.java",
report,
"-deleteProject",
"-max-cpu",
"2",
],
runId: run.run_id,
expectedCommand: null,
env: { JAVA_HOME: installation.javaHome },
signal: controller.signal,
});
decoderCleaned = false;
supervisor = new ProviderProcessSupervisor(
{
...launch,
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
},
{ maxDiagnosticBytes: 8 * 1024 * 1024 },
);
assert.ok(
await Promise.race([supervisor.waitForExit(120_000), interrupted]),
controller.signal.aborted
? "Swift decoder probe interrupted"
: "Swift decoder probe timed out",
);
assert.ok(
await supervisor.waitForOutputClose(1_000),
"Swift decoder probe output did not close",
);
const snapshot = supervisor.snapshot();
assert.equal(
snapshot.exitCode,
0,
`${snapshot.stdout.text}\n${snapshot.stderr.text}`,
);
assert.ok(
!snapshot.diagnosticTruncated,
"Swift decoder probe exceeded its diagnostic capacity",
);
} finally {
try {
if (supervisor !== undefined) {
const stopped = await supervisor.stop();
decoderCleaned = stopped.status === "verified-cleanup";
assert.ok(
decoderCleaned,
`Owned Ghidra cleanup incomplete; retained ${root}: ${JSON.stringify(stopped)}`,
);
}
} finally {
process.off("SIGINT", interrupt);
process.off("SIGTERM", interrupt);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { pathToFileURL } from "node:url";
import { performance } from "node:perf_hooks";
import { inspectSolidityMetadata } from "../dist/evm/SolidityMetadata.js";
const compilerModule = process.env.REA_VERIFY_SOLC_MODULE;
if (!isAbsolute(compilerModule ?? ""))
throw new Error(
"Select an absolute caller-installed solc 0.8.30 module with REA_VERIFY_SOLC_MODULE.",
);
const solc = (await import(pathToFileURL(compilerModule).href)).default;
assert.equal(solc.version(), "0.8.30+commit.73712a01.Emscripten.clang");
const source = await readFile(
new URL("./fixtures/evm-interface.sol", import.meta.url),
"utf8",
);
const cases = [
{ name: "unoptimized", optimizer: false, viaIR: false, metadata: {} },
{ name: "viaIR", optimizer: true, viaIR: true, metadata: {} },
{
name: "without-content-hash",
optimizer: false,
viaIR: false,
metadata: { bytecodeHash: "none" },
},
{
name: "without-CBOR",
optimizer: false,
viaIR: false,
metadata: { appendCBOR: false },
},
];
const reports = [];
for (const fixture of cases) {
const compiled = JSON.parse(
solc.compile(
JSON.stringify({
language: "Solidity",
sources: { "fixture.sol": { content: source } },
settings: {
optimizer: { enabled: fixture.optimizer },
viaIR: fixture.viaIR,
metadata: fixture.metadata,
outputSelection: { "*": { "*": ["evm.deployedBytecode.object"] } },
},
}),
),
);
assert.ok(
!(compiled.errors ?? []).some((error) => error.severity === "error"),
JSON.stringify(compiled.errors),
);
const bytes = Buffer.from(
compiled.contracts["fixture.sol"].InterfaceFixture.evm.deployedBytecode
.object,
"hex",
);
const cpu = process.cpuUsage();
const started = performance.now();
const parsed = inspectSolidityMetadata(bytes);
const firstMs = performance.now() - started;
const firstCpu = process.cpuUsage(cpu);
if (fixture.metadata.appendCBOR === false)
assert.equal(parsed.status, "unavailable");
else {
assert.equal(parsed.status, "decoded");
assert.deepEqual(parsed.compiler_version, {
representation: "release-bytes",
value: "0.8.30",
});
assert.equal(parsed.bytecode_offset + parsed.cbor_bytes + 2, bytes.length);
if (fixture.metadata.bytecodeHash === "none")
assert.deepEqual(
parsed.entries.map(({ key }) => key),
[{ kind: "text", value: "solc" }],
);
}
const warmResults = new Array(100);
const warmStart = performance.now();
for (let index = 0; index < warmResults.length; index++)
warmResults[index] = inspectSolidityMetadata(bytes);
const warmMs = performance.now() - warmStart;
for (const result of warmResults) assert.deepEqual(result, parsed);
reports.push({
fixture: fixture.name,
bytes: bytes.length,
status: parsed.status,
first_decode_ms: firstMs,
first_decode_cpu_us: firstCpu,
warm_100_decode_ms: warmMs,
});
}
console.log(
JSON.stringify({
ok: true,
compiler_version: solc.version(),
contract_execution: "not-performed",
cases: reports,
process_peak_rss_kib: process.resourceUsage().maxRSS,
measurements:
"In-process decode timings; peak RSS includes the compiler. End-to-end Linux provider/CLI costs and agent tokens were not measured.",
}),
);
+3 -4
View File
@@ -472,10 +472,9 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
signal: AbortSignal.timeout(15_000),
});
assert.equal(String(ready).trim(), "ready");
await assert.rejects(
retryRuntime.close(),
/owned cleanup object|Delete owned object/u,
);
// The reader denies delete sharing. Cleanup must retain ownership when
// its handle-relative open receives ERROR_SHARING_VIOLATION (32).
await assert.rejects(retryRuntime.close(), { win32Code: 32 });
assert.equal(
windowsPrivateRuntime(retryRuntime.observation.path),
retryRuntime,
+3
View File
@@ -52,8 +52,10 @@ try {
"bridge/ghidra/ReaGhidraBridge.java",
"native/windows/build/manifest.json",
"native/windows/build/rea-windows-x64.node",
"dist/android/JadxWorkspaceRoot.js",
"dist/main.js",
"dist/cli.js",
"scripts/rea-android-workspace-guardian.mjs",
"scripts/rea.mjs",
])
if (!packagedPaths.has(required))
@@ -108,6 +110,7 @@ try {
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
XDG_CONFIG_HOME: join(home, ".config"),
+50 -1
View File
@@ -173,6 +173,50 @@ try {
assert.deepEqual(structured.normalized_result, evidence.normalized_result);
console.log(`PASS MCP/CLI parity ${task.name}`);
}
const overloadedInput = {
path,
class_name: "androidx.activity.ComponentActivity",
method_name: "setContentView",
overload_index: 0,
};
const overloadedCli = await execute(
process.execPath,
[
entrypoint,
"trace-android-references",
path,
overloadedInput.class_name,
"--method-name",
overloadedInput.method_name,
"--overload-index",
"0",
"--format",
"json",
],
{
cwd: repository,
env: environment,
timeout: 150_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const overloadedEvidence = parseEvidence(JSON.parse(overloadedCli.stdout));
assert.equal(overloadedEvidence.normalized_result.overload_index, 0);
assert.equal(
overloadedEvidence.normalized_result.reported_signature,
"setContentView(int): void",
);
assert.equal(overloadedEvidence.normalized_result.total, 1);
const overloadedMcp = await client.callTool(
{ name: "trace_android_references", arguments: overloadedInput },
{ timeout: 150_000 },
);
assert.notEqual(overloadedMcp.isError, true, JSON.stringify(overloadedMcp));
assert.deepEqual(
parseEvidence(overloadedMcp.structuredContent).normalized_result,
overloadedEvidence.normalized_result,
);
console.log("PASS real overloaded method references through CLI/MCP");
} catch (cause) {
process.stderr.write(stderr);
throw cause;
@@ -205,7 +249,7 @@ assert.equal(method.body_status, "available");
assert.equal(method.source.status, "complete");
assert.ok(method.source.text.includes("getIntent()"));
assert.ok(method.source.text.includes("setListAdapter"));
assert.equal(method.method.dex_descriptor, null);
assert.equal(method.method.dex_descriptor, "(Landroid/os/Bundle;)V");
const references = cliResults.get("trace_android_references").normalized_result;
assert.ok(references.total > 0);
assert.ok(
@@ -264,3 +308,8 @@ if (process.env.REA_ANDROID_REPORT !== undefined)
JSON.stringify(report, null, 2) + "\n",
);
console.log(JSON.stringify(report, null, 2));
assert.notEqual(
windowsLifecycle?.cli_abrupt_owner_exit_workspace_cleanup,
false,
"Android workspace remained after abrupt owner exit",
);
+112
View File
@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../../dist/domain/evidence.js";
import { hashAndroidFile } from "./digest.mjs";
/** The public result keeps one using node and exposes its two raw DEX invokes. */
export const verifyAndroidDirectInvokes = async ({
apk,
dexEntry,
repository,
entrypoint,
environment,
execute,
}) => {
const digest = await hashAndroidFile(apk);
const check = (value) => {
const evidence = parseEvidence(value);
assert.equal(evidence.subject.digest.sha256, digest);
assert.equal(evidence.operation, "trace_android_references");
const result = evidence.normalized_result;
assert.equal(result.class_name, "fixture.Target");
assert.equal(result.method_name, "mark");
assert.equal(result.dex_descriptor, "()V");
assert.equal(result.reference_unit, "using_node");
assert.equal(result.total, 1);
assert.equal(result.references.length, 1);
const source = result.references[0];
assert.equal(source.reported_full_name, "fixture.Caller.twice");
assert.deepEqual(
source.direct_invokes.map((site) => site.dex_code_unit_offset),
[0, 3],
);
for (const site of source.direct_invokes) {
assert.equal(site.dex_entry, dexEntry);
assert.equal(site.source_raw_method_id, "fixture.Caller.twice()V");
assert.equal(site.opcode, "INVOKE_STATIC");
}
assert.equal(
source.direct_invokes[0].dex_method_index,
source.direct_invokes[1].dex_method_index,
);
return evidence;
};
const cliResponse = await execute(
process.execPath,
[
entrypoint,
"trace-android-references",
apk,
"fixture.Target",
"--method-name",
"mark",
"--format",
"json",
],
{
cwd: repository,
env: environment,
timeout: 150_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const cli = check(JSON.parse(cliResponse.stdout));
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
cwd: repository,
env: environment,
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
const client = new Client({
name: "rea-real-android-direct-invokes",
version: "1",
});
try {
await client.connect(transport, { timeout: 30_000 });
const response = await client.callTool(
{
name: "trace_android_references",
arguments: {
path: apk,
class_name: "fixture.Target",
method_name: "mark",
},
},
{ timeout: 150_000 },
);
assert.notEqual(response.isError, true, JSON.stringify(response));
const mcp = check(response.structuredContent);
assert.deepEqual(mcp.normalized_result, cli.normalized_result);
assert.deepEqual(mcp.parameters, cli.parameters);
assert.deepEqual(mcp.provider, cli.provider);
} catch (cause) {
process.stderr.write(stderr);
throw cause;
} finally {
await client.close();
}
assert.equal(
await hashAndroidFile(apk),
digest,
"Static inspection changed fixture bytes",
);
console.log(
`PASS real CLI/MCP direct-invoke offsets from ${dexEntry}, parity and unchanged APK`,
);
};
+25
View File
@@ -3,6 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join, resolve } from "node:path";
import { verifyAndroidInitializers } from "./initializers.mjs";
import { verifyAndroidDirectInvokes } from "./direct-invokes.mjs";
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
export const verifyAndroidMetadata = async ({
@@ -60,6 +61,7 @@ export const verifyAndroidMetadata = async ({
apk,
resolve(repository, "tests/fixtures/android/static-initializers"),
classes,
resolve(repository, "tests/fixtures/android/direct-invokes"),
],
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
);
@@ -71,12 +73,19 @@ export const verifyAndroidMetadata = async ({
result.stdout,
/INITIALIZER_REGRESSION_PASS folded=2 bounded=true/u,
);
assert.match(
result.stdout,
/DIRECT_INVOKE_REGRESSION_PASS nodes=1 offsets=0,3 cross_dex=true/u,
);
console.log(
"PASS real cold metadata, full generic types and stable overloaded method selection",
);
console.log(
"PASS real folded and mixed static initializers, absent bodies and bounded smali",
);
console.log(
"PASS real raw DEX direct-invoke offsets and cross-DEX source identity",
);
await verifyAndroidInitializers({
apk: resolve(classes, "initializers.apk"),
repository,
@@ -84,6 +93,22 @@ export const verifyAndroidMetadata = async ({
environment,
execute,
});
await verifyAndroidDirectInvokes({
apk: resolve(classes, "direct-invokes.apk"),
dexEntry: "classes.dex",
repository,
entrypoint,
environment,
execute,
});
await verifyAndroidDirectInvokes({
apk: resolve(classes, "direct-invokes-multidex.apk"),
dexEntry: "classes2.dex",
repository,
entrypoint,
environment,
execute,
});
} finally {
await rm(classes, { recursive: true, force: true });
}
@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createInterface } from "node:readline";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { createJadxWorkspaceRoot } from "../../../dist/android/JadxWorkspaceRoot.js";
const source = fileURLToPath(import.meta.url);
const marker = "REA synthetic guardian isolation test\n";
const exists = (path) =>
stat(path).then(
() => true,
() => false,
);
if (process.argv[1] === source && process.argv[2] === "owner") {
const root = await createJadxWorkspaceRoot();
await writeFile(join(root.path, "synthetic.txt"), marker);
process.stdout.write(JSON.stringify({ path: root.path }) + "\n");
await new Promise(() => {});
}
const bounded = async (promise, milliseconds, label) => {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(label)), milliseconds);
}),
]);
} finally {
clearTimeout(timer);
}
};
const launch = async () => {
const child = spawn(process.execPath, [source, "owner"], {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-8192);
});
const exit = new Promise((resolve, reject) => {
child.once("close", resolve);
child.once("error", reject);
});
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
try {
const ready = await bounded(
lines[Symbol.asyncIterator]().next(),
20000,
`Android guardian owner readiness timed out: ${stderr}`,
);
assert.equal(ready.done, false, stderr);
const { path } = JSON.parse(ready.value);
assert.equal(typeof path, "string");
assert.equal(await exists(join(path, "synthetic.txt")), true);
return { child, exit, path };
} catch (cause) {
child.kill();
await bounded(exit, 15000, "Android guardian owner did not exit");
throw cause;
}
};
const stop = async ({ child, exit, path }) => {
assert.equal(child.kill(), true);
await bounded(exit, 15000, "Android guardian owner did not exit");
for (let attempt = 0; attempt < 100; attempt++) {
if (!(await exists(path))) return;
await delay(100);
}
throw new Error(`Android guardian retained ${path} after owner death`);
};
/** Two independent owners must never delete one another's private workspace. */
export async function verifyWindowsGuardianIsolation() {
assert.equal(process.platform, "win32");
const first = await launch();
let second;
try {
second = await launch();
assert.notEqual(first.path, second.path);
await stop(first);
assert.equal(
await exists(join(second.path, "synthetic.txt")),
true,
"First owner deleted the second owner's workspace",
);
await stop(second);
console.log("PASS Windows Android guardian two-owner cleanup isolation");
} finally {
for (const owner of [first, second]) {
if (owner === undefined) continue;
if (owner.child.exitCode === null && owner.child.signalCode === null)
owner.child.kill();
await bounded(owner.exit, 15000, "Android guardian owner did not exit");
}
}
}
+40 -4
View File
@@ -17,6 +17,7 @@ import {
resolveJadxConfigurationInputs,
validateJadxConfiguration,
} from "../../../dist/android/JadxConfiguration.js";
import { verifyWindowsGuardianIsolation } from "./windows-guardian-isolation.mjs";
/** Verify real Windows cancellation, disconnect and abrupt CLI owner exit. */
export async function verifyWindowsAndroidLifecycle(options) {
@@ -24,15 +25,16 @@ export async function verifyWindowsAndroidLifecycle(options) {
await verifyJavaSelection(options.environment);
const cancellation = await verifyMcp(options, "cancel");
await verifyMcp(options, "disconnect");
await verifyCliOwnerExit(options);
const abruptWorkspaceRemoved = await verifyCliOwnerExit(options);
await verifyWindowsGuardianIsolation();
return {
mcp_cancelled_engine_exited: true,
mcp_cancelled_workspace_removed: true,
mcp_ping_and_next_call: true,
mcp_disconnect_engine_exited: true,
cli_abrupt_owner_exit_engine_exited: true,
// A forced Windows process exit cannot execute JavaScript workspace cleanup.
cli_abrupt_owner_exit_workspace_cleanup: false,
cli_abrupt_owner_exit_workspace_cleanup: abruptWorkspaceRemoved,
guardian_two_owner_isolation: true,
observed_resources: cancellation,
java_home_and_quoted_case_insensitive_path: true,
};
@@ -161,6 +163,7 @@ async function verifyCliOwnerExit({
path,
repository,
}) {
const baseline = new Set(await runtimeRoots());
const child = spawn(
process.execPath,
[entrypoint, "inspect-android-package", path, "--format", "json"],
@@ -185,7 +188,40 @@ async function verifyCliOwnerExit({
await deadline(exited, 15000);
await observer.verifyExit();
assert.equal(output, "", "Aborted CLI published successful Evidence");
console.log("PASS Windows CLI abrupt owner exit: real Java job cleanup");
const created = (await runtimeRoots()).filter(
(name) => !baseline.has(name),
);
assert.ok(
created.length <= 1,
"Concurrent Android workspaces make abrupt cleanup observation ambiguous",
);
if (created.length === 0) {
console.log(
"PASS Windows CLI abrupt owner exit: real Java job and workspace cleanup",
);
return true;
}
const ownedName = created[0];
try {
await waitFor(
async () => !(await runtimeRoots()).includes(ownedName),
5000,
"Android workspace remained after abrupt owner exit",
);
} catch (cause) {
if (
!(cause instanceof Error) ||
cause.message !== "Android workspace remained after abrupt owner exit"
)
throw cause;
}
const removed = !(await runtimeRoots()).includes(ownedName);
console.log(
removed
? "PASS Windows CLI abrupt owner exit: real Java job and workspace cleanup"
: "OBSERVED Windows CLI abrupt owner exit: Java job cleaned, workspace residue remains",
);
return removed;
} finally {
if (child.exitCode === null && child.signalCode === null) child.kill();
await deadline(exited, 15000);
+18 -3
View File
@@ -25,11 +25,27 @@ LITERALS = {"REA", "Reverse Engineer Anything", "REA · Reverse Engineer Anythin
FALLBACK_NOTICE = "Translation is being updated. Read this page in English for now."
def readme_edition_paths(root):
"""Return repo-relative README editions: root English plus docs/readme translations."""
editions = set()
if (root / "README.md").is_file():
editions.add("README.md")
folder = root / "docs" / "readme"
if folder.is_dir():
editions.update(
f"docs/readme/{path.name}"
for path in folder.glob("README_*.md")
if path.is_file()
)
return editions
def languages(root):
"""Load the website language inventory and check it against README editions."""
path = root / "website/locales/languages.json"
editions = readme_edition_paths(root)
if not path.exists():
if any(root.glob("README_*.md")):
if any(edition != "README.md" for edition in editions):
raise ValueError("Missing languages.json for the translated README editions")
return [{"code": "en", "name": "English", "readme": "README.md", "dir": "ltr"}]
data = json.loads(path.read_text(encoding="utf-8"))
@@ -49,8 +65,7 @@ def languages(root):
raise ValueError(f"Duplicate README edition: {item['readme']}")
codes.add(item["code"])
filenames.add(item["readme"])
readmes = {path.name for path in root.glob("README*.md")}
if readmes and ({item["readme"] for item in data} != readmes):
if editions and ({item["readme"] for item in data} != editions):
raise ValueError("Website languages must match the README language editions")
return data