Skip to content

Commit 73f96be

Browse files
committed
fix(clearsign-abi): reject Solidity types that do not exist
encode_static_args accepted uint7, uint0 and int264 and emitted plausible 32-byte words for them, so a fixture could read as a real ABI encoding while encoding a type no compiler can produce. bool coerced truthiness, turning 'false', 0.0 or 2 into ABI true/false. bytes0 was accepted, and bytes33 was worse than invalid: ljust() does not truncate, so a 33-byte value emitted a 33-byte word and shifted every following argument one byte to the right -- silently corrupt calldata. Validate intN/uintN widths as 8..256 in steps of 8, require an actual bool, and restrict fixed bytes to bytes1..bytes32. Arrays now reach the existing dynamic-type error instead of being parsed as a width. All 51 catalog flows still build unchanged.
1 parent 44d82ef commit 73f96be

2 files changed

Lines changed: 100 additions & 5 deletions

File tree

keepkeylib/clearsign_abi.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,46 @@ def _addr_word(address):
4545
return b'\x00' * 12 + address
4646

4747

48+
def _int_bits(digits, typ):
49+
"""Validate and return the bit width of a Solidity intN/uintN type.
50+
51+
Solidity defines uint8..uint256 and int8..int256 in steps of 8, plus the
52+
bare `uint`/`int` aliases for 256. Nothing else exists. Accepting `uint7`,
53+
`uint0` or `int264` here does not produce unusual calldata -- it produces
54+
32-byte words for a type no compiler will ever emit, so the fixture reads
55+
as a real ABI encoding while encoding a fiction. Fail loudly instead.
56+
"""
57+
if digits == '':
58+
return 256
59+
if not digits.isdigit():
60+
raise ValueError(
61+
'unsupported type %r -- expected %s8..%s256 in steps of 8'
62+
% (typ, typ[:-len(digits)], typ[:-len(digits)]))
63+
bits = int(digits)
64+
if bits < 8 or bits > 256 or bits % 8 != 0:
65+
raise ValueError(
66+
'invalid Solidity integer width in %r -- must be 8..256 '
67+
'in steps of 8' % typ)
68+
return bits
69+
70+
4871
def encode_static_args(types, values):
4972
"""ABI-encode STATIC Solidity types into concatenated 32-byte words.
5073
Raises on any dynamic type (string/bytes/arrays) — build those by hand."""
5174
assert len(types) == len(values), (
5275
'arg count mismatch: %d types, %d values' % (len(types), len(values)))
5376
out = bytearray()
5477
for typ, val in zip(types, values):
78+
# Route arrays to the explicit dynamic-type error below rather than
79+
# letting 'uint256[]' reach the width parser as digits '256[]'.
80+
if typ.endswith(']'):
81+
raise ValueError(
82+
'dynamic/unsupported type %r — build this call by hand '
83+
'(see module docstring)' % typ)
5584
if typ == 'address':
5685
out += _addr_word(val)
5786
elif typ.startswith('uint'):
58-
digits = typ[4:]
59-
bits = int(digits) if digits else 256
87+
bits = _int_bits(typ[4:], typ)
6088
n = int(val)
6189
assert 0 <= n < (1 << bits), (
6290
'value %r out of range for %s' % (val, typ))
@@ -68,17 +96,32 @@ def encode_static_args(types, values):
6896
# every negative value and silently accepted values at or above
6997
# 2^(N-1), which the EVM reads back as NEGATIVE -- calldata that
7098
# does not mean what the declared type says.
71-
digits = typ[3:]
72-
bits = int(digits) if digits else 256
99+
bits = _int_bits(typ[3:], typ)
73100
n = int(val)
74101
lo, hi = -(1 << (bits - 1)), (1 << (bits - 1)) - 1
75102
assert lo <= n <= hi, (
76103
'value %r out of range for %s (%d..%d)' % (val, typ, lo, hi))
77104
out += n.to_bytes(32, 'big', signed=True)
78105
elif typ == 'bool':
106+
# Require an actual bool. Coercing truthiness here silently turns
107+
# 'false', 0.0 or 2 into ABI true/false, and a fixture that says
108+
# bool should not be the place a type confusion is laundered.
109+
if not isinstance(val, bool):
110+
raise ValueError(
111+
'bool argument must be a real bool, got %r (%s)'
112+
% (val, type(val).__name__))
79113
out += (1 if val else 0).to_bytes(32, 'big')
80114
elif typ.startswith('bytes') and typ != 'bytes' and not typ.endswith('[]'):
81-
n = int(typ[5:])
115+
digits = typ[5:]
116+
# bytes1..bytes32 only. bytes0 is not a Solidity type, and bytes33
117+
# is worse than invalid: ljust() does not truncate, so a 33-byte
118+
# value emitted a 33-byte "word" and shifted every following
119+
# argument by one byte -- silently corrupt calldata.
120+
if not digits.isdigit() or not 1 <= int(digits) <= 32:
121+
raise ValueError(
122+
'invalid fixed-bytes type %r -- must be bytes1..bytes32'
123+
% typ)
124+
n = int(digits)
82125
b = val if isinstance(val, (bytes, bytearray)) else bytes.fromhex(
83126
val[2:] if val.startswith('0x') else val)
84127
assert len(b) == n, 'bytes%d value has wrong length' % n

