diff --git a/src/flutter/DartElf.test.ts b/src/flutter/DartElf.test.ts index ff2724695b..da1557138a 100644 --- a/src/flutter/DartElf.test.ts +++ b/src/flutter/DartElf.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { readElfSymbols, readSnapshotHeader } from "./DartElf.js"; import { REFERENCE_HASH, + buildElf32Image, buildElf64Image, buildStrippedElf64Image, snapshotSection, @@ -299,6 +300,106 @@ describe("ELF symbol-table ownership", () => { }); }); +/** Section-header field positions for each ELF class. */ +const ELF_CLASSES = { + elf64: { + build: buildElf64Image, + word: { + read: (image: Buffer, at: number) => Number(image.readBigUInt64LE(at)), + write: (image: Buffer, value: number, at: number) => + image.writeBigUInt64LE(BigInt(value), at), + }, + shoff: 0x28, + shent: 64, + flags: 8, + address: 0x10, + offset: 0x18, + }, + elf32: { + build: buildElf32Image, + word: { + read: (image: Buffer, at: number) => image.readUInt32LE(at), + write: (image: Buffer, value: number, at: number) => + image.writeUInt32LE(value, at), + }, + shoff: 0x20, + shent: 40, + flags: 8, + address: 0x0c, + offset: 0x10, + }, +} as const; + +describe.each(Object.entries(ELF_CLASSES))( + "allocated %s symbol tables", + (_, layout) => { + const { word } = layout; + /** Snapshot sections, with dynamic tables allocated at their offsets. */ + const allocatedTables = (): { + image: Buffer; + headers: number[]; + tables: number[]; + } => { + const elf = layout.build([ + { name: "_kDartVmSnapshotData", value: 0x400, size: 52 }, + ]); + const section = snapshotSection(GALLERY_HASH); + const image = Buffer.concat([ + elf, + Buffer.alloc(0x400 - elf.length), + section, + ]); + const shoff = word.read(image, layout.shoff); + const headers = [shoff + layout.shent, shoff + 2 * layout.shent]; + const tables = headers.map((header) => { + const offset = word.read(image, header + layout.offset); + word.write(image, 2, header + layout.flags); + word.write(image, offset, header + layout.address); + return offset; + }); + return { image, headers, tables }; + }; + + it("accepts tables whose mapping matches their sections", async () => { + const { image } = allocatedTables(); + expect(await readElfSymbols(image, ["_kDartVmSnapshotData"])).toEqual({ + symbols: [{ name: "_kDartVmSnapshotData", offset: 0x400, size: 52 }], + failures: [], + }); + }); + + it.each([ + [0, "Dynamic symbol table has conflicting PT_LOAD mappings"], + [1, "Dynamic symbol string table has conflicting PT_LOAD mappings"], + ])( + "refuses table %i under a conflicting PT_LOAD interpretation", + async (table, reason) => { + const { image, tables } = allocatedTables(); + const conflict = appendElfRanges(image, "program", [ + { type: 1, address: tables[table]!, offset: 0x300, size: 4 }, + ]); + expect( + await readElfSymbols(conflict, ["_kDartVmSnapshotData"]), + ).toEqual({ symbols: [], failures: [{ reason }] }); + }, + ); + + it("refuses a table whose section offset disagrees with its mapping", async () => { + const { image, headers } = allocatedTables(); + word.write(image, 0x300, headers[0]! + layout.address); + expect(await readElfSymbols(image, ["_kDartVmSnapshotData"])).toEqual({ + symbols: [], + failures: [ + { + reason: + "Dynamic symbol table section offset disagrees with its PT_LOAD mapping", + }, + ], + }); + }); + }, +); + describe("readSnapshotHeader", () => { it("reads a synthesized data header", async () => { const header = readSnapshotHeader(snapshotSection(GALLERY_HASH), 0); diff --git a/src/flutter/DartElf.ts b/src/flutter/DartElf.ts index b1c6fc584f..981f689a62 100644 --- a/src/flutter/DartElf.ts +++ b/src/flutter/DartElf.ts @@ -59,6 +59,7 @@ export interface ElfParseFailure { } const SHT_DYNSYM = 11; +const SHF_ALLOC = 2; const PT_LOAD = 1; const PT_DYNAMIC = 2; /** Position-independent executable images carry PT_DYNAMIC. */ @@ -225,6 +226,28 @@ export const readElfSymbols = async ( stringOffset: number; stringSize: number; }[] = []; + /** + * The loader reads an allocated section through PT_LOAD, so its section + * offset must be the single file interpretation of its address range. + */ + const sectionMappingFailure = ( + header: number, + offset: number, + size: number, + ): string | undefined => { + const flags = is64 ? reader.u64(header + 8) : reader.u32(header + 8); + const address = is64 + ? reader.u64(header + 0x10) + : reader.u32(header + 0x0c); + if (flags === null || address === null) + return "has an unrepresentable section header"; + if ((flags & SHF_ALLOC) === 0) return undefined; + const mapped = toOffset(address, size); + if (!mapped.ok) return mapped.error.reason; + return mapped.value === offset + ? undefined + : "section offset disagrees with its PT_LOAD mapping"; + }; if (!dynamicFallback) { for (let index = 0; index < e_shnum; index += 1) { if (index % 4096 === 0) { @@ -269,6 +292,16 @@ export const readElfSymbols = async ( return failure( "Dynamic symbol strings are out of bounds or not a string table", ); + const symbolMapping = sectionMappingFailure(header, offset, size); + if (symbolMapping !== undefined) + return failure(`Dynamic symbol table ${symbolMapping}`); + const stringMapping = sectionMappingFailure( + stringHeader, + stringOffset, + stringSize, + ); + if (stringMapping !== undefined) + return failure(`Dynamic symbol string table ${stringMapping}`); tables.push({ offset, count: size / entrySize, diff --git a/src/flutter/FlutterBuild.fixture.ts b/src/flutter/FlutterBuild.fixture.ts index 27c43c3bb2..65ce3323da 100644 --- a/src/flutter/FlutterBuild.fixture.ts +++ b/src/flutter/FlutterBuild.fixture.ts @@ -115,6 +115,63 @@ export const buildElf64Image = ( return image; }; +/** The ELF32 (ARM) counterpart of {@link buildElf64Image}, with the same layout roles. */ +export const buildElf32Image = ( + symbols: ReadonlyArray<{ name: string; value: number; size: number }>, +): Buffer => { + const ELF_HEADER = 52; + const PHENT = 32; + const SHENT = 40; + const SYMENT = 16; + const phoff = ELF_HEADER; + const shoff = phoff + PHENT; + const dynsymOff = shoff + 3 * SHENT + 16; + const dynstrOff = dynsymOff + symbols.length * SYMENT; + const image = Buffer.alloc(dynstrOff + 256); + image.writeUInt32LE(0x464c457f, 0); + image[4] = 1; // ELFCLASS32 + image[5] = 1; // little-endian + image[6] = 1; + image.writeUInt16LE(3, 0x10); + image.writeUInt16LE(0x0028, 0x12); // e_machine ARM + image.writeUInt32LE(1, 0x14); + image.writeUInt32LE(phoff, 0x1c); + image.writeUInt32LE(shoff, 0x20); + image.writeUInt16LE(ELF_HEADER, 0x28); + image.writeUInt16LE(PHENT, 0x2a); + image.writeUInt16LE(1, 0x2c); + image.writeUInt16LE(SHENT, 0x2e); + image.writeUInt16LE(3, 0x30); // null, dynsym, dynstr + // One identity PT_LOAD covering the materialized payload. + image.writeUInt32LE(1, phoff); + image.writeUInt32LE( + Math.max(image.length, ...symbols.map(({ value, size }) => value + size)), + phoff + 0x10, + ); + const dyn = shoff + SHENT; + image.writeUInt32LE(11, dyn + 4); // SHT_DYNSYM + image.writeUInt32LE(dynsymOff, dyn + 0x10); + image.writeUInt32LE(symbols.length * SYMENT, dyn + 0x14); + image.writeUInt32LE(2, dyn + 0x18); // sh_link -> strtab + image.writeUInt32LE(SYMENT, dyn + 0x24); + const str = dyn + SHENT; + image.writeUInt32LE(3, str + 4); // SHT_STRTAB + image.writeUInt32LE(dynstrOff, str + 0x10); + image.writeUInt32LE(256, str + 0x14); + let strCursor = dynstrOff + 1; + symbols.forEach((symbol, index) => { + image.write(symbol.name, strCursor, "latin1"); + const base = dynsymOff + index * SYMENT; + image.writeUInt32LE(strCursor - dynstrOff, base); + image.writeUInt32LE(symbol.value, base + 4); + image.writeUInt32LE(symbol.size, base + 8); + image[base + 0xc] = 0x10; // GLOBAL + image.writeUInt16LE(1, base + 0xe); // Defined export. + strCursor += symbol.name.length + 1; + }); + return image; +}; + /** * Build a section-stripped ELF64 image (e_shnum 0, ET_DYN) whose exported * symbols are reachable only through the PT_DYNAMIC tag-value pairs, the