CVE-2023-30837 (published 2023-05-08T17:15Z)

Vyper is a pythonic smart contract language for the EVM. The storage allocator does not guard against allocation overflows in versions prior to 0.3.8. An attacker can overwrite the owner variable. This issue was fixed in version 0.3.8.\n

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-789
cwe_name: Memory Allocation with Excessive Size Value
description: The product allocates memory based on an untrusted, large size value, but it does not ensure that the size is within expected limits, allowing arbitrary amounts of memory to be allocated.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/vyperlang/vyper
commit_details: repo_url: https://github.com/vyperlang/vyper
author: Charles Cooper
author_date: 2023-04-28 17:40:15-07:00
msg: Merge pull request from GHSA-mgv8-gggw-mrg6

* fix: block storage allocator overflows

the storage allocator did not guard against overflow when no storage
layout override was provided. this could result in vulnerabilities like
the following:

```vyper
owner: public(address)
buffer: public(uint256[max_value(uint256)])

@external
def initialize():
    self.owner = msg.sender

@external
def foo(idx: uint256, data: uint256):
    self.buffer[idx] = data
```

while the get_element_ptr calculation for `self.buffer[idx]` is checked,
it is not checked in `mod_{2**256}` arithmetic, which can lead to
arithmetic wrapping back to the `owner` variable if the provided `idx`
is large enough.

* clean up allocator logic

also fix a bug where large allocations would use too much storage due to
floating point rounding precision

* add warning for large arrays

