CVE-2023-46247 (Python, published 2023-12-13T20:15Z)
Description: value: Vyper is a Pythonic Smart Contract Language for the Ethereum Virtual Machine (EVM). Contracts containing large arrays might underallocate the number of slots they need by 1. Prior to v0.3.8, the calculation to determine how many slots a storage variable needed used `math.ceil(type_.size_in_bytes / 32)`. The intermediate floating point step can produce a rounding error if there are enough bits set in the IEEE-754 mantissa. Roughly speaking, if `type_.size_in_bytes` is large (> 2**46), and slightly less than a power of 2, the calculation can overestimate how many slots are needed by 1. If `type_.size_in_bytes` is slightly more than a power of 2, the calculation can underestimate how many slots are needed by 1. This issue is patched in version 0.3.8.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-193 \u2014 Off-by-one Error
A product calculates or uses an incorrect maximum or minimum value that is 1 more, or 1 less, than the correct value.
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!")