fix(elf): share section-table presence validation with notes (#7706)
This commit is contained in:
1 parent
d3b31a0e29
commit
c01ed783ac
5 files changed
+136
-5
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user