feat(il2cpp): link native methods and export Ghidra seeds

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.
This commit is contained in:
JANG BYUNGKUN authored and GitHub committed 2026-10-20 21:56:54 +08:00
1 parent e50a271c72
commit 52d1a72af0
43 files changed
+5060 -368

No files matched your search

@@ -1,3 +1,5 @@
import { createEmptyIl2cppMetadataFixture } from "../../domain/il2cpp/il2cppMetadata.fixture.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { expect, it } from "vitest";
import { Il2cppMetadataService } from "./Il2cppMetadataService.js";
import {
@@ -6,48 +8,18 @@ import {
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import {
IL2CPP_METADATA_SECTION_NAMES,
type Il2cppMetadata,
} from "../../domain/il2cpp/il2cppMetadata.js";
import { ok } from "../../domain/result.js";
import type { Il2cppMetadata } from "../../domain/il2cpp/il2cppMetadata.js";
import { err, ok } from "../../domain/result.js";
import { EvidenceFileError } from "../../domain/evidenceErrors.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import type { Il2cppSeedWriterPort } from "./Il2cppSeedWriterPort.js";
const identity = {
id: "fixture-il2cpp",
name: "Source-owned IL2CPP reader seam",
version: "1",
};
const report: Il2cppMetadata = {
artifact: {
path: "/artifacts/global-metadata.dat",
sha256: "a".repeat(64),
bytes: 1024,
},
magic: 0xfab11baf,
metadata_version: 39,
index_widths: {
type_definition: 1,
generic_container: 1,
parameter: 1,
type: 1,
type_width_source: "interface-offsets-record",
},
sections: IL2CPP_METADATA_SECTION_NAMES.map((name) => ({
name,
offset: 380,
size: 0,
count: 0,
})),
images: [],
types: [],
methods: [],
fields: [],
properties: [],
events: [],
parameters: [],
string_literals: [],
limitations: [],
};
const report = createEmptyIl2cppMetadataFixture();
const serviceReturning = (value: Il2cppMetadata, calls = { count: 0 }) =>
new Il2cppMetadataService({
@@ -68,8 +40,10 @@ it("rejects invalid paths and extra arguments before invoking the reader", async
{ path: report.artifact.path, detail: "rows" },
]) {
const result = await service.inspect(input);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisInputError);
expect(result).toMatchObject({
ok: false,
error: expect.any(AnalysisInputError),
});
}
expect(calls.count).toBe(0);
});
@@ -150,3 +124,148 @@ it("reports cancellation before and after reading", async () => {
});
}
});
const linkedReport = (): Il2cppMetadata => ({
...report,
binary: {
artifact: {
path: "/artifacts/GameAssembly.dll",
sha256: "b".repeat(64),
bytes: 4096,
},
format: "pe",
architecture: "amd64",
image_base: "0x1000",
code_gen_modules_field: "0x1100",
code_gen_modules_array: "0x1200",
code_gen_modules: [],
types_field: "0x1300",
types_count: 0,
},
});
const seedWriter = (
write: Il2cppSeedWriterPort["write"],
close: () => Promise<void> = async () => {},
): Il2cppSeedWriterPort => ({ check: async () => ok(undefined), write, close });
const selectedPair = {
path: report.artifact.path,
binary_path: "/artifacts/GameAssembly.dll",
detail: "complete",
} as const;
it("preserves source Evidence and requested output choices when publication fails", async () => {
const failure = new EvidenceFileError("write", "io", {
path: "/output/seeds.tsv",
});
const service = new Il2cppMetadataService(
{ identity, inspect: async () => ok(linkedReport()) },
seedWriter(async () => err(failure)),
);
const result = await service.inspectWithGhidraSeeds(selectedPair, {
path: "/output/seeds.tsv",
base: "0x2000",
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Failed publication accepted");
expect(result.error).toBe(failure);
expect(result.error.partialObservation).toMatchObject({
operation: "inspect_il2cpp_metadata",
parameters: {
path: selectedPair.path,
binary_path: selectedPair.binary_path,
ghidra_seeds: "/output/seeds.tsv",
seed_base: "0x2000",
},
});
await service.close();
});
it("retains a published seed receipt if cancellation arrives after the atomic write", async () => {
const controller = new AbortController(),
reason = new Error("cancel after publication");
const receipt = {
path: "/output/seeds.tsv",
bytes: 200,
functions: 0,
folded: 0,
no_body: 0,
unknown: 0,
};
const service = new Il2cppMetadataService(
{ identity, inspect: async () => ok(linkedReport()) },
seedWriter(async () => {
controller.abort(reason);
return ok(receipt);
}),
);
const result = await service.inspectWithGhidraSeeds(
selectedPair,
{ path: receipt.path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Cancellation ignored");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.diagnostics).toMatchObject({
published_seed_output: receipt,
});
await service.close();
});
it("closes both ports and keeps combined cleanup incomplete until both owners resolve", async () => {
const metadata = new ProviderCleanupError(
identity.id,
["metadata-reader"],
{},
),
output = new ProviderCleanupError(identity.id, ["seed-output"], {});
let metadataCloses = 0,
seedCloses = 0;
const service = new Il2cppMetadataService(
{
identity,
inspect: async () => ok(report),
close: async () => {
metadataCloses++;
throw metadata;
},
},
seedWriter(
async () => {
throw new Error("not used");
},
async () => {
seedCloses++;
throw output;
},
),
);
let combined;
try {
await service.close();
} catch (cause) {
combined = cause;
}
expect(metadataCloses).toBe(1);
expect(seedCloses).toBe(1);
expect(combined).toBeInstanceOf(AnalysisError);
if (!(combined instanceof AnalysisError))
throw new Error("Missing cleanup failure");
const failure = combined;
expect([...failure.cleanupResources].sort()).toEqual([
"metadata-reader",
"seed-output",
]);
output.resolveCleanup();
expect(failure.cleanupIncomplete).toBe(true);
metadata.resolveCleanup();
expect(failure.cleanupIncomplete).toBe(false);
});
it.each([false, true])(
"classifies native association confidence (inferred: %s)",
async (inferred) => {
const value = linkedReport();
if (inferred) value.binary!.code_gen_modules_field = null;
const result = await serviceReturning(value).inspect(selectedPair);
if (!result.ok) throw result.error;
expect(result.value.confidence).toBe(inferred ? "inferred" : "derived");
},
);