From 624ebb2a1e633ce14b6d6828eef50bf7d7db68ac Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:46:08 +0800 Subject: [PATCH] refactor(elf): consolidate bounded string-table interpretation --- .../{elfSectionName.ts => elfStringTable.ts} | 12 ++++----- src/flutter/DartElf.test.ts | 26 +++++++++++++++++++ src/flutter/DartElf.ts | 16 ++++++------ src/native/elf/ElfNoteParser.ts | 10 +++---- src/native/exports/NativeExportParser.test.ts | 20 ++++++++++++++ src/native/metadata/ElfStringTable.ts | 11 ++++---- src/native/metadata/NativeSectionDirectory.ts | 10 +++---- 7 files changed, 75 insertions(+), 30 deletions(-) rename src/domain/{elfSectionName.ts => elfStringTable.ts} (81%) diff --git a/src/domain/elfSectionName.ts b/src/domain/elfStringTable.ts similarity index 81% rename from src/domain/elfSectionName.ts rename to src/domain/elfStringTable.ts index d995c3f238..18b3cb5e52 100644 --- a/src/domain/elfSectionName.ts +++ b/src/domain/elfStringTable.ts @@ -1,5 +1,5 @@ /** Bounded source observation; consumers own scan limits, charging and caching. */ -export type ElfSectionNameObservation = +export type ElfStringTableObservation = | { readonly state: "outside"; readonly bytes: null; @@ -19,12 +19,12 @@ export type ElfSectionNameObservation = readonly contentBytes: number; }; -/** Observe an ELF string-table name without treating a bounded missing NUL as absence. */ -export const observeElfSectionName = ( +/** Observe one ELF string-table entry without treating a bounded missing NUL as absence. */ +export const observeElfStringTableEntry = ( table: Buffer, offset: number, maximumBytes: number, -): ElfSectionNameObservation => { +): ElfStringTableObservation => { if (offset < 0 || offset >= table.length) return { state: "outside", bytes: null, examinedBytes: 0, contentBytes: 0 }; const window = table.subarray(offset, offset + maximumBytes); @@ -45,6 +45,6 @@ export const observeElfSectionName = ( }; /** Project admitted source bytes without inferring an original text encoding. */ -export const elfSectionNameText = ( - observed: ElfSectionNameObservation, +export const elfStringTableText = ( + observed: ElfStringTableObservation, ): string | null => observed.bytes?.toString("latin1") ?? null; diff --git a/src/flutter/DartElf.test.ts b/src/flutter/DartElf.test.ts index d1d00b0065..b854c27c81 100644 --- a/src/flutter/DartElf.test.ts +++ b/src/flutter/DartElf.test.ts @@ -522,6 +522,32 @@ it("keeps section admission ahead of program admission for unrepresentable raw o }); }); +it.each([256, 257])( + "preserves Dart's narrower symbol-name window for a %i-byte name", + async (length) => { + const name = "n".repeat(length); + const original = buildElf64Image([{ name: "seed", value: 64, size: 4 }]); + const sectionTable = Number(original.readBigUInt64LE(0x28)); + const stringHeader = sectionTable + 2 * 64; + const stringOffset = Number(original.readBigUInt64LE(stringHeader + 0x18)); + const bytes = Buffer.concat([original, Buffer.alloc(length + 2)]); + bytes.write(name, stringOffset + 1, "latin1"); + bytes[stringOffset + 1 + length] = 0; + bytes.writeBigUInt64LE(BigInt(length + 2), stringHeader + 0x20); + bytes.writeBigUInt64LE(BigInt(bytes.length), 64 + 0x20); + expect(await readElfSymbols(bytes, [name])).toEqual( + length === 256 + ? { symbols: [{ name, offset: 64, size: 4 }], failures: [] } + : { + symbols: [], + failures: [ + { reason: `symbol ${name} is not in the dynamic symbol table` }, + ], + }, + ); + }, +); + it("checks raw symbol precision only after definition and requested-name selection", async () => { const bytes = buildElf64Image([ { name: "unselected", value: 64, size: 4 }, diff --git a/src/flutter/DartElf.ts b/src/flutter/DartElf.ts index 7da95184c3..d547cfa9d1 100644 --- a/src/flutter/DartElf.ts +++ b/src/flutter/DartElf.ts @@ -1,3 +1,7 @@ +import { + observeElfStringTableEntry, + elfStringTableText, +} from "../domain/elfStringTable.js"; import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js"; import { readElfSectionHeader } from "../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../domain/elfProgramHeader.js"; @@ -35,14 +39,10 @@ class BoundedReader { } cString(offset: number, tableEnd: number): string | null { - if (offset < 0 || offset >= tableEnd || tableEnd > this.bytes.length) - return null; - const end = this.bytes - .subarray(offset, Math.min(tableEnd, offset + 257)) - .indexOf(0); - return end < 0 - ? null - : this.bytes.subarray(offset, offset + end).toString("latin1"); + if (tableEnd < 0 || tableEnd > this.bytes.length) return null; + return elfStringTableText( + observeElfStringTableEntry(this.bytes.subarray(0, tableEnd), offset, 257), + ); } } diff --git a/src/native/elf/ElfNoteParser.ts b/src/native/elf/ElfNoteParser.ts index 3b0bb17758..f06ba6cca8 100644 --- a/src/native/elf/ElfNoteParser.ts +++ b/src/native/elf/ElfNoteParser.ts @@ -1,7 +1,7 @@ import { - observeElfSectionName, - elfSectionNameText, -} from "../../domain/elfSectionName.js"; + observeElfStringTableEntry, + elfStringTableText, +} from "../../domain/elfStringTable.js"; import { readElfSectionHeader } from "../../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../../domain/elfProgramHeader.js"; import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js"; @@ -128,7 +128,7 @@ class ElfReader { offset: number, text: TextBudget, ): { name: string | null; limitation: string | null } { - const observed = observeElfSectionName( + const observed = observeElfStringTableEntry( table, offset, text.limit - text.used + 1, @@ -141,7 +141,7 @@ class ElfReader { }; chargeText(text, observed.contentBytes); return { - name: elfSectionNameText(observed), + name: elfStringTableText(observed), limitation: observed.state === "truncated" ? "ELF section names are incomplete because a name is not NUL-terminated." diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts index 72e7c1b37f..6076328563 100644 --- a/src/native/exports/NativeExportParser.test.ts +++ b/src/native/exports/NativeExportParser.test.ts @@ -677,6 +677,26 @@ describe("export inventory review regressions", () => { }); describe("ELF symbol-name text", () => { + it.each([65535, 65536])( + "keeps the ELF export name scan bounded when its NUL follows %i bytes", + (length) => { + const name = "n".repeat(length); + const result = parse(elfExportImage({ symbols: [{ name }] })); + if (result.format !== "elf") throw new TypeError("expected ELF result"); + expect(result.exports.map((entry) => entry.name)).toEqual( + length === 65535 ? [name] : [], + ); + if (length === 65536) + expect(result.issues).toContainEqual( + expect.objectContaining({ + message: expect.stringContaining( + "unterminated or exceeds the string budget", + ), + }), + ); + }, + ); + it("decodes UTF-8 names and retains exact bytes", () => { const result = parse(elfExportImage({ symbols: [{ name: "café" }] })); expect(result.format).toBe("elf"); diff --git a/src/native/metadata/ElfStringTable.ts b/src/native/metadata/ElfStringTable.ts index ef258e78c4..c60a179a57 100644 --- a/src/native/metadata/ElfStringTable.ts +++ b/src/native/metadata/ElfStringTable.ts @@ -1,3 +1,4 @@ +import { observeElfStringTableEntry } from "../../domain/elfStringTable.js"; import { BinaryFormatFailure } from "./BinaryContainer.js"; export const readElfString = (bytes: Buffer, offset: number): string => { @@ -6,18 +7,16 @@ export const readElfString = (bytes: Buffer, offset: number): string => { /** Read exact string-table bytes; ELF does not prescribe a character encoding. */ export const readElfStringBytes = (bytes: Buffer, offset: number): Buffer => { - if (offset >= bytes.length) + const observed = observeElfStringTableEntry(bytes, offset, 65536); + if (observed.state === "outside") throw new BinaryFormatFailure( "malformed", "ELF string offset is outside its string table", ); - const end = bytes - .subarray(offset, Math.min(bytes.length, offset + 65536)) - .indexOf(0); - if (end < 0) + if (observed.state === "truncated") throw new BinaryFormatFailure( "malformed", "ELF string is unterminated or exceeds the string budget", ); - return bytes.subarray(offset, offset + end); + return observed.bytes; }; diff --git a/src/native/metadata/NativeSectionDirectory.ts b/src/native/metadata/NativeSectionDirectory.ts index 69ed2040af..7ff4730dea 100644 --- a/src/native/metadata/NativeSectionDirectory.ts +++ b/src/native/metadata/NativeSectionDirectory.ts @@ -1,7 +1,7 @@ import { - observeElfSectionName, - elfSectionNameText, -} from "../../domain/elfSectionName.js"; + observeElfStringTableEntry, + elfStringTableText, +} from "../../domain/elfStringTable.js"; import { observeMzWindowsHeader } from "../../domain/dosMz.js"; import { readElfSectionHeader } from "../../domain/elfSectionHeader.js"; import { readElfProgramHeader } from "../../domain/elfProgramHeader.js"; @@ -221,7 +221,7 @@ export function* readElfSectionDirectory( if (names !== null) { let decoded = decodedNames.get(nameOffset); if (decoded === undefined) { - const observed = observeElfSectionName(names, nameOffset, 65536); + const observed = observeElfStringTableEntry(names, nameOffset, 65536); nameBytes += observed.examinedBytes; if (nameBytes > STRUCTURE_BYTES) throw new BinaryResourceFailure( @@ -230,7 +230,7 @@ export function* readElfSectionDirectory( "Decoded ELF section names exceed the 16 MiB structural budget", ); decoded = { - name: elfSectionNameText(observed), + name: elfStringTableText(observed), state: observed.state === "terminated" ? "known"