logo

CVE-2025-27104 vyper

Package

Manager: pip
Name: vyper
Vulnerable Version: >=0 <0.4.1

Severity

Level: Low

CVSS v3.1: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N/E:P/RL:O/RC:R

CVSS v4.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

EPSS: 0.00118 pctl0.31388

Details

Vyper has a double eval in For List Iter 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`). ```python 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: ```vyper 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. ```vyper 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. ```vyper 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. ```vyper 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] ```

Metadata

Created: 2025-02-21T22:43:36Z
Modified: 2025-04-09T20:12:39Z
Source: https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2025/02/GHSA-h33q-mhmp-8p67/GHSA-h33q-mhmp-8p67.json
CWE IDs: ["CWE-662"]
Alternative ID: GHSA-h33q-mhmp-8p67
Finding: F124
Auto approve: 1