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"],