Repository: vyperlang/vyper

Issue:
Immutable variables can be read before assignment
### Version Information

* vyper Version (output of `vyper --version`): 0.3.6+commit.4a2124d
* OS: osx
* Python Version: 3.9.13
 
### What's your issue about?

Immutable variables can be read before assignment, their memory location during constructor execution is accessed, but that memory is not initialized yet. Its value is not necessarily `empty(type)` since memory could have been written to ephemerally.

This should not compile:

```python
# @version 0.3.6

a:immutable(uint256)
b:uint256

@payable
@external
def __init__(s:address):
    self.b = a
    a = 12
```

A more interesting example where transient memory written during execution of `create_copy_of()` is written into the storage variable `b`:

```python
# @version 0.3.6

a:immutable(uint256)
b:uint256

@payable
@external
def __init__():
    s:address = 0x9D0464996170c6B9e75eED71c68B99dDEDf279e8  #random contract containing code that is copied to memory before redeploying
    c:address = create_copy_of(s)
    self.b = a
    a = 12
```

### How can it be fixed?

check that immutable variables are assigned before usage



Discussion:
probably a good solution is to zero out the immutables section of memory before use. it's a bit hard to track whether an immutable has been explicitly initialized or not

Resolving patch:
diff --git a/vyper/codegen/module.py b/vyper/codegen/module.py
--- a/vyper/codegen/module.py
+++ b/vyper/codegen/module.py
@@ -153,12 +153,31 @@ def generate_ir_for_module(global_ctx: GlobalContext) -> tuple[IRnode, IRnode]:
         init_func_ir = generate_ir_for_function(
             init_function, global_ctx, skip_nonpayable_check=False, is_ctor_context=True
         )
-        deploy_code.append(init_func_ir)
 
         # pass the amount of memory allocated for the init function
         # so that deployment does not clobber while preparing immutables
         # note: (deploy mem_ofst, code, extra_padding)
         init_mem_used = init_function._metadata["type"]._ir_info.frame_info.mem_used
+
+        # force msize to be initialized past the end of immutables section
+        # so that builtins which use `msize` for "dynamic" memory
+        # allocation do not clobber uninitialized immutables.
+        # cf. GH issue 3101.
+        # note mload/iload X touches bytes from X to X+32, and msize rounds up
+        # to the nearest 32, so `iload`ing `immutables_len - 32` guarantees
+        # that `msize` will refer to a memory location of at least
+        # `<immutables_start> + immutables_len` (where <immutables_start> ==
+        # `_mem_deploy_end` as defined in the assembler).
+        # note:
+        #   mload 32 => msize == 64
+        #   mload 33 => msize == 96
+        # assumption in general: (mload X) => msize == ceil32(X + 32)
+        # see py-evm extend_memory: after_size = ceil32(start_position + size)
+        if immutables_len > 0:
+            deploy_code.append(["iload", max(0, immutables_len - 32)])
+
+        deploy_code.append(init_func_ir)
+
         deploy_code.append(["deploy", init_mem_used, runtime, immutables_len])
 
         # internal functions come after everything else


Test patch:
diff --git a/tests/parser/features/test_immutable.py b/tests/parser/features/test_immutable.py
--- a/tests/parser/features/test_immutable.py
+++ b/tests/parser/features/test_immutable.py
@@ -241,6 +241,56 @@ def get_immutable() -> uint256:
     assert c.get_immutable() == n + 2
 
 
+# GH issue 3101
+def test_immutables_initialized(get_contract):
+    dummy_code = """
+@external
+def foo() -> uint256:
+    return 1
+    """
+    dummy_contract = get_contract(dummy_code)
+
+    code = """
+a: public(immutable(uint256))
+b: public(uint256)
+
+@payable
+@external
+def __init__(to_copy: address):
+    c: address = create_copy_of(to_copy)
+    self.b = a
+    a = 12
+    """
+    c = get_contract(code, dummy_contract.address)
+
+    assert c.b() == 0
+
+
+# GH issue 3101, take 2
+def test_immutables_initialized2(get_contract, get_contract_from_ir):
+    dummy_contract = get_contract_from_ir(
+        ["deploy", 0, ["seq"] + ["invalid"] * 600, 0], no_optimize=True
+    )
+
+    # rekt because immutables section extends past allocated memory
+    code = """
+a0: immutable(uint256[10])
+a: public(immutable(uint256))
+b: public(uint256)
+
+@payable
+@external
+def __init__(to_copy: address):
+    c: address = create_copy_of(to_copy)
+    self.b = a
+    a = 12
+    a0 = empty(uint256[10])
+    """
+    c = get_contract(code, dummy_contract.address)
+
+    assert c.b() == 0
+
+
 # GH issue 3292
 def test_internal_functions_called_by_ctor_location(get_contract):
     code = """