mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 00:07:18 +00:00
* docs: organize canonical documentation layout (URGENTE) Group all existing authored guides by domain and ownership, expose complete navigation, and preserve their public routes. Update physical path consumers and release configuration while retaining historical records. Document domain-based specs and ADR slugs without global numbering. Co-Authored-By: Codex <noreply@openai.com> * fix(docs): honor home layout in all-page route verification * style: format reviewed source changes * fix(docs): preserve routes and navigation cache inputs * fix: repair the static-check failures on the default branch (#2895) * fix: repair the static-check failures on the default branch The default branch fails its own required `Static checks` job three ways, so every pull request reports that job as failed regardless of its own change. - `npm run format:check` reports `src/domain/functionComparison.test.ts` and `src/ida/IdaOperationRunner.ts`. - `npm run lint` reports a duplicate `import { join } from "node:path"` in `tests/boundary/mcp/ghidraInventoryMcp.test.ts` (lines 4 and 17). - `npm run typecheck` reports `format: "devin"` missing from the client format union in `src/application/SupportedClients.ts`, and an unchecked index in `tests/boundary/cli/clientProfileLifecycle.test.ts`, whose pair array is inferred as `string[][]`. Apply the pinned formatter, drop the duplicate import, list the `"devin"` format that its own definition already uses, and mark the pair array `as const`. Validation: `npm run check:fast` and `npm run format:check` pass. * fix(setup): keep Devin on the JSON configuration dialect --------- Co-authored-by: 吴佳浩 <wujiahao@supremind.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> * fix(docs): update links after guide relocation * docs: correct verified documentation route counts * fix(docs): update browser capture guide link after move (#2820) Refresh gettext protected-link comments alongside the authored href. Co-authored-by: Codex <noreply@openai.com> * fix(docs): include http3 lab in stable routes * fix(docs): preserve historical release evidence and links * fix(ghidra): restore Windows verifier identity import * docs(wakaru): preserve pinned upstream policy link * docs(index): retain recently added guide links * docs(mcp): preserve parent retention clarification * fix(cli): preserve standalone import guidance * fix(test): preserve translated guide link drift coverage --------- Co-authored-by: Matheus Breguêz <matbrgz@gmail.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: morluto <17176810+morluto@users.noreply.github.com> Co-authored-by: 吴佳浩Alben <35291372+XiaoBinGan@users.noreply.github.com> Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
1104 lines
36 KiB
JavaScript
1104 lines
36 KiB
JavaScript
/** Public SDK/stdio JNI investigation, checked against independent NDK/DEX facts. */
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
readFile,
|
|
writeFile,
|
|
mkdir,
|
|
copyFile,
|
|
unlink,
|
|
stat,
|
|
} from "node:fs/promises";
|
|
import { constants } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { parseArgs, promisify } from "node:util";
|
|
import { performance } from "node:perf_hooks";
|
|
import { execFile } from "node:child_process";
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
repository: { type: "string" },
|
|
target: { type: "string" },
|
|
output: { type: "string" },
|
|
"java-home": { type: "string" },
|
|
jar: { type: "string" },
|
|
expectations: { type: "string" },
|
|
variant: { type: "string", default: "ordinary" },
|
|
repeats: { type: "string", default: "2" },
|
|
proof: { type: "boolean", default: false },
|
|
detail: { type: "string", default: "complete" },
|
|
"omit-detail": { type: "boolean", default: false },
|
|
pages: { type: "boolean", default: false },
|
|
cli: { type: "boolean", default: false },
|
|
"retained-removal": { type: "boolean", default: false },
|
|
},
|
|
});
|
|
for (const name of [
|
|
"repository",
|
|
"target",
|
|
"output",
|
|
"java-home",
|
|
"jar",
|
|
"expectations",
|
|
])
|
|
assert.ok(values[name], `Missing --${name}`);
|
|
assert.ok(["complete", "summary"].includes(values.detail));
|
|
assert.ok(
|
|
!values["omit-detail"] || values.detail === "summary",
|
|
"--omit-detail verifies the summary default",
|
|
);
|
|
assert.ok(
|
|
values.detail !== "summary" || values.proof,
|
|
"Summary verification requires its full proof export",
|
|
);
|
|
assert.ok(["ordinary", "relr", "aps2", "conscrypt"].includes(values.variant));
|
|
const repeats = Number(values.repeats);
|
|
assert.ok(Number.isSafeInteger(repeats) && repeats >= 1 && repeats <= 20);
|
|
const repo = resolve(values.repository),
|
|
target = resolve(values.target),
|
|
output = resolve(values.output);
|
|
await mkdir(output, { mode: 0o700 });
|
|
const expectations = JSON.parse(
|
|
await readFile(resolve(values.expectations), "utf8"),
|
|
);
|
|
const targetSha = createHash("sha256")
|
|
.update(await readFile(target))
|
|
.digest("hex");
|
|
const expected =
|
|
values.variant === "conscrypt"
|
|
? expectations.conscrypt
|
|
: expectations.variants[values.variant];
|
|
assert.equal(
|
|
targetSha,
|
|
expected.sha256,
|
|
"Selected target must be the independent oracle's exact APK",
|
|
);
|
|
const canonical = (value) =>
|
|
Array.isArray(value)
|
|
? value.map(canonical)
|
|
: value !== null && typeof value === "object"
|
|
? Object.fromEntries(
|
|
Object.keys(value)
|
|
.sort()
|
|
.map((key) => [key, canonical(value[key])]),
|
|
)
|
|
: value;
|
|
const factDigest = (report) => {
|
|
const { engine: _engine, ...facts } = report;
|
|
return createHash("sha256")
|
|
.update(JSON.stringify(canonical(facts)))
|
|
.digest("hex");
|
|
};
|
|
const declarationKey = (row) =>
|
|
JSON.stringify([
|
|
row.class_name ?? row.class,
|
|
row.method_name ?? row.name,
|
|
row.descriptor,
|
|
]);
|
|
const goldenMethods = new Map(
|
|
expectations.dex.native_methods.map((row) => [declarationKey(row), row]),
|
|
);
|
|
const expectedSlots = (candidate, row) => {
|
|
assert.equal(candidate.address, row.function_address);
|
|
if (row.table_address !== undefined)
|
|
assert.equal(candidate.table.table_address, row.table_address);
|
|
if (row.entry_index !== undefined)
|
|
assert.equal(candidate.table.entry.entry_index, row.entry_index);
|
|
assert.equal(candidate.table.entry.function_address, row.function_address);
|
|
assert.equal(candidate.table.entry.name, row.name);
|
|
assert.equal(candidate.table.entry.descriptor, row.descriptor);
|
|
assert.equal(candidate.table.entry.slots.length, 3);
|
|
for (let index = 0; index < 3; index++) {
|
|
const slot = candidate.table.entry.slots[index],
|
|
gold = row.slots[index];
|
|
assert.equal(slot.role, ["name", "signature", "function"][index]);
|
|
assert.equal(slot.address, gold.address);
|
|
assert.equal(slot.file_offset, Number(BigInt(gold.file_offset)));
|
|
assert.equal(slot.value, gold.resolved_word);
|
|
assert.equal(slot.relocation?.encoding, gold.encoding.toLowerCase());
|
|
}
|
|
};
|
|
|
|
function checkFixture(report, query) {
|
|
assert.equal(report.native_inventory.coverage.status, "complete");
|
|
assert.equal(report.library_inventory.status, "complete");
|
|
assert.equal(
|
|
report.declarations.length,
|
|
goldenMethods.size,
|
|
"Selection must not erase global pair ambiguity",
|
|
);
|
|
assert.deepEqual(
|
|
new Set(report.declarations.map(declarationKey)),
|
|
new Set(goldenMethods.keys()),
|
|
);
|
|
const declarations = new Map(
|
|
report.declarations.map((row) => [row.declaration_id, row]),
|
|
);
|
|
const libraries = new Map(
|
|
report.libraries.map((row) => [row.library_id, row]),
|
|
);
|
|
const candidates = new Map(
|
|
report.candidates.map((row) => [row.candidate_id, row]),
|
|
);
|
|
const expectedLibraries = Object.entries(expected.libraries).filter(
|
|
([path]) =>
|
|
(query.abis === undefined || query.abis.includes(path.split("/")[1])) &&
|
|
(query.library_paths === undefined || query.library_paths.includes(path)),
|
|
);
|
|
assert.deepEqual(
|
|
report.libraries.map((row) => row.path).sort(),
|
|
expectedLibraries.map(([path]) => path).sort(),
|
|
);
|
|
for (const library of report.libraries) {
|
|
const gold = expected.libraries[library.path];
|
|
assert.equal(library.sha256, gold.sha256);
|
|
assert.equal(library.abi, library.path.split("/")[1]);
|
|
assert.equal(library.failure, null);
|
|
if (values.variant === "aps2") {
|
|
assert.notEqual(library.coverage.tables_status, "complete");
|
|
assert.ok(library.unknowns.some((text) => /APS2|packed/i.test(text)));
|
|
} else {
|
|
assert.ok(["complete", "partial"].includes(library.coverage.status));
|
|
if (library.coverage.status === "partial")
|
|
assert.ok(library.unknowns.length > 0);
|
|
}
|
|
}
|
|
const selectedMethods = report.declarations.filter(
|
|
(row) =>
|
|
(query.class_name === undefined || row.class_name === query.class_name) &&
|
|
(query.method_names === undefined ||
|
|
query.method_names.includes(row.method_name)) &&
|
|
(query.descriptors === undefined ||
|
|
query.descriptors.includes(row.descriptor)),
|
|
);
|
|
assert.deepEqual(
|
|
report.selected_declaration_ids,
|
|
selectedMethods.map((row) => row.declaration_id),
|
|
);
|
|
const abis = query.abis ?? [
|
|
...new Set(report.libraries.map((row) => row.abi)),
|
|
];
|
|
assert.equal(report.bindings.length, selectedMethods.length * abis.length);
|
|
for (const binding of report.bindings) {
|
|
const declaration = declarations.get(binding.declaration_id);
|
|
assert.ok(selectedMethods.includes(declaration));
|
|
const gold = expected.expected_methods.find(
|
|
(row) =>
|
|
declarationKey(row) === declarationKey(declaration) &&
|
|
row.abi === binding.abi,
|
|
);
|
|
assert.ok(gold);
|
|
const wanted = gold.implementations.filter((row) =>
|
|
expectedLibraries.some(([path]) => path === row.library),
|
|
);
|
|
const observed = binding.candidate_ids.map((id) => candidates.get(id));
|
|
assert.equal(observed.length, wanted.length);
|
|
for (const impl of wanted) {
|
|
const candidate = observed.find(
|
|
(row) =>
|
|
libraries.get(row.library_id)?.path === impl.library &&
|
|
row.address === (impl.address ?? impl.function_address),
|
|
);
|
|
assert.ok(
|
|
candidate,
|
|
`Missing source-known candidate for ${declarationKey(declaration)}`,
|
|
);
|
|
assert.equal(
|
|
libraries.get(candidate.library_id).sha256,
|
|
impl.library_sha256,
|
|
);
|
|
assert.ok(candidate.declaration_ids.includes(binding.declaration_id));
|
|
if (gold.basis === "export") {
|
|
assert.equal(candidate.basis, "jni-export");
|
|
assert.equal(candidate.name, gold.symbol);
|
|
} else {
|
|
assert.equal(candidate.basis, "static-registration-table");
|
|
expectedSlots(candidate, impl);
|
|
if (gold.candidate_classes) {
|
|
assert.equal(candidate.class_attribution, "ambiguous-native-pair");
|
|
assert.deepEqual(
|
|
candidate.declaration_ids
|
|
.map((id) => declarations.get(id).class_name)
|
|
.sort(),
|
|
[...gold.candidate_classes].sort(),
|
|
);
|
|
} else assert.equal(candidate.class_attribution, "unique-native-pair");
|
|
}
|
|
}
|
|
const ambiguousPair = gold.candidate_classes !== undefined;
|
|
const distinct = new Set(
|
|
wanted.map((row) =>
|
|
JSON.stringify([row.library, row.address ?? row.function_address]),
|
|
),
|
|
).size;
|
|
if ((ambiguousPair && wanted.length > 0) || distinct > 1)
|
|
assert.equal(binding.status, "ambiguous");
|
|
else if (distinct === 1)
|
|
assert.equal(
|
|
binding.status,
|
|
"matched",
|
|
"Known candidate presence survives partial coverage",
|
|
);
|
|
else if (binding.coverage !== "complete") {
|
|
assert.equal(
|
|
binding.status,
|
|
"unknown",
|
|
"Incomplete coverage must not establish absence",
|
|
);
|
|
assert.ok(binding.unknowns.length > 0);
|
|
} else assert.equal(binding.status, "unmatched");
|
|
}
|
|
}
|
|
|
|
function checkConscrypt(report, query) {
|
|
const descriptor = "([B[B[B[B[B[B[B[B)J";
|
|
assert.equal(report.native_inventory.coverage.status, "complete");
|
|
const declaration = report.declarations.filter(
|
|
(row) =>
|
|
row.class_name === query.class_name &&
|
|
row.method_name === "EVP_PKEY_new_RSA" &&
|
|
row.descriptor === descriptor,
|
|
);
|
|
assert.equal(declaration.length, 1);
|
|
assert.deepEqual(report.selected_declaration_ids, [
|
|
declaration[0].declaration_id,
|
|
]);
|
|
assert.equal(report.bindings.length, 1);
|
|
const binding = report.bindings[0];
|
|
assert.equal(binding.declaration_id, declaration[0].declaration_id);
|
|
assert.equal(binding.abi, "arm64-v8a");
|
|
assert.equal(binding.status, "matched");
|
|
const library = report.libraries.find(
|
|
(row) => row.path === "lib/arm64-v8a/libconscrypt_jni.so",
|
|
);
|
|
assert.ok(library);
|
|
assert.equal(
|
|
library.sha256,
|
|
expected.libraries.find((row) => row.member === library.path).sha256,
|
|
);
|
|
assert.equal(library.jni_on_load, true);
|
|
const candidate = report.candidates.find(
|
|
(row) =>
|
|
binding.candidate_ids.includes(row.candidate_id) &&
|
|
row.address === "0xfa11c",
|
|
);
|
|
assert.ok(candidate);
|
|
assert.equal(candidate.library_id, library.library_id);
|
|
assert.equal(candidate.basis, "static-registration-table");
|
|
assert.equal(candidate.class_attribution, "unique-native-pair");
|
|
expectedSlots(candidate, {
|
|
name: "EVP_PKEY_new_RSA",
|
|
descriptor,
|
|
function_address: "0xfa11c",
|
|
slots: [
|
|
{
|
|
address: "0x2ada68",
|
|
file_offset: "0x2a5a68",
|
|
resolved_word: "0x749a8",
|
|
encoding: "RELA",
|
|
},
|
|
{
|
|
address: "0x2ada70",
|
|
file_offset: "0x2a5a70",
|
|
resolved_word: "0x7affa",
|
|
encoding: "RELA",
|
|
},
|
|
{
|
|
address: "0x2ada78",
|
|
file_offset: "0x2a5a78",
|
|
resolved_word: "0xfa11c",
|
|
encoding: "RELA",
|
|
},
|
|
],
|
|
});
|
|
assert.equal(
|
|
BigInt(candidate.table.table_address) +
|
|
BigInt(candidate.table.entry.entry_index) * 24n,
|
|
0x2ada68n,
|
|
);
|
|
}
|
|
|
|
function checkSummary(view, report) {
|
|
assert.equal(view.kind, "summary");
|
|
assert.equal(view.parent_operation, "inspect_android_jni_bindings");
|
|
assert.deepEqual(view.artifact, report.apk);
|
|
const summary = view.summary;
|
|
for (const collection of [
|
|
"bindings",
|
|
"declarations",
|
|
"candidates",
|
|
"libraries",
|
|
])
|
|
assert.equal(summary.counts[collection], report[collection].length);
|
|
assert.equal(summary.bindings.length, Math.min(64, report.bindings.length));
|
|
assert.deepEqual(summary.bindings, report.bindings.slice(0, 64));
|
|
assert.equal(view.coverage.total, report.bindings.length);
|
|
assert.deepEqual(summary.native_inventory, report.native_inventory);
|
|
assert.deepEqual(summary.library_inventory, report.library_inventory);
|
|
assert.deepEqual(
|
|
summary.libraries,
|
|
report.libraries,
|
|
"Summary retains diagnostics even without positive candidate edges",
|
|
);
|
|
assert.equal(
|
|
summary.counts.selected_declarations,
|
|
report.selected_declaration_ids.length,
|
|
);
|
|
assert.deepEqual(
|
|
summary.selected_declaration_ids,
|
|
report.selected_declaration_ids.slice(0, 64),
|
|
);
|
|
assert.equal(
|
|
summary.declaration_coverage.total,
|
|
report.selected_declaration_ids.length,
|
|
);
|
|
assert.equal(
|
|
summary.declaration_coverage.examined,
|
|
Math.min(64, report.selected_declaration_ids.length),
|
|
);
|
|
assert.equal(
|
|
summary.declaration_coverage.exhausted,
|
|
report.selected_declaration_ids.length <= 64,
|
|
);
|
|
assert.equal(
|
|
summary.declaration_coverage.next_offset,
|
|
report.selected_declaration_ids.length <= 64 ? null : 64,
|
|
);
|
|
for (const [collection, identity] of [
|
|
["declarations", "declaration_id"],
|
|
["candidates", "candidate_id"],
|
|
["libraries", "library_id"],
|
|
]) {
|
|
const originals = new Map(
|
|
report[collection].map((row) => [row[identity], row]),
|
|
);
|
|
for (const row of summary[collection])
|
|
assert.deepEqual(row, originals.get(row[identity]));
|
|
}
|
|
const declarations = new Set(
|
|
summary.declarations.map((row) => row.declaration_id),
|
|
);
|
|
const libraries = new Set(summary.libraries.map((row) => row.library_id));
|
|
const candidates = new Map(
|
|
summary.candidates.map((row) => [row.candidate_id, row]),
|
|
);
|
|
for (const id of summary.selected_declaration_ids)
|
|
assert.ok(declarations.has(id));
|
|
for (const binding of summary.bindings) {
|
|
assert.ok(declarations.has(binding.declaration_id));
|
|
for (const id of binding.candidate_ids) {
|
|
const candidate = candidates.get(id);
|
|
assert.ok(candidate && libraries.has(candidate.library_id));
|
|
for (const declaration of candidate.declaration_ids)
|
|
assert.ok(declarations.has(declaration));
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkPage(view, report) {
|
|
assert.equal(view.kind, "android-jni");
|
|
assert.equal(view.parent_operation, "inspect_android_jni_bindings");
|
|
assert.deepEqual(view.artifact, report.apk);
|
|
const request = view.view;
|
|
assert.deepEqual(view.source, {
|
|
native_inventory: report.native_inventory,
|
|
library_inventory: report.library_inventory,
|
|
selection: report.selection,
|
|
});
|
|
const accepts = (values, value) =>
|
|
values === undefined || values.includes(value);
|
|
const rows = report[request.collection]
|
|
.map((row, row_index) => ({ ...row, row_index }))
|
|
.filter((row) => {
|
|
if (!accepts(request.indices, row.row_index)) return false;
|
|
switch (request.collection) {
|
|
case "declarations":
|
|
return (
|
|
accepts(request.declaration_ids, row.declaration_id) &&
|
|
accepts(request.descriptors, row.descriptor) &&
|
|
accepts(request.class_names, row.class_name) &&
|
|
accepts(request.method_names, row.method_name)
|
|
);
|
|
case "libraries":
|
|
return (
|
|
accepts(request.library_ids, row.library_id) &&
|
|
accepts(request.abis, row.abi)
|
|
);
|
|
case "candidates":
|
|
return (
|
|
accepts(request.candidate_ids, row.candidate_id) &&
|
|
accepts(request.library_ids, row.library_id) &&
|
|
(request.declaration_ids === undefined ||
|
|
row.declaration_ids.some((id) =>
|
|
request.declaration_ids.includes(id),
|
|
))
|
|
);
|
|
case "bindings":
|
|
return (
|
|
accepts(request.declaration_ids, row.declaration_id) &&
|
|
accepts(request.abis, row.abi) &&
|
|
accepts(request.statuses, row.status)
|
|
);
|
|
default:
|
|
throw new Error(`Unknown retained collection ${request.collection}`);
|
|
}
|
|
});
|
|
const selected = rows
|
|
.slice(request.offset, request.offset + request.limit)
|
|
.map((row) =>
|
|
Object.fromEntries(
|
|
Object.entries(row).filter(
|
|
([key]) =>
|
|
key === "row_index" ||
|
|
request.fields === undefined ||
|
|
request.fields.includes(key),
|
|
),
|
|
),
|
|
);
|
|
assert.deepEqual(view.items, selected);
|
|
assert.equal(view.coverage.total, rows.length);
|
|
assert.equal(view.coverage.examined, selected.length);
|
|
const exhausted =
|
|
request.offset >= rows.length ||
|
|
request.offset + selected.length >= rows.length;
|
|
assert.equal(view.coverage.exhausted, exhausted);
|
|
assert.equal(
|
|
view.coverage.next_offset,
|
|
exhausted ? null : request.offset + selected.length,
|
|
);
|
|
}
|
|
|
|
function collectionPages(preview) {
|
|
const declarationIds = preview.selected_declaration_ids.slice(0, 2);
|
|
const candidateIds = preview.candidates
|
|
.slice(0, 2)
|
|
.map((row) => row.candidate_id);
|
|
assert.ok(declarationIds.length > 0 && candidateIds.length > 0);
|
|
const page = (collection, extra) => ({
|
|
kind: "android-jni",
|
|
collection,
|
|
offset: 0,
|
|
limit: 2,
|
|
...extra,
|
|
});
|
|
return [
|
|
page("bindings", {
|
|
limit: 7,
|
|
fields: ["declaration_id", "abi", "status", "coverage", "candidate_ids"],
|
|
}),
|
|
page("bindings", {
|
|
offset: 1,
|
|
statuses: ["ambiguous"],
|
|
fields: ["declaration_id", "abi", "status"],
|
|
}),
|
|
page("bindings", { statuses: [], fields: ["declaration_id", "coverage"] }),
|
|
page("declarations", {
|
|
offset: 1,
|
|
fields: [
|
|
"declaration_id",
|
|
"class_name",
|
|
"method_name",
|
|
"descriptor",
|
|
"source_index",
|
|
"dex_entry",
|
|
"dex_entry_index",
|
|
"class_index",
|
|
"method_index",
|
|
],
|
|
}),
|
|
page("declarations", {
|
|
class_names: [],
|
|
fields: ["declaration_id", "source_index"],
|
|
}),
|
|
page("libraries", {
|
|
offset: 1,
|
|
fields: [
|
|
"library_id",
|
|
"path",
|
|
"source_path",
|
|
"abi",
|
|
"sha256",
|
|
"source_key",
|
|
"source_index",
|
|
"coverage",
|
|
"failure",
|
|
],
|
|
}),
|
|
page("libraries", { library_ids: [], fields: ["library_id", "coverage"] }),
|
|
page("candidates", {
|
|
indices: [0, 2],
|
|
fields: [
|
|
"candidate_id",
|
|
"library_id",
|
|
"basis",
|
|
"name",
|
|
"descriptor",
|
|
"address",
|
|
"declaration_ids",
|
|
"table",
|
|
],
|
|
}),
|
|
page("candidates", {
|
|
declaration_ids: [],
|
|
fields: ["candidate_id", "table"],
|
|
}),
|
|
page("declarations", { limit: 1, fields: [] }),
|
|
page("declarations", {
|
|
declaration_ids: declarationIds,
|
|
fields: [
|
|
"declaration_id",
|
|
"class_name",
|
|
"method_name",
|
|
"descriptor",
|
|
"dex_entry_index",
|
|
"class_index",
|
|
"method_index",
|
|
"source_index",
|
|
],
|
|
}),
|
|
page("candidates", {
|
|
candidate_ids: candidateIds,
|
|
fields: [
|
|
"candidate_id",
|
|
"library_id",
|
|
"declaration_ids",
|
|
"address",
|
|
"table",
|
|
],
|
|
}),
|
|
];
|
|
}
|
|
|
|
async function qualifyRetainedRemoval(invoke) {
|
|
const ownedCopy = join(output, "owned-retained-source.apk");
|
|
assert.notEqual(ownedCopy, target);
|
|
let copyOwned = false;
|
|
const start = performance.now(),
|
|
firstCall = calls.length;
|
|
try {
|
|
await copyFile(target, ownedCopy, constants.COPYFILE_EXCL);
|
|
copyOwned = true;
|
|
const query =
|
|
values.variant === "conscrypt"
|
|
? { ...queries[0], path: ownedCopy }
|
|
: {
|
|
path: ownedCopy,
|
|
class_name: "fixture.jni.Bindings",
|
|
method_names: ["registeredData"],
|
|
descriptors: ["(I)I"],
|
|
abis: ["arm64-v8a"],
|
|
library_paths: ["lib/arm64-v8a/libfixture.so"],
|
|
};
|
|
const parent = await invoke("inspect_android_jni_bindings", {
|
|
...query,
|
|
detail: "complete",
|
|
});
|
|
assert.equal(parent.operation, "inspect_android_jni_bindings");
|
|
assert.equal(parent.subject.digest.sha256, targetSha);
|
|
assert.equal(parent.normalized_result.apk.path, ownedCopy);
|
|
const report = parent.normalized_result;
|
|
(values.variant === "conscrypt" ? checkConscrypt : checkFixture)(
|
|
report,
|
|
query,
|
|
);
|
|
const before = factDigest(report);
|
|
await unlink(ownedCopy);
|
|
copyOwned = false;
|
|
await assert.rejects(stat(ownedCopy), { code: "ENOENT" });
|
|
// After deletion only retained views and a bundle export are requested.
|
|
// No target-analysis call or caller's original path is used for recovery.
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: parent.evidence_id,
|
|
};
|
|
const summary = await invoke("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "summary" },
|
|
});
|
|
assert.equal(
|
|
summary.normalized_result.parent_evidence_id,
|
|
parent.evidence_id,
|
|
);
|
|
checkSummary(summary.normalized_result, report);
|
|
const declarations = report.bindings.map(
|
|
(binding) => binding.declaration_id,
|
|
);
|
|
const pages = [
|
|
{
|
|
kind: "android-jni",
|
|
collection: "candidates",
|
|
offset: 0,
|
|
limit: 64,
|
|
declaration_ids: declarations,
|
|
fields: [
|
|
"candidate_id",
|
|
"library_id",
|
|
"basis",
|
|
"name",
|
|
"descriptor",
|
|
"address",
|
|
"declaration_ids",
|
|
"class_attribution",
|
|
"table",
|
|
],
|
|
},
|
|
{
|
|
kind: "android-jni",
|
|
collection: "bindings",
|
|
offset: 0,
|
|
limit: 1,
|
|
fields: [
|
|
"declaration_id",
|
|
"abi",
|
|
"status",
|
|
"coverage",
|
|
"candidate_ids",
|
|
],
|
|
},
|
|
];
|
|
let tableEntries = 0;
|
|
for (const view of pages) {
|
|
const evidence = await invoke("inspect_analysis_view", { source, view });
|
|
assert.equal(
|
|
evidence.normalized_result.parent_evidence_id,
|
|
parent.evidence_id,
|
|
);
|
|
checkPage(evidence.normalized_result, report);
|
|
tableEntries += evidence.normalized_result.items.filter(
|
|
(row) => row.table != null,
|
|
).length;
|
|
}
|
|
if (values.variant !== "aps2")
|
|
assert.ok(
|
|
tableEntries > 0,
|
|
"Relocated slot evidence must remain available after APK removal",
|
|
);
|
|
const proof = join(output, "retained-removal-evidence.json");
|
|
await invoke("export_evidence_bundle", { path: proof });
|
|
const bundle = JSON.parse(await readFile(proof, "utf8"));
|
|
const retained = bundle.records.find(
|
|
(row) => row.evidence_id === parent.evidence_id,
|
|
);
|
|
assert.ok(retained);
|
|
assert.deepEqual(retained, parent);
|
|
assert.equal(factDigest(retained.normalized_result), before);
|
|
assert.equal(
|
|
createHash("sha256")
|
|
.update(await readFile(target))
|
|
.digest("hex"),
|
|
targetSha,
|
|
);
|
|
return {
|
|
name: "retained-removal",
|
|
elapsed_ms: performance.now() - start,
|
|
calls: calls.length - firstCall,
|
|
first_call: firstCall,
|
|
question:
|
|
"After deleting only the harness-owned APK copy, retrieve retained summary, exact candidate slots and selected binding fields, then export unchanged complete parent Evidence.",
|
|
owned_copy: ownedCopy,
|
|
original_path: target,
|
|
original_sha256: targetSha,
|
|
parent_evidence_id: parent.evidence_id,
|
|
fact_digest: before,
|
|
retained_table_rows: tableEntries,
|
|
scope:
|
|
"Observed public SDK success after owned-copy removal; only retained-view/export calls follow deletion. No target code executes; internal engine calls are not directly instrumented by this public harness.",
|
|
};
|
|
} finally {
|
|
if (copyOwned) await unlink(ownedCopy);
|
|
}
|
|
}
|
|
|
|
const require = createRequire(join(repo, "package.json"));
|
|
const { Client } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
|
|
);
|
|
const { StdioClientTransport } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
|
|
);
|
|
const { Ajv2020 } = await import(
|
|
pathToFileURL(require.resolve("ajv/dist/2020.js"))
|
|
);
|
|
const environment = {
|
|
PATH: process.env.PATH,
|
|
JAVA_HOME: resolve(values["java-home"]),
|
|
REA_JADX_MCP_JAR: resolve(values.jar),
|
|
REA_JADX_HEAP_MIB: "1024",
|
|
REA_JADX_ACTIVE_PROCESSOR_COUNT: "2",
|
|
NODE_OPTIONS: "--max-old-space-size=3072",
|
|
UV_THREADPOOL_SIZE: "2",
|
|
REA_LOG_LEVEL: "silent",
|
|
REA_MCP_TOOL_FAMILIES:
|
|
values.pages || values["retained-removal"]
|
|
? "android,application,session"
|
|
: values.proof
|
|
? "android,session"
|
|
: "android",
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
|
};
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(repo, "scripts/rea.mjs"), "mcp"],
|
|
cwd: repo,
|
|
env: environment,
|
|
stderr: "pipe",
|
|
maxBufferSize: 16 * 1024 * 1024,
|
|
});
|
|
let stderr = "";
|
|
transport.stderr?.on("data", (chunk) => {
|
|
if (stderr.length < 65536)
|
|
stderr += chunk.toString().slice(0, 65536 - stderr.length);
|
|
});
|
|
const client = new Client({ name: "rea-real-android-jni", version: "1" });
|
|
const calls = [],
|
|
runs = [],
|
|
parents = [];
|
|
let firstReport;
|
|
let receipt;
|
|
let callPhase = "task";
|
|
let validateJniOutput;
|
|
const queries =
|
|
values.variant === "conscrypt"
|
|
? [
|
|
{
|
|
path: target,
|
|
class_name: "org.conscrypt.NativeCrypto",
|
|
method_names: ["EVP_PKEY_new_RSA"],
|
|
descriptors: ["([B[B[B[B[B[B[B[B)J"],
|
|
abis: ["arm64-v8a"],
|
|
library_paths: ["lib/arm64-v8a/libconscrypt_jni.so"],
|
|
},
|
|
]
|
|
: [
|
|
{ path: target },
|
|
{
|
|
path: target,
|
|
class_name: "fixture.jni.Bindings",
|
|
method_names: ["ambiguous"],
|
|
descriptors: ["(I)I"],
|
|
abis: ["arm64-v8a"],
|
|
},
|
|
{ path: target, method_names: ["registered_表𐐀"], abis: ["arm64-v8a"] },
|
|
{
|
|
path: target,
|
|
method_names: ["sharedExport"],
|
|
abis: ["arm64-v8a"],
|
|
library_paths: ["lib/arm64-v8a/libduplicate.so"],
|
|
},
|
|
];
|
|
try {
|
|
const discoveryStart = performance.now();
|
|
await client.connect(transport);
|
|
const catalog = await client.listTools();
|
|
const startup_catalog_elapsed_ms = performance.now() - discoveryStart;
|
|
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
|
const contracts = new Map();
|
|
for (const name of [
|
|
"inspect_android_jni_bindings",
|
|
...(values.proof || values["retained-removal"]
|
|
? ["export_evidence_bundle"]
|
|
: []),
|
|
...(values.pages || values["retained-removal"]
|
|
? ["inspect_analysis_view"]
|
|
: []),
|
|
]) {
|
|
const selected = catalog.tools.filter((tool) => tool.name === name);
|
|
assert.equal(selected.length, 1);
|
|
assert.ok(selected[0].outputSchema);
|
|
contracts.set(name, {
|
|
definition: selected[0],
|
|
input: ajv.compile(selected[0].inputSchema),
|
|
output: ajv.compile(selected[0].outputSchema),
|
|
});
|
|
}
|
|
validateJniOutput = contracts.get("inspect_android_jni_bindings").output;
|
|
const invoke = async (name, args) => {
|
|
const contract = contracts.get(name);
|
|
const sent = { ...args };
|
|
if (
|
|
values["omit-detail"] &&
|
|
name === "inspect_android_jni_bindings" &&
|
|
sent.detail === "summary"
|
|
)
|
|
delete sent.detail;
|
|
assert.ok(contract.input(sent), JSON.stringify(contract.input.errors));
|
|
const start = performance.now();
|
|
const result = await client.callTool(
|
|
{ name, arguments: sent },
|
|
{ toolDefinition: contract.definition, timeout: 120000 },
|
|
);
|
|
calls.push({
|
|
phase: callPhase,
|
|
name,
|
|
arguments: sent,
|
|
result,
|
|
elapsed_ms: performance.now() - start,
|
|
});
|
|
assert.notEqual(result.isError, true, JSON.stringify(result.content));
|
|
assert.ok(
|
|
contract.output(result.structuredContent),
|
|
JSON.stringify(contract.output.errors),
|
|
);
|
|
return result.structuredContent;
|
|
};
|
|
let coldDigests;
|
|
for (let pass = 0; pass < repeats; pass++) {
|
|
const start = performance.now(),
|
|
firstCall = calls.length,
|
|
firstParent = parents.length;
|
|
const digests = [];
|
|
for (const query of queries) {
|
|
const selectedView =
|
|
values.detail === "summary" && values.pages && query === queries[1]
|
|
? {
|
|
kind: "android-jni",
|
|
collection: "bindings",
|
|
offset: 0,
|
|
limit: 1,
|
|
fields: [
|
|
"declaration_id",
|
|
"abi",
|
|
"status",
|
|
"coverage",
|
|
"candidate_ids",
|
|
],
|
|
}
|
|
: undefined;
|
|
const evidence = await invoke("inspect_android_jni_bindings", {
|
|
...query,
|
|
detail: values.detail,
|
|
...(selectedView === undefined ? {} : { view: selectedView }),
|
|
});
|
|
assert.equal(
|
|
evidence.operation,
|
|
values.detail === "summary"
|
|
? "inspect_analysis_view"
|
|
: "inspect_android_jni_bindings",
|
|
);
|
|
assert.equal(evidence.subject.digest.sha256, targetSha);
|
|
const parent = {
|
|
id:
|
|
values.detail === "summary"
|
|
? evidence.normalized_result.parent_evidence_id
|
|
: evidence.evidence_id,
|
|
query,
|
|
normalized:
|
|
values.detail === "complete" ? evidence.normalized_result : undefined,
|
|
view:
|
|
values.detail === "summary" ? evidence.normalized_result : undefined,
|
|
pages: [],
|
|
};
|
|
if (values.detail === "summary") assert.equal(evidence.raw_result, null);
|
|
parents.push(parent);
|
|
if (values.pages && query === queries[0]) {
|
|
const preview =
|
|
values.detail === "summary"
|
|
? evidence.normalized_result.summary
|
|
: evidence.normalized_result;
|
|
for (const view of collectionPages(preview)) {
|
|
const result = await invoke("inspect_analysis_view", {
|
|
source: { kind: "retained-evidence", evidence_id: parent.id },
|
|
view,
|
|
});
|
|
parent.pages.push(result.normalized_result);
|
|
assert.equal(result.normalized_result.parent_evidence_id, parent.id);
|
|
}
|
|
}
|
|
}
|
|
let bundle;
|
|
if (values.proof) {
|
|
const path = join(output, `pass-${pass}-evidence.json`);
|
|
await invoke("export_evidence_bundle", { path });
|
|
bundle = JSON.parse(await readFile(path, "utf8"));
|
|
}
|
|
for (const delivered of parents.slice(firstParent)) {
|
|
let report = delivered.normalized;
|
|
if (values.proof) {
|
|
const parent = bundle.records.find(
|
|
(record) => record.evidence_id === delivered.id,
|
|
);
|
|
assert.ok(parent, "Complete source Evidence must remain exportable");
|
|
assert.equal(parent.operation, "inspect_android_jni_bindings");
|
|
assert.equal(parent.subject.digest.sha256, targetSha);
|
|
if (report !== undefined)
|
|
assert.deepEqual(parent.normalized_result, report);
|
|
report = parent.normalized_result;
|
|
for (const [key, value] of Object.entries(delivered.query))
|
|
assert.deepEqual(parent.parameters[key], value);
|
|
}
|
|
assert.ok(report);
|
|
const { path: _path, ...selection } = delivered.query;
|
|
assert.deepEqual(report.selection, selection);
|
|
assert.equal(report.apk.path, target);
|
|
assert.equal(report.apk.sha256, targetSha);
|
|
(values.variant === "conscrypt" ? checkConscrypt : checkFixture)(
|
|
report,
|
|
delivered.query,
|
|
);
|
|
if (delivered.view?.kind === "android-jni")
|
|
checkPage(delivered.view, report);
|
|
else if (delivered.view) checkSummary(delivered.view, report);
|
|
for (const page of delivered.pages) checkPage(page, report);
|
|
digests.push(factDigest(report));
|
|
firstReport ??= report;
|
|
}
|
|
if (pass === 0) coldDigests = digests;
|
|
else
|
|
assert.deepEqual(
|
|
digests,
|
|
coldDigests,
|
|
"Warm reuse must preserve exact normalized facts",
|
|
);
|
|
runs.push({
|
|
temperature: pass === 0 ? "cold-provider" : "warm-session",
|
|
elapsed_ms: performance.now() - start,
|
|
calls: calls.length - firstCall,
|
|
fact_digests: digests,
|
|
});
|
|
}
|
|
const summary = {
|
|
targetSha,
|
|
variant: values.variant,
|
|
detail: values.detail,
|
|
omit_detail: values["omit-detail"],
|
|
proof: values.proof,
|
|
pages: values.pages,
|
|
question:
|
|
values.variant === "conscrypt"
|
|
? "Resolve NativeCrypto.EVP_PKEY_new_RSA to its static arm64 Conscrypt implementation and retain exact library identity and relocated slot evidence."
|
|
: "Resolve every fixture native declaration by ABI and library; preserve overloads, Unicode, global class ambiguity, duplicate libraries, unmatched methods and relocation uncertainty.",
|
|
baseline:
|
|
"Earlier main has no batch JNI operation. Manual inventory/export lookup cannot recover RegisterNatives with equivalent coverage; no equivalent before-speedup claim.",
|
|
startup_catalog_elapsed_ms,
|
|
elapsed_scope:
|
|
"Per-run task includes investigation calls, selected pages, proof export and local oracle checks. Startup/catalog and final transport cleanup are separate one-time costs. Resource supervisor measures the whole process family, including these phases.",
|
|
runs,
|
|
task_calls: calls.length,
|
|
calls: calls.length,
|
|
qualifications: [],
|
|
};
|
|
receipt = summary;
|
|
await writeFile(
|
|
join(output, "summary.json"),
|
|
JSON.stringify(summary, null, 2),
|
|
);
|
|
await writeFile(join(output, "calls.json"), JSON.stringify(calls));
|
|
if (values["retained-removal"]) {
|
|
callPhase = "qualification";
|
|
summary.qualifications.push(await qualifyRetainedRemoval(invoke));
|
|
summary.calls = calls.length;
|
|
await writeFile(
|
|
join(output, "summary.json"),
|
|
JSON.stringify(summary, null, 2),
|
|
);
|
|
}
|
|
console.log(JSON.stringify(summary));
|
|
} finally {
|
|
const closeStart = performance.now();
|
|
try {
|
|
await client.close();
|
|
} finally {
|
|
if (receipt) {
|
|
receipt.transport_close_elapsed_ms = performance.now() - closeStart;
|
|
await writeFile(
|
|
join(output, "summary.json"),
|
|
JSON.stringify(receipt, null, 2),
|
|
);
|
|
}
|
|
await writeFile(join(output, "calls.json"), JSON.stringify(calls));
|
|
await writeFile(join(output, "stderr.log"), stderr);
|
|
}
|
|
}
|
|
|
|
// CLI parity runs once after the MCP transport has joined its provider cleanup,
|
|
// keeping a second JVM out of the live warm-session measurement and memory set.
|
|
if (values.cli) {
|
|
const { path: _path, ...query } = queries[0];
|
|
const proof = join(output, "cli-evidence.json");
|
|
const args = [
|
|
join(repo, "scripts/rea.mjs"),
|
|
"inspect-android-jni-bindings",
|
|
target,
|
|
"--query",
|
|
JSON.stringify(query),
|
|
"--detail",
|
|
values.detail,
|
|
"--json",
|
|
];
|
|
if (values.detail === "summary") args.push("--evidence_output", proof);
|
|
const page =
|
|
values.pages && values.detail === "summary"
|
|
? {
|
|
kind: "android-jni",
|
|
collection: "bindings",
|
|
offset: 0,
|
|
limit: 7,
|
|
fields: [
|
|
"declaration_id",
|
|
"abi",
|
|
"status",
|
|
"coverage",
|
|
"candidate_ids",
|
|
],
|
|
}
|
|
: undefined;
|
|
if (page) args.push("--view", JSON.stringify(page));
|
|
const start = performance.now();
|
|
let command;
|
|
try {
|
|
command = await promisify(execFile)(process.execPath, args, {
|
|
cwd: repo,
|
|
env: environment,
|
|
encoding: "utf8",
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
timeout: 120000,
|
|
killSignal: "SIGINT",
|
|
});
|
|
} catch (cause) {
|
|
await writeFile(join(output, "cli-stdout.json"), cause.stdout ?? "");
|
|
await writeFile(join(output, "cli-stderr.log"), cause.stderr ?? "");
|
|
throw cause;
|
|
}
|
|
const { stdout, stderr } = command;
|
|
await writeFile(join(output, "cli-stdout.json"), stdout);
|
|
await writeFile(join(output, "cli-stderr.log"), stderr);
|
|
const response = JSON.parse(stdout);
|
|
const evidence = values.detail === "summary" ? response.summary : response;
|
|
assert.ok(
|
|
validateJniOutput(evidence),
|
|
JSON.stringify(validateJniOutput.errors),
|
|
);
|
|
let report = evidence.normalized_result;
|
|
if (values.detail === "summary") {
|
|
const bundle = JSON.parse(await readFile(proof, "utf8"));
|
|
const parent = bundle.records.find(
|
|
(row) =>
|
|
row.evidence_id === evidence.normalized_result.parent_evidence_id,
|
|
);
|
|
assert.ok(parent);
|
|
report = parent.normalized_result;
|
|
if (page) checkPage(evidence.normalized_result, report);
|
|
else checkSummary(evidence.normalized_result, report);
|
|
}
|
|
assert.equal(factDigest(report), factDigest(firstReport));
|
|
await writeFile(
|
|
join(output, "cli-parity.json"),
|
|
JSON.stringify(
|
|
{
|
|
arguments: args,
|
|
elapsed_ms: performance.now() - start,
|
|
fact_digest: factDigest(report),
|
|
response,
|
|
stderr,
|
|
scope:
|
|
"One cold CLI parity check after MCP cleanup; separate from MCP task/token measurements.",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|