Files
rea/src/native/elf/ElfNoteParser.test.ts
T
morluto bd4f5e9c19 fix(parsers): preserve format evidence and caller input identity
Retain bounded FAT and offset-tagged PE observations through archive hashing,
classification, and content nodes. Preserve PyInstaller member paths according
to the observed carrier dialect and share APK v1 signing-name semantics.

Read SQLite's implicit sqlite_schema table without inventing a stored schema
declaration. Distinguish unsupported ELF numbering and decoded-name admission
from malformed data and fixed scan allowances. Preserve Sigrok basenames and
classify matching explicit input-format refusals at their owning boundary.

Validation: 554 distinct tests passed, 3 skipped across serial focused runs;
build, typecheck/lint, docs, formatting, and discovery (2141 files) passed.
Sigrok command-boundary simulators do not establish real-tool decoding.
Small repeated public PyInstaller CLI runs had equivalent inventory facts;
warm median elapsed 0.5431s before / 0.5426s after, with no speed claim.

Fixes #6733
Fixes #6741
Fixes #6722
Fixes #6732
Fixes #6736
Fixes #6734
Fixes #6764
Fixes #6766
Refs #6739
2026-10-23 22:54:28 +08:00

506 lines
15 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { BinaryFormatFailure } from "../metadata/BinaryContainer.js";
import {
compressedElfNoteFixture,
elfNote,
elfNoteFixture,
elfNoteTablePastEnd,
elfExtendedSectionCountFixture,
gnuAbiTagNote,
gnuBuildIdNote,
gnuPropertyNote,
} from "./ElfNotes.fixture.js";
import {
ElfNoteSectionNameBudgetFailure,
parseElfNotes,
} from "./ElfNoteParser.js";
const defaults = inspectElfNotesInputSchema.parse({
path: "/artifacts/app",
});
const parse = (bytes: Buffer, input: typeof defaults = defaults) =>
parseElfNotes(
bytes,
{
path: input.path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
input,
);
const elf64 = (
fields: {
readonly phoff?: number;
readonly shoff?: number;
readonly phnum?: number;
readonly shnum?: number;
readonly phentsize?: number;
readonly shentsize?: number;
},
tail?: Buffer,
): Buffer => {
const header = Buffer.alloc(64);
header.write("\x7fELF", 0, "latin1");
header[4] = 2;
header[5] = 1;
header[6] = 1;
header.writeUInt16LE(2, 16);
header.writeUInt16LE(62, 18);
header.writeUInt32LE(1, 20);
header.writeBigUInt64LE(BigInt(fields.phoff ?? 0), 32);
header.writeBigUInt64LE(BigInt(fields.shoff ?? 0), 40);
header.writeUInt16LE(64, 52);
header.writeUInt16LE(fields.phentsize ?? 56, 54);
header.writeUInt16LE(fields.phnum ?? 0, 56);
header.writeUInt16LE(fields.shentsize ?? 64, 58);
header.writeUInt16LE(fields.shnum ?? 0, 60);
return tail === undefined ? header : Buffer.concat([header, tail]);
};
it("reads GNU build-id and ABI-tag notes from ELF32 and ELF64", () => {
for (const bits of [32, 64] as const) {
const bytes = elfNoteFixture({ bits, ownership: "program" });
const report = parse(bytes);
expect(report.format).toBe(bits === 64 ? "elf64" : "elf32");
expect(report.byte_order).toBe("little");
expect(report.tables).toHaveLength(1);
expect(report.tables[0]?.kind).toBe("program");
const buildId = report.entries.find(
(entry) => entry.type_name === "gnu_build_id",
);
const abi = report.entries.find(
(entry) => entry.type_name === "gnu_abi_tag",
);
expect(buildId?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
expect(abi?.gnu?.abi_tag).toMatchObject({
os: 0,
os_name: "linux",
major: 3,
minor: 2,
subminor: 0,
});
}
});
it("reads the same notes from an SHT_NOTE section including its name", () => {
const report = parse(elfNoteFixture({ ownership: "section" }));
expect(report.tables).toEqual([
expect.objectContaining({
kind: "section",
name: ".note.gnu.build-id",
}),
]);
expect(
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
).toBe(true);
});
it.each([
[32, true],
[64, false],
] as const)(
"decompresses a zlib section-name table for ELF%i %s-endian notes",
(bits, little) => {
const report = parse(compressedElfNoteFixture(bits, little));
expect(report.tables).toEqual([
expect.objectContaining({ kind: "section", name: ".note.gnu.build-id" }),
]);
expect(report.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
},
);
it("admits a decoded section name larger than its compressed source", () => {
const name = "n".repeat(512);
const bytes = compressedElfNoteFixture(64, true, 1, name);
expect(bytes.length).toBeLessThan(name.length);
const report = parse(bytes);
expect(report.tables[0]?.name).toBe(name);
});
it("keeps note facts but marks section names unavailable for unknown compression", () => {
const report = parse(compressedElfNoteFixture(64, true, 2));
expect(report.tables[0]?.name).toBeNull();
expect(report.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
expect(report.limitations).toContain(
"ELF section names are unavailable because compression type 2 is unsupported.",
);
});
it("tags valid extended section numbering as unsupported", () => {
let failure: unknown;
try {
parse(elfExtendedSectionCountFixture());
} catch (cause) {
failure = cause;
}
expect(failure).toBeInstanceOf(BinaryFormatFailure);
expect(failure).toMatchObject({
kind: "unsupported",
message: "ELF extended numbering is unsupported.",
});
});
it("admits compressed section names when max_file_bytes includes expansion", () => {
const bytes = compressedElfNoteFixture();
const sectionOffset = Number(bytes.readBigUInt64LE(40));
const nameHeader = sectionOffset + bytes.readUInt16LE(62) * 64;
const namesOffset = Number(bytes.readBigUInt64LE(nameHeader + 24));
const decodedNamesBytes = Number(bytes.readBigUInt64LE(namesOffset + 8));
const minimumMaxFileBytes = bytes.length + decodedNamesBytes;
const input = inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: bytes.length + 1,
});
let failure: unknown;
try {
parse(bytes, input);
} catch (cause) {
failure = cause;
}
expect(failure).toBeInstanceOf(ElfNoteSectionNameBudgetFailure);
expect(failure).toMatchObject({
minimumMaxFileBytes: BigInt(minimumMaxFileBytes),
selectedMaxFileBytes: bytes.length + 1,
});
const admitted = parse(
bytes,
inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: minimumMaxFileBytes,
}),
);
expect(admitted.tables[0]?.name).toBe(".note.gnu.build-id");
});
it("reports independent PT_NOTE and SHT_NOTE declarations of the same bytes", () => {
const report = parse(elfNoteFixture({ ownership: "both" }));
expect(report.tables.map((table) => table.kind).sort()).toEqual([
"program",
"section",
]);
expect(
report.entries.filter((entry) => entry.type_name === "gnu_build_id"),
).toHaveLength(2);
});
it("reports missing note tables without claiming notes never existed", () => {
const report = parse(elfNoteFixture({ ownership: "none" }));
expect(report.tables).toEqual([]);
expect(report.entries).toEqual([]);
expect(report.limitations[0]).toMatch(/No PT_NOTE/u);
});
it("fails a truncated note payload without partial success", () => {
const notes = gnuBuildIdNote();
notes.writeUInt32LE(64, 0);
const bytes = elfNoteFixture({ ownership: "program", notes });
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
});
it("keeps non-GNU owners as unknown types with descriptor hashes", () => {
const payload = Buffer.from("hello");
const notes = elfNote("CORE", 1, payload);
const report = parse(elfNoteFixture({ ownership: "program", notes }));
expect(report.entries[0]).toMatchObject({
owner: "CORE",
type_name: "unknown",
gnu: null,
});
expect(report.entries[0]?.descriptor.sha256).toBe(
createHash("sha256").update(payload).digest("hex"),
);
});
it("reads a big-endian ELF32 GNU build-id", () => {
const notes = gnuBuildIdNote(undefined, false);
const report = parse(
elfNoteFixture({
bits: 32,
little: false,
machine: 20,
ownership: "program",
notes,
}),
);
expect(report.byte_order).toBe("big");
expect(report.machine).toBe(20);
expect(report.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
});
it("reads an 8-aligned GNU property note from a PT_NOTE program header", () => {
const notes = gnuPropertyNote();
expect(notes.length).toBe(48);
const report = parse(
elfNoteFixture({ ownership: "program", notes, alignment: 8 }),
);
expect(report.tables).toEqual([
expect.objectContaining({ kind: "program", alignment: 8 }),
]);
expect(report.entries).toHaveLength(1);
expect(report.entries[0]).toMatchObject({
owner: "GNU",
type: 5,
type_name: "gnu_property",
namesz: 4,
descsz: 32,
});
expect(report.entries[0]?.descriptor.location.offset).toBe(
(report.entries[0]?.location.offset ?? 0) + 16,
);
});
it("reads an 8-aligned GNU property note from an SHT_NOTE section", () => {
const report = parse(
elfNoteFixture({
ownership: "section",
notes: gnuPropertyNote(),
alignment: 8,
sectionName: ".note.gnu.property",
}),
);
expect(report.tables).toEqual([
expect.objectContaining({
kind: "section",
name: ".note.gnu.property",
alignment: 8,
}),
]);
expect(report.entries[0]?.type_name).toBe("gnu_property");
expect(report.entries[0]?.descriptor.bytes).toBe(32);
});
it.each([0, 1] as const)(
"treats declared note alignment %i as 4-byte padding",
(alignment) => {
const report = parse(elfNoteFixture({ ownership: "program", alignment }));
expect(report.tables[0]?.alignment).toBe(4);
expect(
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
).toBe(true);
expect(
report.entries.some((entry) => entry.type_name === "gnu_abi_tag"),
).toBe(true);
},
);
const hostElf = [
"/bin/ls",
"/usr/bin/ls",
"/usr/bin/env",
].reduce<Buffer | null>((found, path) => {
if (found) return found;
try {
const bytes = readFileSync(path);
if (
bytes.length > 64 &&
bytes[0] === 0x7f &&
bytes[1] === 0x45 &&
bytes[2] === 0x4c &&
bytes[3] === 0x46
)
return bytes;
} catch {
return found;
}
return found;
}, null);
it.skipIf(hostElf === null)(
"parses a host ELF without requiring it to contain notes",
() => {
const report = parse(hostElf!);
expect(report.coverage.status).toBe("complete");
const buildId = report.entries.find(
(entry) => entry.type_name === "gnu_build_id",
);
const abi = report.entries.find(
(entry) => entry.type_name === "gnu_abi_tag",
);
if (buildId !== undefined)
expect(buildId.gnu?.build_id_hex).toMatch(/^[0-9a-f]+$/u);
if (abi !== undefined)
expect(abi.gnu?.abi_tag).toMatchObject({
os: expect.any(Number),
major: expect.any(Number),
minor: expect.any(Number),
subminor: expect.any(Number),
});
},
);
const unterminatedNoteSections = (
sections: number,
nameBytes: number,
): Buffer => {
const shnum = sections + 2;
const tableOffset = 64 + shnum * 64;
const shdr = (type: number, offset = 0, size = 0): Buffer => {
const header = Buffer.alloc(64);
header.writeUInt32LE(type, 4);
header.writeBigUInt64LE(BigInt(offset), 24);
header.writeBigUInt64LE(BigInt(size), 32);
return header;
};
const image = Buffer.concat([
elf64({ shoff: 64, shnum }),
shdr(0),
shdr(3, tableOffset, nameBytes),
...Array.from({ length: sections }, () => shdr(7, tableOffset)),
Buffer.alloc(nameBytes, 0x41),
]);
image.writeUInt16LE(1, 62);
return image;
};
it("bounds repeated scans of an unterminated section name", () => {
expect(parse(unterminatedNoteSections(1, 64)).tables[0]?.name).toBeNull();
let failure: unknown;
try {
parse(unterminatedNoteSections(3, 400));
} catch (cause) {
failure = cause;
}
expect(failure).toBeInstanceOf(ArtifactReaderFailure);
expect(failure).toMatchObject({ reason: "limit", boundary: "memory" });
expect(failure).toHaveProperty(
"message",
expect.stringMatching(/byte budget/u),
);
});
it("rejects note text that repeats beyond the shared byte budget", () => {
const notes = elfNote("A".repeat(40), 1, Buffer.alloc(0));
const once = elfNoteFixture({ ownership: "program", notes });
const twice = elfNoteFixture({ ownership: "both", notes });
const input = inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: 200,
});
expect(parse(once, input).entries).toHaveLength(1);
expect(() => parse(twice, input)).toThrow(ArtifactReaderFailure);
expect(() => parse(twice, input)).toThrow(/byte budget/u);
});
it("rejects program and section header tables that do not fit", () => {
expect(() =>
parse(elf64({ phoff: 64, phnum: 1 }, Buffer.from([1, 0, 0, 0]))),
).toThrow(/leaves the artifact/u);
expect(() =>
parse(
elf64({ shoff: 64, shnum: 1, shentsize: 64 }, Buffer.from([0, 0, 0, 0])),
),
).toThrow(/leaves the artifact/u);
});
it("rejects an extended section count stored in section zero", () => {
expect(() => parse(elf64({ shoff: 64, shnum: 0 }))).toThrow(
/extended numbering/u,
);
});
const repeatedNotes = (count: number, notes: Buffer): Buffer => {
const headers = Buffer.alloc(56 * count);
const offset = 64 + headers.length;
for (let index = 0; index < count; index += 1) {
const at = index * 56;
headers.writeUInt32LE(4, at);
headers.writeBigUInt64LE(BigInt(offset), at + 8);
headers.writeBigUInt64LE(BigInt(notes.length), at + 32);
headers.writeBigUInt64LE(4n, at + 48);
}
return Buffer.concat([elf64({ phoff: 64, phnum: count }), headers, notes]);
};
it("reuses one descriptor hash and GNU NUL scan across note headers", () => {
const payload = Buffer.alloc(400);
const hashed = parse(repeatedNotes(6, elfNote("X", 1, payload)));
expect(hashed.entries).toHaveLength(6);
expect(
new Set(hashed.entries.map((entry) => entry.descriptor.sha256)).size,
).toBe(1);
const gold = parse(repeatedNotes(6, elfNote("GNU", 4, payload)));
expect(gold.entries).toHaveLength(6);
expect(gold.entries.every((entry) => entry.gnu?.gold_version === "")).toBe(
true,
);
const distinct = Buffer.concat([
elfNote("X", 1, payload),
elfNote("X", 1, Buffer.alloc(400, 1)),
]);
expect(() =>
parse(
elfNoteFixture({ ownership: "program", notes: distinct }),
inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: payload.length + 160,
}),
),
).toThrow(/byte budget/u);
});
it("decodes a GNU gold version by scanning trailing NULs", () => {
const trailing = elfNote("GNU", 4, Buffer.from("gold\0\0x\0", "latin1"));
expect(
parse(elfNoteFixture({ ownership: "program", notes: trailing })).entries[0]
?.gnu?.gold_version,
).toBe("gold\0\0x");
const descriptor = Buffer.concat([Buffer.alloc(131_072), Buffer.from("x")]);
const started = Date.now();
const report = parse(
elfNoteFixture({
ownership: "program",
notes: elfNote("GNU", 4, descriptor),
}),
);
expect(Date.now() - started).toBeLessThan(1_000);
expect(report.entries[0]?.gnu?.gold_version).toBe(`${"\0".repeat(131_072)}x`);
});
it("rejects a declared note table that extends past the file", () => {
const truncated = elfNoteTablePastEnd();
expect(truncated.length).toBe(137);
expect(() => parse(truncated)).toThrow(ArtifactReaderFailure);
expect(() => parse(truncated)).toThrow(/leaves the artifact/u);
const present = Buffer.concat([truncated, Buffer.alloc(3)]);
expect(parse(present).entries).toHaveLength(1);
expect(parse(present).tables[0]?.location).toEqual({
offset: 120,
bytes: 20,
});
});
it("fails when the entry budget is exhausted", () => {
const notes = Buffer.concat([
gnuBuildIdNote(),
gnuAbiTagNote(),
elfNote("GNU", 4, Buffer.from("gold")),
]);
const bytes = elfNoteFixture({ ownership: "program", notes });
expect(() =>
parseElfNotes(
bytes,
{
path: defaults.path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
inspectElfNotesInputSchema.parse({ path: defaults.path, max_entries: 1 }),
),
).toThrow(ArtifactReaderFailure);
});