VYPR
Moderate severityNVD Advisory· Published Apr 25, 2024· Updated Aug 2, 2024

vyper's range(start, start + N) reverts for negative numbers

CVE-2024-32481

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Vyper smart contracts using `range(start, start+N)` with negative `start` values always revert due to an incorrect signed comparison in code generation.

Root

Cause

CVE-2024-32481 affects Vyper versions 0.3.8 through 0.4.0b1. The vulnerability lies in the code generation of the range statement within stmt.parse_For_range(). When a loop is defined as range(start, start + N), an assertion is inserted to ensure start <= start + N. However, if start is a signed integer, the generated assertion incorrectly uses an unsigned less-than-or-equal (le) comparison instead of a signed less-than-or-equal (sle). This causes a negative start — whose two's complement representation has the 255th bit set — to be interpreted as a very large unsigned integer, making the assertion always fail and the execution revert [1][4].

Exploitation

An attacker can trigger this bug simply by supplying a negative integer as the start argument to any Vyper function that iterates over a range with the pattern range(start, start+N). The bug does not require any special privileges; it is triggered during normal contract execution whenever the loop is encountered with a negative start. The attacker must be able to call a function on a Vyper contract that uses such a loop and control the start value (or cause it to become negative). This can be a denial-of-service vector: any call that passes a negative start will revert, potentially locking funds or breaking contract logic that relies on the loop completing [1][4].

Impact

Successful exploitation leads to an unconditional revert of the transaction. This can be used to deny service to legitimate users, block critical contract operations, or even permanently lock funds if the contract logic depends on the loop's completion. The impact is availability disruption; integrity and confidentiality are not directly affected [1].

Mitigation

