vyper
vyper copied to clipboard
Builtins that access literal lists cannot be compiled
Submitted by obront, Bauchibred, DarkTower . Selected submission by: obront.
Relevant GitHub Links
https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/builtins/functions.py#L460-L463
https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/builtins/_signatures.py#L82-L103
https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/semantics/analysis/utils.py#L527
Summary
When types are validated for literal lists passed to builtin functions, we perform the following check:
if not isinstance(expected, (DArrayT, SArrayT)):
However, in this scenario, expected is the type class, not an instance, so it always fails. As a result, the compilation fails.
Vulnerability Details
We will use the builtin len() function to demonstrate this issue.
The len() function accepts a single argument, which can be either a string, byte array or dynamic array:
_inputs = [("b", (StringT.any(), BytesT.any(), DArrayT.any()))]
All builtin functions implement the BuiltinFunction class, which calls the _validate_arg_types() function, which calls self._validate_single() for all arguments.
In the case of the len() function being called with a literal list, the argument passed to _validate_single() is the list node, and the expected type is a tuple of the allowed type classes:
(<class 'vyper.semantics.types.bytestrings.StringT'>, <class 'vyper.semantics.types.bytestrings.BytesT'>, <class 'vyper.semantics.types.subscriptable.DArrayT'>)
This calls the validate_expected_type(), where the given_types returns all the possible types for the literal list.
In the event that the node is a literal list, we go down this code path:
# if it's a literal list, validate: expected contains array, lengths match, each item matches
if isinstance(node, vy_ast.List):
# special case - for literal arrays we individually validate each item
for expected in expected_type:
if not isinstance(expected, (DArrayT, SArrayT)):
continue
if _validate_literal_array(node, expected):
return
As we can see, this checks that isinstance(expected, (DArrayT, SArrayT)). Only when this is the case does it proceed to the _validate_literal_array() function, which allows us to return safely without an error.
Unfortunately, isinstance() tells us if an instance fits a given type. But expected is not an instance — it is the type class itself. As a result, this check will always fail, and the compilation will fail.
Proof of Concept
The following Vyper contracts will fail to compile due to this error:
# @version ^0.3.9
x: uint256
@external
def __init__():
self.x = len([1, 2, 3])
# @version ^0.3.9
number: public(uint256)
exampleList: constant(DynArray[uint256, 3]) = [1, 2, 3]
@external
def __init__():
self.number = len(exampleList)
Impact
Contracts that include literal lists as arguments to builtin functions will fail to compile.
Tools Used
Manual Review
Recommendations
In validate_expected_type(), adjust the check to ensure that the expected type matches with DArrayT or SArrayT, rather than requiring it to be an instance of it.
related to https://github.com/vyperlang/vyper/issues/3632