refactor(elf): centralize raw table declarations

This commit is contained in:
morluto committed 2026-10-26 16:04:09 +08:00
1 parent c994870f6b
commit a7d71f47ee
11 files changed
+226 -90

No files matched your search

+11 -7
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
import type { FileHandle } from "node:fs/promises";
import type {
MipsAbiFlags,
@@ -91,12 +92,15 @@ export const readMipsElfMetadata = async (
if (fixedHeader.value.headerSize !== 52)
return err("invalid MIPS ELF32 version or header size");
const raw = readElfRawTableDeclarations(bytes, fixedHeader.value);
if (!raw.ok) return err(raw.error.message);
const { programs, sections } = raw.value;
const tables = [
{
label: "program header table",
offset: u32(bytes, 28),
count: u16(bytes, 44),
stride: u16(bytes, 42),
offset: Number(programs.offset),
count: programs.count,
stride: programs.width,
entrySize: 32,
typeOffset: 0,
dataOffset: 4,
@@ -106,9 +110,9 @@ export const readMipsElfMetadata = async (
},
{
label: "section header table",
offset: u32(bytes, 32),
count: u16(bytes, 48),
stride: u16(bytes, 46),
offset: Number(sections.offset),
count: sections.count,
stride: sections.width,
entrySize: 40,
typeOffset: 4,
dataOffset: 16,
@@ -119,7 +123,7 @@ export const readMipsElfMetadata = async (
];
// Extended numbering needs a separate section-zero reader. Refuse it rather
// than silently treating an unexamined table as having no ABI declarations.
if (u16(bytes, 44) === 0xffff || u16(bytes, 48) >= 0xff00)
if (programs.count === 0xffff || sections.count >= 0xff00)
return err("extended or reserved MIPS ELF table numbering is unsupported");
let record: Buffer | undefined;
let sharedLoad: MipsElfMetadata["sharedLoad"];
+17 -13
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
import { parsePeResources } from "../../native/pe/PeResourceParser.js";
import {
@@ -323,16 +324,22 @@ const locateElfNotes = async (
Math.min(carrier.fileSize, header.value.headerSize),
);
if (fields === undefined) return [];
const phoff = elf64
? readU64(fields, 32, little)
: readU32(fields, 28, little);
const shoff = elf64
? readU64(fields, 40, little)
: readU32(fields, 32, little);
const phentsize = readU16(fields, elf64 ? 54 : 42, little);
const phnum = readU16(fields, elf64 ? 56 : 44, little);
const shentsize = readU16(fields, elf64 ? 58 : 46, little);
const shnum = readU16(fields, elf64 ? 60 : 48, little);
const raw = readElfRawTableDeclarations(fields, header.value);
if (!raw.ok) return [];
const { programs: programDeclarations, sections: sectionDeclarations } =
raw.value;
const phoff =
programDeclarations.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(programDeclarations.offset)
: undefined;
const shoff =
sectionDeclarations.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(sectionDeclarations.offset)
: undefined;
const phentsize = programDeclarations.width;
const phnum = programDeclarations.count;
const shentsize = sectionDeclarations.width;
const shnum = sectionDeclarations.count;
const minimumProgram = elf64 ? 56 : 32;
const minimumSection = elf64 ? 64 : 40;
const found: BlobRange[] = [];
@@ -645,9 +652,6 @@ const cString = (bytes: Buffer, offset: number, length: number): string => {
return bytes.toString("latin1", offset, end);
};
const readU16 = (bytes: Buffer, offset: number, little: boolean): number =>
little ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset);
const readU32 = (bytes: Buffer, offset: number, little: boolean): number =>
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
+13 -36
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js";
import { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js";
import { err, ok, type Result } from "./result.js";
@@ -21,8 +22,6 @@ export interface ElfTableDeclarations {
const reader = (bytes: Buffer, header: ElfFixedHeader) => {
const little = header.byteOrder === "little";
const u16 = (at: number) =>
little ? bytes.readUInt16LE(at) : bytes.readUInt16BE(at);
const u32 = (at: number) =>
little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at);
const word = (at: number) =>
@@ -31,7 +30,7 @@ const reader = (bytes: Buffer, header: ElfFixedHeader) => {
: little
? bytes.readBigUInt64LE(at)
: bytes.readBigUInt64BE(at);
return { u16, u32, word };
return { u32, word };
};
/** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */
@@ -110,29 +109,6 @@ const extendedCounts = (
return ok(counts);
};
const TABLE_FIELDS = {
32: {
programOffset: 28,
sectionOffset: 32,
programWidth: 42,
programCount: 44,
sectionWidth: 46,
sectionCount: 48,
namesIndex: 50,
sectionHeaderSize: 40,
},
64: {
programOffset: 32,
sectionOffset: 40,
programWidth: 54,
programCount: 56,
sectionWidth: 58,
sectionCount: 60,
namesIndex: 62,
sectionHeaderSize: 64,
},
} as const;
/** Canonical table declarations, including extended counts in inactive section zero. */
export function* readElfTableDeclarations(
bytes: Buffer,
@@ -141,15 +117,16 @@ export function* readElfTableDeclarations(
const fixed = readElfFixedHeader(bytes, fileSize);
if (!fixed.ok) return fixed;
const header = fixed.value;
const r = reader(bytes, header);
const fields = TABLE_FIELDS[header.elfClass];
const programOffset = r.word(fields.programOffset);
const sectionOffset = r.word(fields.sectionOffset);
const programWidth = r.u16(fields.programWidth);
const sectionWidth = r.u16(fields.sectionWidth);
let programCount = BigInt(r.u16(fields.programCount));
let sectionCount = BigInt(r.u16(fields.sectionCount));
let namesIndex = r.u16(fields.namesIndex);
const raw = readElfRawTableDeclarations(bytes, header);
if (!raw.ok) return err(raw.error.message);
const { programs, sections } = raw.value;
const programOffset = programs.offset;
const sectionOffset = sections.offset;
const programWidth = programs.width;
const sectionWidth = sections.width;
let programCount = BigInt(programs.count);
let sectionCount = BigInt(sections.count);
let namesIndex = sections.namesIndex;
if (
sectionOffset === 0n &&
(sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn)
@@ -168,7 +145,7 @@ export function* readElfTableDeclarations(
if (failure !== undefined) return err(failure);
const first = yield {
offset: Number(sectionOffset),
size: fields.sectionHeaderSize,
size: header.elfClass === 64 ? 64 : 40,
};
const counts = extendedCounts(first, header, {
programCount,
@@ -0,0 +1,68 @@
import { expect, it } from "vitest";
import { readElfRawTableDeclarations } from "./elfRawTableDeclarations.js";
const formats = [
{ elfClass: 32, byteOrder: "little" },
{ elfClass: 32, byteOrder: "big" },
{ elfClass: 64, byteOrder: "little" },
{ elfClass: 64, byteOrder: "big" },
] as const;
it.each(formats)(
"preserves unresolved raw ELF$elfClass $byteOrder declarations",
(format) => {
const wide = format.elfClass === 64;
const little = format.byteOrder === "little";
const length = wide ? 64 : 52;
const backing = Buffer.alloc(length + 17, 0xa5);
const bytes = backing.subarray(7, 7 + length);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const programOffset = wide ? 0xfedcba9876543210n : 0xfedcba98n;
const sectionOffset = wide ? 0xffffffffffffffffn : 0xffffffffn;
if (wide) {
view.setBigUint64(32, programOffset, little);
view.setBigUint64(40, sectionOffset, little);
} else {
view.setUint32(28, Number(programOffset), little);
view.setUint32(32, Number(sectionOffset), little);
}
[1, 0xffff, 2, 0, 0xffff].forEach((value, index) =>
view.setUint16((wide ? 54 : 42) + index * 2, value, little),
);
const before = Buffer.from(backing);
const parsed = readElfRawTableDeclarations(backing.subarray(7), format);
expect(backing.equals(before)).toBe(true);
backing.fill(0);
expect(parsed).toEqual({
ok: true,
value: {
programs: { offset: programOffset, width: 1, count: 0xffff },
sections: {
offset: sectionOffset,
width: 2,
count: 0,
namesIndex: 0xffff,
},
},
});
// Format identity and whether these unresolved declarations are admissible belong to callers.
},
);
it.each(formats)(
"refuses truncated ELF$elfClass $byteOrder declaration records",
(format) => {
const length = format.elfClass === 64 ? 64 : 52;
const backing = Buffer.alloc(length + 7);
for (let observed = 0; observed < length; observed++)
expect(
readElfRawTableDeclarations(backing.subarray(7, 7 + observed), format),
).toEqual({
ok: false,
error: {
kind: "malformed",
message: "ELF table declarations are truncated",
},
});
},
);
+33
View File
@@ -0,0 +1,33 @@
import { err, ok } from "./result.js";
/** Fixed-header table words, before extended numbering or file-range admission. */
export const readElfRawTableDeclarations = (
bytes: Uint8Array,
format: { readonly elfClass: 32 | 64; readonly byteOrder: "little" | "big" },
) => {
const wide = format.elfClass === 64;
if (bytes.byteLength < (wide ? 64 : 52))
return err({
kind: "malformed" as const,
message: "ELF table declarations are truncated",
});
const little = format.byteOrder === "little";
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const word = (offset: number): bigint =>
wide
? view.getBigUint64(offset, little)
: BigInt(view.getUint32(offset, little));
return ok({
programs: {
offset: word(wide ? 32 : 28),
width: view.getUint16(wide ? 54 : 42, little),
count: view.getUint16(wide ? 56 : 44, little),
},
sections: {
offset: word(wide ? 40 : 32),
width: view.getUint16(wide ? 58 : 46, little),
count: view.getUint16(wide ? 60 : 48, little),
namesIndex: view.getUint16(wide ? 62 : 50, little),
},
});
};
+19
View File
@@ -502,3 +502,22 @@ describe("readSnapshotHeader", () => {
});
});
});
it("keeps section admission ahead of program admission for unrepresentable raw offsets", async () => {
const bytes = buildStrippedElf64Image([]);
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x28);
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x20);
expect(await readElfSymbols(bytes, ["_kDartVmSnapshotData"])).toEqual({
symbols: [],
failures: [
{ reason: "ELF section header table is missing or out of bounds" },
],
});
bytes.writeBigUInt64LE(0n, 0x28);
expect(await readElfSymbols(bytes, ["_kDartVmSnapshotData"])).toEqual({
symbols: [],
failures: [
{ reason: "ELF program header table is missing or out of bounds" },
],
});
});
+18 -15
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../domain/elfRawTableDeclarations.js";
import { Buffer } from "node:buffer";
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
import { createElfLoadMap } from "./ElfLoadMap.js";
@@ -84,7 +85,6 @@ export const readElfHeader = (
bytes: Buffer,
totalBytes: number = bytes.length,
) => {
const reader = new BoundedReader(bytes);
const fixedHeader = readElfFixedHeader(bytes, totalBytes);
if (!fixedHeader.ok) return { failure: { reason: fixedHeader.error } };
const is64 = fixedHeader.value.elfClass === 64;
@@ -96,21 +96,28 @@ export const readElfHeader = (
const e_type = fixedHeader.value.type;
if (e_type !== 1 && e_type !== 2 && e_type !== ET_DYN)
return { failure: { reason: "unsupported ELF type" } };
const e_shoff = is64 ? reader.u64(0x28) : reader.u32(0x20);
const e_shentsize = reader.u16(is64 ? 0x3a : 0x2e);
const e_shnum = reader.u16(is64 ? 0x3c : 0x30);
const e_phoff = is64 ? reader.u64(0x20) : reader.u32(0x1c);
const e_phentsize = reader.u16(is64 ? 0x36 : 0x2a);
const e_phnum = reader.u16(is64 ? 0x38 : 0x2c);
const raw = readElfRawTableDeclarations(bytes, fixedHeader.value);
if (!raw.ok) return { failure: { reason: raw.error.message } };
const { programs, sections } = raw.value;
const e_shoff =
sections.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(sections.offset)
: null;
const e_shentsize = sections.width;
const e_shnum = sections.count;
const e_phoff =
programs.offset <= BigInt(Number.MAX_SAFE_INTEGER)
? Number(programs.offset)
: null;
const e_phentsize = programs.width;
const e_phnum = programs.count;
const tableFits = (
offset: number | null,
width: number | null,
count: number | null,
width: number,
count: number,
minimumWidth: number,
): boolean =>
offset !== null &&
width !== null &&
count !== null &&
offset >= headerBytes &&
offset <= totalBytes &&
width >= minimumWidth &&
@@ -125,8 +132,6 @@ export const readElfHeader = (
e_type === ET_DYN && e_shoff === 0 && e_shnum === 0;
if (
e_shoff === null ||
e_shentsize === null ||
e_shnum === null ||
(!sectionTableAbsent &&
!tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT))
)
@@ -138,8 +143,6 @@ export const readElfHeader = (
const MIN_PHENT = is64 ? 56 : 32;
if (
e_phoff === null ||
e_phentsize === null ||
e_phnum === null ||
e_phnum === 0xffff ||
!tableFits(e_phoff, e_phentsize, e_phnum, MIN_PHENT)
)
+8
View File
@@ -617,3 +617,11 @@ it("fails when the entry budget is exhausted", () => {
),
).toThrow(ArtifactReaderFailure);
});
it("keeps unsafe table offsets ahead of unsupported extended numbering", () => {
const bytes = elfExtendedSectionCountFixture();
bytes.writeBigUInt64LE(0xffffffffffffffffn, 32);
expect(() => parse(bytes)).toThrow(
"ELF offset or size exceeds a safe integer.",
);
});
+16 -14
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
@@ -98,13 +99,6 @@ class ElfReader {
readonly bits: 32 | 64,
) {}
u16(offset: number): number {
this.require(offset, 2);
return this.little
? this.bytes.readUInt16LE(offset)
: this.bytes.readUInt16BE(offset);
}
u32(offset: number): number {
this.require(offset, 4);
return this.little
@@ -328,13 +322,21 @@ export const parseElfNotes = (
);
const wide = header.elfClass === 64;
const type = header.type;
const phoff = reader.word(wide ? 32 : 28);
const shoff = reader.word(wide ? 40 : 32);
const phentsize = reader.u16(wide ? 54 : 42);
const phnum = reader.u16(wide ? 56 : 44);
const shentsize = reader.u16(wide ? 58 : 46);
const shnum = reader.u16(wide ? 60 : 48);
const shstrndx = reader.u16(wide ? 62 : 50);
const raw = readElfRawTableDeclarations(bytes, header);
if (!raw.ok) return fail(raw.error.message);
const { programs, sections } = raw.value;
if (
programs.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
sections.offset > BigInt(Number.MAX_SAFE_INTEGER)
)
return fail("ELF offset or size exceeds a safe integer.");
const phoff = Number(programs.offset);
const shoff = Number(sections.offset);
const phentsize = programs.width;
const phnum = programs.count;
const shentsize = sections.width;
const shnum = sections.count;
const shstrndx = sections.namesIndex;
if (
phnum === 0xffff ||
shnum === 0xffff ||
@@ -50,3 +50,14 @@ it.each([3, 0x46494c45])(
expect(recordedCrashSchema.safeParse(report).success).toBe(false);
},
);
it("keeps snapshot rebinding independent of unused ELF section and header-size admission", () => {
const report = recordedCrashSchema.parse(recordedCrashFixture());
const bytes = recordedCrashFixtureBytes(report);
bytes.writeUInt16LE(0, 52);
bytes.writeBigUInt64LE(0xffffffffffffffffn, 40);
bytes.writeUInt16LE(0xffff, 60);
expect(() =>
validateRecordedCrashStructure(report, bytes, report.decoder_diagnostics),
).not.toThrow();
});
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import {
RECORDED_LINUX_CORE_PROFILES,
@@ -103,12 +104,18 @@ export function validateRecordedCrashStructure(
header.readUInt16LE(18) !== expectedMachine
)
fail();
const table = header.readBigUInt64LE(32);
const entryBytes = header.readUInt16LE(54);
let count = header.readUInt16LE(56);
const rawTables = readElfRawTableDeclarations(header, {
elfClass: 64,
byteOrder: "little",
});
if (!rawTables.ok) return fail();
const { programs, sections } = rawTables.value;
const table = programs.offset;
const entryBytes = programs.width;
let count = programs.count;
if (count === 0xffff) {
if (header.readUInt16LE(58) < 64) return fail();
count = bytesAt(header.readBigUInt64LE(40), 64).readUInt32LE(44);
if (sections.width < 64) return fail();
count = bytesAt(sections.offset, 64).readUInt32LE(44);
}
if (count !== report.segments.length || (count > 0 && entryBytes < 56))
fail();