vyper: Ternary expressions misread nested arrays with different inner bounds
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
When a ternary selects between nested arrays with different inner bounds, selecting the narrower array can corrupt the result. The compiler reads it using the wider array's memory layout without first converting it. Inner arrays after the first are therefore read at the wrong offsets. This can silently produce incorrect values at all optimization levels. The equivalent if/else assignment gives the correct result.
Details
In Vyper 0.4.3, parse_IfExp (vyper/codegen/expr.py:781-802) returns a pointer to the selected branch, labelled with the ternary's result type. When both branches are in memory, it leaves their layouts unchanged. In the PoC below, each inner array in narrow occupies 96 bytes: a 32-byte length followed by space for two values. The result type reserves 160 bytes per inner array, allowing four values. _get_element_ptr_array in vyper/codegen/core.py uses that wider spacing, so it reads the second inner array 64 bytes past its actual start.
Fixed on master by [1fb7dfe2 (#5236)](https://github.com/vyperlang/vyper/commit/1fb7dfe2). The fix copies branches with incompatible layouts into memory using the result type before returning their pointers.
Affected versions: 0.3.8 (which introduced ternaries) through 0.5.0b1.
PoC
remaining: public(DynArray[DynArray[uint256, 4], 4])
@external
def set(flag: bool, narrow: DynArray[DynArray[uint256, 2], 4], wide: DynArray[DynArray[uint256, 4], 4]):
self.remaining = narrow if flag else wideset(True, [[6, 4], [7, 3]], []) stores [[6, 4], [0, 0, 0]]. The compiler mistakes narrow[1][1], which holds 3, for the second inner array's length, then reads three zero values from unused memory. An if/else assignment stores the expected [[6, 4], [7, 3]]. Returning narrow if flag else wide as the wider type produces the same corrupted result.
Impact
Contracts can silently store or use corrupted values when the misread array lengths remain within bounds, as in this PoC. If a misread length exceeds its bound, the call can revert, depending on how the array is used.
Text from GitHub Advisory Database
Affected versions 1
- vyper, pip>= 0.3.8, <= 0.5.0b1GitHub