CVE-2023-42441 (published 2023-09-18T21:16Z)

Vyper is a Pythonic Smart Contract Language for the Ethereum Virtual Machine (EVM). Starting in version 0.2.9 and prior to version 0.3.10, locks of the type `@nonreentrant("")` or `@nonreentrant(\'\')` do not produce reentrancy checks at runtime. This issue is fixed in version 0.3.10. As a workaround, ensure the lock name is a non-empty string.

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-667
cwe_name: Improper Locking
description: The product does not properly acquire or release a lock on a resource, leading to unexpected resource state changes and behaviors.
extended_description: 
	   Locking is a type of synchronization behavior that ensures that multiple independently-operating processes or threads do not interfere with each other when accessing the same resource. All processes/threads are expected to follow the same steps for locking. If these steps are not followed precisely - or if no locking is done at all - then another process/thread could modify the shared resource in a way that is not visible or predictable to the original process.  This can lead to data or memory corruption, denial of service, etc.
         

Fix information:
repo_url: https://github.com/vyperlang/vyper
commit_details: repo_url: https://github.com/vyperlang/vyper
author: Charles Cooper
author_date: 2023-09-15 18:01:03-04:00
msg: fix: only allow valid identifiers to be nonreentrant keys (#3605)

disallow invalid identifiers like `" "`, `"123abc"` from being keys for
non-reentrant locks.

this commit also refactors the `validate_identifiers` helper function to
be in the `ast/` subdirectory, and slightly improves the VyperException
constructor by allowing None (optional) annotations.
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_structure_exception.py
change_type: ModificationType.MODIFY
diff: @@ -56,9 +56,26 @@ struct X:
     """,
     """
 @external
-@nonreentrant("B")
-@nonreentrant("C")
-def double_nonreentrant():
+@nonreentrant(" ")
+def invalid_nonreentrant_key():
+    pass
+    """,
+    """
+@external
+@nonreentrant("")
+def invalid_nonreentrant_key():
+    pass
+    """,
+    """
+@external
+@nonreentrant("123")
+def invalid_nonreentrant_key():
+    pass
+    """,
+    """
+@external
+@nonreentrant("!123abcd")
+def invalid_nonreentrant_key():
     pass
     """,
     """

programming_language: Python
filename: test_nonreentrant.py
change_type: ModificationType.MODIFY
diff: @@ -142,7 +142,7 @@ def set_callback(c: address):
 
 @external
 @payable
-@nonreentrant('default')
+@nonreentrant("lock")
 def protected_function(val: String[100], do_callback: bool) -> uint256:
     self.special_value = val
     _amount: uint256 = msg.value
@@ -166,7 +166,7 @@ def unprotected_function(val: String[100], do_callback: bool):
 
 @external
 @payable
-@nonreentrant('default')
+@nonreentrant("lock")
 def __default__():
     pass
     """

programming_language: Python
filename: test_call_graph_stability.py
change_type: ModificationType.MODIFY
diff: @@ -6,8 +6,8 @@ import pytest
 from hypothesis import given, settings
 
 import vyper.ast as vy_ast
+from vyper.ast.identifiers import RESERVED_KEYWORDS
 from vyper.compiler.phases import CompilerData
-from vyper.semantics.namespace import RESERVED_KEYWORDS
 
 
 def _valid_identifier(attr):

programming_language: Python
filename: test_identifier_naming.py
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,10 @@
 import pytest
 
 from vyper.ast.folding import BUILTIN_CONSTANTS
+from vyper.ast.identifiers import RESERVED_KEYWORDS
 from vyper.builtins.functions import BUILTIN_FUNCTIONS
 from vyper.codegen.expr import ENVIRONMENT_VARIABLES
 from vyper.exceptions import NamespaceCollision, StructureException, SyntaxException
-from vyper.semantics.namespace import RESERVED_KEYWORDS
 from vyper.semantics.types.primitives import AddressT
 
 BUILTIN_CONSTANTS = set(BUILTIN_CONSTANTS.keys())

programming_language: Python
filename: identifiers.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,111 @@
+import re
+
+from vyper.exceptions import StructureException
+
+
+def validate_identifier(attr, ast_node=None):
+    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
+        raise StructureException(f"'{attr}' contains invalid character(s)", ast_node)
+    if attr.lower() in RESERVED_KEYWORDS:
+        raise StructureException(f"'{attr}' is a reserved keyword", ast_node)
+
+
+# https://docs.python.org/3/reference/lexical_analysis.html#keywords
+# note we don't technically need to block all python reserved keywords,
+# but do it for hygiene
+_PYTHON_RESERVED_KEYWORDS = {
+    "False",
+    "None",
+    "True",
+    "and",
+    "as",
+    "assert",
+    "async",
+    "await",
+    "break",
+    "class",
+    "continue",
+    "def",
+    "del",
+    "elif",
+    "else",
+    "except",
+    "finally",
+    "for",
+    "from",
+    "global",
+    "if",
+    "import",
+    "in",
+    "is",
+    "lambda",
+    "nonlocal",
+    "not",
+    "or",
+    "pass",
+    "raise",
+    "return",
+    "try",
+    "while",
+    "with",
+    "yield",
+}
+_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}
+
+# Cannot be used for variable or member naming
+RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
+    # decorators
+    "public",
+    "external",
+    "nonpayable",
+    "constant",
+    "immutable",
+    "transient",
+    "internal",
+    "payable",
+    "nonreentrant",
+    # "class" keywords
+    "interface",
+    "struct",
+    "event",
+    "enum",
+    # EVM operations
+    "unreachable",
+    # special functions (no name mangling)
+    "init",
+    "_init_",
+    "___init___",
+    "____init____",
+    "default",
+    "_default_",
+    "___default___",
+    "____default____",
+    # more control flow and special operations
+    "range",
+    # more special operations
+    "indexed",
+    # denominations
+    "ether",
+    "wei",
+    "finney",
+    "szabo",
+    "shannon",
+    "lovelace",
+    "ada",
+    "babbage",
+    "gwei",
+    "kwei",
+    "mwei",
+    "twei",
+    "pwei",
+    # sentinal constant values
+    # TODO remove when these are removed from the language
+    "zero_address",
+    "empty_bytes32",
+    "max_int128",
+    "min_int128",
+    "max_decimal",
+    "min_decimal",
+    "max_uint256",
+    "zero_wei",
+}

programming_language: Python
filename: exceptions.py
change_type: ModificationType.MODIFY
diff: @@ -54,7 +54,9 @@ class _BaseVyperException(Exception):
             # support older exceptions that don't annotate - remove this in the future!
             self.lineno, self.col_offset = items[0][:2]
         else:
-            self.annotations = items
+            # strip out None sources so that None can be passed as a valid
+            # annotation (in case it is only available optionally)
+            self.annotations = [k for k in items if k is not None]
 
     def with_annotation(self, *annotations):
         """

programming_language: Python
filename: namespace.py
change_type: ModificationType.MODIFY
diff: @@ -1,12 +1,7 @@
 import contextlib
-import re
-
-from vyper.exceptions import (
-    CompilerPanic,
-    NamespaceCollision,
-    StructureException,
-    UndeclaredDefinition,
-)
+
+from vyper.ast.identifiers import validate_identifier
+from vyper.exceptions import CompilerPanic, NamespaceCollision, UndeclaredDefinition
 from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
 
 
@@ -121,111 +116,3 @@ def override_global_namespace(ns):
     finally:
         # unclobber
         _namespace = tmp
-
-
-def validate_identifier(attr):
-    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
-        raise StructureException(f"'{attr}' contains invalid character(s)")
-    if attr.lower() in RESERVED_KEYWORDS:
-        raise StructureException(f"'{attr}' is a reserved keyword")
-
-
-# https://docs.python.org/3/reference/lexical_analysis.html#keywords
-# note we don't technically need to block all python reserved keywords,
-# but do it for hygiene
-_PYTHON_RESERVED_KEYWORDS = {
-    "False",
-    "None",
-    "True",
-    "and",
-    "as",
-    "assert",
-    "async",
-    "await",
-    "break",
-    "class",
-    "continue",
-    "def",
-    "del",
-    "elif",
-    "else",
-    "except",
-    "finally",
-    "for",
-    "from",
-    "global",
-    "if",
-    "import",
-    "in",
-    "is",
-    "lambda",
-    "nonlocal",
-    "not",
-    "or",
-    "pass",
-    "raise",
-    "return",
-    "try",
-    "while",
-    "with",
-    "yield",
-}
-_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}
-
-# Cannot be used for variable or member naming
-RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
-    # decorators
-    "public",
-    "external",
-    "nonpayable",
-    "constant",
-    "immutable",
-    "transient",
-    "internal",
-    "payable",
-    "nonreentrant",
-    # "class" keywords
-    "interface",
-    "struct",
-    "event",
-    "enum",
-    # EVM operations
-    "unreachable",
-    # special functions (no name mangling)
-    "init",
-    "_init_",
-    "___init___",
-    "____init____",
-    "default",
-    "_default_",
-    "___default___",
-    "____default____",
-    # more control flow and special operations
-    "range",
-    # more special operations
-    "indexed",
-    # denominations
-    "ether",
-    "wei",
-    "finney",
-    "szabo",
-    "shannon",
-    "lovelace",
-    "ada",
-    "babbage",
-    "gwei",
-    "kwei",
-    "mwei",
-    "twei",
-    "pwei",
-    # sentinal constant values
-    # TODO remove when these are removed from the language
-    "zero_address",
-    "empty_bytes32",
-    "max_int128",
-    "min_int128",
-    "max_decimal",
-    "min_decimal",
-    "max_uint256",
-    "zero_wei",
-}

