vyperlang / vyper

Pythonic Smart Contract Language for the EVM
https://vyperlang.org
Other
4.81k stars 788 forks source link

Builtins that access literal lists cannot be compiled #4140

Open cyberthirst opened 2 months ago

cyberthirst commented 2 months ago

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.

trocher commented 2 months ago

related to https://github.com/vyperlang/vyper/issues/3632