Skip to content

Commit

Permalink
Merge pull request from GHSA-3p37-3636-q8wv
Browse files Browse the repository at this point in the history
in dynarray_make_setter, the length is copied before the data. when the
dst and src arrays do not overlap, this is not a problem. however, when
the dst and src are the same dynarray, this can lead to a
store-before-load, leading any array bounds checks on the right hand
side to function incorrectly. here is an example:

```vyper
@external
def should_revert() -> DynArray[uint256,3]:
    a: DynArray[uint256, 3] = [1, 2, 3]
    a = empty(DynArray[uint256, 3])
    a = [self.a[0], self.a[1], self.a[2]]
    return a  # if bug: returns [1,2,3]
```

this commit moves the length store to after the data copy in
dynarray_make_setter. for hygiene, it also moves the length store to
after the data copy in several other routines. I left pop_dyn_array()
unchanged, because moving the routine does not actually perform any data
copy, it just writes the new length (and optionally returns a pointer to
the popped item).
  • Loading branch information
charles-cooper authored May 11, 2023
1 parent 92c32f8 commit 4f8289a
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 15 deletions.
92 changes: 92 additions & 0 deletions tests/parser/types/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1748,3 +1748,95 @@ def foo(i: uint256) -> {return_type}:
return MY_CONSTANT[i]
"""
assert_compile_failed(lambda: get_contract(code), TypeMismatch)


dynarray_length_no_clobber_cases = [
# GHSA-3p37-3636-q8wv cases
"""
a: DynArray[uint256,3]
@external
def should_revert() -> DynArray[uint256,3]:
self.a = [1,2,3]
self.a = empty(DynArray[uint256,3])
self.a = [self.a[0], self.a[1], self.a[2]]
return self.a # if bug: returns [1,2,3]
""",
"""
@external
def should_revert() -> DynArray[uint256,3]:
self.a()
return self.b() # if bug: returns [1,2,3]
@internal
def a():
a: uint256 = 0
b: uint256 = 1
c: uint256 = 2
d: uint256 = 3
@internal
def b() -> DynArray[uint256,3]:
a: DynArray[uint256,3] = empty(DynArray[uint256,3])
a = [a[0],a[1],a[2]]
return a
""",
"""
a: DynArray[uint256,4]
@external
def should_revert() -> DynArray[uint256,4]:
self.a = [1,2,3]
self.a = empty(DynArray[uint256,4])
self.a = [4, self.a[0]]
return self.a # if bug: return [4, 4]
""",
"""
@external
def should_revert() -> DynArray[uint256,4]:
a: DynArray[uint256, 4] = [1,2,3]
a = []
a = [a.pop()] # if bug: return [1]
return a
""",
"""
@external
def should_revert():
c: DynArray[uint256, 1] = []
c.append(c[0])
""",
"""
@external
def should_revert():
c: DynArray[uint256, 1] = [1]
c[0] = c.pop()
""",
"""
@external
def should_revert():
c: DynArray[DynArray[uint256, 1], 2] = [[]]
c[0] = c.pop()
""",
"""
a: DynArray[String[65],2]
@external
def should_revert() -> DynArray[String[65], 2]:
self.a = ["hello", "world"]
self.a = []
self.a = [self.a[0], self.a[1]]
return self.a # if bug: return ["hello", "world"]
""",
]


@pytest.mark.parametrize("code", dynarray_length_no_clobber_cases)
def test_dynarray_length_no_clobber(get_contract, assert_tx_failed, code):
# check that length is not clobbered before dynarray data copy happens
c = get_contract(code)
assert_tx_failed(lambda: c.should_revert())
46 changes: 31 additions & 15 deletions vyper/codegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,15 @@ def make_byte_array_copier(dst, src):
max_bytes = src.typ.maxlen

ret = ["seq"]

dst_ = bytes_data_ptr(dst)
src_ = bytes_data_ptr(src)

ret.append(copy_bytes(dst_, src_, len_, max_bytes))

# store length
ret.append(STORE(dst, len_))

dst = bytes_data_ptr(dst)
src = bytes_data_ptr(src)

ret.append(copy_bytes(dst, src, len_, max_bytes))
return b1.resolve(b2.resolve(ret))


Expand All @@ -148,25 +150,34 @@ def _dynarray_make_setter(dst, src):
if src.value == "~empty":
return IRnode.from_list(STORE(dst, 0))

# copy contents of src dynarray to dst.
# note that in case src and dst refer to the same dynarray,
# in order for get_element_ptr oob checks on the src dynarray
# to work, we need to wait until after the data is copied
# before we clobber the length word.

if src.value == "multi":
ret = ["seq"]
# handle literals

# write the length word
store_length = STORE(dst, len(src.args))
ann = None
if src.annotation is not None:
ann = f"len({src.annotation})"
store_length = IRnode.from_list(store_length, annotation=ann)
ret.append(store_length)

# copy each item
n_items = len(src.args)

for i in range(n_items):
k = IRnode.from_list(i, typ=UINT256_T)
dst_i = get_element_ptr(dst, k, array_bounds_check=False)
src_i = get_element_ptr(src, k, array_bounds_check=False)
ret.append(make_setter(dst_i, src_i))

# write the length word after data is copied
store_length = STORE(dst, n_items)
ann = None
if src.annotation is not None:
ann = f"len({src.annotation})"
store_length = IRnode.from_list(store_length, annotation=ann)

ret.append(store_length)

return ret

with src.cache_when_complex("darray_src") as (b1, src):
Expand All @@ -190,8 +201,6 @@ def _dynarray_make_setter(dst, src):
with get_dyn_array_count(src).cache_when_complex("darray_count") as (b2, count):
ret = ["seq"]

ret.append(STORE(dst, count))

if should_loop:
i = IRnode.from_list(_freshname("copy_darray_ix"), typ=UINT256_T)

Expand All @@ -213,6 +222,9 @@ def _dynarray_make_setter(dst, src):
dst_ = dynarray_data_ptr(dst)
ret.append(copy_bytes(dst_, src_, n_bytes, max_bytes))

# write the length word after data is copied
ret.append(STORE(dst, count))

return b1.resolve(b2.resolve(ret))


Expand Down Expand Up @@ -336,12 +348,14 @@ def append_dyn_array(darray_node, elem_node):
with len_.cache_when_complex("old_darray_len") as (b2, len_):
assertion = ["assert", ["lt", len_, darray_node.typ.count]]
ret.append(IRnode.from_list(assertion, error_msg=f"{darray_node.typ} bounds check"))
ret.append(STORE(darray_node, ["add", len_, 1]))
# NOTE: typechecks elem_node
# NOTE skip array bounds check bc we already asserted len two lines up
ret.append(
make_setter(get_element_ptr(darray_node, len_, array_bounds_check=False), elem_node)
)

# store new length
ret.append(STORE(darray_node, ["add", len_, 1]))
return IRnode.from_list(b1.resolve(b2.resolve(ret)))


Expand All @@ -354,6 +368,7 @@ def pop_dyn_array(darray_node, return_popped_item):
new_len = IRnode.from_list(["sub", old_len, 1], typ=UINT256_T)

with new_len.cache_when_complex("new_len") as (b2, new_len):
# store new length
ret.append(STORE(darray_node, new_len))

# NOTE skip array bounds check bc we already asserted len two lines up
Expand All @@ -364,6 +379,7 @@ def pop_dyn_array(darray_node, return_popped_item):
location = popped_item.location
else:
typ, location = None, None

return IRnode.from_list(b1.resolve(b2.resolve(ret)), typ=typ, location=location)


Expand Down

0 comments on commit 4f8289a

Please sign in to comment.