diff --git a/docs/il2cpp-metadata.md b/docs/il2cpp-metadata.md index ffc1428fe6..091120374f 100644 --- a/docs/il2cpp-metadata.md +++ b/docs/il2cpp-metadata.md @@ -298,7 +298,7 @@ Format and selection failures name `binary_path`; capacity refusals remain typed Scans yield cooperatively in bounded batches. Name-address retention and reverse-trie construction are each limited to65,536 entries/nodes and fallback module-array candidates to64 (`native-index-capacity`); exceeding capacity does not prove absence. Rendered type text has a64MiB UTF-8 cache-workspace guard, separate from the64MiB encoded-report admission, and64 nested rendering frames (`native-type-text-capacity` / `type-nesting-capacity`). Genuine cycles are malformed; unsupported type kinds are explicit coverage failures. Native row/text admission runs before allocation or retention. Unused generic parameter names are read on demand from the same stable metadata snapshot. There is no cross-inspection file cache; validated immutable Evidence is reused for session views. -ELF RELATIVE writes into zero-fill are kept as a private virtual-pointer overlay. Unresolved supported-width symbol relocations, including RELA PLT entries, remain unknown; stored zeros never establish no-body. COPY, TLS descriptor, REL, packed APS2 and other unsupported relocation encodings fail closed. Input bytes are unchanged. +ELF RELATIVE writes into zero-fill are kept as a private virtual-pointer overlay. Unresolved supported-width symbol relocations, including RELA PLT entries, remain unknown only for the bytes their ELF ABI relocation type covers; an adjacent 32-bit relocation does not make a separate 64-bit method slot unknown. Stored zeros never establish no-body. COPY, TLS descriptor, REL, packed APS2 and other unsupported relocation encodings fail closed. Input bytes are unchanged. Native address queries batch exact64-bit hexadecimal `addresses` and `states` (`code`, `no-body`, `unknown`), intersecting indices and pagination. Numerically equal address spellings match; original row indices and every shared-body alternative survive: diff --git a/src/il2cpp/ElfRelocations.ts b/src/il2cpp/ElfRelocations.ts index ed8b540093..a3ea8465a4 100644 --- a/src/il2cpp/ElfRelocations.ts +++ b/src/il2cpp/ElfRelocations.ts @@ -21,7 +21,7 @@ const unsupported = (message: string) => interface RelocatedImage { readonly bytes: Buffer; - readonly unresolved: Set; + readonly unresolved: Map; readonly zeroFillPointers: Map; } interface RelocationSource { @@ -29,7 +29,7 @@ interface RelocationSource { readonly mappings: readonly FileMapping[]; readonly zeroFills: readonly ZeroFillMapping[]; readonly relative: number; - readonly unknownPointerKinds: ReadonlySet; + readonly unknownPointerWidths: ReadonlyMap; } /** Standard dynamic relocations whose destination spans at most eight bytes. @@ -41,8 +41,19 @@ const RELOCATION_KINDS = new Map([ 183, { relative: 1027, - unknownPointerKinds: new Set([ - 257, 258, 259, 260, 261, 262, 1025, 1026, 1028, 1029, 1030, 1032, + unknownPointerWidths: new Map([ + [257, 8], + [258, 4], + [259, 2], + [260, 8], + [261, 4], + [262, 2], + [1025, 8], + [1026, 8], + [1028, 8], + [1029, 8], + [1030, 8], + [1032, 8], ]), }, ], @@ -50,7 +61,19 @@ const RELOCATION_KINDS = new Map([ 62, { relative: 8, - unknownPointerKinds: new Set([1, 2, 6, 7, 10, 11, 16, 17, 18, 24, 37]), + unknownPointerWidths: new Map([ + [1, 8], + [2, 4], + [6, 8], + [7, 8], + [10, 4], + [11, 4], + [16, 8], + [17, 8], + [18, 8], + [24, 8], + [37, 8], + ]), }, ], ]); @@ -148,19 +171,20 @@ function* applyRelaTable( kind = Number(info & 0xffffffffn); if (kind === 0) continue; const destination = source.image.readBigUInt64LE(at); - const target = fileOffset(source.mappings, destination, 8); if (kind !== source.relative || info >> 32n !== 0n) { - if (!source.unknownPointerKinds.has(kind)) + const width = source.unknownPointerWidths.get(kind); + if (width === undefined) throw unsupported(`ELF relocation kind ${kind} is not decoded`); - if (!output.unresolved.has(destination)) { - admitOverlay(output); - output.unresolved.add(destination); - } + const priorWidth = output.unresolved.get(destination); + if (priorWidth === undefined) admitOverlay(output); + output.unresolved.set(destination, Math.max(priorWidth ?? 0, width)); // Registration scans read this private image directly. Clear unknown // fields so their stored bytes cannot masquerade as resolved pointers. - if (target !== null) output.bytes.fill(0, target, target + 8); + const target = fileOffset(source.mappings, destination, width); + if (target !== null) output.bytes.fill(0, target, target + width); continue; } + const target = fileOffset(source.mappings, destination, 8); const value = source.image.readBigInt64LE(at + 16) & 0xffffffffffffffffn; if (target !== null) output.bytes.writeBigUInt64LE(value, target); else if ( @@ -196,7 +220,7 @@ export function* applyElfRelativeRelocationsSteps( yield; const output: RelocatedImage = { bytes: Buffer.from(image), - unresolved: new Set(), + unresolved: new Map(), zeroFillPointers: new Map(), }; const source = { image, mappings, zeroFills, ...kinds }; diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts index 9540da86b1..5e6630c70c 100644 --- a/src/il2cpp/Il2cppBinaryImage.ts +++ b/src/il2cpp/Il2cppBinaryImage.ts @@ -66,7 +66,7 @@ export interface NativeImage { readonly bytes: Buffer; readonly container: BinaryContainer; readonly imageBase: bigint; - readonly unresolvedPointerSlots: ReadonlySet; + readonly unresolvedPointerSlots: ReadonlyMap; readonly zeroFillPointers: ReadonlyMap; } @@ -109,7 +109,7 @@ export function* openImageSteps( let relocated: | { bytes: Buffer; - unresolved: ReadonlySet; + unresolved: ReadonlyMap; zeroFillPointers: ReadonlyMap; } | undefined; @@ -151,7 +151,7 @@ export function* openImageSteps( bytes: image, container, imageBase, - unresolvedPointerSlots: relocated?.unresolved ?? new Set(), + unresolvedPointerSlots: relocated?.unresolved ?? new Map(), zeroFillPointers: relocated?.zeroFillPointers ?? new Map(), }; } @@ -295,10 +295,10 @@ const hasUnresolvedPointer = ( size = 8, ): boolean => { if (image.unresolvedPointerSlots.size === 0) return false; - // Unsupported relocation widths are unknown; conservatively cover any - // overlap with the pointer rather than infer a zero or trust stored bytes. - for (let delta = -7; delta < size; delta++) - if (image.unresolvedPointerSlots.has(address + BigInt(delta))) return true; + for (let delta = -7; delta < size; delta++) { + const width = image.unresolvedPointerSlots.get(address + BigInt(delta)); + if (width !== undefined && delta + width > 0) return true; + } return false; }; diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index c3917f9d7b..4830402d80 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -511,6 +511,29 @@ it("reports a symbol-relocated method slot as unknown rather than no-body", () = expect(linked.method_addresses[0]).toEqual(expectedAddresses[0]); }); +it.each([ + ["PC32", 2], + ["ABS32", 10], +] as const)( + "keeps a method pointer beside a non-overlapping %s relocation", + (_, kind) => { + const binary = createIl2cppBinaryFixture(); + const adjacent = createElfFixture(binary, { + machine: 62, + unresolvedRelocations: [{ offset: 0x2fc, kind }], + }); + expect(link(adjacent).method_addresses).toEqual(expectedAddresses); + + const overlapping = createElfFixture(binary, { + machine: 62, + unresolvedRelocations: [{ offset: 0x300, kind }], + }); + expect(link(overlapping).method_addresses[1]).toEqual( + expectedAddress("unknown"), + ); + }, +); + it("reads a relative relocation into a zero-filled pointer array as a known value", () => { const bytes = createElfFixture(createIl2cppBinaryFixture(), { zeroFilledTable: true, diff --git a/tests/fixtures/il2cpp/binary.ts b/tests/fixtures/il2cpp/binary.ts index e463e9d7f2..24c07eefcd 100644 --- a/tests/fixtures/il2cpp/binary.ts +++ b/tests/fixtures/il2cpp/binary.ts @@ -223,6 +223,11 @@ export interface ElfFixtureOptions { readonly dynamic?: boolean; /** Give every relocation a symbol index, so none is RELATIVE-only. */ readonly symbolRelocations?: boolean; + /** Add unresolved dynamic relocations at offsets relative to the load base. */ + readonly unresolvedRelocations?: readonly { + readonly offset: number; + readonly kind: number; + }[]; /** Write DT_RELA twice. */ readonly duplicateRela?: boolean; /** Override DT_RELASZ or DT_RELAENT. */ @@ -243,7 +248,7 @@ export const createElfFixture = ( ): Buffer => { const body = Buffer.from(image); body.fill(0, 0, 0x100); - const relocations: [slot: bigint, value: bigint][] = []; + const relocations: [slot: bigint, value: bigint, kind?: number][] = []; for (let at = 0x100; at + 8 <= SIZE; at += 8) { const value = body.readBigUInt64LE(at); if ( @@ -254,6 +259,12 @@ export const createElfFixture = ( relocations.push([FIXTURE_BINARY_BASE + BigInt(at), value]); body.writeBigUInt64LE(0n, at); } + for (const relocation of options.unresolvedRelocations ?? []) + relocations.push([ + FIXTURE_BINARY_BASE + BigInt(relocation.offset), + 0n, + relocation.kind, + ]); const tagCount = 4 + (options.duplicateRela === true ? 1 : 0) + @@ -266,11 +277,12 @@ export const createElfFixture = ( entry[1] = FIXTURE_BINARY_BASE + BigInt(fileSize); } const rela = Buffer.alloc(relocations.length * 24); - relocations.forEach(([slot, value], index) => { + relocations.forEach(([slot, value, kind], index) => { rela.writeBigUInt64LE(slot, index * 24); rela.writeBigUInt64LE( - (options.symbolRelocations === true ? 1n << 32n : 0n) | - (options.machine === 62 ? 8n : 1027n), + (options.symbolRelocations === true || kind !== undefined + ? 1n << 32n + : 0n) | BigInt(kind ?? (options.machine === 62 ? 8 : 1027)), index * 24 + 8, ); rela.writeBigInt64LE(value, index * 24 + 16);