Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix[codegen]: zero-length dynarray abi_decode validation #4060

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5a76c75
add a test for another abi_decode edge case
charles-cooper May 29, 2024
34dfcb6
fix the validation
charles-cooper May 29, 2024
2a08a4d
add a performance note
charles-cooper May 29, 2024
230f368
Merge branch 'master' into fix/abi-decode-recursion
charles-cooper May 29, 2024
f023ea2
update comment
charles-cooper May 29, 2024
d15c788
add test for dynarray complex value type
charles-cooper May 30, 2024
3a54fd7
multiply by item size
charles-cooper May 30, 2024
bd0f000
update comments
charles-cooper May 30, 2024
4e3d7a5
Merge branch 'master' into fix/abi-decode-recursion
charles-cooper May 30, 2024
ce377b5
Merge branch 'master' into fix/abi-decode-recursion
charles-cooper May 30, 2024
db8af50
add decode tests for dynarrays
cyberthirst May 30, 2024
46291cd
Merge pull request #39 from cyberthirst/fix/abi-decode-recursion
charles-cooper May 30, 2024
ca82bd2
manual optimization
charles-cooper May 30, 2024
df81057
fix bad IRnode construction
charles-cooper May 30, 2024
2aaf8ce
add more abi decode tests
cyberthirst May 31, 2024
b5e1cf0
Merge pull request #40 from cyberthirst/fix/abi-decode-recursion
charles-cooper May 31, 2024
0d5463a
Merge branch 'master' into fix/abi-decode-recursion
charles-cooper May 31, 2024
2a80e67
Merge branch 'master' into fix/abi-decode-recursion
charles-cooper Jun 2, 2024
ed06e4e
remove asserts from tests which fail in the decoder
cyberthirst Jun 2, 2024
b038729
keep structure of original poc
cyberthirst Jun 2, 2024
d32dd73
Merge pull request #41 from cyberthirst/fix/abi-decode-recursion
charles-cooper Jun 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 246 additions & 3 deletions tests/functional/builtins/codegen/test_abi_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,8 @@ def f(x: Bytes[32 * 3]):
decoded_y1: Bytes[32] = _abi_decode(y, Bytes[32])
a = b"bar"
decoded_y2: Bytes[32] = _abi_decode(y, Bytes[32])

assert decoded_y1 != decoded_y2
# original POC:
# assert decoded_y1 != decoded_y2
"""
c = get_contract(code)

Expand Down Expand Up @@ -1043,7 +1043,7 @@ def run():
c.run()


def test_abi_decode_extcall_zero_len_array(get_contract):
def test_abi_decode_extcall_empty_array(get_contract):
code = """
@external
def bar() -> (uint256, uint256):
Expand All @@ -1061,6 +1061,59 @@ def run():
c.run()


def test_abi_decode_extcall_complex_empty_dynarray(get_contract):
# 5th word of the payload points to the last word of the payload
# which is considered the length of the Point.y array
# because the length is 0, the decoding should succeed
code = """
struct Point:
x: uint256
y: DynArray[uint256, 2]
z: uint256

@external
def bar() -> (uint256, uint256, uint256, uint256, uint256, uint256):
return 32, 1, 32, 1, 64, 0

interface A:
def bar() -> DynArray[Point, 2]: nonpayable

@external
def run():
x: DynArray[Point, 2] = extcall A(self).bar()
assert len(x) == 1 and len(x[0].y) == 0
"""
c = get_contract(code)

c.run()


def test_abi_decode_extcall_complex_empty_dynarray2(tx_failed, get_contract):
# top-level head points 1B over the runtime buffer end
# thus the decoding should fail although the length is 0
code = """
struct Point:
x: uint256
y: DynArray[uint256, 2]
z: uint256

@external
def bar() -> (uint256, uint256):
return 33, 0

interface A:
def bar() -> DynArray[Point, 2]: nonpayable

@external
def run():
x: DynArray[Point, 2] = extcall A(self).bar()
"""
c = get_contract(code)

with tx_failed():
c.run()


def test_abi_decode_extcall_zero_len_array2(get_contract):
code = """
@external
Expand All @@ -1080,3 +1133,193 @@ def run() -> uint256:
length = c.run()

assert length == 0


def test_abi_decode_top_level_head_oob(tx_failed, get_contract):
code = """
@external
def run(x: Bytes[256], y: uint256):
player_lost: bool = empty(bool)

if y == 1:
player_lost = True

decoded: DynArray[Bytes[1], 2] = empty(DynArray[Bytes[1], 2])
decoded = _abi_decode(x, DynArray[Bytes[1], 2])
"""
c = get_contract(code)

# head points over the buffer end
payload = (0x0100, *_replicate(0x00, 7))

data = _abi_payload_from_tuple(payload)

with tx_failed():
c.run(data, 1)

with tx_failed():
c.run(data, 0)


def test_abi_decode_dynarray_complex_insufficient_data(env, tx_failed, get_contract):
code = """
struct Point:
x: uint256
y: uint256

@external
def run(x: Bytes[32 * 8]):
y: Bytes[32 * 8] = x
decoded_y1: DynArray[Point, 3] = _abi_decode(y, DynArray[Point, 3])
"""
c = get_contract(code)

# runtime buffer has insufficient size - we decode 3 points, but provide only
# 3 * 32B of payload
payload = (0x20, 0x03, *_replicate(0x03, 3))

data = _abi_payload_from_tuple(payload)

with tx_failed():
c.run(data)


def test_abi_decode_dynarray_complex2(env, tx_failed, get_contract):
# point head to the 1st 0x01 word (ie the length)
# but size of the point is 3 * 32B, thus we'd decode 2B over the buffer end
code = """
struct Point:
x: uint256
y: uint256
z: uint256


