Link IL2CPP metadata names to native addresses and types through shared CLI/MCP workflows, with bounded queries and atomic Ghidra seed export. Preserve the original implementation by 8rulerstar and follow-up correctness, resource, provenance, and CI dependency repairs. Verified against real Windows/Android artifacts, independent references, actual Ghidra, and focused CLI/MCP tests. Fixes #5112.
195 lines
7.4 KiB
JavaScript
195 lines
7.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { resolve } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { absoluteCallerPath } from "../dist/filesystem/callerPath.js";
|
|
import { toolContract } from "../dist/contracts/toolContracts.js";
|
|
import { analysisViewResultSchema } from "../dist/domain/analysisView/analysisView.js";
|
|
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
|
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
|
|
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
|
|
import { hashVerifierFile } from "./lib/verifier-file-resources.mjs";
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args.includes("--help") || args.includes("-h")) {
|
|
console.log(
|
|
"Usage: npm run verify:il2cpp:metadata -- [--metadata PATH]\nPATH is a caller-owned, unencrypted global-metadata.dat (version 24, 27, 29, 31 or 39). Without --metadata, only the advertised contract is verified.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
if (!(args.length === 0 || (args.length === 2 && args[0] === "--metadata")))
|
|
throw new Error("Expected --metadata PATH");
|
|
const path = args[1] === undefined ? null : absoluteCallerPath(args[1]);
|
|
const operation = "inspect_il2cpp_metadata";
|
|
const contract = toolContract(operation);
|
|
const entrypoint = resolve("scripts/rea.mjs");
|
|
const execute = promisify(execFile);
|
|
const client = new Client({ name: "il2cpp-metadata-verifier", version: "1" });
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [entrypoint, "mcp"],
|
|
env: Object.fromEntries(
|
|
Object.entries(process.env).filter(
|
|
([, value]) => typeof value === "string",
|
|
),
|
|
),
|
|
stderr: "pipe",
|
|
});
|
|
transport.stderr?.resume();
|
|
const call = async (name, input) => {
|
|
const response = await client.callTool(
|
|
{ name, arguments: input },
|
|
{ timeout: 120_000 },
|
|
);
|
|
assert.notEqual(response.isError, true, mcpTextValue(response));
|
|
return JSON.parse(mcpTextValue(response));
|
|
};
|
|
const resources = new ArtifactResourceScope();
|
|
let primaryError;
|
|
try {
|
|
await client.connect(transport);
|
|
const { tools } = await client.listTools();
|
|
assert(tools.some((tool) => tool.name === operation));
|
|
if (path === null)
|
|
console.log("[unverified] Metadata inspection: no --metadata was supplied");
|
|
else {
|
|
const identity = await hashVerifierFile(path, resources);
|
|
const started = performance.now();
|
|
const { stdout } = await execute(
|
|
process.execPath,
|
|
[entrypoint, "inspect-il2cpp-metadata", path, "--json"],
|
|
{ timeout: 120_000, maxBuffer: 128 * 1024 * 1024 },
|
|
);
|
|
const cliMs = Math.round(performance.now() - started);
|
|
const cli = contract.outputSchema.parse(JSON.parse(stdout));
|
|
const report = cli.normalized_result;
|
|
assert.equal(report.artifact.path, path);
|
|
assert.equal(report.artifact.sha256, identity.sha256);
|
|
assert.equal(report.magic, 0xfab11baf);
|
|
const complete = await client.callTool(
|
|
{ name: operation, arguments: { path, detail: "complete" } },
|
|
{ timeout: 120_000 },
|
|
);
|
|
if (complete.isError === true) {
|
|
// Large inventories exceed the stdio budget; the result must stay
|
|
// retained and addressable rather than truncated.
|
|
const { error } = JSON.parse(mcpTextValue(complete));
|
|
assert.equal(error.code, "resource_constraint");
|
|
assert.equal(error.details.reported_limits.boundary, "mcp-response");
|
|
assert.match(error.details.reported_limits.evidence_id, /^ev_/u);
|
|
console.log(
|
|
`[ok] MCP complete delivery refused at the stdio budget (${error.details.reported_limits.response_bytes_at_least} bytes) with retained Evidence`,
|
|
);
|
|
} else {
|
|
const mcp = contract.outputSchema.parse(
|
|
JSON.parse(mcpTextValue(complete)),
|
|
);
|
|
assert.deepEqual(mcp.normalized_result, report);
|
|
console.log("[ok] CLI/MCP complete Evidence parity");
|
|
}
|
|
|
|
// Cross-check independently reported ranges against the row tables.
|
|
assert.equal(
|
|
report.images.reduce((total, image) => total + image.type_count, 0),
|
|
report.types.length,
|
|
);
|
|
assert.equal(
|
|
report.types.reduce((total, type) => total + type.method_count, 0),
|
|
report.methods.length,
|
|
);
|
|
report.types.forEach((type, index) => {
|
|
for (let at = 0; at < type.method_count; at += 1)
|
|
assert.equal(
|
|
report.methods[type.method_start + at].declaring_type,
|
|
index,
|
|
`method ${type.method_start + at} is outside its declaring type range`,
|
|
);
|
|
});
|
|
console.log("[ok] image type ranges and type method ranges agree");
|
|
|
|
const summary = contract.outputSchema.parse(
|
|
await call(operation, { path }),
|
|
).normalized_result;
|
|
assert.equal(summary.summary.magic, report.magic);
|
|
assert.deepEqual(summary.summary.counts, {
|
|
sections: report.sections.length,
|
|
images: report.images.length,
|
|
types: report.types.length,
|
|
methods: report.methods.length,
|
|
nested_types: report.types.filter((type) => type.declaring_type !== -1)
|
|
.length,
|
|
fields: report.fields.length,
|
|
properties: report.properties.length,
|
|
events: report.events.length,
|
|
parameters: report.parameters.length,
|
|
string_literals: report.string_literals.length,
|
|
// The verifier selects no binary, so native observations are unrequested.
|
|
code_gen_modules: null,
|
|
method_addresses: null,
|
|
method_address_states: null,
|
|
return_types: null,
|
|
field_types: null,
|
|
parameter_types: null,
|
|
});
|
|
const page = analysisViewResultSchema.parse(
|
|
(
|
|
await call("inspect_analysis_view", {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: summary.parent_evidence_id,
|
|
},
|
|
view: { kind: "il2cpp-metadata", collection: "methods", limit: 16 },
|
|
})
|
|
).normalized_result,
|
|
);
|
|
assert.deepEqual(
|
|
page.items,
|
|
report.methods.slice(0, 16).map((row, index) => ({ ...row, index })),
|
|
);
|
|
const last = analysisViewResultSchema.parse(
|
|
(
|
|
await call("inspect_analysis_view", {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: summary.parent_evidence_id,
|
|
},
|
|
view: {
|
|
kind: "il2cpp-metadata",
|
|
collection: "types",
|
|
offset: Math.max(0, report.types.length - 4),
|
|
limit: 16,
|
|
},
|
|
})
|
|
).normalized_result,
|
|
);
|
|
const lastOffset = Math.max(0, report.types.length - 4);
|
|
assert.deepEqual(
|
|
last.items,
|
|
report.types.slice(lastOffset).map((row, index) => ({
|
|
...row,
|
|
index: lastOffset + index,
|
|
})),
|
|
);
|
|
assert.equal(last.coverage.next_offset ?? null, null);
|
|
assert.deepEqual(
|
|
await hashVerifierFile(path, resources),
|
|
identity,
|
|
"Input metadata must remain unchanged",
|
|
);
|
|
console.log(
|
|
`[ok] summary and page parity; ${report.images.length} images, ${report.types.length} types, ${report.methods.length} methods; complete Evidence ${Buffer.byteLength(stdout)} bytes, summary ${Buffer.byteLength(JSON.stringify(summary))} bytes; CLI ${cliMs} ms`,
|
|
);
|
|
}
|
|
} catch (cause) {
|
|
primaryError = cause;
|
|
throw cause;
|
|
} finally {
|
|
await closeVerifierResources(
|
|
[() => client.close(), () => transport.close(), () => resources.close()],
|
|
primaryError,
|
|
);
|
|
}
|
|
console.log("All asserted IL2CPP metadata CLI/MCP checks passed");
|