From 90199ff0ec83a7bce39e4b01fb8b3f771bf3bfc0 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:10:46 +0800 Subject: [PATCH] refactor(elf): centralize symbol record decoding --- src/domain/elfSymbol.test.ts | 64 +++++++++++++++++++ src/domain/elfSymbol.ts | 30 +++++++++ src/flutter/DartElf.test.ts | 24 +++++++ src/flutter/DartElf.ts | 27 ++++---- src/gpu/GpuElfKernels.ts | 11 +++- src/native/exports/NativeExportParser.ts | 11 +++- .../{ElfSymbol.ts => ElfStringTable.ts} | 22 +------ 7 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 src/domain/elfSymbol.test.ts create mode 100644 src/domain/elfSymbol.ts rename src/native/metadata/{ElfSymbol.ts => ElfStringTable.ts} (52%) diff --git a/src/domain/elfSymbol.test.ts b/src/domain/elfSymbol.test.ts new file mode 100644 index 0000000000..e6573b15b3 --- /dev/null +++ b/src/domain/elfSymbol.test.ts @@ -0,0 +1,64 @@ +import { expect, it } from "vitest"; +import { readElfSymbol } from "./elfSymbol.js"; + +const formats = [ + { bits: 32, littleEndian: false }, + { bits: 32, littleEndian: true }, + { bits: 64, littleEndian: false }, + { bits: 64, littleEndian: true }, +] as const; + +it.each(formats)( + "preserves raw ELF$bits symbols (littleEndian=$littleEndian)", + ({ bits, littleEndian }) => { + const wide = bits === 64; + const length = wide ? 24 : 16; + const backing = Buffer.alloc(length + 19, 0xa5); + const bytes = backing.subarray(7, 7 + length); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const value = wide ? 0xfedcba9876543210n : 0xfedcba98n; + const size = wide ? 0xffffffffffffffffn : 0xffffffffn; + view.setUint32(0, 0x89abcdef, littleEndian); + view.setUint8(wide ? 4 : 12, 0xf3); + view.setUint8(wide ? 5 : 13, 0xee); + view.setUint16(wide ? 6 : 14, 0xffff, littleEndian); + if (wide) { + view.setBigUint64(8, value, littleEndian); + view.setBigUint64(16, size, littleEndian); + } else { + view.setUint32(4, Number(value), littleEndian); + view.setUint32(8, Number(size), littleEndian); + } + const before = Buffer.from(backing); + const parsed = readElfSymbol(backing.subarray(7), bits, littleEndian); + expect(backing.equals(before)).toBe(true); + backing.fill(0); + expect(parsed).toEqual({ + ok: true, + value: { + name_offset: 0x89abcdef, + binding: 15, + type: 3, + other: 0xee, + section_index: 0xffff, + value, + size, + }, + }); + }, +); + +it.each(formats)( + "refuses each incomplete ELF$bits symbol (littleEndian=$littleEndian)", + ({ bits, littleEndian }) => { + const length = bits === 64 ? 24 : 16; + const backing = Buffer.alloc(length + 7, 0xff); + for (let observed = 0; observed < length; observed++) + expect( + readElfSymbol(backing.subarray(7, 7 + observed), bits, littleEndian), + ).toEqual({ + ok: false, + error: { kind: "malformed", message: "ELF symbol record is truncated" }, + }); + }, +); diff --git a/src/domain/elfSymbol.ts b/src/domain/elfSymbol.ts new file mode 100644 index 0000000000..be255e8186 --- /dev/null +++ b/src/domain/elfSymbol.ts @@ -0,0 +1,30 @@ +import { err, ok } from "./result.js"; + +/** Fixed ELF32/ELF64 symbol facts; consumers own definition, visibility and processor-flag policies. */ +export const readElfSymbol = ( + bytes: Uint8Array, + bits: 32 | 64, + littleEndian: boolean, +) => { + const wide = bits === 64; + if (bytes.byteLength < (wide ? 24 : 16)) + return err({ + kind: "malformed" as const, + message: "ELF symbol record is truncated", + }); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const info = view.getUint8(wide ? 4 : 12); + return ok({ + name_offset: view.getUint32(0, littleEndian), + binding: info >>> 4, + type: info & 15, + other: view.getUint8(wide ? 5 : 13), + section_index: view.getUint16(wide ? 6 : 14, littleEndian), + value: wide + ? view.getBigUint64(8, littleEndian) + : BigInt(view.getUint32(4, littleEndian)), + size: wide + ? view.getBigUint64(16, littleEndian) + : BigInt(view.getUint32(8, littleEndian)), + }); +}; diff --git a/src/flutter/DartElf.test.ts b/src/flutter/DartElf.test.ts index 48f3e7be0c..ac5cef7b31 100644 --- a/src/flutter/DartElf.test.ts +++ b/src/flutter/DartElf.test.ts @@ -521,3 +521,27 @@ it("keeps section admission ahead of program admission for unrepresentable raw o ], }); }); + +it("checks raw symbol precision only after definition and requested-name selection", async () => { + const bytes = buildElf64Image([ + { name: "unselected", value: 64, size: 4 }, + { name: "selected", value: 64, size: 4 }, + ]); + const expected = await readElfSymbols(bytes, ["selected"]); + const sectionTable = Number(bytes.readBigUInt64LE(0x28)); + const symbols = Number(bytes.readBigUInt64LE(sectionTable + 64 + 0x18)); + bytes.writeBigUInt64LE(0xffffffffffffffffn, symbols + 8); + bytes.writeBigUInt64LE(0xffffffffffffffffn, symbols + 16); + expect(await readElfSymbols(bytes, ["selected"])).toEqual(expected); + expect(await readElfSymbols(bytes, ["unselected"])).toEqual({ + symbols: [], + failures: [{ reason: "Symbol unselected has unrepresentable bounds" }], + }); + bytes.writeUInt16LE(0, symbols + 6); + expect(await readElfSymbols(bytes, ["unselected"])).toEqual({ + symbols: [], + failures: [ + { reason: "symbol unselected is not in the dynamic symbol table" }, + ], + }); +}); diff --git a/src/flutter/DartElf.ts b/src/flutter/DartElf.ts index 1ea20fdcaa..b8a1131d01 100644 --- a/src/flutter/DartElf.ts +++ b/src/flutter/DartElf.ts @@ -1,3 +1,4 @@ +import { readElfSymbol } from "../domain/elfSymbol.js"; import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js"; import { Buffer } from "node:buffer"; import { setImmediate as yieldToEventLoop } from "node:timers/promises"; @@ -17,12 +18,6 @@ class BoundedReader { return offset < this.bytes.length ? this.bytes.readUInt8(offset) : null; } - u16(offset: number): number | null { - return offset + 2 <= this.bytes.length - ? this.bytes.readUInt16LE(offset) - : null; - } - u32(offset: number): number | null { return offset + 4 <= this.bytes.length ? this.bytes.readUInt32LE(offset) @@ -459,8 +454,15 @@ export const readElfSymbols = async ( signal?.throwIfAborted(); } const base = table.offset + index * entrySize; - const nameOffset = reader.u32(base)!; - const sectionIndex = reader.u16(base + (is64 ? 6 : 14))!; + const parsedSymbol = readElfSymbol( + bytes.subarray(base, base + entrySize), + is64 ? 64 : 32, + true, + ); + if (!parsedSymbol.ok) return failure(parsedSymbol.error.message); + const symbol = parsedSymbol.value; + const nameOffset = symbol.name_offset; + const sectionIndex = symbol.section_index; const defined = sectionIndex === 0xfff1 || (sectionIndex > 0 && @@ -472,13 +474,16 @@ export const readElfSymbols = async ( table.stringOffset + table.stringSize, ); if (name === null || !wanted.has(name)) continue; - const value = is64 ? reader.u64(base + 8) : reader.u32(base + 4); - const size = is64 ? reader.u64(base + 16) : reader.u32(base + 8); - if (value === null || size === null) { + if ( + symbol.value > BigInt(Number.MAX_SAFE_INTEGER) || + symbol.size > BigInt(Number.MAX_SAFE_INTEGER) + ) { failures.add(`Symbol ${name} has unrepresentable bounds`); unresolved.add(name); continue; } + const value = Number(symbol.value); + const size = Number(symbol.size); const offset = toOffset(value, size); if (!offset.ok) { failures.add(`Symbol ${name} ${offset.error.reason}`); diff --git a/src/gpu/GpuElfKernels.ts b/src/gpu/GpuElfKernels.ts index 2d892b428f..1e4e1c5976 100644 --- a/src/gpu/GpuElfKernels.ts +++ b/src/gpu/GpuElfKernels.ts @@ -23,7 +23,8 @@ import { type NativeSection, type NativeSectionDirectory, } from "../native/metadata/NativeSectionDirectory.js"; -import { readElfString, readElfSymbol } from "../native/metadata/ElfSymbol.js"; +import { readElfString } from "../native/metadata/ElfStringTable.js"; +import { readElfSymbol } from "../domain/elfSymbol.js"; import { gpuCount, readGpuNativeDirectory, @@ -277,11 +278,17 @@ const cudaKernels = async ( let extendedIndices: Buffer | null = null; for (let offset = 0; offset < bytes.length; offset += width) { index.budget.visit(); - const symbol = readElfSymbol( + const parsedSymbol = readElfSymbol( bytes.subarray(offset, offset + width), directory.bits, directory.little, ); + if (!parsedSymbol.ok) + throw new BinaryFormatFailure( + parsedSymbol.error.kind, + parsedSymbol.error.message, + ); + const symbol = parsedSymbol.value; // STO_CUDA_ENTRY distinguishes launchable kernels from ordinary device functions. if (symbol.type !== 2 || (symbol.other & 0x10) === 0) continue; let sectionIndex = symbol.section_index; diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts index 20fcb18663..2927d214aa 100644 --- a/src/native/exports/NativeExportParser.ts +++ b/src/native/exports/NativeExportParser.ts @@ -8,7 +8,8 @@ import { } from "../metadata/BinaryContainer.js"; import { architectureForElfMachine } from "../metadata/ElfImage.js"; import { isUtf8 } from "node:buffer"; -import { readElfStringBytes, readElfSymbol } from "../metadata/ElfSymbol.js"; +import { readElfStringBytes } from "../metadata/ElfStringTable.js"; +import { readElfSymbol } from "../../domain/elfSymbol.js"; import { readElfDirectoryFromBuffer, type NativeSection, @@ -548,11 +549,17 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { const exports: ElfExport[] = []; for (let index = 1; index < count; index++) { const at = table + index * width; - const symbol = readElfSymbol( + const parsedSymbol = readElfSymbol( bytes.subarray(at, at + width), directory.bits, directory.little, ); + if (!parsedSymbol.ok) + throw new BinaryFormatFailure( + parsedSymbol.error.kind, + parsedSymbol.error.message, + ); + const symbol = parsedSymbol.value; const binding = ELF_BINDINGS[symbol.binding]; const visibility = symbol.other & 3; // Undefined, local and hidden/internal symbols are not exports. diff --git a/src/native/metadata/ElfSymbol.ts b/src/native/metadata/ElfStringTable.ts similarity index 52% rename from src/native/metadata/ElfSymbol.ts rename to src/native/metadata/ElfStringTable.ts index d6962e73e4..ef258e78c4 100644 --- a/src/native/metadata/ElfSymbol.ts +++ b/src/native/metadata/ElfStringTable.ts @@ -1,23 +1,5 @@ -import { BinaryReader, BinaryFormatFailure } from "./BinaryContainer.js"; -/** Raw ELF symbol fields; GPU and other consumers own the meaning of processor-specific flags. */ -export const readElfSymbol = ( - bytes: Buffer, - bits: 32 | 64, - little: boolean, -) => { - const r = new BinaryReader(bytes, little); - const wide = bits === 64; - const info = r.range(wide ? 4 : 12, 1, "ELF symbol info")[0]!; - return { - name_offset: r.u32(0), - binding: info >>> 4, - type: info & 15, - other: r.range(wide ? 5 : 13, 1, "ELF symbol other")[0]!, - section_index: r.u16(wide ? 6 : 14), - value: r.word(wide ? 8 : 4, bits), - size: r.word(wide ? 16 : 8, bits), - }; -}; +import { BinaryFormatFailure } from "./BinaryContainer.js"; + export const readElfString = (bytes: Buffer, offset: number): string => { return readElfStringBytes(bytes, offset).toString("latin1"); };