fix(evm): reject nested duplicate CBOR map keys

Preserve boundary facts and uncertainty through their consumers. Add focused production-boundary regressions and update affected documentation.

Fixes #6780
This commit is contained in:
morluto
2026-10-23 23:49:26 +08:00
parent 7fd722e15b
commit 52b0a54afd
4 changed files with 60 additions and 7 deletions
+2 -1
View File
@@ -45,7 +45,8 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md)
map from the selected bytes. Integers use decimal strings; byte strings use hex,
and scalar keys retain their types. The recorded offset/length addresses the
original bytecode, so summary delivery does not copy an opaque CBOR payload.
Nested containers have partial scalar coverage; unsupported or malformed
Nested containers have partial scalar coverage; map keys are still checked for
duplicates throughout nested maps and arrays. Unsupported or malformed
candidates report `not-decoded` with a diagnostic. Decoding is bounded to 64
container levels and the terminal payload's encoded size.
- An unavailable trailer does not prove metadata absence or Solidity origin.
+11
View File
@@ -122,6 +122,12 @@ it("decodes distinct byte keys and distinct nested-container keys", () => {
expect(inspectSolidityMetadata(bytecode("a281800181a002"))).toMatchObject({
status: "partial",
});
expect(
inspectSolidityMetadata(bytecode("a264736f6c634300081e6178a2410101410202")),
).toMatchObject({
status: "partial",
compiler_version: { representation: "release-bytes", value: "0.8.30" },
});
});
it.each([
@@ -131,6 +137,11 @@ it.each([
["floating point", "a16178fa3f800000"],
["duplicate key", "a2617801617802"],
["duplicate byte key", "a2410101410102"],
[
"duplicate byte key in nested map",
"a264736f6c634300081e6178a2410101410102",
],
["duplicate byte key in map inside array", "a1617881a2410101410102"],
["duplicate array key", "a280018002"],
["duplicate map key", "a2a001a002"],
["duplicate map key in another order", "a2a20102030401a20304010202"],
+20 -6
View File
@@ -101,6 +101,23 @@ const keyIdentity = (value: unknown): string => {
return JSON.stringify(scalar(value));
};
/** Check every map boundary, even when nested values remain uninterpreted. */
const validateMapKeys = (value: unknown): void => {
if (value instanceof Map) {
const seen = new Set<string>();
for (const [key, item] of value) {
const identity = keyIdentity(key);
if (seen.has(identity))
throw new Error(`CBOR decode error: found repeat map key ${identity}`);
seen.add(identity);
validateMapKeys(key);
validateMapKeys(item);
}
} else if (Array.isArray(value)) {
for (const item of value) validateMapKeys(item);
}
};
/** Parse only a plausible terminal root-map candidate; never establish absence. */
export const inspectSolidityMetadata = (
bytes: Uint8Array,
@@ -133,14 +150,11 @@ export const inspectSolidityMetadata = (
});
if (!(raw instanceof Map))
throw new Error("Terminal CBOR candidate did not decode to a root map.");
validateMapKeys(raw);
// 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>();
// keys (each its own object) pass its duplicate check; compare by value at
// every map boundary before projecting any entries.
for (const [key, value] of raw as Map<unknown, unknown>) {
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(
@@ -13,6 +13,21 @@ it.each([
["integer", "00a1011bffffffffffffffff000b", "decoded"],
["BOM text", "00a1617864efbbbf610008", "decoded"],
["nested value", "00a1617881000005", "partial"],
[
"distinct nested byte keys",
"00a264736f6c634300081e6178a24101014102020013",
"partial",
],
[
"duplicate nested byte keys",
"00a264736f6c634300081e6178a24101014101020013",
"not-decoded",
],
[
"duplicate nested text keys",
"00a264736f6c634300081e6178a26161016161020013",
"not-decoded",
],
["unavailable trailer", "00", "unavailable"],
["unsupported tag", "00a16178c0000005", "not-decoded"],
["floating point", "00a16178fa3f8000000008", "not-decoded"],
@@ -71,6 +86,18 @@ it.each([
kind: "uninterpreted",
container: "array",
});
if (kind === "distinct nested byte keys") {
expect(metadata.compiler_version?.value).toBe("0.8.30");
expect(metadata.bytecode_offset).toBe(1);
expect(metadata.cbor_bytes).toBe(19);
}
if (kind.startsWith("duplicate nested")) {
expect(metadata.entries).toEqual([]);
expect(metadata.compiler_version).toBeNull();
expect(metadata.diagnostic).toContain("repeat map key");
expect(metadata.bytecode_offset).toBe(1);
expect(metadata.cbor_bytes).toBe(19);
}
if (kind === "unavailable trailer") {
expect(metadata.bytecode_offset).toBeNull();
expect(metadata.entries).toEqual([]);