CVE-2023-32675 (published 2023-05-19T20:15Z)

Vyper is a pythonic Smart Contract Language for the ethereum virtual machine. In contracts with more than one regular nonpayable function, it is possible to send funds to the default function, even if the default function is marked `nonpayable`. This applies to contracts compiled with vyper versions prior to 0.3.8. This issue was fixed by the removal of the global `calldatasize` check in commit `02339dfda`. Users are advised to upgrade to version 0.3.8. Users unable to upgrade should avoid use of nonpayable default functions.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/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: emc415
author_date: 2023-01-20 08:06:25-08:00
msg: refactor: optimize calldatasize check (#3104)

this changes the calldatasize check from a global/contract-entry check
to a function-local check - the calldatasize check only needs to be
present if a selector is 0. this maintains the invariant protecting
against the bug in #1603 while being more efficient.

Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
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_init.py
change_type: ModificationType.MODIFY
diff: @@ -15,12 +15,13 @@ def __init__(a: uint256):
     assert c.val() == 123
 
     # Make sure the init code does not access calldata
-    opcodes = vyper.compile_code(code, ["opcodes"])["opcodes"].split(" ")
-    ir_return_idx = opcodes.index("JUMP")
+    assembly = vyper.compile_code(code, ["asm"])["asm"].split(" ")
+    ir_return_idx_start = assembly.index("{")
+    ir_return_idx_end = assembly.index("}")
 
-    assert "CALLDATALOAD" in opcodes
-    assert "CALLDATACOPY" not in opcodes[:ir_return_idx]
-    assert "CALLDATALOAD" not in opcodes[:ir_return_idx]
+    assert "CALLDATALOAD" in assembly
+    assert "CALLDATACOPY" not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:]
+    assert "CALLDATALOAD" not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:]
 
 
 def test_init_calls_internal(get_contract, assert_compile_failed, assert_tx_failed):

programming_language: Python
filename: external_function.py
change_type: ModificationType.MODIFY
diff: @@ -123,7 +123,24 @@ def _generate_kwarg_handlers(context: Context, sig: FunctionSignature) -> List[A
 
         ret.append(["goto", sig.external_function_base_entry_label])
 
-        ret = ["if", ["eq", "_calldata_method_id", method_id], ret]
+        method_id_check = ["eq", "_calldata_method_id", method_id]
+
+        # if there is a function whose selector is 0, it won't be distinguished
+        # from the case where nil calldata is supplied, b/c calldataload loads
+        # 0s past the end of physical calldata (cf. yellow paper).
+        # since supplying 0 calldata is expected to trigger the fallback fn,
+        # we check that calldatasize > 0, which distinguishes the 0 selector
+        # from the fallback function "selector"
+        # (equiv. to "all selectors not in the selector table").
+
+        # note: cases where not enough calldata is supplied (besides
+        # calldatasize==0) are not addressed here b/c a calldatasize
+        # well-formedness check is already present in the function body
+        # as part of abi validation
+        if method_id.value == 0:
+            method_id_check = ["and", ["gt", "calldatasize", 0], method_id_check]
+
+        ret = ["if", method_id_check, ret]
         return ret
 
     ret = ["seq"]

programming_language: Python
filename: module.py
change_type: ModificationType.MODIFY
diff: @@ -120,9 +120,6 @@ def _runtime_ir(runtime_functions, all_sigs, global_ctx):
 
     runtime = [
         "seq",
-        # check that calldatasize is at least 4, otherwise
-        # calldataload will load zeros (cf. yellow paper).
-        ["if", ["lt", "calldatasize", 4], ["goto", "fallback"]],
         ["with", "_calldata_method_id", shr(224, ["calldataload", 0]), selector_section],
         close_selector_section,
         ["label", "fallback", ["var_list"], fallback_ir],

programming_language: Python