The Vyper team fixed the issue in version 0.4.0b1 by correcting the assertion to use a signed comparison (sle) when start is signed. Users should upgrade to 0.4.0b1 or later. There is no known workaround for the affected versions; contracts must be recompiled with the patched compiler [1][2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vyperPyPI
>= 0.3.8, < 0.4.00.4.0

Affected products

1

Patches

2
5319cfbe1495

feat: allow `range(x, y, bound=N)` (#3679)

https://github.com/vyperlang/vyperDaniel SchiaviniDec 24, 2023via ghsa
10 files changed · +390 147
  • docs/control-structures.rst+5 3 modified
    @@ -287,9 +287,11 @@ Another use of range can be with ``START`` and ``STOP`` bounds.
     
     Here, ``START`` and ``STOP`` are literal integers, with ``STOP`` being a greater value than ``START``. ``i`` begins as ``START`` and increments by one until it is equal to ``STOP``.
     
    +Finally, it is possible to use ``range`` with runtime `start` and `stop` values as long as a constant `bound` value is provided.
    +In this case, Vyper checks at runtime that `end - start <= bound`.
    +``N`` must be a compile-time constant.
    +
     .. code-block:: python
     
    -    for i in range(a, a + N):
    +    for i in range(start, end, bound=N):
             ...
    -
    -``a`` is a variable with an integer type and ``N`` is a literal integer greater than zero.  ``i`` begins as ``a`` and increments by one until it is equal to ``a + N``. If ``a + N`` would overflow, execution will revert.
    
  • tests/functional/codegen/features/iteration/test_for_in_list.py+13 6 modified
    @@ -1,3 +1,4 @@
    +import re
     from decimal import Decimal
     
     import pytest
    @@ -700,13 +701,16 @@ def foo():
         """,
             StateAccessViolation,
         ),
    -    """
    +    (
    +        """
     @external
     def foo():
         a: int128 = 6
         for i in range(a,a-3):
             pass
         """,
    +        StateAccessViolation,
    +    ),
         # invalid argument length
         (
             """
    @@ -789,10 +793,13 @@ def test_for() -> int128:
         ),
     ]
     
    +BAD_CODE = [code if isinstance(code, tuple) else (code, StructureException) for code in BAD_CODE]
    +for_code_regex = re.compile(r"for .+ in (.*):")
    +bad_code_names = [
    +    f"{i} {for_code_regex.search(code).group(1)}" for i, (code, _) in enumerate(BAD_CODE)
    +]
    +
     
    -@pytest.mark.parametrize("code", BAD_CODE)
    -def test_bad_code(assert_compile_failed, get_contract, code):
    -    err = StructureException
    -    if not isinstance(code, str):
    -        code, err = code
    +@pytest.mark.parametrize("code,err", BAD_CODE, ids=bad_code_names)
    +def test_bad_code(assert_compile_failed, get_contract, code, err):
         assert_compile_failed(lambda: get_contract(code), err)
    
  • tests/functional/codegen/features/iteration/test_for_range.py+107 9 modified
    @@ -32,6 +32,102 @@ def repeat(n: uint256) -> uint256:
             c.repeat(7)
     
     
    +def test_range_bound_constant_end(get_contract, tx_failed):
    +    code = """
    +@external
    +def repeat(n: uint256) -> uint256:
    +    x: uint256 = 0
    +    for i in range(n, 7, bound=6):
    +        x += i + 1
    +    return x
    +    """
    +    c = get_contract(code)
    +    for n in range(1, 5):
    +        assert c.repeat(n) == sum(i + 1 for i in range(n, 7))
    +
    +    # check assertion for `start <= end`
    +    with tx_failed():
    +        c.repeat(8)
    +    # check assertion for `start + bound <= end`
    +    with tx_failed():
    +        c.repeat(0)
    +
    +
    +def test_range_bound_two_args(get_contract, tx_failed):
    +    code = """
    +@external
    +def repeat(n: uint256) -> uint256:
    +    x: uint256 = 0
    +    for i in range(1, n, bound=6):
    +        x += i + 1
    +    return x
    +    """
    +    c = get_contract(code)
    +    for n in range(1, 8):
    +        assert c.repeat(n) == sum(i + 1 for i in range(1, n))
    +
    +    # check assertion for `start <= end`
    +    with tx_failed():
    +        c.repeat(0)
    +
    +    # check codegen inserts assertion for `start + bound <= end`
    +    with tx_failed():
    +        c.repeat(8)
    +
    +
    +def test_range_bound_two_runtime_args(get_contract, tx_failed):
    +    code = """
    +@external
    +def repeat(start: uint256, end: uint256) -> uint256:
    +    x: uint256 = 0
    +    for i in range(start, end, bound=6):
    +        x += i
    +    return x
    +    """
    +    c = get_contract(code)
    +    for n in range(0, 7):
    +        assert c.repeat(0, n) == sum(range(0, n))
    +        assert c.repeat(n, n * 2) == sum(range(n, n * 2))
    +
    +    # check assertion for `start <= end`
    +    with tx_failed():
    +        c.repeat(1, 0)
    +    with tx_failed():
    +        c.repeat(7, 0)
    +    with tx_failed():
    +        c.repeat(8, 7)
    +
    +    # check codegen inserts assertion for `start + bound <= end`
    +    with tx_failed():
    +        c.repeat(0, 7)
    +    with tx_failed():
    +        c.repeat(14, 21)
    +
    +
    +def test_range_overflow(get_contract, tx_failed):
    +    code = """
    +@external
    +def get_last(start: uint256, end: uint256) -> uint256:
    +    x: uint256 = 0
    +    for i in range(start, end, bound=6):
    +        x = i
    +    return x
    +    """
    +    c = get_contract(code)
    +    UINT_MAX = 2**256 - 1
    +    assert c.get_last(UINT_MAX, UINT_MAX) == 0  # initial value of x
    +
    +    for n in range(1, 6):
    +        assert c.get_last(UINT_MAX - n, UINT_MAX) == UINT_MAX - 1
    +
    +    # check for `start + bound <= end`, overflow cases
    +    for n in range(1, 7):
    +        with tx_failed():
    +            c.get_last(UINT_MAX - n, 0)
    +        with tx_failed():
    +            c.get_last(UINT_MAX, UINT_MAX - n)
    +
    +
     def test_digit_reverser(get_contract_with_gas_estimation):
         digit_reverser = """
     @external
    @@ -89,7 +185,7 @@ def test_offset_repeater_2(get_contract_with_gas_estimation, typ):
     @external
     def sum(frm: {typ}, to: {typ}) -> {typ}:
         out: {typ} = 0
    -    for i in range(frm, frm + 101):
    +    for i in range(frm, frm + 101, bound=101):
             if i == to:
                 break
             out = out + i
    @@ -146,26 +242,28 @@ def foo(a: {typ}) -> {typ}:
         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):
    +    """
    +    Check that we can get to the upper range of an integer.
    +    Note that to avoid overflow in the bounds check for range(),
    +    we need to calculate i+1 inside the loop.
    +    """
         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}):
    +    for i in range(x - 1, x, bound=1):
    +        if i + 1 == 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}):
    +    for i in range(x - 1, x + 1, bound=2):
    +        if i + 1 == max_value({typ}):
                 found = True
    -
         assert found
         """
         c = get_contract(code)
    @@ -178,7 +276,7 @@ def test_for_range_oob_check(get_contract, tx_failed, typ):
     @external
     def test():
         x: {typ} = max_value({typ})
    -    for i in range(x, x+2):
    +    for i in range(x, x + 2, bound=2):
             pass
         """
         c = get_contract(code)
    
  • tests/functional/codegen/integration/test_crowdfund.py+2 2 modified
    @@ -52,7 +52,7 @@ def finalize():
     @external
     def refund():
         ind: int128 = self.refundIndex
    -    for i in range(ind, ind + 30):
    +    for i in range(ind, ind + 30, bound=30):
             if i >= self.nextFunderIndex:
                 self.refundIndex = self.nextFunderIndex
                 return
    @@ -147,7 +147,7 @@ def finalize():
     @external
     def refund():
         ind: int128 = self.refundIndex
    -    for i in range(ind, ind + 30):
    +    for i in range(ind, ind + 30, bound=30):
             if i >= self.nextFunderIndex:
                 self.refundIndex = self.nextFunderIndex
                 return
    
  • tests/functional/syntax/exceptions/test_invalid_literal_exception.py+0 7 modified
    @@ -18,13 +18,6 @@ def foo():
         """,
         """
     @external
    -def foo(x: int128):
    -    y: int128 = 7
    -    for i in range(x, x + y):
    -        pass
    -    """,
    -    """
    -@external
     def foo():
         x: String[100] = "these bytes are nо gооd because the o's are from the Russian alphabet"
         """,
    
  • tests/functional/syntax/test_for_range.py+188 9 modified
    @@ -1,7 +1,9 @@
    +import re
    +
     import pytest
     
     from vyper import compiler
    -from vyper.exceptions import StructureException
    +from vyper.exceptions import ArgumentException, StateAccessViolation, StructureException
     
     fail_list = [
         (
    @@ -12,33 +14,191 @@ def foo():
             pass
         """,
             StructureException,
    +        "Invalid syntax for loop iterator",
    +        "a[1]",
    +    ),
    +    (
    +        """
    +@external
    +def foo():
    +    x: uint256 = 100
    +    for _ in range(10, bound=x):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Bound must be a literal",
    +        "x",
    +    ),
    +    (
    +        """
    +@external
    +def foo():
    +    for _ in range(10, 20, bound=5):
    +        pass
    +    """,
    +        StructureException,
    +        "Please remove the `bound=` kwarg when using range with constants",
    +        "5",
    +    ),
    +    (
    +        """
    +@external
    +def foo():
    +    for _ in range(10, 20, bound=0):
    +        pass
    +    """,
    +        StructureException,
    +        "Bound must be at least 1",
    +        "0",
    +    ),
    +    (
    +        """
    +@external
    +def bar():
    +    x:uint256 = 1
    +    for i in range(x,x+1,bound=2,extra=3):
    +        pass
    +    """,
    +        ArgumentException,
    +        "Invalid keyword argument 'extra'",
    +        "extra=3",
         ),
         (
             """
     @external
     def bar():
    -    for i in range(1,2,bound=2):
    +    for i in range(0):
             pass
         """,
             StructureException,
    +        "End must be greater than start",
    +        "0",
    +    ),
    +    (
    +        """
    +@external
    +def bar():
    +    x:uint256 = 1
    +    for i in range(x):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x",
    +    ),
    +    (
    +        """
    +@external
    +def bar():
    +    x:uint256 = 1
    +    for i in range(0, x):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x",
    +    ),
    +    (
    +        """
    +@external
    +def repeat(n: uint256) -> uint256:
    +    for i in range(0, n * 10):
    +        pass
    +    return n
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "n * 10",
         ),
         (
             """
     @external
     def bar():
         x:uint256 = 1
    -    for i in range(x,x+1,bound=2):
    +    for i in range(0, x + 1):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x + 1",
    +    ),
    +    (
    +        """
    +@external
    +def bar():
    +    for i in range(2, 1):
             pass
         """,
             StructureException,
    +        "End must be greater than start",
    +        "1",
    +    ),
    +    (
    +        """
    +@external
    +def bar():
    +    x:uint256 = 1
    +    for i in range(x, x):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x",
    +    ),
    +    (
    +        """
    +@external
    +def foo():
    +    x: int128 = 5
    +    for i in range(x, x + 10):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x",
    +    ),
    +    (
    +        """
    +@external
    +def repeat(n: uint256) -> uint256:
    +    for i in range(n, 6):
    +        pass
    +    return x
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "n",
    +    ),
    +    (
    +        """
    +@external
    +def foo(x: int128):
    +    y: int128 = 7
    +    for i in range(x, x + y):
    +        pass
    +    """,
    +        StateAccessViolation,
    +        "Value must be a literal integer, unless a bound is specified",
    +        "x",
         ),
     ]
     
    +for_code_regex = re.compile(r"for .+ in (.*):")
    +fail_test_names = [
    +    (
    +        f"{i:02d}: {for_code_regex.search(code).group(1)}"  # type: ignore[union-attr]
    +        f" raises {type(err).__name__}"
    +    )
    +    for i, (code, err, msg, src) in enumerate(fail_list)
    +]
     
    -@pytest.mark.parametrize("bad_code", fail_list)
    -def test_range_fail(bad_code):
    -    with pytest.raises(bad_code[1]):
    -        compiler.compile_code(bad_code[0])
    +
    +@pytest.mark.parametrize("bad_code,error_type,message,source_code", fail_list, ids=fail_test_names)
    +def test_range_fail(bad_code, error_type, message, source_code):
    +    with pytest.raises(error_type) as exc_info:
    +        compiler.compile_code(bad_code)
    +    assert message == exc_info.value.message
    +    assert source_code == exc_info.value.args[1].node_source_code
     
     
     valid_list = [
    @@ -58,7 +218,21 @@ def foo():
     @external
     def foo():
         x: int128 = 5
    -    for i in range(x, x + 10):
    +    for i in range(1, x, bound=4):
    +        pass
    +    """,
    +    """
    +@external
    +def foo():
    +    x: int128 = 5
    +    for i in range(x, bound=4):
    +        pass
    +    """,
    +    """
    +@external
    +def foo():
    +    x: int128 = 5
    +    for i in range(0, x, bound=4):
             pass
         """,
         """
    @@ -72,7 +246,12 @@ def kick_foos():
         """,
     ]
     
    +valid_test_names = [
    +    f"{i} {for_code_regex.search(code).group(1)}"  # type: ignore[union-attr]
    +    for i, code in enumerate(valid_list)
    +]
    +
     
    -@pytest.mark.parametrize("good_code", valid_list)
    +@pytest.mark.parametrize("good_code", valid_list, ids=valid_test_names)
     def test_range_success(good_code):
         assert compiler.compile_code(good_code) is not None
    
  • vyper/codegen/ir_node.py+6 2 modified
    @@ -444,11 +444,15 @@ def unique_symbols(self):
             return ret
     
         @property
    -    def is_literal(self):
    +    def is_literal(self) -> bool:
             return isinstance(self.value, int) or self.value == "multi"
     
    +    def int_value(self) -> int:
    +        assert isinstance(self.value, int)
    +        return self.value
    +
         @property
    -    def is_pointer(self):
    +    def is_pointer(self) -> bool:
             # not used yet but should help refactor/clarify downstream code
             # eventually
             return self.location is not None
    
  • vyper/codegen/stmt.py+27 40 modified
    @@ -225,15 +225,6 @@ def parse_Raise(self):
             else:
                 return IRnode.from_list(["revert", 0, 0], error_msg="user raise")
     
    -    def _check_valid_range_constant(self, arg_ast_node):
    -        with self.context.range_scope():
    -            arg_expr = Expr.parse_value_expr(arg_ast_node, self.context)
    -        return arg_expr
    -
    -    def _get_range_const_value(self, arg_ast_node):
    -        arg_expr = self._check_valid_range_constant(arg_ast_node)
    -        return arg_expr.value
    -
         def parse_For(self):
             with self.context.block_scope():
                 if self.stmt.get("iter.func.id") == "range":
    @@ -249,41 +240,37 @@ def _parse_For_range(self):
                 iter_typ = INT256_T
     
             # Get arg0
    -        arg0 = self.stmt.iter.args[0]
    -        num_of_args = len(self.stmt.iter.args)
    -
    -        kwargs = {
    -            s.arg: Expr.parse_value_expr(s.value, self.context)
    -            for s in self.stmt.iter.keywords or []
    -        }
    -
    -        # Type 1 for, e.g. for i in range(10): ...
    -        if num_of_args == 1:
    -            n = Expr.parse_value_expr(arg0, self.context)
    -            start = IRnode.from_list(0, typ=iter_typ)
    -            rounds = n
    -            rounds_bound = kwargs.get("bound", rounds)
    -
    -        # Type 2 for, e.g. for i in range(100, 110): ...
    -        elif self._check_valid_range_constant(self.stmt.iter.args[1]).is_literal:
    -            arg0_val = self._get_range_const_value(arg0)
    -            arg1_val = self._get_range_const_value(self.stmt.iter.args[1])
    -            start = IRnode.from_list(arg0_val, typ=iter_typ)
    -            rounds = IRnode.from_list(arg1_val - arg0_val, typ=iter_typ)
    -            rounds_bound = rounds
    +        for_iter: vy_ast.Call = self.stmt.iter
    +        args_len = len(for_iter.args)
    +        if args_len == 1:
    +            arg0, arg1 = (IRnode.from_list(0, typ=iter_typ), for_iter.args[0])
    +        elif args_len == 2:
    +            arg0, arg1 = for_iter.args
    +        else:  # pragma: nocover
    +            raise TypeCheckFailure("unreachable: bad # of arguments to range()")
     
    -        # 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)
    +        with self.context.range_scope():
                 start = Expr.parse_value_expr(arg0, self.context)
    -            _, hi = start.typ.int_bounds
    -            start = clamp("le", start, hi + 1 - rounds)
    +            end = Expr.parse_value_expr(arg1, self.context)
    +            kwargs = {
    +                s.arg: Expr.parse_value_expr(s.value, self.context) for s in for_iter.keywords
    +            }
    +
    +        if "bound" in kwargs:
    +            with end.cache_when_complex("end") as (b1, end):
    +                # note: the check for rounds<=rounds_bound happens in asm
    +                # generation for `repeat`.
    +                clamped_start = clamp("le", start, end)
    +                rounds = b1.resolve(IRnode.from_list(["sub", end, clamped_start]))
    +            rounds_bound = kwargs.pop("bound").int_value()
    +        else:
    +            rounds = end.int_value() - start.int_value()
                 rounds_bound = rounds
     
    -        bound = rounds_bound if isinstance(rounds_bound, int) else rounds_bound.value
    -        if bound < 1:
    -            return
    +        assert len(kwargs) == 0  # sanity check stray keywords
    +
    +        if rounds_bound < 1:  # pragma: nocover
    +            raise TypeCheckFailure("unreachable: unchecked 0 bound")
     
             varname = self.stmt.target.id
             i = IRnode.from_list(self.context.fresh_varname("range_ix"), typ=UINT256_T)
    
  • vyper/exceptions.py+1 1 modified
    @@ -41,7 +41,7 @@ def __init__(self, message="Error Message not found.", *items):
                 Error message to display with the exception.
             *items : VyperNode | Tuple[str, VyperNode], optional
                 Vyper ast node(s), or tuple of (description, node) indicating where
    -            the exception occured. Source annotations are generated in the order
    +            the exception occurred. Source annotations are generated in the order
                 the nodes are given.
     
                 A single tuple of (lineno, col_offset) is also understood to support
    
  • vyper/semantics/analysis/local.py+41 68 modified
    @@ -7,7 +7,6 @@
         ExceptionList,
         FunctionDeclarationException,
         ImmutableViolation,
    -    InvalidLiteral,
         InvalidOperation,
         InvalidType,
         IteratorException,
    @@ -355,71 +354,7 @@ def visit_For(self, node):
                     raise IteratorException(
                         "Cannot iterate over the result of a function call", node.iter
                     )
    -            range_ = node.iter
    -            validate_call_args(range_, (1, 2), kwargs=["bound"])
    -
    -            args = range_.args
    -            kwargs = {s.arg: s.value for s in range_.keywords or []}
    -            if len(args) == 1:
    -                # range(CONSTANT)
    -                n = args[0]
    -                bound = kwargs.pop("bound", None)
    -                validate_expected_type(n, IntegerT.any())
    -
    -                if bound is None:
    -                    if not isinstance(n, vy_ast.Num):
    -                        raise StateAccessViolation("Value must be a literal", n)
    -                    if n.value <= 0:
    -                        raise StructureException("For loop must have at least 1 iteration", args[0])
    -                    type_list = get_possible_types_from_node(n)
    -
    -                else:
    -                    if not isinstance(bound, vy_ast.Num):
    -                        raise StateAccessViolation("bound must be a literal", bound)
    -                    if bound.value <= 0:
    -                        raise StructureException("bound must be at least 1", args[0])
    -                    type_list = get_common_types(n, bound)
    -
    -            else:
    -                if range_.keywords:
    -                    raise StructureException(
    -                        "Keyword arguments are not supported for `range(N, M)` and"
    -                        "`range(x, x + N)` expressions",
    -                        range_.keywords[0],
    -                    )
    -
    -                validate_expected_type(args[0], IntegerT.any())
    -                type_list = get_common_types(*args)
    -                if not isinstance(args[0], vy_ast.Constant):
    -                    # range(x, x + CONSTANT)
    -                    if not isinstance(args[1], vy_ast.BinOp) or not isinstance(
    -                        args[1].op, vy_ast.Add
    -                    ):
    -                        raise StructureException(
    -                            "Second element must be the first element plus a literal value", args[0]
    -                        )
    -                    if not vy_ast.compare_nodes(args[0], args[1].left):
    -                        raise StructureException(
    -                            "First and second variable must be the same", args[1].left
    -                        )
    -                    if not isinstance(args[1].right, vy_ast.Int):
    -                        raise InvalidLiteral("Literal must be an integer", args[1].right)
    -                    if args[1].right.value < 1:
    -                        raise StructureException(
    -                            f"For loop has invalid number of iterations ({args[1].right.value}),"
    -                            " the value must be greater than zero",
    -                            args[1].right,
    -                        )
    -                else:
    -                    # range(CONSTANT, CONSTANT)
    -                    if not isinstance(args[1], vy_ast.Int):
    -                        raise InvalidType("Value must be a literal integer", args[1])
    -                    validate_expected_type(args[1], IntegerT.any())
    -                    if args[0].value >= args[1].value:
    -                        raise StructureException("Second value must be > first value", args[1])
    -
    -                if not type_list:
    -                    raise TypeMismatch("Iterator values are of different types", node.iter)
    +            type_list = _analyse_range_call(node.iter)
     
             else:
                 # iteration over a variable or literal list
    @@ -490,8 +425,8 @@ def visit_For(self, node):
     
                     try:
                         with NodeMetadata.enter_typechecker_speculation():
    -                        for n in node.body:
    -                            self.visit(n)
    +                        for stmt in node.body:
    +                            self.visit(stmt)
                     except (TypeMismatch, InvalidOperation) as exc:
                         for_loop_exceptions.append(exc)
                     else:
    @@ -801,3 +736,41 @@ def visit_IfExp(self, node: vy_ast.IfExp, typ: VyperType) -> None:
             self.visit(node.body, typ)
             validate_expected_type(node.orelse, typ)
             self.visit(node.orelse, typ)
    +
    +
    +def _analyse_range_call(node: vy_ast.Call) -> list[VyperType]:
    +    """
    +    Check that the arguments to a range() call are valid.
    +    :param node: call to range()
    +    :return: None
    +    """
    +    validate_call_args(node, (1, 2), kwargs=["bound"])
    +    kwargs = {s.arg: s.value for s in node.keywords or []}
    +    start, end = (vy_ast.Int(value=0), node.args[0]) if len(node.args) == 1 else node.args
    +
    +    all_args = (start, end, *kwargs.values())
    +    for arg1 in all_args:
    +        validate_expected_type(arg1, IntegerT.any())
    +
    +    type_list = get_common_types(*all_args)
    +    if not type_list:
    +        raise TypeMismatch("Iterator values are of different types", node)
    +
    +    if "bound" in kwargs:
    +        bound = kwargs["bound"]
    +        if not isinstance(bound, vy_ast.Num):
    +            raise StateAccessViolation("Bound must be a literal", bound)
    +        if bound.value <= 0:
    +            raise StructureException("Bound must be at least 1", bound)
    +        if isinstance(start, vy_ast.Num) and isinstance(end, vy_ast.Num):
    +            error = "Please remove the `bound=` kwarg when using range with constants"
    +            raise StructureException(error, bound)
    +    else:
    +        for arg in (start, end):
    +            if not isinstance(arg, vy_ast.Num):
    +                error = "Value must be a literal integer, unless a bound is specified"
    +                raise StateAccessViolation(error, arg)
    +        if end.value <= start.value:
    +            raise StructureException("End must be greater than start", end)
    +
    +    return type_list
    
3de1415ee77a

Merge pull request from GHSA-6r8q-pfpv-7cgj

https://github.com/vyperlang/vyperCharles CooperMay 11, 2023via ghsa
2 files changed · +42 0
  • tests/parser/features/iteration/test_for_range.py+39 0 renamed
    @@ -128,6 +128,45 @@ def foo(a: {typ}) -> {typ}:
         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"""
    
  • vyper/codegen/stmt.py+3 0 modified
    @@ -10,6 +10,7 @@
         IRnode,
         append_dyn_array,
         check_assign,
    +    clamp,
         dummy_node_for_type,
         get_dyn_array_count,
         get_element_ptr,
    @@ -264,6 +265,8 @@ def _parse_For_range(self):
                 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:
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.