fix(native): reject nonzero reserved ELF dynamic symbols (#7528)

fix(native): validate the reserved ELF dynamic symbol

Signed-off-by: Rudy Celekli <[email protected]>
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-11-05 17:32:20 +08:00
1 parent 21ad5b1f67
commit 7504f2ddfc
3 files changed
+106 -1

No files matched your search

@@ -821,6 +821,37 @@ describe("export inventory review regressions", () => {
});
});
it.each([32, 64] as const)(
"rejects nonzero reserved fields in the ELF%s null symbol",
(bits) => {
for (const little of [true, false]) {
// ELF32/64 symbol records have no padding: every field in entry zero
// is reserved as zero by the ABI, independently of host endianness.
const width = bits === 64 ? 24 : 16;
for (let byte = 0; byte < width; byte++) {
const bytes = elfExportImage({
bits,
little,
symbols: [{ name: "kept_export" }],
});
bytes[0x100 + byte] = 1;
expect(() => parseNativeExports(bytes, artifact)).toThrow(
BinaryFormatFailure,
);
}
expect(
parse(
elfExportImage({
bits,
little,
symbols: [{ name: "kept_export" }],
}),
).exports.map(({ name }) => name),
).toEqual(["kept_export"]);
}
},
);
it("rejects a dynamic symbol table without its null entry", () => {
expect(() =>
parseNativeExports(
+7
View File
@@ -624,6 +624,13 @@ function* parseElfExports(
width,
"ELF dynamic symbols",
);
// Entry zero is the reserved STN_UNDEF record. Skipping it is valid only
// after every field is observed as zero; neither ELF layout has padding.
if (bytes.subarray(table, table + width).some((byte) => byte !== 0))
throw new BinaryFormatFailure(
"malformed",
"ELF dynamic symbol table null entry has nonzero fields",
);
const nameRange = reader.fileRange(
strings.offset,
strings.size,
@@ -1,9 +1,10 @@
import { writeFile } from "node:fs/promises";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { nativeExportsSchema } from "../../../src/domain/native/nativeExports.js";
import { elfExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
@@ -67,3 +68,69 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
}
},
);
cliTest.for(["CLI", "stdio MCP"] as const)(
"rejects a non-null reserved ELF symbol through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-elf-null-symbol-");
const mcp =
surface === "stdio MCP" ? await connectLocalToolsMcp() : undefined;
for (const encoding of [
{ bits: 64, little: true },
{ bits: 32, little: false },
] as const) {
const path = join(root, `exports-${encoding.bits}.so`);
const bytes = elfExportImage({
...encoding,
symbols: [{ name: "kept_export" }],
});
// The authored fixture's dynamic symbol table starts at file offset 0x100.
bytes[0x100] = 1;
await writeFile(path, bytes);
let error: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["inspect-native-exports", path, "--json"],
});
expect(response.exitCode).toBe(1);
error = response.json;
} else {
const response = await mcp!.call("inspect_native_exports", { path });
expect(response.isError).toBe(true);
error = parseMcpToolError(response).error;
}
expect(error).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["path"],
reason: "invalid_format",
message: expect.stringContaining("null entry"),
},
],
},
});
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
bytes[0x100] = 0;
await writeFile(path, bytes);
const document =
surface === "CLI"
? (
await cli.run({
arguments: ["inspect-native-exports", path, "--json"],
})
).json
: (await mcp!.call("inspect_native_exports", { path }))
.structuredContent;
const evidence = toolContract(
"inspect_native_exports",
).outputSchema.parse(document);
expect(
nativeExportsSchema
.parse(evidence.normalized_result)
.exports.map(({ name }) => name),
).toEqual(["kept_export"]);
}
},
);