refactor(elf): consolidate bounded string-table interpretation

This commit is contained in:
morluto committed 2026-10-27 01:46:08 +08:00
1 parent e405d685bc
commit 624ebb2a1e
7 files changed
+75 -30

No files matched your search

@@ -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;
+26
View File
@@ -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 },
+8 -8
View File
@@ -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),
);
}
}
+5 -5
View File
@@ -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."
@@ -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");
+5 -6
View File
@@ -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;
};
@@ -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"