From 87745af52ab0ccd3a172c2534c516d41b0095035 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:13:52 +0800 Subject: [PATCH] fix(il2cpp): consume resolved ELF program declarations Retain relevant program facts in the shared native directory and consume them for mappings and relocations. Remove redundant table coordinates and presence flags. This preserves extended program counts so PT_DYNAMIC beyond the raw sentinel remains visible, while load and relocation admission keep their distinct policies. --- src/il2cpp/ElfRelocations.ts | 45 ++++++++-------- src/il2cpp/Il2cppBinaryImage.ts | 1 + src/native/exports/NativeExportParser.ts | 2 +- src/native/metadata/BinaryContainer.ts | 22 +++++++- src/native/metadata/ElfImage.ts | 37 ++++++------- src/native/metadata/NativeSectionDirectory.ts | 53 +++++++++++++------ .../boundary/il2cpp/il2cppBinaryFile.test.ts | 53 +++++++++++++++++++ 7 files changed, 149 insertions(+), 64 deletions(-) diff --git a/src/il2cpp/ElfRelocations.ts b/src/il2cpp/ElfRelocations.ts index 041d403ef5..59174ea338 100644 --- a/src/il2cpp/ElfRelocations.ts +++ b/src/il2cpp/ElfRelocations.ts @@ -2,6 +2,8 @@ import { BinaryMappingFailure, mappedFileOffset, isZeroFilledSpan, + type BinaryContainer, + type ElfDynamicSegment, type FileMapping, type ZeroFillMapping, } from "../native/metadata/BinaryContainer.js"; @@ -78,31 +80,28 @@ const RELOCATION_KINDS = new Map([ ], ]); -function* dynamicTags(image: Buffer): Generator> { - if (image.length < 64) throw malformed("ELF header is truncated"); - const offset = Number(image.readBigUInt64LE(32)), - width = image.readUInt16LE(54), - count = image.readUInt16LE(56); - if (width < 56 || offset + count * width > image.length) - throw malformed("ELF program headers extend beyond the file"); - let dynamic: { offset: number; size: number } | undefined; - for (let index = 0; index < count; index++) { - if (index % 256 === 0) yield; - const at = offset + index * width; - if (image.readUInt32LE(at) !== 2) continue; - if (dynamic !== undefined) throw malformed("multiple PT_DYNAMIC segments"); - dynamic = { - offset: Number(image.readBigUInt64LE(at + 8)), - size: Number(image.readBigUInt64LE(at + 32)), - }; - } - if (dynamic === undefined) +function* dynamicTags( + image: Buffer, + declarations: readonly ElfDynamicSegment[], +): Generator> { + yield; + if (declarations.length > 1) throw malformed("multiple PT_DYNAMIC segments"); + const declaration = declarations[0]; + if (declaration === undefined) throw new Il2cppBinaryFormatFailure( "unsupported-container", "ELF image has no PT_DYNAMIC segment", ); - if (dynamic.size % 16 !== 0 || dynamic.offset + dynamic.size > image.length) + if ( + declaration.size % 16n !== 0n || + declaration.offset > BigInt(image.length) || + declaration.size > BigInt(image.length) - declaration.offset + ) throw malformed("PT_DYNAMIC is truncated or outside the file"); + const dynamic = { + offset: Number(declaration.offset), + size: Number(declaration.size), + }; const tags = new Map(); const unique = new Set([ DT_RELA, @@ -203,17 +202,17 @@ function* applyRelaTable( /** Materialize relative pointers in a private copy, preserving unknown and .bss facts. */ export function* applyElfRelativeRelocationsSteps( image: Buffer, + elf: Extract["elf"], mappings: readonly FileMapping[], zeroFills: readonly ZeroFillMapping[], ): Generator { - if (image.length < 64) throw malformed("ELF header is truncated"); - const kinds = RELOCATION_KINDS.get(image.readUInt16LE(18)); + const kinds = RELOCATION_KINDS.get(elf.machine); if (kinds === undefined) throw new Il2cppBinaryFormatFailure( "unsupported-container", "ELF relocations require AArch64 or x86-64", ); - const tags = yield* dynamicTags(image); + const tags = yield* dynamicTags(image, elf.dynamicSegments); if (tags.has(0x6000000fn) || tags.has(0x60000011n) || tags.has(17n)) throw unsupported( "Android packed (APS2) or REL relocations are not decoded", diff --git a/src/il2cpp/Il2cppBinaryImage.ts b/src/il2cpp/Il2cppBinaryImage.ts index 3abf781856..f373a472a4 100644 --- a/src/il2cpp/Il2cppBinaryImage.ts +++ b/src/il2cpp/Il2cppBinaryImage.ts @@ -128,6 +128,7 @@ export function* openImageSteps( if (container.format === "elf") relocated = yield* applyElfRelativeRelocationsSteps( image, + container.elf, container.mappings, container.zero_fills, ); diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts index e32eb74f4e..bfcd4a27ac 100644 --- a/src/native/exports/NativeExportParser.ts +++ b/src/native/exports/NativeExportParser.ts @@ -462,7 +462,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { const dynsym = sections.find(({ type }) => type === SHT_DYNSYM); if ( sections.length === 0 || - (dynsym === undefined && directory.has_dynamic_segment) + (dynsym === undefined && directory.programs.some(({ type }) => type === 2)) ) return { ...result, diff --git a/src/native/metadata/BinaryContainer.ts b/src/native/metadata/BinaryContainer.ts index 0e52965016..e8fd6ff4d0 100644 --- a/src/native/metadata/BinaryContainer.ts +++ b/src/native/metadata/BinaryContainer.ts @@ -101,9 +101,14 @@ export type ImageSection = { } ); +/** Declared PT_DYNAMIC file extent, before consumer-specific admission. */ +export interface ElfDynamicSegment { + readonly offset: bigint; + readonly size: bigint; +} + /** Native container facts, without language metadata search policy. */ -export interface BinaryContainer { - readonly format: "elf" | "pe" | "macho"; +interface BinaryContainerFacts { readonly architecture: string; readonly bits: 32 | 64; readonly byte_order: "little" | "big"; @@ -131,6 +136,19 @@ export interface BinaryContainer { readonly sections: readonly ImageSection[]; } +/** ELF identity and dynamic declarations are present only for an ELF container. */ +export type BinaryContainer = BinaryContainerFacts & + ( + | { + readonly format: "elf"; + readonly elf: { + readonly machine: number; + readonly dynamicSegments: readonly ElfDynamicSegment[]; + }; + } + | { readonly format: "pe" | "macho" } + ); + /** All declared file-backed image memory, including PE headers at the image base. */ export const imageFileMappings = ( container: BinaryContainer, diff --git a/src/native/metadata/ElfImage.ts b/src/native/metadata/ElfImage.ts index 07377c602f..0b56fe291d 100644 --- a/src/native/metadata/ElfImage.ts +++ b/src/native/metadata/ElfImage.ts @@ -3,6 +3,7 @@ import { BinaryFormatFailure, BinaryReader, type BinaryContainer, + type ElfDynamicSegment, type FileMapping, type ImageSection, type LoadRegion, @@ -14,33 +15,26 @@ import { readNativeDirectoryFromBuffer } from "./NativeSectionDirectory.js"; /** Read ELF sections and file-backed program mappings without interpreting language metadata. */ export const readElfImage = (bytes: Buffer): BinaryContainer => { const directory = readNativeDirectoryFromBuffer(bytes, "elf"); - const { - bits, - program_offset: phOffset, - program_count: phCount, - program_width: phWidth, - } = directory; + const { bits } = directory; const reader = new BinaryReader(bytes, directory.little); - const wide = bits === 64; - const phStart = reader.table( - phOffset, - phCount, - phWidth, - "ELF program headers", - ); const mappings: FileMapping[] = []; const loadRegions: LoadRegion[] = []; const executableMappings: FileMapping[] = []; const zeroFills: ZeroFillMapping[] = []; const sections: ImageSection[] = []; - for (let index = 0; index < phCount; index++) { - const offset = phStart + index * phWidth; - if (reader.u32(offset) !== 1) continue; - const flags = reader.u32(offset + (wide ? 4 : 24)); - const fileOffset = reader.word(offset + (wide ? 8 : 4), bits); - const address = reader.word(offset + (wide ? 16 : 8), bits); - const size = reader.word(offset + (wide ? 32 : 16), bits); - const memorySize = reader.word(offset + (wide ? 40 : 20), bits); + const dynamicSegments: ElfDynamicSegment[] = []; + for (const program of directory.programs) { + if (program.type === 2) { + dynamicSegments.push(program); + continue; + } + const { + flags, + file_offset: fileOffset, + virtual_address: address, + file_size: size, + memory_size: memorySize, + } = program; if (size > memorySize || address + memorySize > 1n << BigInt(bits)) throw new BinaryFormatFailure( "malformed", @@ -92,6 +86,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => { } return { format: "elf", + elf: { machine: directory.machine, dynamicSegments }, architecture: architectureForElfMachine(directory.machine), bits, byte_order: reader.little ? "little" : "big", diff --git a/src/native/metadata/NativeSectionDirectory.ts b/src/native/metadata/NativeSectionDirectory.ts index 7c2fe417c4..1bae3fba3a 100644 --- a/src/native/metadata/NativeSectionDirectory.ts +++ b/src/native/metadata/NativeSectionDirectory.ts @@ -4,6 +4,7 @@ import { BinaryReader, BinaryResourceFailure, declaredFileRange, + type ElfDynamicSegment, } from "./BinaryContainer.js"; import { decodeElfSectionNameTable } from "./ElfSectionNameTable.js"; @@ -27,6 +28,18 @@ export interface NativeSection { readonly entry_size: bigint; readonly file_backed: boolean; } +/** Raw relevant program declarations; consumers own load/dynamic admission. */ +export type ElfProgramDeclaration = + | { + readonly type: 1; + readonly flags: number; + readonly file_offset: bigint; + readonly file_size: bigint; + readonly virtual_address: bigint; + readonly memory_size: bigint; + } + | ({ readonly type: 2 } & ElfDynamicSegment); + export interface NativeSectionDirectory { readonly format: "elf" | "pe"; readonly bits: 32 | 64; @@ -35,10 +48,7 @@ export interface NativeSectionDirectory { readonly elf_os_abi: number | null; readonly flags: number; readonly section_names_limitation?: string; - readonly program_offset: bigint; - readonly program_count: number; - readonly program_width: number; - readonly has_dynamic_segment: boolean; + readonly programs: readonly ElfProgramDeclaration[]; readonly sections: readonly NativeSection[]; } const STRUCTURE_BYTES = 16 * 1024 * 1024; @@ -96,7 +106,7 @@ export function* readElfSectionDirectory( count, namesIndex, } = sectionTable; - let has_dynamic_segment = false; + const programDeclarations: ElfProgramDeclaration[] = []; if (program_count !== 0) { const programs = new BinaryReader( yield request( @@ -106,11 +116,26 @@ export function* readElfSectionDirectory( ), little, ); + // At most one record per table entry, bounded before allocation by the + // existing 16 MiB table allowance and minimum 32/56-byte ELF record width. for (let index = 0; index < program_count; index++) { - if (programs.u32(index * program_width) === 2) { - has_dynamic_segment = true; - break; - } + const at = index * program_width; + const type = programs.u32(at); + if (type === 1) + programDeclarations.push({ + type, + flags: programs.u32(at + (wide ? 4 : 24)), + file_offset: programs.word(at + (wide ? 8 : 4), bits), + virtual_address: programs.word(at + (wide ? 16 : 8), bits), + file_size: programs.word(at + (wide ? 32 : 16), bits), + memory_size: programs.word(at + (wide ? 40 : 20), bits), + }); + else if (type === 2) + programDeclarations.push({ + type, + offset: programs.word(at + (wide ? 8 : 4), bits), + size: programs.word(at + (wide ? 32 : 16), bits), + }); } } const sections: NativeSection[] = []; @@ -228,10 +253,7 @@ export function* readElfSectionDirectory( ...(section_names_limitation === undefined ? {} : { section_names_limitation }), - program_offset, - program_count, - program_width, - has_dynamic_segment, + programs: programDeclarations, sections, }; } @@ -316,10 +338,7 @@ export function* readPeSectionDirectory( little: true, machine: coff.u16(4), flags: coff.u16(22), - program_offset: 0n, - program_count: 0, - program_width: 0, - has_dynamic_segment: false, + programs: [], sections, }; } diff --git a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts index 5abc5646f6..bb736a9f3f 100644 --- a/tests/boundary/il2cpp/il2cppBinaryFile.test.ts +++ b/tests/boundary/il2cpp/il2cppBinaryFile.test.ts @@ -1,4 +1,5 @@ import { Il2cppBinaryFormatFailure } from "../../../src/il2cpp/Il2cppBinaryFormat.js"; +import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js"; import { openImageSteps, readUInt32, @@ -480,6 +481,58 @@ it("links an ELF shared object after applying its RELATIVE relocations", () => { expect(link(elf)).toEqual({ ...fromMacho, format: "elf" }); }); +it.each([183, 62])( + "links ELF relocations using the resolved PN_XNUM program count (machine %s)", + (machine) => { + const original = createElfFixture(createIl2cppBinaryFixture(), { machine }); + const count = 65536; + const table = original.length; + const sectionZero = table + count * 56; + const extended = Buffer.concat([original, Buffer.alloc(count * 56 + 64)]); + // Keep mapped payload bytes untouched; the last declaration is beyond + // the raw 0xffff sentinel that the old relocation scan treated as a count. + original.copy(extended, table, 64, 120); + original.copy(extended, table + (count - 1) * 56, 120, 176); + extended.writeBigUInt64LE(BigInt(table), 32); + extended.writeBigUInt64LE(BigInt(sectionZero), 40); + extended.writeUInt16LE(0xffff, 56); + extended.writeUInt16LE(64, 58); + extended.writeUInt16LE(1, 60); + extended.writeUInt32LE(count, sectionZero + 44); + expect(link(extended).method_addresses).toEqual( + link(original).method_addresses, + ); + }, +); + +it("ignores undefined PT_NULL fields while linking ELF relocations", () => { + const original = createElfFixture(createIl2cppBinaryFixture()); + const bytes = Buffer.from(original); + bytes.writeUInt16LE(3, 56); + bytes.fill(0xff, 176, 232); + bytes.writeUInt32LE(0, 176); + expect(link(bytes).method_addresses).toEqual(link(original).method_addresses); +}); + +it.each(["outside", "misaligned", "duplicate"] as const)( + "refuses a %s dynamic declaration at relocation admission", + (shape) => { + const bytes = createElfFixture(createIl2cppBinaryFixture()); + if (shape === "outside") bytes.writeBigUInt64LE(1n << 63n, 128); + else if (shape === "misaligned") bytes.writeBigUInt64LE(17n, 152); + else { + bytes.copy(bytes, 176, 120, 176); + bytes.writeUInt16LE(3, 56); + } + expect(() => readBinaryContainer(bytes)).not.toThrow(); + expect(() => link(bytes)).toThrow( + shape === "duplicate" + ? "multiple PT_DYNAMIC segments" + : "PT_DYNAMIC is truncated or outside the file", + ); + }, +); + it.each([ [{ dynamic: false }, "unsupported-container"], [{ machine: 243 }, "unsupported-container"],