Files
reverse-engineering-anything/tests/support/analysis-efficiency/android-jni-library.mjs
T
gensokyo-dev 570abe95f2 feat(android): link native declarations to static JNI candidates (#7472)
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.
2026-10-27 18:53:22 +08:00

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,
}),
);