feat(elf): decode Zstandard section-name tables (#6987)

* feat(elf): decode Zstandard section-name tables

* test(verifiers): follow shared command output and identity
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-10-25 02:16:33 +08:00
1 parent 85bc8c8977
commit fcebd79ed8
10 files changed
+180 -91

No files matched your search

+4 -2
View File
@@ -24,8 +24,8 @@ requires an absolute filesystem path and preserves that exact selection:
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`tables` lists each declared note program header or section, including the
section name when a string table is present. Zlib-compressed section-name
tables are decompressed within the selected file-size budget. An unsupported
section name when a string table is present. Zlib- and Zstandard-compressed
section-name tables are decompressed within the selected file-size budget. An unsupported
compression type leaves section names unknown and adds a limitation; note bytes
remain independently examined. Each `entries[]` item reports owner, type,
descriptor SHA-256, and a hex preview for descriptors of 64 bytes or less.
@@ -63,6 +63,8 @@ numbering (`PN_XNUM`, including a section count stored when `e_shnum` is 0) is
unsupported and returns an unsupported-target outcome rather than invalid-input
recovery.
Zstandard section-name tables also use a 128 MiB decoder window limit.
`SHF_COMPRESSED` note sections return `unsupported_target` before their payload
is interpreted or decompressed. Their compression header is storage metadata,
not a note header. The current result requires physical file offsets for each
+2 -1
View File
@@ -83,7 +83,8 @@ empty dependency set. Retained views describe the captured artifact digest;
they do not assert that the current file or running application is unchanged.
ELF section names are resolved through the section-header string table.
Zlib-compressed name tables are decoded within the native structural budget.
Zlib- and Zstandard-compressed name tables are decoded within the native structural
budget. Zstandard decoding also uses a 128 MiB window limit.
If a name table uses unsupported compression, the result reports that
limitation and leaves `.dep-v0` presence unknown rather than reporting the
section absent.
+106 -44
View File
@@ -4,6 +4,7 @@ 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 { decodeElfSectionNameTable } from "../metadata/ElfSectionNameTable.js";
import {
compressedElfNoteFixture,
compressedElfNoteSectionFixture,
@@ -149,12 +150,20 @@ it("preserves unknown ELF64 section flag bits without treating them as file size
});
it.each([
[32, true],
[64, false],
[32, true, 1],
[32, false, 1],
[64, true, 1],
[64, false, 1],
[32, true, 2],
[32, false, 2],
[64, true, 2],
[64, false, 2],
] as const)(
"decompresses a zlib section-name table for ELF%i %s-endian notes",
(bits, little) => {
const report = parse(compressedElfNoteFixture(bits, little));
"decompresses an ELF%i little-endian=%s section-name table using algorithm %i",
(bits, little, compressionType) => {
const report = parse(
compressedElfNoteFixture(bits, little, compressionType),
);
expect(report.tables).toEqual([
expect.objectContaining({ kind: "section", name: ".note.gnu.build-id" }),
]);
@@ -164,22 +173,72 @@ it.each([
},
);
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.each([1, 2])(
"admits a decoded section name larger than algorithm %i storage",
(compressionType) => {
const name = "n".repeat(512);
const bytes = compressedElfNoteFixture(64, true, compressionType, name);
expect(bytes.length).toBeLessThan(name.length);
const report = parse(bytes);
expect(report.tables[0]?.name).toBe(name);
},
);
it.each([1, 2])(
"rejects malformed algorithm %i section-name storage",
(compressionType) => {
for (const damage of ["corrupt", "truncated", "too-large", "too-small"]) {
const bytes = compressedElfNoteFixture(64, true, compressionType);
const sectionOffset = Number(bytes.readBigUInt64LE(40));
const nameHeader = sectionOffset + bytes.readUInt16LE(62) * 64;
const namesOffset = Number(bytes.readBigUInt64LE(nameHeader + 24));
if (damage === "corrupt") bytes[namesOffset + 24] = 0;
else if (damage === "truncated")
bytes.writeBigUInt64LE(
bytes.readBigUInt64LE(nameHeader + 32) - 1n,
nameHeader + 32,
);
else
bytes.writeBigUInt64LE(
bytes.readBigUInt64LE(namesOffset + 8) +
(damage === "too-large" ? 1n : -1n),
namesOffset + 8,
);
expect(() => parse(bytes), damage).toThrow(
/compressed section-name table/u,
);
}
},
);
it("refuses a Zstandard frame requiring more than the decoder window allowance", () => {
const header = Buffer.alloc(24);
header.writeUInt32LE(2, 0);
header.writeBigUInt64LE(1n, 16);
// An empty last raw block with Window_Descriptor 0x90: a 2^28-byte window.
const frame = Buffer.from("28b52ffd0090010000", "hex");
expect(
decodeElfSectionNameTable(
Buffer.concat([header, frame]),
0x800n,
64,
true,
0,
),
).toMatchObject({
state: "malformed",
message: expect.stringMatching(/memory/iu),
});
});
it("keeps note facts but marks section names unavailable for unknown compression", () => {
const report = parse(compressedElfNoteFixture(64, true, 2));
const report = parse(compressedElfNoteFixture(64, true, 3));
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.",
"ELF section names are unavailable because compression type 3 is unsupported.",
);
});
@@ -197,38 +256,41 @@ it("tags valid extended section numbering as 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({
it.each([1, 2])(
"admits algorithm %i section names when max_file_bytes includes expansion",
(compressionType) => {
const bytes = compressedElfNoteFixture(64, true, compressionType);
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: minimumMaxFileBytes,
}),
);
expect(admitted.tables[0]?.name).toBe(".note.gnu.build-id");
});
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" }));
+4 -3
View File
@@ -1,4 +1,4 @@
import { deflateSync } from "node:zlib";
import { deflateSync, zstdCompressSync } from "node:zlib";
const concat = (...parts: Buffer[]): Buffer => Buffer.concat(parts);
@@ -293,7 +293,7 @@ export const elfExtendedSectionCountFixture = (): Buffer => {
return bytes;
};
/** ELF note fixture with a zlib-compressed section-name string table. */
/** ELF note fixture with a compressed section-name string table. */
export const compressedElfNoteFixture = (
bits: 32 | 64 = 64,
little = true,
@@ -326,7 +326,8 @@ export const compressedElfNoteFixture = (
const namesOffset = word(namesHeader + (wide ? 24 : 16));
const namesSize = word(namesHeader + (wide ? 32 : 20));
const names = image.subarray(namesOffset, namesOffset + namesSize);
const compressed = deflateSync(names);
const compressed =
compressionType === 2 ? zstdCompressSync(names) : deflateSync(names);
const compressionHeaderBytes = wide ? 24 : 12;
const replacement = Buffer.alloc(compressionHeaderBytes);
if (little) replacement.writeUInt32LE(compressionType, 0);
+17 -6
View File
@@ -1,7 +1,8 @@
import { inflateSync } from "node:zlib";
import { constants, inflateSync, zstdDecompressSync } from "node:zlib";
const SHF_COMPRESSED = 0x800n;
const ELFCOMPRESS_ZLIB = 1;
const ELFCOMPRESS_ZSTD = 2;
export type ElfSectionNameTableResult =
| {
@@ -37,7 +38,10 @@ export const decodeElfSectionNameTable = (
const compressionType = little
? bytes.readUInt32LE(0)
: bytes.readUInt32BE(0);
if (compressionType !== ELFCOMPRESS_ZLIB)
if (
compressionType !== ELFCOMPRESS_ZLIB &&
compressionType !== ELFCOMPRESS_ZSTD
)
return { state: "unsupported", compressionType };
const declaredSize =
@@ -55,13 +59,20 @@ export const decodeElfSectionNameTable = (
const expected = Number(declaredSize);
let decoded: Buffer;
try {
decoded = inflateSync(bytes.subarray(headerBytes), {
maxOutputLength: Math.max(1, expected),
});
const payload = bytes.subarray(headerBytes);
const maxOutputLength = Math.max(1, expected);
decoded =
compressionType === ELFCOMPRESS_ZSTD
? zstdDecompressSync(payload, {
maxOutputLength,
// Retain Zstandard's default maximum decoder window of 128 MiB.
params: { [constants.ZSTD_d_windowLogMax]: 27 },
})
: inflateSync(payload, { maxOutputLength });
} catch (cause: unknown) {
return {
state: "malformed",
message: `ELF compressed section-name table could not be decompressed: ${cause instanceof Error ? cause.message : "invalid zlib data"}`,
message: `ELF compressed section-name table could not be decompressed: ${cause instanceof Error ? cause.message : "invalid compressed data"}`,
};
}
if (decoded.length !== expected)
+20 -16
View File
@@ -31,24 +31,28 @@ cliTest("reports a GNU build-id through the public CLI", async ({ cli }) => {
});
cliTest(
"resolves section note names from a compressed string table through the CLI",
"resolves section note names from zlib and Zstandard string tables through the CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-notes-compressed-cli-");
const path = join(root, "program");
await writeFile(path, compressedElfNoteFixture());
const result = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.tables[0]?.name).toBe(
".note.gnu.build-id",
);
expect(evidence.normalized_result.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
for (const compressionType of [1, 2]) {
const path = join(root, `program-${String(compressionType)}`);
const bytes = compressedElfNoteFixture(64, true, compressionType);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.tables[0]?.name).toBe(
".note.gnu.build-id",
);
expect(evidence.normalized_result.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
expect(await readFile(path)).toEqual(bytes);
}
},
);
+3 -2
View File
@@ -267,7 +267,8 @@ cliTest(
);
for (const fixture of [
{ name: "zlib", type: 1, supported: true },
{ name: "unknown", type: 2, supported: false },
{ name: "zstd", type: 2, supported: true },
{ name: "unknown", type: 3, supported: false },
]) {
const path = join(root, `${fixture.name}.elf`);
await writeFile(
@@ -297,7 +298,7 @@ cliTest(
} else {
expect(report.auditable_metadata).toBeNull();
expect(report.limitations.join("\n")).toContain(
"ELF section names are unavailable because compression type 2 is unsupported",
"ELF section names are unavailable because compression type 3 is unsupported",
);
expect(report.limitations.join("\n")).toContain(
"The presence of .dep-v0 could not be determined",
+2 -2
View File
@@ -104,7 +104,7 @@ it("reports unreadable ELF names alongside the no-record limitation", async () =
const nameTableHeader = 256 + 64;
bytes.writeBigUInt64LE(0x800n, nameTableHeader + 8);
bytes.writeBigUInt64LE(24n, nameTableHeader + 32);
bytes.writeUInt32LE(2, 768);
bytes.writeUInt32LE(3, 768);
const root = await createTestTempDirectory("rea-go-unknown-section-names-");
const path = join(root, "unknown-names.elf");
await writeFile(path, bytes);
@@ -116,7 +116,7 @@ it("reports unreadable ELF names alongside the no-record limitation", async () =
"No recognized Go build-info record was found in the inspected image; absent metadata does not establish that the binary is not Go.",
);
expect(result.value.limitations).toContain(
"ELF section names are unavailable because compression type 2 is unsupported",
"ELF section names are unavailable because compression type 3 is unsupported",
);
expect(result.value).not.toHaveProperty("section_names_limitation");
});
+19 -13
View File
@@ -221,25 +221,31 @@ it("returns null metadata when the dependency section is absent", () => {
]);
});
it("reads dependency metadata through a compressed ELF section-name table", () => {
const image = readRustBinaryImage(
compressRustElfSectionNames(createRustImage({ format: "elf" })),
);
expect(image.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
file_offset: 1024,
packages,
dependency_edges: [{ from_index: 1, to_index: 0 }],
});
});
it.each([1, 2])(
"reads dependency metadata through ELF section-name compression %i",
(compressionType) => {
const image = readRustBinaryImage(
compressRustElfSectionNames(
createRustImage({ format: "elf" }),
compressionType,
),
);
expect(image.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
file_offset: 1024,
packages,
dependency_edges: [{ from_index: 1, to_index: 0 }],
});
},
);
it("keeps dependency presence unknown for unsupported ELF name compression", () => {
const image = readRustBinaryImage(
compressRustElfSectionNames(createRustImage({ format: "elf" }), 2),
compressRustElfSectionNames(createRustImage({ format: "elf" }), 3),
);
expect(image.auditable_metadata).toBeNull();
expect(image.section_names_limitation).toMatch(
/compression type 2 is unsupported/u,
/compression type 3 is unsupported/u,
);
expect(image.rustc_commit?.hashes).toEqual([
{ hash: RUSTC_HASH, file_offset: 2048 },
+3 -2
View File
@@ -1,4 +1,4 @@
import { deflateSync } from "node:zlib";
import { deflateSync, zstdCompressSync } from "node:zlib";
/** zlib payload matching the embedded dependency JSON shape. */
export const rustDependencyPayload = (
@@ -31,7 +31,8 @@ export const compressRustElfSectionNames = (
const names = Buffer.from(
bytes.subarray(namesOffset, namesOffset + namesSize),
);
const compressed = deflateSync(names);
const compressed =
compressionType === 2 ? zstdCompressSync(names) : deflateSync(names);
const compressionHeader = Buffer.alloc(24);
compressionHeader.writeUInt32LE(compressionType, 0);
compressionHeader.writeBigUInt64LE(BigInt(names.length), 8);