mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 20:06:14 +00:00
Link original DEX native declarations to per-ABI function exports and relocated static JNI table candidates through shared CLI/MCP workflows. Preserve global attribution ambiguity, exact provenance, partial coverage and retained Evidence; default MCP delivery to bounded summaries with filtered/batch pages. Use one immutable ELF relocation decoder with streaming IL2CPP materialization, cooperative bounded export/version parsing and bounded JNI state. Reuse validated session inventories without loading target libraries or executing JNI code. Validation: relevant type/lint/build and parser/domain/public CLI/MCP regressions; genuine four-ABI NDK fixtures and independent library/slot oracles; actual JDK/JADX Conscrypt workflows with retained-source removal. Full measurements and limitations are recorded in docs/verification/android-jni-bindings-2026-10-27.md. Closes #6403.
157 lines
5.3 KiB
JavaScript
157 lines
5.3 KiB
JavaScript
// Static parser qualification against the separately produced NDK/readelf/byte oracle.
|
|
// The caller supplies built parser and fixture paths; this script never executes an ELF.
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, extname, join, resolve } from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { pathToFileURL } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
parser: { type: "string" },
|
|
"fixture-root": { type: "string" },
|
|
output: { type: "string" },
|
|
},
|
|
});
|
|
for (const name of ["parser", "fixture-root", "output"])
|
|
assert.ok(values[name]?.startsWith("/"), `select absolute --${name}`);
|
|
const parserBytes = await readFile(values.parser);
|
|
const { inspectAndroidJniLibrary } = await import(
|
|
pathToFileURL(values.parser).href
|
|
);
|
|
const oracleBytes = await readFile(
|
|
join(values["fixture-root"], "full-expectations.json"),
|
|
);
|
|
const oracle = JSON.parse(oracleBytes);
|
|
const receipts = [];
|
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
|
|
async function verify(expected, variant) {
|
|
const bytes = await readFile(expected.path);
|
|
assert.equal(sha256(bytes), expected.sha256);
|
|
const before = performance.now();
|
|
const parsed = await inspectAndroidJniLibrary(bytes);
|
|
const elapsed = performance.now() - before;
|
|
assert.ok(parsed.ok, JSON.stringify(parsed));
|
|
const facts = parsed.value;
|
|
assert.equal(facts.bits, expected.pointer_bytes * 8);
|
|
assert.equal(facts.byte_order, "little");
|
|
const expectedNames = Object.keys(expected.exports)
|
|
.filter((name) => name === "JNI_OnLoad" || name.startsWith("Java_"))
|
|
.sort();
|
|
assert.deepEqual(facts.exports.map((row) => row.name).sort(), expectedNames);
|
|
for (const row of facts.exports)
|
|
assert.equal(row.address, expected.exports[row.name].address);
|
|
const rows = facts.tables.flatMap((table) => table.entries);
|
|
if (variant === "aps2") {
|
|
assert.equal(facts.coverage.tables_status, "unknown");
|
|
assert.equal(rows.length, 0);
|
|
assert.ok(facts.unknowns.some((unknown) => unknown.includes("APS2")));
|
|
} else
|
|
for (const row of expected.tables) {
|
|
const actual = rows.find(
|
|
(candidate) => candidate.slots[0]?.address === row.slots[0]?.address,
|
|
);
|
|
assert.ok(
|
|
actual,
|
|
`missing ${expected.path} ${row.name} at ${row.slots[0]?.address}`,
|
|
);
|
|
assert.equal(actual.name, row.name);
|
|
assert.equal(actual.descriptor, row.descriptor);
|
|
assert.equal(actual.function_address, row.function_address);
|
|
const code =
|
|
expected.machine === 40
|
|
? BigInt(row.function_address) & ~1n
|
|
: BigInt(row.function_address);
|
|
assert.equal(actual.code_address, `0x${code.toString(16)}`);
|
|
const load = expected.loads.find(
|
|
(load) =>
|
|
load.type === 1 &&
|
|
(load.flags & 1) !== 0 &&
|
|
code >= BigInt(load.address) &&
|
|
code < BigInt(load.address + load.file_bytes),
|
|
);
|
|
assert.ok(load, "independent executable segment must cover the function");
|
|
assert.equal(
|
|
actual.function_file_offset,
|
|
Number(code - BigInt(load.address) + BigInt(load.offset)),
|
|
);
|
|
for (const [index, slot] of row.slots.entries()) {
|
|
assert.equal(actual.slots[index]?.address, slot.address);
|
|
assert.equal(
|
|
actual.slots[index]?.file_offset,
|
|
Number(BigInt(slot.file_offset)),
|
|
);
|
|
assert.equal(actual.slots[index]?.value, slot.resolved_word);
|
|
assert.equal(
|
|
actual.slots[index]?.relocation?.encoding,
|
|
slot.encoding.toLowerCase(),
|
|
);
|
|
}
|
|
}
|
|
receipts.push({
|
|
path: expected.path,
|
|
sha256: expected.sha256,
|
|
variant,
|
|
elapsed_ms: elapsed,
|
|
exports: facts.exports.length,
|
|
entries: rows.length,
|
|
verified_entries: variant === "aps2" ? 0 : expected.tables.length,
|
|
coverage: facts.coverage,
|
|
unknowns: facts.unknowns,
|
|
});
|
|
}
|
|
for (const [variant, value] of Object.entries(oracle.variants))
|
|
for (const expected of Object.values(value.libraries))
|
|
await verify(expected, variant);
|
|
if (Object.hasOwn(oracle, "conscrypt")) {
|
|
const conscrypt = JSON.parse(
|
|
await readFile(
|
|
join(values["fixture-root"], "oracle/conscrypt-arm64.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
await verify(conscrypt, "conscrypt");
|
|
}
|
|
const fingerprints = {};
|
|
for (const relative of [
|
|
".",
|
|
"../metadata/ElfPointerRelocations",
|
|
"../exports/NativeExportParser",
|
|
"../metadata/ElfImage",
|
|
"../metadata/NativeSectionDirectory",
|
|
"../../domain/elfRelocation",
|
|
"../../domain/elfDynamicEntry",
|
|
]) {
|
|
const path =
|
|
relative === "."
|
|
? values.parser
|
|
: resolve(dirname(values.parser), relative + extname(values.parser));
|
|
fingerprints[relative] = { path, sha256: sha256(await readFile(path)) };
|
|
}
|
|
const output = {
|
|
parser: values.parser,
|
|
parser_sha256: sha256(parserBytes),
|
|
oracle_sha256: sha256(oracleBytes),
|
|
node: process.version,
|
|
static_only: true,
|
|
module_fingerprints: fingerprints,
|
|
receipts,
|
|
};
|
|
await writeFile(values.output, JSON.stringify(output, null, 2) + "\n", {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
});
|
|
console.log(
|
|
JSON.stringify({
|
|
libraries: receipts.length,
|
|
verified_entries: receipts.reduce(
|
|
(sum, row) => sum + row.verified_entries,
|
|
0,
|
|
),
|
|
output: values.output,
|
|
}),
|
|
);
|