fix(elf): share section-table presence validation with notes (#7706)

This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-28 12:08:25 +08:00
1 parent d3b31a0e29
commit c01ed783ac
5 files changed
+136 -5

No files matched your search

+18 -5
View File
@@ -104,6 +104,22 @@ const extendedCounts = (
return ok(counts);
};
/** A name index or extended count cannot refer to an absent section table. */
export const validateElfSectionTablePresence = (tables: {
readonly programs: { readonly count: number };
readonly sections: {
readonly offset: bigint;
readonly count: number;
readonly namesIndex: number;
};
}): Result<null, string> =>
tables.sections.offset === 0n &&
(tables.sections.count !== 0 ||
tables.sections.namesIndex !== 0 ||
tables.programs.count === 0xffff)
? err("ELF section table is missing")
: ok(null);
/** Canonical table declarations, including extended counts in inactive section zero. */
export function* readElfTableDeclarations(
bytes: Buffer,
@@ -122,11 +138,8 @@ export function* readElfTableDeclarations(
let programCount = BigInt(programs.count);
let sectionCount = BigInt(sections.count);
let namesIndex = sections.namesIndex;
if (
sectionOffset === 0n &&
(sectionCount !== 0n || namesIndex !== 0 || programCount === 0xffffn)
)
return err("ELF section table is missing");
const presence = validateElfSectionTablePresence(raw.value);
if (!presence.ok) return presence;
if (
sectionOffset !== 0n &&
(sectionCount === 0n || namesIndex === 0xffff || programCount === 0xffffn)
+22
View File
@@ -695,3 +695,25 @@ it("keeps compressed-note refusal ahead of alignment precision admission", () =>
"Compressed ELF note sections are unsupported",
);
});
it.each([
{ bits: 32, little: true },
{ bits: 32, little: false },
{ bits: 64, little: true },
{ bits: 64, little: false },
] as const)(
"rejects dangling section names with and without notes (ELF$bits $little)",
({ bits, little }) => {
for (const ownership of ["program", "none"] as const) {
const bytes = elfNoteFixture({ bits, little, ownership });
const before = parse(bytes);
const nameOffset = bits === 64 ? 62 : 50;
if (little) bytes.writeUInt16LE(5, nameOffset);
else bytes.writeUInt16BE(5, nameOffset);
expect(() => parse(bytes)).toThrow("ELF section table is missing");
if (little) bytes.writeUInt16LE(0, nameOffset);
else bytes.writeUInt16BE(0, nameOffset);
expect(parse(bytes)).toEqual(before);
}
},
);
+3
View File
@@ -4,6 +4,7 @@ import {
} from "../../domain/elfStringTable.js";
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
import { validateElfSectionTablePresence } from "../../domain/elfDeclaredRanges.js";
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
import { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
@@ -308,6 +309,8 @@ export const parseElfNotes = (
sections.offset > BigInt(Number.MAX_SAFE_INTEGER)
)
return fail("ELF offset or size exceeds a safe integer.");
const presence = validateElfSectionTablePresence(raw.value);
if (!presence.ok) return fail(presence.error);
const phoff = Number(programs.offset);
const shoff = Number(sections.offset);
const phentsize = programs.width;
+45
View File
@@ -160,3 +160,48 @@ cliTest(
expect((await readFile(path)).equals(before)).toBe(true);
},
);
cliTest(
"rejects dangling section-name references and preserves sectionless notes",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-notes-presence-cli-");
const path = join(root, "program");
for (const ownership of ["program", "none"] as const) {
const bytes = elfNoteFixture({ ownership });
bytes.writeUInt16LE(5, 62);
await writeFile(path, bytes);
const result = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
operation: "inspect_elf_notes",
issues: [
expect.objectContaining({
path: ["path"],
reason: "invalid_format",
message: "ELF section table is missing",
}),
],
},
});
expect((await readFile(path)).equals(bytes)).toBe(true);
bytes.writeUInt16LE(0, 62);
await writeFile(path, bytes);
const accepted = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(accepted.exitCode, accepted.stderr).toBe(0);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
accepted.json,
);
expect(evidence.normalized_result.coverage.status).toBe("complete");
expect(evidence.normalized_result.tables).toHaveLength(
ownership === "none" ? 0 : 1,
);
}
},
);
+48
View File
@@ -163,3 +163,51 @@ mcpTest(
}
},
);
mcpTest(
"rejects dangling section-name references and recovers in the same session",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const root = await createTestTempDirectory("rea-elf-notes-presence-mcp-");
const path = join(root, "program");
const bytes = elfNoteFixture({ ownership: "program" });
bytes.writeUInt16LE(5, 62);
await writeFile(path, bytes);
const rejected = await client.callTool({
name: "inspect_elf_notes",
arguments: { path },
});
expect(rejected.isError).toBe(true);
expect(parseMcpToolError(rejected).error).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
issues: [
expect.objectContaining({
path: ["path"],
reason: "invalid_format",
message: "ELF section table is missing",
}),
],
},
});
expect((await readFile(path)).equals(bytes)).toBe(true);
bytes.writeUInt16LE(0, 62);
await writeFile(path, bytes);
const accepted = await client.callTool({
name: "inspect_elf_notes",
arguments: { path },
});
expect(accepted.isError).not.toBe(true);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
accepted.structuredContent,
);
expect(evidence.normalized_result.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
} finally {
await session.close();
}
},
);