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,6 +1,6 @@
import { createEmptyIl2cppMetadataFixture } from "../il2cpp/il2cppMetadata.fixture.js";
import { afterEach, expect, it, vi } from "vitest";
import {
IL2CPP_METADATA_SECTION_NAMES,
il2cppMetadataParentSchema,
type Il2cppMetadata,
} from "../il2cpp/il2cppMetadata.js";
@@ -14,26 +14,7 @@ afterEach(() => {
});
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,
})),
...createEmptyIl2cppMetadataFixture(),
images: [{ name: "A.dll", type_start: 0, type_count: 2 }],
types: [
{
@@ -61,6 +42,11 @@ const report: Il2cppMetadata = {
events: [],
parameters: [{ name: "times", method: 0 }],
string_literals: [{ value: "Go", value_base64: null }],
binary: null,
method_addresses: [],
return_types: [],
field_types: [],
parameter_types: [],
limitations: [],
};
@@ -72,6 +58,12 @@ const parent = (normalizedResult: unknown, sealedResult: boolean) => ({
limitations: [],
});
const summarize = (source: ReturnType<typeof parent>) => {
const result = projectIl2cppMetadataView(source, { kind: "summary" });
if (!result.ok) throw result.error;
return result.value;
};
const page = {
kind: "il2cpp-metadata",
collection: "types",
@@ -98,6 +90,10 @@ it("validates a sealed parent once across pages and keeps counts exact", () => {
events: 0,
parameters: 1,
string_literals: 1,
method_addresses: null,
return_types: null,
field_types: null,
parameter_types: null,
},
});
}
@@ -123,9 +119,8 @@ it("preserves historical omission as unknown while keeping existing metadata que
])
delete historical[key];
const source = parent(freezeJsonSnapshot(historical), true);
const summary = projectIl2cppMetadataView(source, { kind: "summary" });
if (!summary.ok) throw summary.error;
expect(summary.value).toMatchObject({
const summary = summarize(source);
expect(summary).toMatchObject({
summary: {
counts: { types: 2, methods: 1, fields: null, parameters: null },
},
@@ -147,6 +142,141 @@ it("preserves historical omission as unknown while keeping existing metadata que
expect(unavailable.ok).toBe(false);
});
it("treats Evidence retained before binary linking as unknown, not unlinked", () => {
const historical = jsonObjectSchema.parse(report);
for (const key of [
"binary",
"method_addresses",
"return_types",
"field_types",
"parameter_types",
])
delete historical[key];
const source = parent(freezeJsonSnapshot(historical), true);
const summary = summarize(source);
expect(summary).not.toHaveProperty("summary.binary");
expect(summary).toMatchObject({
summary: {
counts: {
fields: 1,
method_addresses: null,
method_address_states: null,
return_types: null,
},
},
unknowns: expect.arrayContaining([
expect.stringContaining("did not collect method_addresses"),
]),
});
expect(projectIl2cppMetadataView(source, page).ok).toBe(true);
const unavailable = projectIl2cppMetadataView(source, {
kind: "il2cpp-metadata",
collection: "method_addresses",
offset: 0,
limit: 1,
});
expect(unavailable.ok).toBe(false);
const current = projectIl2cppMetadataView(parent(report, false), {
kind: "summary",
});
if (!current.ok) throw current.error;
expect(current.value).toMatchObject({
summary: {
binary: null,
counts: {
method_addresses: null,
method_address_states: null,
return_types: null,
},
},
});
expect(
projectIl2cppMetadataView(parent(report, false), {
kind: "il2cpp-metadata",
collection: "method_addresses",
offset: 0,
limit: 64,
}).ok,
).toBe(false);
});
const linkedBinary = {
artifact: {
path: "/fixture/libil2cpp.so",
sha256: "c".repeat(64),
bytes: 64,
},
format: "elf" as const,
architecture: "arm64",
image_base: "0x0",
code_gen_modules_field: "0x10",
code_gen_modules_array: "0x20",
code_gen_modules: [],
types_field: "0x30",
types_count: 4,
};
it("selects parameter types by owning method through the parameters collection", () => {
const linked = {
...report,
parameters: [
{ name: "a", method: 0 },
{ name: "b", method: 0 },
],
binary: { ...linkedBinary },
method_addresses: report.methods.map(() => ({
state: "no-body" as const,
address: null,
rva: null,
})),
return_types: report.methods.map(() => ({ type: -1, name: null })),
field_types: report.fields.map(() => ({ type: 1, name: "int" })),
parameter_types: [
{ type: 2, name: "string" },
{ type: 3, name: "bool" },
],
};
const select = (
source: ReturnType<typeof parent>,
method_indices: number[],
) =>
projectIl2cppMetadataView(source, {
kind: "il2cpp-metadata",
collection: "parameter_types",
method_indices,
offset: 0,
limit: 8,
});
const owned = select(parent(linked, false), [0]);
if (!owned.ok) throw owned.error;
expect(owned.value).toMatchObject({
items: [
{ index: 0, type: 2, name: "string" },
{ index: 1, type: 3, name: "bool" },
],
});
const none = select(parent(linked, false), [1]);
if (!none.ok) throw none.error;
expect(none.value).toMatchObject({ items: [] });
// Without retained parameters the owners are unknown, so the filter is refused.
const historical = jsonObjectSchema.parse(linked);
delete historical.parameters;
expect(select(parent(freezeJsonSnapshot(historical), true), [0]).ok).toBe(
false,
);
});
it.each([{ method_addresses: undefined }, { binary: undefined }])(
"rejects a binary link field retained without its collections: %j",
(changed) => {
const result = projectIl2cppMetadataView(
parent({ ...report, ...changed }, false),
{ kind: "summary" },
);
expect(result.ok).toBe(false);
},
);
it.each([
{ fields: [{ name: "bad", declaring_type: 2 }] },
{ parameters: [{ name: "bad", method: 1 }] },
@@ -288,3 +418,137 @@ it("refuses a parent whose subject contradicts the recorded artifact identity",
);
expect(result.ok).toBe(false);
});
const linkedReport = (): Il2cppMetadata => ({
...structuredClone(report),
types: report.types.map((type, index) => ({
...type,
method_start: index * 2,
method_count: 2,
})),
methods: [
{ name: "A", declaring_type: 0 },
{ name: "B", declaring_type: 0 },
{ name: "Abstract", declaring_type: 1 },
{ name: "Unresolved", declaring_type: 1 },
],
binary: {
artifact: {
path: "/artifacts/GameAssembly.dll",
sha256: "c".repeat(64),
bytes: 4096,
},
format: "pe",
architecture: "amd64",
image_base: "0x1000",
code_gen_modules_field: "0x1200",
code_gen_modules_array: "0x1300",
code_gen_modules: [
{ image: 0, address: "0x1400", method_pointer_count: 4 },
],
types_field: "0x1500",
types_count: 1,
},
method_addresses: [
{ state: "code", address: "0x1100", rva: "0x100" },
{ state: "code", address: "0x1100", rva: "0x100" },
{ state: "no-body", address: null, rva: null },
{ state: "unknown", address: null, rva: null },
],
return_types: Array.from({ length: 4 }, () => ({ type: 0, name: "void" })),
field_types: [{ type: 0, name: "int" }],
parameter_types: [{ type: 0, name: "int" }],
});
it("selects every folded method by numeric address with stable indices and both source identities", () => {
const source = parent(freezeJsonSnapshot(linkedReport()), true);
const view = projectIl2cppMetadataView(source, {
kind: "il2cpp-metadata",
collection: "method_addresses",
addresses: ["0x00001100", "0x1100"],
states: ["code"],
fields: [],
offset: 1,
limit: 1,
});
if (!view.ok) throw view.error;
expect(view.value).toMatchObject({
items: [{ index: 1 }],
artifact: { sha256: report.artifact.sha256 },
binary_source: {
artifact: { path: "/artifacts/GameAssembly.dll", sha256: "c".repeat(64) },
image_base: "0x1000",
},
coverage: { total: 2, exhausted: true },
});
});
it.each([
{ addresses: [] },
{ states: [] },
{ indices: [] },
{ addresses: ["0x1100"], indices: [2] },
{ states: ["no-body" as const], addresses: ["0x1100"] },
])(
"intersects native selectors and preserves empty selection: %j",
(selection) => {
const view = projectIl2cppMetadataView(parent(linkedReport(), false), {
kind: "il2cpp-metadata",
collection: "method_addresses",
...selection,
offset: 0,
limit: 64,
});
if (!view.ok) throw view.error;
expect(view.value).toMatchObject({
items: [],
coverage: { total: 0, exhausted: true },
});
},
);
it.each([
{ state: "no-body", address: "0x1100", rva: null },
{ state: "unknown", address: null, rva: "0x100" },
{ state: "code", address: "0x1100", rva: "0x101" },
{ state: "code", address: "0x10000000000000000", rva: "0x100" },
{ state: "code", address: "malformed", rva: "0x100" },
])(
"rejects malformed native facts without throwing during boundary validation: %j",
(row) => {
const value = jsonObjectSchema.parse(linkedReport());
value.method_addresses = [row, ...linkedReport().method_addresses.slice(1)];
const view = projectIl2cppMetadataView(parent(value, false), {
kind: "summary",
});
expect(view.ok).toBe(false);
},
);
it("keeps native summaries bounded while paging selected module facts", () => {
const value = linkedReport(),
source = parent(value, false);
const summary = summarize(source);
expect(summary).toMatchObject({
summary: {
counts: { code_gen_modules: 1 },
binary: { artifact: value.binary!.artifact },
},
});
expect("summary" in summary && summary.summary).not.toHaveProperty(
"binary.code_gen_modules",
);
const page = projectIl2cppMetadataView(source, {
kind: "il2cpp-metadata",
collection: "code_gen_modules",
image_indices: [0],
fields: ["image", "address"],
offset: 0,
limit: 1,
});
if (!page.ok) throw page.error;
expect(page.value).toMatchObject({
items: [{ index: 0, image: 0, address: "0x1400" }],
coverage: { total: 1, exhausted: true },
});
});