programming_language: Python
filename: base.py
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Tuple, Union
 
 from vyper import ast as vy_ast
 from vyper.abi_types import ABIType
+from vyper.ast.identifiers import validate_identifier
 from vyper.exceptions import (
     CompilerPanic,
     InvalidLiteral,
@@ -12,7 +13,6 @@ from vyper.exceptions import (
     UnknownAttribute,
 )
 from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
-from vyper.semantics.namespace import validate_identifier
 
 
 # Some fake type with an overridden `compare_type` which accepts any RHS

programming_language: Python
filename: function.py
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ from functools import cached_property
 from typing import Any, Dict, List, Optional, Tuple
 
 from vyper import ast as vy_ast
+from vyper.ast.identifiers import validate_identifier
 from vyper.ast.validation import validate_call_args
 from vyper.exceptions import (
     ArgumentException,
@@ -220,7 +221,10 @@ class ContractFunctionT(VyperType):
                         msg = "Nonreentrant decorator disallowed on `__init__`"
                         raise FunctionDeclarationException(msg, decorator)
 
-                    kwargs["nonreentrant"] = decorator.args[0].value
+                    nonreentrant_key = decorator.args[0].value
+                    validate_identifier(nonreentrant_key, decorator.args[0])
+
+                    kwargs["nonreentrant"] = nonreentrant_key
 
                 elif isinstance(decorator, vy_ast.Name):
                     if FunctionVisibility.is_valid_value(decorator.id):

programming_language: Python