@external
def run(x: Bytes[32 * 8]):
y: Bytes[32 * 11] = x
decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2])
"""
c = get_contract(code)

payload = (
0xC0, # points to the 1st 0x01 word (ie the length)
*_replicate(0x03, 5),
*_replicate(0x01, 2),
)

data = _abi_payload_from_tuple(payload)

with tx_failed():
c.run(data)


def test_abi_decode_complex_empty_dynarray(env, tx_failed, get_contract):
# point head to the last word of the payload
# this will be the length, but because it's set to 0, the decoding should succeed
code = """
struct Point:
x: uint256
y: DynArray[uint256, 2]
z: uint256


@external
def run(x: Bytes[32 * 16]):
y: Bytes[32 * 16] = x
decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2])
assert len(decoded_y1) == 1 and len(decoded_y1[0].y) == 0
"""
c = get_contract(code)

payload = (
0x20,
0x01,
0x20,
0x01,
0xA0, # points to the last word of the payload
0x04,
0x02,
0x02,
0x00, # length is 0, so decoding should succeed
)

data = _abi_payload_from_tuple(payload)

c.run(data)


def test_abi_decode_complex_arithmetic_overflow(tx_failed, get_contract):
# inner head roundtrips due to arithmetic overflow
code = """
struct Point:
x: uint256
y: DynArray[uint256, 2]
z: uint256


@external
def run(x: Bytes[32 * 16]):
y: Bytes[32 * 16] = x
decoded_y1: DynArray[Point, 2] = _abi_decode(y, DynArray[Point, 2])
"""
c = get_contract(code)

payload = (
0x20,
0x01,
0x20,
0x01, # both Point.x and Point.y length
2**256 - 0x20, # points to the "previous" word of the payload
0x04,
0x02,
0x02,
0x00,
)

data = _abi_payload_from_tuple(payload)

with tx_failed():
c.run(data)


def test_abi_decode_empty_toplevel_dynarray(get_contract):
code = """
@external
def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]):
y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x
assert len(y) == 2 * 32
decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode(
y,
DynArray[DynArray[uint256, 3], 3]
)
assert len(decoded_y1) == 0
"""
c = get_contract(code)

payload = (0x20, 0x00) # DynArray head, DynArray length

data = _abi_payload_from_tuple(payload)

c.run(data)


def test_abi_decode_invalid_toplevel_dynarray_head(tx_failed, get_contract):
# head points 1B over the bounds of the runtime buffer
code = """
@external
def run(x: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4]):
y: Bytes[2 * 32 + 3 * 32 + 3 * 32 * 4] = x
decoded_y1: DynArray[DynArray[uint256, 3], 3] = _abi_decode(
y,
DynArray[DynArray[uint256, 3], 3]
)
"""
c = get_contract(code)

# head points 1B over the bounds of the runtime buffer
payload = (0x21, 0x00) # DynArray head, DynArray length

data = _abi_payload_from_tuple(payload)

with tx_failed():
c.run(data)
17 changes: 13 additions & 4 deletions vyper/codegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,17 @@ def _dirty_read_risk(ir_node):
def _abi_payload_size(ir_node):
SCALE = ir_node.location.word_scale
assert SCALE == 32 # we must be in some byte-addressable region, like memory

OFFSET = DYNAMIC_ARRAY_OVERHEAD * SCALE

if isinstance(ir_node.typ, DArrayT):
return ["add", OFFSET, ["mul", get_dyn_array_count(ir_node), SCALE]]
# the amount of size each value occupies in static section
# (the amount of size it occupies in the dynamic section is handled in
# make_setter recursion)
item_size = ir_node.typ.value_type.abi_type.static_size()
if item_size == 0:
# manual optimization; the mload cannot currently be optimized out
return ["add", OFFSET, 0]
return ["add", OFFSET, ["mul", get_dyn_array_count(ir_node), item_size]]

if isinstance(ir_node.typ, _BytestringT):
return ["add", OFFSET, get_bytearray_length(ir_node)]
Expand Down Expand Up @@ -1175,14 +1181,17 @@ def clamp_dyn_array(ir_node, hi=None):

assert (hi is not None) == _dirty_read_risk(ir_node)

# if the subtype is dynamic, the check will be performed in the recursion
if hi is not None and not t.abi_type.subtyp.is_dynamic():
if hi is not None:
assert t.count < 2**64 # sanity check

# note: this add does not risk arithmetic overflow because
# length is bounded by count * elemsize.
item_end = add_ofst(ir_node, _abi_payload_size(ir_node))
cyberthirst marked this conversation as resolved.
Show resolved Hide resolved

# if the subtype is dynamic, the length check is performed in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe reformulate the comment a bit - the length check was performed even before. what was missing was the in-bounds check ["assert", ["le", item_end, hi]]

# the recursion, UNLESS the count is zero. here we perform the
# check all the time, but it could maybe be optimized out in the
# make_setter loop (in the common case that runtime count > 0).
len_check = ["seq", ["assert", ["le", item_end, hi]], len_check]

return IRnode.from_list(len_check, error_msg=f"{ir_node.typ} bounds check")
Expand Down
Loading