Repository: vyperlang/vyper

Issue:
Access to _placeholder_# generated variables can cause problems
### Version Information

* vyper Version (output of `vyper --version`): 0.1.0b12+commit.ab39d4e

### What's your issue about?

The compiler uses `_placeholder_1`, `_placeholder_2`, etc. variables. Users are able to read from and write to those variables, which can cause significant problems.

Below is the most dramatic example I came up with. Here, the callback pointer is overwritten, causing execution to jump to the wrong place. Both `getNumber()` and `getDoubleNumber()` return `42`. (If you comment out the assignment to `_placeholder_1`, `getDoubleNumber()` returns `84` as expected:

```python
@private
@constant
def meaningOfLife() -> uint256:
    _placeholder_1 = 215
    return 42
    
@public
@constant
def getNumber() -> uint256:
    return self.meaningOfLife()
    
@public
@constant
def getDoubleNumber() -> uint256:
    return self.meaningOfLife() * 2
```

(Note that `215` is specifically the location for the right `JUMPDEST`. If you use a different compiler version, that location might change, so you're likely to see a revert instead.)

### How can it be fixed?

The most obvious fix is probably to disallow users referencing variables that start with `_placeholder_`. They shouldn't be able to declare them or access them.  When fixing this, note that there may be other special variable names that need to be protected in the same way.

A better alternative might be to avoid collisions altogether. It's not clear to me why placeholder variables need names at all, or why those names have to exist in the same namespace as user-defined variables. I simply don't know the compiler code well enough to suggest a better fix, but I'm hoping there can be something more robust than naming conventions.


Discussion:

Resolving patch:
diff --git a/vyper/parser/context.py b/vyper/parser/context.py
--- a/vyper/parser/context.py
+++ b/vyper/parser/context.py
@@ -144,10 +144,19 @@ def is_valid_varname(self, name, pos):
                 raise VariableDeclarationException(f"Duplicate variable name: {name}", name)
         return True
 
+    def _mangle(self, name):
+        # ensure it is not possible to use an internal variable in source
+        # code because source code identifiers cannot start with `#`
+        return '#internal' + name
+
     # TODO location info for errors
     # Add a new variable
-    def new_variable(self, name, typ, pos=None):
-        if self.is_valid_varname(name, pos):
+    def new_variable(self, name, typ, internal_var=False, pos=None):
+        # mangle internally generated variables so they cannot collide
+        # with user variables.
+        if internal_var:
+            name = self._mangle(name)
+        if internal_var or self.is_valid_varname(name, pos):
             var_size = 32 * get_size_of_type(typ)
             var_pos, _ = self.memory_allocator.increase_memory(var_size)
             self.vars[name] = VariableRecord(
@@ -159,11 +168,14 @@ def new_variable(self, name, typ, pos=None):
             )
             return var_pos
 
+    def new_internal_variable(self, name, typ, pos=None):
+        return self.new_variable(name, typ, pos=pos, internal_var=True)
+
     # Add an anonymous variable (used in some complex function definitions)
     def new_placeholder(self, typ):
         name = '_placeholder_' + str(self.placeholder_count)
         self.placeholder_count += 1
-        return self.new_variable(name, typ)
+        return self.new_internal_variable(name, typ)
 
     def get_next_mem(self):
         return self.memory_allocator.get_next_mem()
diff --git a/vyper/parser/events.py b/vyper/parser/events.py
--- a/vyper/parser/events.py
+++ b/vyper/parser/events.py
@@ -224,7 +224,7 @@ def pack_logging_data(expected_data, args, context, pos):
                         f"Data input bytes are to big: {len(arg.s)} {typ}", pos
                     )
 
-            tmp_variable = context.new_variable(
+            tmp_variable = context.new_internal_variable(
                 f'_log_pack_var_{arg.lineno}_{arg.col_offset}',
                 source_lll.typ,
             )
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -629,7 +629,11 @@ def parse_for_list(self):
             varname,
             BaseType(subtype, unit=iter_list_node.typ.subtype.unit),
         )
-        i_pos = self.context.new_variable('_index_for_' + varname, BaseType(subtype))
+        i_pos_raw_name = '_index_for_' + varname
+        i_pos = self.context.new_internal_variable(
+                i_pos_raw_name,
+                BaseType(subtype),
+                )
         self.context.forvars[varname] = True
 
         # Is a list that is already allocated to memory.
@@ -704,7 +708,10 @@ def parse_for_list(self):
                 )
 
         del self.context.vars[varname]
-        del self.context.vars['_index_for_' + varname]
+        # this kind of open access to the vars dict should be disallowed.
+        # we should use member functions to provide an API for these kinds
+        # of operations.
+        del self.context.vars[self.context._mangle(i_pos_raw_name)]
         del self.context.forvars[varname]
         return o
 


Test patch: