refactor(elf): centralize symbol record decoding

This commit is contained in:
morluto committed 2026-10-26 16:10:46 +08:00
1 parent 3d565fa42a
commit 90199ff0ec
7 files changed
+154 -35

No files matched your search

+64
View File
@@ -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" },
});
},
);
+30
View File
@@ -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)),
});
};
+24
View File
@@ -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" },
],
});
});
+16 -11
View File
@@ -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}`);
+9 -2
View File
@@ -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;
+9 -2
View File
@@ -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.
@@ -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");
};