Skip to content

GHSA-h33q-mhmp-8p67 CVE-2025-27104 PYSEC-2025-30

Vyper has a double eval in For List Iter

High PyPI
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.45%
Chance of exploitation in the next 30 days. Higher than 38.86% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-662 Improper Synchronization
Fix
0.4.1
Upgrade vyper to this version or later

Description

Multiple evaluation of a single expression is possible in the iterator target of a for loop. While the iterator expression cannot produce multiple writes, it can consume side effects produced in the loop body (e.g. read a storage variable updated in the loop body) and thus lead to unexpected program behavior. Specifically, reads in iterators which contain an ifexp (e.g. for s: uint256 in ([read(), read()] if True else [])) may interleave reads with writes in the loop body.

The fix is tracked in https://github.com/vyperlang/vyper/pull/4488.

Vulnerability Details

Vyper for loops allow two kinds of iterator targets, namely the range() builtin and an iterable type, like SArray and DArray.

During codegen, iterable lists are required to not produce any side-effects (in the following code, range_scope forces iter_list to be parsed in a constant context, which is checked against is_constant).

def _parse_For_list(self):
    with self.context.range_scope():
        iter_list = Expr(self.stmt.iter, self.context).ir_node
    ...

def range_scope(self):
    prev_value = self.in_range_expr
    self.in_range_expr = True
    yield
    self.in_range_expr = prev_value

def is_constant(self):
    return self.constancy is Constancy.Constant or self.in_range_expr

However, this does not prevent the iterator from consuming side effects provided by the body of the loop. For dynamic arrays, the compiler simply panics:

x: DynArray[uint256, 3]

@external
def test():
    for i: uint256 in (self.usesideeffect() if True else self.usesideeffect()):
        pass

@view
def usesideeffect() -> DynArray[uint256, 3]:
    return self.x

For SArrays on the other hand, iter_list is instantiated in the body of a repeat ir, so it can be evaluated several times.

Here are three illustrating examples. In the first example, the following test case pre-evaluates the iter list and stores the result to a temporary list in memory. So the list is only evaluated once, before entry into the loop body, and the log output will be 0, 0, 0.

event I:
    i: uint256

x: uint256

@deploy
def __init__():
    self.x = 0

@external
def test():
    for i: uint256 in [self.usesideeffect(), self.usesideeffect(), self.usesideeffect()]:
        self.x += 1
        log I(i)

@view
def usesideeffect() -> uint256:
    return self.x

However, in the next two examples, because the iterator target is not a list literal, it will be evaluated in the loop body. In the second example, iter_list is an ifexp, thus it will be evaluated lazily in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.

event I:
    i: uint256

x: uint256

@deploy
def __init__():
    self.x = 0

@external
def test():
    for i: uint256 in ([self.usesideeffect(), self.usesideeffect(), self.usesideeffect()] if True else self.otherclause()):
        self.x += 1
        log I(i)

@view
def usesideeffect() -> uint256:
    return self.x

@view
def otherclause() -> uint256[3]:
    return [0, 0, 0]

In the third example, iter_list is also an ifexp, thus it will only be evaluated in the loop body. The log output will be 0, 1, 2 due to consumption of side effects.

event I:
    i: uint256

x: uint256[3]

@deploy
def __init__():
    self.x = [0, 0, 0]

@external
def test():
    for i: uint256 in (self.usesideeffect() if True else self.otherclause()):
        self.x[0] += 1
        self.x[1] += 1
        self.x[2] += 1
        log I(i)

@view
def usesideeffect() -> uint256[3]:
    return self.x

@view
def otherclause() -> uint256[3]:
    return [0, 0, 0]

Affected versions

PyPI

PackageVulnerableFixed in
vyper
< 0.4.1
0.4.1

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

vyper PyPI → 0.4.1

pip
pip install --upgrade "vyper>=0.4.1"
poetry
poetry add "vyper>=0.4.1"
uv
uv add "vyper>=0.4.1"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
Low
User interaction
None
Confidentiality (vulnerable system)
Low
Integrity (vulnerable system)
Low
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-662: Improper Synchronization

The product utilizes multiple threads, processes, components, or systems to allow temporary access to a shared resource that can only be exclusive to one process at a time, but it does not properly synchronize these actions, which might cause simultaneous accesses of this resource by multiple threads or processes.

How to prevent it
  • Implementation: Use industry standard APIs to synchronize your code.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-2r3x-4mrv-mcxf
Vyper: Memory corruption using function calls within tuples / nested calls
Medium Also affects vyper
GHSA-4v7v-gqf9-ww2g
Vyper: Call stack corruption when passing complex type containing non-base type members as argument
Medium Also affects vyper
GHSA-vg88-3v92-rjx2
Vyper: Return inside for loop more than 1 level deep
Low Also affects vyper
GHSA-3vcg-j39x-cwfm
Vyper's `slice()` may elide side-effects when output length is 0
Low Also affects vyper
GHSA-qhr6-mgqr-mchm
Vyper's `concat()` builtin may elide side-effects for zero-length arguments
Low Also affects vyper
GHSA-4w26-8p97-f4jp
AugAssign evaluation order causing OOB write within the object in Vyper
Critical Also affects vyper
GHSA-2p94-8669-xg86
Vyper's sqrt doesn't define rounding behavior
High Also affects vyper
GHSA-vgf2-gvx8-xwc3
Vyper Does Not Check the Success of Certain Precompile Calls
High Also affects vyper
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor