Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions tests/functional/codegen/features/test_immutable.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,45 @@ def __init__(to_copy: address):
assert c.b() == 0


# GH issue 5053 (cf. GH issue 3101). dynamic (msize-based) memory allocation
# before any other memory write in the constructor must not return a pointer
# inside the immutables staging region. the constructor takes no arguments so
# that nothing bumps msize before `create_copy_of` allocates its scratch
# buffer; it then reads a not-yet-initialized immutable, which would observe
# scratch garbage (the copied code) if the staging region was clobbered.
def test_immutables_initialized3(get_contract, deploy_blueprint_for, env):
code = """
a0: immutable(uint256[10])
a: public(immutable(uint256))
b: public(uint256)

@deploy
def __init__():
# copy whoever is deploying us - in this test, the factory contract
c: address = create_copy_of(msg.sender)
self.b = a0[1]
a = 12
a0 = empty(uint256[10])
"""

factory_code = """
created_address: public(address)

@external
def test(target: address):
self.created_address = create_from_blueprint(target)
"""

blueprint, ContractFactory = deploy_blueprint_for(code)
factory = get_contract(factory_code)

factory.test(blueprint.address)
c = ContractFactory(factory.created_address())

assert c.b() == 0
assert c.a() == 12


# GH issue 3292
def test_internal_functions_called_by_ctor_location(get_contract):
code = """
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/compiler/venom/test_ctor_msize_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
Test that the Venom deploy code touches the immutables staging region
before the constructor body, so that msize-based dynamic allocation
("memtop") cannot return a pointer inside the uninitialized staging
region (GH issue 5053, cf. GH issue 3101).

The guard is an `istore` of zero to the last word of the immutables
region, emitted before constructor args are decoded. These tests check
the *optimized* deploy output, i.e. that the guard survives all venom
passes and reaches the final assembly.
"""

from vyper.compiler import compile_code
from vyper.compiler.settings import Settings

# constructor does dynamic (msize-based) allocation via raw_call(msg.data)
# *before* any immutable is assigned
CODE_WITH_IMMUTABLES = """
A: immutable(uint256)
B: immutable(uint256[10])

@deploy
def __init__(target: address):
raw_call(target, msg.data)
A = 1
B = empty(uint256[10])
"""

CODE_NO_IMMUTABLES = """
@deploy
def __init__(target: address):
raw_call(target, msg.data)
"""

IMMUTABLES_LEN = 11 * 32 # A + B


def _compile(source):
settings = Settings(experimental_codegen=True)
out = compile_code(source, settings=settings, output_formats=["ir", "asm"])
return str(out["ir"]), out["asm"]


def test_ctor_msize_guard_in_deploy_ir():
deploy_ir, _ = _compile(CODE_WITH_IMMUTABLES)

# the deploy IR must contain the memtop (msize) allocation for
# raw_call(msg.data), with an istore guard touching the immutables
# region before it. (all immutable assignments come after the
# raw_call, so the only istore before memtop is the guard.)
assert "memtop" in deploy_ir
assert "istore" in deploy_ir
assert deploy_ir.index("istore") < deploy_ir.index("memtop")


def test_ctor_msize_guard_in_asm():
_, asm = _compile(CODE_WITH_IMMUTABLES)

# the guard mstores to the last word of the immutables region
# (IMMUTABLES_LEN - 32 = 320 = 0x140) before MSIZE is used for
# dynamic allocation
guard_ofst = f"0x{IMMUTABLES_LEN - 32:04x}"
assert f"PUSH2 {guard_ofst}" in asm
assert asm.index(f"PUSH2 {guard_ofst}") < asm.index("MSIZE")
assert asm.index("MSTORE") < asm.index("MSIZE")


def test_no_ctor_msize_guard_without_immutables():
deploy_ir, asm = _compile(CODE_NO_IMMUTABLES)

# no immutables -> no staging region to protect -> no guard
assert "memtop" in deploy_ir
assert "istore" not in deploy_ir
# nothing writes memory before the dynamic allocation
assert "MSTORE" not in asm[: asm.index("MSIZE")]
18 changes: 18 additions & 0 deletions vyper/codegen_venom/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,24 @@ def _generate_constructor(
# never reuses this region for temporary allocas.
builder.ctx.mem_allocator.add_global(imm_alloc)

# Force msize to be initialized past the end of the immutables
# section, so that builtins which use msize ("memtop") for dynamic
# memory allocation cannot return a pointer inside the
# uninitialized immutables staging region (cf. GH issue 3101 and
# the equivalent `iload` guard in legacy `codegen/module.py`).
# note mstore X touches bytes from X to X+32, and msize rounds up
# to the nearest 32, so touching `immutables_len - 32` guarantees
# `msize >= immutables_len`.
# memory is zero-initialized here (this is the first memory write
# in the deploy code), so storing zero is a no-op write. we use
# `istore` (not `mload`/`mstore`) so the guard cannot be removed
# by optimizer passes: `istore` is volatile and carries the
# IMMUTABLES effect, which memory dead store elimination skips.
touch_ptr = builder.add(
codegen_ctx.immutables_alloca, IRLiteral(max(0, immutables_len - 32))
)
builder.istore(touch_ptr, IRLiteral(0))

# Register constructor args from DATA section (not calldata)
# Constructor args are appended to the deploy code
_register_constructor_args(codegen_ctx, func_t)
Expand Down