tests/test_clearsign_abi.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,57 @@ def test_uint8_keeps_unsigned_bounds(self):
3131
encode_static_args(['uint8'], [value])
3232

3333

34+
class TestClearsignAbiTypeValidation(unittest.TestCase):
35+
"""The encoder must refuse types Solidity does not have.
36+
37+
Emitting a plausible 32-byte word for `uint7` or `int264` makes a fixture
38+
read as a real ABI encoding while encoding a type no compiler can produce.
39+
"""
40+
41+
def test_non_multiple_of_eight_widths_are_rejected(self):
42+
for typ in ('uint7', 'int7', 'uint255', 'int13'):
43+
with self.assertRaises(ValueError):
44+
encode_static_args([typ], [1])
45+
46+
def test_zero_and_oversized_widths_are_rejected(self):
47+
for typ in ('uint0', 'int0', 'uint264', 'int264', 'uint512'):
48+
with self.assertRaises(ValueError):
49+
encode_static_args([typ], [0])
50+
51+
def test_valid_widths_still_encode(self):
52+
for typ in ('uint8', 'uint16', 'uint256', 'uint', 'int8', 'int256', 'int'):
53+
self.assertEqual(len(encode_static_args([typ], [1])), 32)
54+
55+
def test_bool_requires_an_actual_bool(self):
56+
# 1 and 'false' would both have become ABI true.
57+
for val in (1, 0, 'false', 'true', 2, None):
58+
with self.assertRaises(ValueError):
59+
encode_static_args(['bool'], [val])
60+
self.assertEqual(encode_static_args(['bool'], [True]),
61+
b'\x00' * 31 + b'\x01')
62+
self.assertEqual(encode_static_args(['bool'], [False]), b'\x00' * 32)
63+
64+
def test_fixed_bytes_width_is_bounded(self):
65+
for typ in ('bytes0', 'bytes33', 'bytes64'):
66+
with self.assertRaises(ValueError):
67+
encode_static_args([typ], [b'\x11' * 32])
68+
69+
def test_oversized_fixed_bytes_cannot_shift_later_arguments(self):
70+
"""bytes33 used to emit 33 bytes -- ljust does not truncate -- which
71+
pushed every following argument one byte to the right."""
72+
with self.assertRaises(ValueError):
73+
encode_static_args(['bytes33', 'uint256'], [b'\x11' * 33, 1])
74+
75+
def test_valid_fixed_bytes_still_encode_left_aligned(self):
76+
self.assertEqual(encode_static_args(['bytes1'], [b'\xab']),
77+
b'\xab' + b'\x00' * 31)
78+
self.assertEqual(len(encode_static_args(['bytes32'], [b'\x11' * 32])), 32)
79+
80+
def test_arrays_report_the_dynamic_type_error(self):
81+
for typ in ('uint256[]', 'address[]', 'uint256[2]'):
82+
with self.assertRaisesRegex(ValueError, 'dynamic/unsupported type'):
83+
encode_static_args([typ], [[1]])
84+
85+
3486
if __name__ == '__main__':
3587
unittest.main()

0 commit comments

Comments
 (0)