CVE-2023-30629 (published 2023-04-24T22:15Z)

Vyper is a Pythonic Smart Contract Language for the ethereum virtual machine. In versions 0.3.1 through 0.3.7, the Vyper compiler generates the wrong bytecode. Any contract that uses the `raw_call` with `revert_on_failure=False` and `max_outsize=0` receives the wrong response from `raw_call`. Depending on the memory garbage, the result can be either `True` or `False`. A patch is available and, as of time of publication, anticipated to be part of Vyper 0.3.8. As a workaround, one may always put  `max_outsize>0`.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-670
cwe_name: Always-Incorrect Control Flow Implementation
description: The code contains a control flow path that does not reflect the algorithm that the path is intended to implement, leading to incorrect behavior any time this path is navigated.
extended_description: This weakness captures cases in which a particular code segment is always incorrect with respect to the algorithm that it is implementing. For example, if a C programmer intends to include multiple statements in a single block but does not include the enclosing braces (CWE-483), then the logic is always incorrect. This issue is in contrast to most weaknesses in which the code usually behaves correctly, except when it is externally manipulated in malicious ways.
         

Fix information:
repo_url: https://github.com/vyperlang/vyper
commit_details: repo_url: https://github.com/vyperlang/vyper
author: Charles Cooper
author_date: 2023-04-24 10:41:02-07:00
msg: Merge pull request from GHSA-w9g2-3w7p-72g9

it was getting tagged as a pointer instead of a stack value during
codegen
repository: repo_url: https://github.com/vyperlang/vyper
repo_name: vyperlang/vyper
description: Pythonic Smart Contract Language for the EVM
repo_language: Python
owner: vyperlang
file_changes: filename: test_raw_call.py
change_type: ModificationType.MODIFY
diff: @@ -296,8 +296,10 @@ baz: int128
 def fail1(should_raise: bool):
     if should_raise:
         raise "fail"
+
 # test both paths for raw_call -
 # they are different depending if callee has or doesn't have returntype
+# (fail2 fails because of staticcall)
 @external
 def fail2(should_raise: bool) -> int128:
     if should_raise:
@@ -320,6 +322,7 @@ def foo(_addr: address, should_raise: bool) -> uint256:
     )
     assert success == (not should_raise)
     return 1
+
 @external
 @view
 def bar(_addr: address, should_raise: bool) -> uint256:
@@ -334,6 +337,19 @@ def bar(_addr: address, should_raise: bool) -> uint256:
     )
     assert success == (not should_raise)
     return 2
+
+# test max_outsize not set case
+@external
+@nonpayable
+def baz(_addr: address, should_raise: bool) -> uint256:
+    success: bool = True
+    success = raw_call(
+        _addr,
+        _abi_encode(should_raise, method_id=method_id("fail1(bool)")),
+        revert_on_failure=False,
+    )
+    assert success == (not should_raise)
+    return 3
     """
 
     target = get_contract(target_source)
@@ -343,6 +359,8 @@ def bar(_addr: address, should_raise: bool) -> uint256:
     assert caller.foo(target.address, False) == 1
     assert caller.bar(target.address, True) == 2
     assert caller.bar(target.address, False) == 2
+    assert caller.baz(target.address, True) == 3
+    assert caller.baz(target.address, False) == 3
 
 
 uncompilable_code = [

programming_language: Python
filename: functions.py
change_type: ModificationType.MODIFY
diff: @@ -1188,7 +1188,9 @@ class RawCall(BuiltinFunction):
 
             if revert_on_failure:
                 typ = bytes_ty
+                # check the call success flag, and store returndata in memory
                 ret_ir = ["seq", check_external_call(call_ir), store_output_size]
+                return IRnode.from_list(ret_ir, typ=typ, location=MEMORY)
             else:
                 typ = TupleT([bool_ty, bytes_ty])
                 ret_ir = [
@@ -1198,16 +1200,22 @@ class RawCall(BuiltinFunction):
                     IRnode.from_list(call_ir, typ=bool_ty),
                     IRnode.from_list(store_output_size, typ=bytes_ty, location=MEMORY),
                 ]
+                # return an IR tuple of call success flag and returndata pointer
+                return IRnode.from_list(ret_ir, typ=typ)
+
+        # max_outsize is 0.
+
+        if not revert_on_failure:
+            # return call flag as stack item
+            typ = bool_ty
+            return IRnode.from_list(call_ir, typ=typ)
 
         else:
-            if revert_on_failure:
-                typ = None
-                ret_ir = check_external_call(call_ir)
-            else:
-                typ = bool_ty
-                ret_ir = call_ir
+            # check the call success flag and don't return anything
+            ret_ir = check_external_call(call_ir)
+            return IRnode.from_list(ret_ir, typ=None)
 
-        return IRnode.from_list(ret_ir, typ=typ, location=MEMORY)
+        raise CompilerPanic("unreachable!")
 
 
 class Send(BuiltinFunction):

programming_language: Python