fix(evm): reject repeated container keys in Solidity metadata (#6230)

* fix(evm): reject repeated container keys in Solidity metadata

* test(evm): check the repeat-key diagnostic for deeply nested keys
This commit is contained in:
Wade authored and GitHub committed 2026-10-23 23:42:45 +09:00
1 parent b5dba08dba
commit fd782cf2b1
2 files changed
+36 -12

No files matched your search

+18
View File
@@ -119,6 +119,9 @@ it("decodes distinct byte keys and distinct nested-container keys", () => {
expect(inspectSolidityMetadata(bytecode("a2810101810202"))).toMatchObject({
status: "partial",
});
expect(inspectSolidityMetadata(bytecode("a281800181a002"))).toMatchObject({
status: "partial",
});
});
it.each([
@@ -128,6 +131,9 @@ it.each([
["floating point", "a16178fa3f800000"],
["duplicate key", "a2617801617802"],
["duplicate byte key", "a2410101410102"],
["duplicate array key", "a280018002"],
["duplicate map key", "a2a001a002"],
["duplicate map key in another order", "a2a20102030401a20304010202"],
["excessive nesting", "a16178" + "81".repeat(65) + "00"],
["impossible collection", "a161788a00"],
])("keeps exact ranges and unavailable interpretation for %s", (_name, hex) => {
@@ -139,6 +145,18 @@ it.each([
});
});
it("rejects repeated deeply nested container keys promptly", () => {
const key = "81".repeat(62) + "00";
const started = performance.now();
expect(
inspectSolidityMetadata(bytecode("a2" + key + "01" + key + "02")),
).toMatchObject({
status: "not-decoded",
diagnostic: expect.stringContaining("found repeat map key"),
});
expect(performance.now() - started).toBeLessThan(1000);
});
it.each(["", "60006000f3", "000000", "00010001"])(
"does not claim absent Solidity metadata for an unavailable trailer: %s",
(hex) =>
+18 -12
View File
@@ -90,6 +90,17 @@ const scalar = (value: unknown): SolidityMetadataScalar => {
);
};
/** Value identity for duplicate-key checks; map entry order is not significant. */
const keyIdentity = (value: unknown): string => {
if (value instanceof Map)
return `{${[...value]
.map(([key, item]) => `${keyIdentity(key)}:${keyIdentity(item)}`)
.sort()
.join(",")}}`;
if (Array.isArray(value)) return `[${value.map(keyIdentity).join(",")}]`;
return JSON.stringify(scalar(value));
};
/** Parse only a plausible terminal root-map candidate; never establish absence. */
export const inspectSolidityMetadata = (
bytes: Uint8Array,
@@ -122,20 +133,15 @@ export const inspectSolidityMetadata = (
});
if (!(raw instanceof Map))
throw new Error("Terminal CBOR candidate did not decode to a root map.");
// cborg compares Map keys by identity, so repeated byte-string keys (each
// its own Uint8Array) pass its duplicate check; compare scalars by value.
// cborg compares Map keys by identity, so repeated byte-string or container
// keys (each its own object) pass its duplicate check; compare by value.
const seen = new Set<string>();
for (const [key, value] of raw as Map<unknown, unknown>) {
const entry = { key: scalar(key), value: scalar(value) };
if (entry.key.kind !== "uninterpreted") {
const identity = JSON.stringify(entry.key);
if (seen.has(identity))
throw new Error(
`CBOR decode error: found repeat map key ${identity}`,
);
seen.add(identity);
}
result.entries.push(entry);
const identity = keyIdentity(key);
if (seen.has(identity))
throw new Error(`CBOR decode error: found repeat map key ${identity}`);
seen.add(identity);
result.entries.push({ key: scalar(key), value: scalar(value) });
}
result.status = result.entries.some(
({ key, value }) =>