* add note about 2**64 behavior
repository: repo_url: https://github.com/vyperlang/vyper
repo_name: vyperlang/vyper
description: Pythonic Smart Contract Language for the EVM
repo_language: Python
owner: vyperlang
file_changes: filename: types.rst
change_type: ModificationType.MODIFY
diff: @@ -520,6 +520,9 @@ A two dimensional list can be declared with ``_name: _ValueType[inner_size][oute
     # Returning the value in row 0 column 4 (in this case 14)
     return exampleList2D[0][4]
 
+.. note::
+    Defining an array in storage whose size is significantly larger than ``2**64`` can result in security vulnerabilities due to risk of overflow.
+
 .. index:: !dynarrays
 
 Dynamic Arrays
@@ -561,6 +564,10 @@ Dynamic arrays represent bounded arrays whose length can be modified at runtime,
 
 In the ABI, they are represented as ``_Type[]``. For instance, ``DynArray[int128, 3]`` gets represented as ``int128[]``, and ``DynArray[DynArray[int128, 3], 3]`` gets represented as ``int128[][]``.
 
+.. note::
+    Defining a dynamic array in storage whose size is significantly larger than ``2**64`` can result in security vulnerabilities due to risk of overflow.
+
+
 .. _types-struct:
 
 Structs

programming_language: Markdown
filename: test_storage_layout_overrides.py
change_type: ModificationType.MODIFY
diff: @@ -95,6 +95,21 @@ symbol: public(String[32])"""
         )
 
 
+def test_overflow():
+    code = """
+x: uint256[2]
+    """
+
+    storage_layout_override = {"x": {"slot": 2**256 - 1, "type": "uint256[2]"}}
+
+    with pytest.raises(
+        StorageLayoutException, match=f"Invalid storage slot for var x, out of bounds: {2**256}\n"
+    ):
+        compile_code(
+            code, output_formats=["layout"], storage_layout_override=storage_layout_override
+        )
+
+
 def test_incomplete_overrides():
     code = """
 name: public(String[64])

programming_language: Python
filename: test_storage_slots.py
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,7 @@
+import pytest
+
+from vyper.exceptions import StorageLayoutException
+
 code = """
 
 struct StructOne:
@@ -97,3 +101,15 @@ def test_reentrancy_lock(get_contract):
     assert [c.foo(0, i) for i in range(3)] == [987, 654, 321]
     assert [c.foo(1, i) for i in range(3)] == [123, 456, 789]
     assert c.h(0) == 123456789
+
+
+def test_allocator_overflow(get_contract):
+    code = """
+x: uint256
+y: uint256[max_value(uint256)]
+    """
+    with pytest.raises(
+        StorageLayoutException,
+        match=f"Invalid storage slot for var y, tried to allocate slots 1 through {2**256}\n",
+    ):
+        get_contract(code)

programming_language: Python
filename: data_positions.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ from vyper import ast as vy_ast
 from vyper.exceptions import StorageLayoutException
 from vyper.semantics.analysis.base import CodeOffset, StorageSlot
 from vyper.typing import StorageLayout
+from vyper.utils import ceil32
 
 
 def set_data_positions(
@@ -121,8 +122,7 @@ def set_storage_slots_with_overrides(
         # Expect to find this variable within the storage layout overrides
         if node.target.id in storage_layout_overrides:
             var_slot = storage_layout_overrides[node.target.id]["slot"]
-            # Calculate how many storage slots are required
-            storage_length = math.ceil(varinfo.typ.size_in_bytes / 32)
+            storage_length = varinfo.typ.storage_size_in_words
             # Ensure that all required storage slots are reserved, and prevents other variables
             # from using these slots
             reserved_slots.reserve_slot_range(var_slot, storage_length, node.target.id)
@@ -139,6 +139,21 @@ def set_storage_slots_with_overrides(
     return ret
 
 
+class SimpleStorageAllocator:
+    def __init__(self, starting_slot: int = 0):
+        self._slot = starting_slot
+
+    def allocate_slot(self, n, var_name):
+        ret = self._slot
+        if self._slot + n >= 2**256:
+            raise StorageLayoutException(
+                f"Invalid storage slot for var {var_name}, tried to allocate"
+                f" slots {self._slot} through {self._slot + n}"
+            )
+        self._slot += n
+        return ret
+
+
 def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout:
     """
     Parse module-level Vyper AST to calculate the layout of storage variables.
@@ -146,7 +161,7 @@ def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout:
     """
     # Allocate storage slots from 0
     # note storage is word-addressable, not byte-addressable
-    storage_slot = 0
+    allocator = SimpleStorageAllocator()
 
     ret: Dict[str, Dict] = {}
 
@@ -165,16 +180,17 @@ def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout:
             type_.set_reentrancy_key_position(StorageSlot(_slot))
             continue
 
-        type_.set_reentrancy_key_position(StorageSlot(storage_slot))
+        # TODO use one byte - or bit - per reentrancy key
+        # requires either an extra SLOAD or caching the value of the
+        # location in memory at entrance
+        slot = allocator.allocate_slot(1, variable_name)
+
+        type_.set_reentrancy_key_position(StorageSlot(slot))
 
         # TODO this could have better typing but leave it untyped until
         # we nail down the format better
-        ret[variable_name] = {"type": "nonreentrant lock", "slot": storage_slot}
+        ret[variable_name] = {"type": "nonreentrant lock", "slot": slot}
 
-        # TODO use one byte - or bit - per reentrancy key
-        # requires either an extra SLOAD or caching the value of the
-        # location in memory at entrance
-        storage_slot += 1
 
     for node in vyper_module.get_children(vy_ast.VariableDecl):
         # skip non-storage variables
@@ -182,19 +198,20 @@ def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout:
             continue
 
         varinfo = node.target._metadata["varinfo"]
-        varinfo.set_position(StorageSlot(storage_slot))
-
         type_ = varinfo.typ
 
-        # this could have better typing but leave it untyped until
-        # we understand the use case better
-        ret[node.target.id] = {"type": str(type_), "slot": storage_slot}
-
         # CMC 2021-07-23 note that HashMaps get assigned a slot here.
         # I'm not sure if it's safe to avoid allocating that slot
         # for HashMaps because downstream code might use the slot
         # ID as a salt.
-        storage_slot += math.ceil(type_.size_in_bytes / 32)
+        n_slots = type_.storage_size_in_words
+        slot = allocator.allocate_slot(n_slots, node.target.id)
+
+        varinfo.set_position(StorageSlot(slot))
+
+        # this could have better typing but leave it untyped until
+        # we understand the use case better
+        ret[node.target.id] = {"type": str(type_), "slot": slot}
 
     return ret
 
@@ -216,7 +233,7 @@ def set_code_offsets(vyper_module: vy_ast.Module) -> Dict:
         type_ = varinfo.typ
         varinfo.set_position(CodeOffset(offset))
 
-        len_ = math.ceil(type_.size_in_bytes / 32) * 32
+        len_ = ceil32(type_.size_in_bytes)
 
         # this could have better typing but leave it untyped until
         # we understand the use case better

programming_language: Python
filename: subscriptable.py
change_type: ModificationType.MODIFY
diff: @@ -103,6 +103,9 @@ class _SequenceT(_SubscriptableT):
         if not 0 < length < 2**256:
             raise InvalidType("Array length is invalid")
 
+        if length >= 2**64:
+            warnings.warn("Use of large arrays can be unsafe!")
+
         super().__init__(UINT256_T, value_type)
         self.length = length
 

programming_language: Python