CVE-2023-32058: Integer Overflow or Wraparound (CWE-190)
value: Vyper is a Pythonic smart contract language for the Ethereum virtual machine. Prior to version 0.3.8, due to missing overflow check for loop variables, by assigning the iterator of a loop to a variable, it is possible to overflow the type of the latter. The issue seems to happen only in loops of type `for i in range(a, a + N)` as in loops of type `for i in range(start, stop)` and `for i in range(stop)`, the compiler is able to raise a `TypeMismatch` when trying to overflow the variable. The problem has been patched in version 0.3.8.
CWE description: The product performs a calculation that can
         produce an integer overflow or wraparound when the logic
         assumes that the resulting value will always be larger than
         the original value. This occurs when an integer value is
         incremented to a value that is too large to store in the
         associated representation. When this occurs, the value may
         become a very small or negative number.
CVSS v3 base score: 7.5
Published: 2023-05-11T21: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 3c95882d2..30f4bb87e 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 204e86118..01c1d5f12 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)