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 { FileHandle } from "node:fs/promises";
import type { import type {
MipsAbiFlags, MipsAbiFlags,
@@ -91,12 +92,15 @@ export const readMipsElfMetadata = async (
if (fixedHeader.value.headerSize !== 52) if (fixedHeader.value.headerSize !== 52)
return err("invalid MIPS ELF32 version or header size"); 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 = [ const tables = [
{ {
label: "program header table", label: "program header table",
offset: u32(bytes, 28), offset: Number(programs.offset),
count: u16(bytes, 44), count: programs.count,
stride: u16(bytes, 42), stride: programs.width,
entrySize: 32, entrySize: 32,
typeOffset: 0, typeOffset: 0,
dataOffset: 4, dataOffset: 4,
@@ -106,9 +110,9 @@ export const readMipsElfMetadata = async (
}, },
{ {
label: "section header table", label: "section header table",
offset: u32(bytes, 32), offset: Number(sections.offset),
count: u16(bytes, 48), count: sections.count,
stride: u16(bytes, 46), stride: sections.width,
entrySize: 40, entrySize: 40,
typeOffset: 4, typeOffset: 4,
dataOffset: 16, dataOffset: 16,
@@ -119,7 +123,7 @@ export const readMipsElfMetadata = async (
]; ];
// Extended numbering needs a separate section-zero reader. Refuse it rather // Extended numbering needs a separate section-zero reader. Refuse it rather
// than silently treating an unexamined table as having no ABI declarations. // 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"); return err("extended or reserved MIPS ELF table numbering is unsupported");
let record: Buffer | undefined; let record: Buffer | undefined;
let sharedLoad: MipsElfMetadata["sharedLoad"]; let sharedLoad: MipsElfMetadata["sharedLoad"];
+17 -13
View File
@@ -1,3 +1,4 @@
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { parsePeResources } from "../../native/pe/PeResourceParser.js"; import { parsePeResources } from "../../native/pe/PeResourceParser.js";
import { import {
@@ -323,16 +324,22 @@ const locateElfNotes = async (
Math.min(carrier.fileSize, header.value.headerSize), Math.min(carrier.fileSize, header.value.headerSize),
); );
if (fields === undefined) return []; if (fields === undefined) return [];
const phoff = elf64 const raw = readElfRawTableDeclarations(fields, header.value);
? readU64(fields, 32, little) if (!raw.ok) return [];
: readU32(fields, 28, little); const { programs: programDeclarations, sections: sectionDeclarations } =
const shoff = elf64 raw.value;
? readU64(fields, 40, little) const phoff =
: readU32(fields, 32, little); programDeclarations.offset <= BigInt(Number.MAX_SAFE_INTEGER)
const phentsize = readU16(fields, elf64 ? 54 : 42, little); ? Number(programDeclarations.offset)
const phnum = readU16(fields, elf64 ? 56 : 44, little); : undefined;
const shentsize = readU16(fields, elf64 ? 58 : 46, little); const shoff =
const shnum = readU16(fields, elf64 ? 60 : 48, little); 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 minimumProgram = elf64 ? 56 : 32;
const minimumSection = elf64 ? 64 : 40; const minimumSection = elf64 ? 64 : 40;
const found: BlobRange[] = []; const found: BlobRange[] = [];
@@ -645,9 +652,6 @@ const cString = (bytes: Buffer, offset: number, length: number): string => {
return bytes.toString("latin1", offset, end); 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 => const readU32 = (bytes: Buffer, offset: number, little: boolean): number =>
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); 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 { readElfFixedHeader, type ElfFixedHeader } from "./elfFixedHeader.js";
import { err, ok, type Result } from "./result.js"; import { err, ok, type Result } from "./result.js";
@@ -21,8 +22,6 @@ export interface ElfTableDeclarations {
const reader = (bytes: Buffer, header: ElfFixedHeader) => { const reader = (bytes: Buffer, header: ElfFixedHeader) => {
const little = header.byteOrder === "little"; const little = header.byteOrder === "little";
const u16 = (at: number) =>
little ? bytes.readUInt16LE(at) : bytes.readUInt16BE(at);
const u32 = (at: number) => const u32 = (at: number) =>
little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at); little ? bytes.readUInt32LE(at) : bytes.readUInt32BE(at);
const word = (at: number) => const word = (at: number) =>
@@ -31,7 +30,7 @@ const reader = (bytes: Buffer, header: ElfFixedHeader) => {
: little : little
? bytes.readBigUInt64LE(at) ? bytes.readBigUInt64LE(at)
: bytes.readBigUInt64BE(at); : bytes.readBigUInt64BE(at);
return { u16, u32, word }; return { u32, word };
}; };
/** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */ /** Zero-sized/inactive declarations own no file bytes. Arithmetic stays exact. */
@@ -110,29 +109,6 @@ const extendedCounts = (
return ok(counts); 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. */ /** Canonical table declarations, including extended counts in inactive section zero. */
export function* readElfTableDeclarations( export function* readElfTableDeclarations(
bytes: Buffer, bytes: Buffer,
@@ -141,15 +117,16 @@ export function* readElfTableDeclarations(
const fixed = readElfFixedHeader(bytes, fileSize); const fixed = readElfFixedHeader(bytes, fileSize);
if (!fixed.ok) return fixed; if (!fixed.ok) return fixed;
const header = fixed.value; const header = fixed.value;
const r = reader(bytes, header); const raw = readElfRawTableDeclarations(bytes, header);
const fields = TABLE_FIELDS[header.elfClass]; if (!raw.ok) return err(raw.error.message);
const programOffset = r.word(fields.programOffset); const { programs, sections } = raw.value;
const sectionOffset = r.word(fields.sectionOffset); const programOffset = programs.offset;
const programWidth = r.u16(fields.programWidth); const sectionOffset = sections.offset;
const sectionWidth = r.u16(fields.sectionWidth); const programWidth = programs.width;
let programCount = BigInt(r.u16(fields.programCount)); const sectionWidth = sections.width;
let sectionCount = BigInt(r.u16(fields.sectionCount)); let programCount = BigInt(programs.count);
let namesIndex = r.u16(fields.namesIndex); let sectionCount = BigInt(sections.count);
let namesIndex = sections.namesIndex;
if ( if (
sectionOffset === 0n && sectionOffset === 0n &&
(sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn) (sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn)
@@ -168,7 +145,7 @@ export function* readElfTableDeclarations(
if (failure !== undefined) return err(failure); if (failure !== undefined) return err(failure);
const first = yield { const first = yield {
offset: Number(sectionOffset), offset: Number(sectionOffset),
size: fields.sectionHeaderSize, size: header.elfClass === 64 ? 64 : 40,
}; };
const counts = extendedCounts(first, header, { const counts = extendedCounts(first, header, {
programCount, 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 { Buffer } from "node:buffer";
import { setImmediate as yieldToEventLoop } from "node:timers/promises"; import { setImmediate as yieldToEventLoop } from "node:timers/promises";
import { createElfLoadMap } from "./ElfLoadMap.js"; import { createElfLoadMap } from "./ElfLoadMap.js";
@@ -84,7 +85,6 @@ export const readElfHeader = (
bytes: Buffer, bytes: Buffer,
totalBytes: number = bytes.length, totalBytes: number = bytes.length,
) => { ) => {
const reader = new BoundedReader(bytes);
const fixedHeader = readElfFixedHeader(bytes, totalBytes); const fixedHeader = readElfFixedHeader(bytes, totalBytes);
if (!fixedHeader.ok) return { failure: { reason: fixedHeader.error } }; if (!fixedHeader.ok) return { failure: { reason: fixedHeader.error } };
const is64 = fixedHeader.value.elfClass === 64; const is64 = fixedHeader.value.elfClass === 64;
@@ -96,21 +96,28 @@ export const readElfHeader = (
const e_type = fixedHeader.value.type; const e_type = fixedHeader.value.type;
if (e_type !== 1 && e_type !== 2 && e_type !== ET_DYN) if (e_type !== 1 && e_type !== 2 && e_type !== ET_DYN)
return { failure: { reason: "unsupported ELF type" } }; return { failure: { reason: "unsupported ELF type" } };
const e_shoff = is64 ? reader.u64(0x28) : reader.u32(0x20); const raw = readElfRawTableDeclarations(bytes, fixedHeader.value);
const e_shentsize = reader.u16(is64 ? 0x3a : 0x2e); if (!raw.ok) return { failure: { reason: raw.error.message } };
const e_shnum = reader.u16(is64 ? 0x3c : 0x30); const { programs, sections } = raw.value;
const e_phoff = is64 ? reader.u64(0x20) : reader.u32(0x1c); const e_shoff =
const e_phentsize = reader.u16(is64 ? 0x36 : 0x2a); sections.offset <= BigInt(Number.MAX_SAFE_INTEGER)
const e_phnum = reader.u16(is64 ? 0x38 : 0x2c); ? 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 = ( const tableFits = (
offset: number | null, offset: number | null,
width: number | null, width: number,
count: number | null, count: number,
minimumWidth: number, minimumWidth: number,
): boolean => ): boolean =>
offset !== null && offset !== null &&
width !== null &&
count !== null &&
offset >= headerBytes && offset >= headerBytes &&
offset <= totalBytes && offset <= totalBytes &&
width >= minimumWidth && width >= minimumWidth &&
@@ -125,8 +132,6 @@ export const readElfHeader = (
e_type === ET_DYN && e_shoff === 0 && e_shnum === 0; e_type === ET_DYN && e_shoff === 0 && e_shnum === 0;
if ( if (
e_shoff === null || e_shoff === null ||
e_shentsize === null ||
e_shnum === null ||
(!sectionTableAbsent && (!sectionTableAbsent &&
!tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT)) !tableFits(e_shoff, e_shentsize, e_shnum, MIN_SHENT))
) )
@@ -138,8 +143,6 @@ export const readElfHeader = (
const MIN_PHENT = is64 ? 56 : 32; const MIN_PHENT = is64 ? 56 : 32;
if ( if (
e_phoff === null || e_phoff === null ||
e_phentsize === null ||
e_phnum === null ||
e_phnum === 0xffff || e_phnum === 0xffff ||
!tableFits(e_phoff, e_phentsize, e_phnum, MIN_PHENT) !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); ).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 { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js"; import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
@@ -98,13 +99,6 @@ class ElfReader {
readonly bits: 32 | 64, 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 { u32(offset: number): number {
this.require(offset, 4); this.require(offset, 4);
return this.little return this.little
@@ -328,13 +322,21 @@ export const parseElfNotes = (
); );
const wide = header.elfClass === 64; const wide = header.elfClass === 64;
const type = header.type; const type = header.type;
const phoff = reader.word(wide ? 32 : 28); const raw = readElfRawTableDeclarations(bytes, header);
const shoff = reader.word(wide ? 40 : 32); if (!raw.ok) return fail(raw.error.message);
const phentsize = reader.u16(wide ? 54 : 42); const { programs, sections } = raw.value;
const phnum = reader.u16(wide ? 56 : 44); if (
const shentsize = reader.u16(wide ? 58 : 46); programs.offset > BigInt(Number.MAX_SAFE_INTEGER) ||
const shnum = reader.u16(wide ? 60 : 48); sections.offset > BigInt(Number.MAX_SAFE_INTEGER)
const shstrndx = reader.u16(wide ? 62 : 50); )
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 ( if (
phnum === 0xffff || phnum === 0xffff ||
shnum === 0xffff || shnum === 0xffff ||
@@ -50,3 +50,14 @@ it.each([3, 0x46494c45])(
expect(recordedCrashSchema.safeParse(report).success).toBe(false); 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 { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { import {
RECORDED_LINUX_CORE_PROFILES, RECORDED_LINUX_CORE_PROFILES,
@@ -103,12 +104,18 @@ export function validateRecordedCrashStructure(
header.readUInt16LE(18) !== expectedMachine header.readUInt16LE(18) !== expectedMachine
) )
fail(); fail();
const table = header.readBigUInt64LE(32); const rawTables = readElfRawTableDeclarations(header, {
const entryBytes = header.readUInt16LE(54); elfClass: 64,
let count = header.readUInt16LE(56); 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 (count === 0xffff) {
if (header.readUInt16LE(58) < 64) return fail(); if (sections.width < 64) return fail();
count = bytesAt(header.readBigUInt64LE(40), 64).readUInt32LE(44); count = bytesAt(sections.offset, 64).readUInt32LE(44);
} }
if (count !== report.segments.length || (count > 0 && entryBytes < 56)) if (count !== report.segments.length || (count > 0 && entryBytes < 56))
fail(); fail();