CVE-2024-32481: Insufficient Information (NVD-CWE-noinfo)
value: Vyper is a pythonic Smart Contract Language for the Ethereum virtual machine. Starting in version 0.3.8 and prior to version 0.4.0b1, when looping over a `range` of the form `range(start, start + N)`, if `start` is negative, the execution will always revert. This issue is caused by an incorrect assertion inserted by the code generation of the range `stmt.parse_For_range()`. The issue arises when `start` is signed, instead of using `sle`, `le` is used and `start` is interpreted as an unsigned integer for the comparison. If it is a negative number, its 255th bit is set to `1` and is hence interpreted as a very large unsigned integer making the assertion always fail. Any contract having a `range(start, start + N)` where `start` is a signed integer with the possibility for `start` to be negative is affected. If a call goes through the loop while supplying a negative `start` the execution will revert. Version 0.4.0b1 fixes the issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-25T17:15Z
Repository: https://github.com/vyperlang/vyper
Language: Python
Files changed: tests/parser/features/iteration/test_for_range.py, vyper/codegen/stmt.py
Commit date: 2023-05-11 13:35:44 -0700
Commit message: Merge pull request from GHSA-6r8q-pfpv-7cgj

for loops of the form `for i in range(x, x+N)`, the range of the
iterator is not checked, leading to potential overflow. the following
example demonstrates the potential for overflow:

```
@external
def test() -> uint16:
    x:uint8 = 255
    a:uint8 = 0
    for i in range(x, x+2):
        a = i
    return convert(a,uint16)  # returns 256
```

this commit fixes the issue by adding a range check before entering the
loop body.
Diff: diff --git a/tests/parser/features/iteration/test_repeater.py b/tests/parser/features/iteration/test_for_range.py
similarity index 86%
rename from tests/parser/features/iteration/test_repeater.py
rename to tests/parser/features/iteration/test_for_range.py
index 3c95882d2d..30f4bb87e3 100644
--- a/tests/parser/features/iteration/test_repeater.py
+++ b/tests/parser/features/iteration/test_for_range.py
@@ -126,10 +126,49 @@ def foo(a: {typ}) -> {typ}:
     c = get_contract(code)
     assert c.foo(6) == 7
     assert c.foo(100) == 31337
 
 
+# test that we can get to the upper range of an integer
+@pytest.mark.parametrize("typ", ["uint8", "int128", "uint256"])
+def test_for_range_edge(get_contract, typ):
+    code = f"""
+@external
+def test():
+    found: bool = False
+    x: {typ} = max_value({typ})
+    for i in range(x, x + 1):
+        if i == max_value({typ}):
+            found = True
+
+    assert found
+
+    found = False
+    x = max_value({typ}) - 1
+    for i in range(x, x + 2):
+        if i == max_value({typ}):
+            found = True
+
+    assert found
+    """
+    c = get_contract(code)
+    c.test()
+
+
+@pytest.mark.parametrize("typ", ["uint8", "int128", "uint256"])
+def test_for_range_oob_check(get_contract, assert_tx_failed, typ):
+    code = f"""
+@external
+def test():
+    x: {typ} = max_value({typ})
+    for i in range(x, x+2):
+        pass
+    """
+    c = get_contract(code)
+    assert_tx_failed(lambda: c.test())
+
+
 @pytest.mark.parametrize("typ", ["int128", "uint256"])
 def test_return_inside_nested_repeater(get_contract, typ):
     code = f"""
 @internal
 def _final(a: {typ}) -> {typ}:
diff --git a/vyper/codegen/stmt.py b/vyper/codegen/stmt.py
index 204e861184..01c1d5f121 100644
--- a/vyper/codegen/stmt.py
+++ b/vyper/codegen/stmt.py
@@ -8,10 +8,11 @@ from vyper.codegen.core import (
     LOAD,
     STORE,
     IRnode,
     append_dyn_array,
     check_assign,
+    clamp,
     dummy_node_for_type,
     get_dyn_array_count,
     get_element_ptr,
     getpos,
     is_return_from_function,
@@ -262,10 +263,12 @@ class Stmt:
         # Type 3 for, e.g. for i in range(x, x + 10): ...
         else:
             arg1 = self.stmt.iter.args[1]
             rounds = self._get_range_const_value(arg1.right)
             start = Expr.parse_value_expr(arg0, self.context)
+            _, hi = start.typ.int_bounds
+            start = clamp("le", start, hi + 1 - rounds)
 
         r = rounds if isinstance(rounds, int) else rounds.value
         if r < 1:
             return
 

Fixed code: # test that we can get to the upper range of an integer
@pytest.mark.parametrize("typ", ["uint8", "int128", "uint256"])
def test_for_range_edge(get_contract, typ):
    code = f"""
@external
def test():
    found: bool = False
    x: {typ} = max_value({typ})
    for i in range(x, x + 1):
        if i == max_value({typ}):
            found = True

    assert found

    found = False
    x = max_value({typ}) - 1
    for i in range(x, x + 2):
        if i == max_value({typ}):
            found = True

    assert found
    """
    c = get_contract(code)
    c.test()


@pytest.mark.parametrize("typ", ["uint8", "int128", "uint256"])
def test_for_range_oob_check(get_contract, assert_tx_failed, typ):
    code = f"""
@external
def test():
    x: {typ} = max_value({typ})
    for i in range(x, x+2):
        pass
    """
    c = get_contract(code)
    assert_tx_failed(lambda: c.test())


    clamp,
            _, hi = start.typ.int_bounds
            start = clamp("le", start, hi + 1 - rounds)