CVE-2023-30837 (Python, published 2023-05-08T17:15Z)
Description: value: 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.

Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-789 \u2014 Memory Allocation with Excessive Size Value
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.
Repository: https://github.com/vyperlang/vyper
Files changed:
docs/types.rst
tests/cli/outputs/test_storage_layout_overrides.py
tests/functional/test_storage_slots.py
vyper/semantics/analysis/data_positions.py
vyper/semantics/types/subscriptable.py
Commit message: 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

Vulnerable code:
            # Calculate how many storage slots are required
            storage_length = math.ceil(varinfo.typ.size_in_bytes / 32)
    storage_slot = 0
        type_.set_reentrancy_key_position(StorageSlot(storage_slot))
        ret[variable_name] = {"type": "nonreentrant lock", "slot": 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
        storage_slot += 1
        varinfo.set_position(StorageSlot(storage_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": storage_slot}

        storage_slot += math.ceil(type_.size_in_bytes / 32)
        len_ = math.ceil(type_.size_in_bytes / 32) * 32

Fixed code:
.. note::
    Defining an array in storage whose size is significantly larger than ``2**64`` can result in security vulnerabilities due to risk of overflow.

.. 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.


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
        )


import pytest

from vyper.exceptions import StorageLayoutException



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)
from vyper.utils import ceil32
            storage_length = varinfo.typ.storage_size_in_words
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


    allocator = SimpleStorageAllocator()
        # 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))
        ret[variable_name] = {"type": "nonreentrant lock", "slot": slot}
        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}
        len_ = ceil32(type_.size_in_bytes)
        if length >= 2**64:
            warnings.warn("Use of large arrays can be unsafe!")