fix(flutter): check section-backed symbol tables against PT_LOAD (#3859)

* fix(flutter): check section-backed symbol tables against PT_LOAD

The section-backed ELF path read .dynsym and .dynstr only through their
section file offsets. The stripped PT_DYNAMIC path and snapshot symbols
resolve through the PT_LOAD map. A second PT_LOAD interpretation over an
allocated symbol or string table was therefore ignored, and
inspect-dart-aot reported complete coverage from metadata the loader may
read differently.

An allocated (SHF_ALLOC) dynamic symbol table and its string table now
need a single file-backed PT_LOAD interpretation that matches the section
offset, the same check the dynamic path applies to its tables. Otherwise
the symbol inventory is refused with the mapping reason, and
inspect-dart-aot reports that no recognizable snapshot selection remains.
Non-allocated tables keep their section-offset reading.

Fixes #3346

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(flutter): cover allocated ELF32 symbol-table mapping checks

Add an ELF32 (ARM) counterpart of the synthetic ELF64 image and run the
allocated-table cases for both classes, so the ELF32 section-header reads
in the mapping check are exercised: matching mappings, conflicting symbol
and string mappings, and a disagreeing section offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-14 23:02:49 -07:00
1 parent 933f216698
commit 1b036ecbff
3 files changed
+191

No files matched your search

+101
View File
@@ -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);
+33
View File
@@ -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,
+57
View File
@@ -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