Skip to content

Commit 219b49c

Browse files
fix[lang]: fix invalid memory read in raw_create (#4624)
`raw_create` could issue an invalid memory read, if the `value` kwarg modifies the `initcode` argument (e.g., `DynArray.pop()`). note that the invalid read was not observable in userspace, the program would still behave correctly because the memory could not have been written to in between the invalidation and the read however, to be extra safe, this commit performs a memory copy if the overlap is detected. this commit checks for overlap and issues a memory copy if necessary. note that the overlap analysis also checks for calls, which currently aren't problematic - the initcode is in memory, which can't be affected by a reentrant call. --------- Co-authored-by: Charles Cooper <[email protected]>
1 parent 11522b8 commit 219b49c

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

tests/functional/builtins/codegen/test_create_functions.py

+56
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,30 @@ def deploy_from_memory() -> address:
785785
assert env.get_code(res) == runtime
786786

787787

788+
# `initcode` and `value` arguments overlap
789+
def test_raw_create_memory_overlap(get_contract, env):
790+
to_deploy_code = """
791+
foo: public(uint256)
792+
"""
793+
794+
out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
795+
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
796+
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))
797+
798+
deployer_code = """
799+
@external
800+
def deploy_from_calldata(s: Bytes[49152]) -> address:
801+
v: DynArray[Bytes[49152], 2] = [s]
802+
x: address = raw_create(v[0], value = 0 if v.pop() == b'' else 0, revert_on_failure=False)
803+
return x
804+
"""
805+
806+
deployer = get_contract(deployer_code)
807+
808+
res = deployer.deploy_from_calldata(initcode)
809+
assert env.get_code(res) == runtime
810+
811+
788812
def test_raw_create_double_eval(get_contract, env):
789813
to_deploy_code = """
790814
foo: public(uint256)
@@ -1095,6 +1119,8 @@ def __init__(arg: uint256):
10951119
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
10961120
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))
10971121

1122+
# the implementation of `raw_create` firstly caches
1123+
# `value` and then `salt`, here the source order is `salt` then `value`
10981124
deployer_code = """
10991125
c: Bytes[1024]
11001126
salt: bytes32
@@ -1122,3 +1148,33 @@ def deploy_from_calldata(s: Bytes[1024], arg: uint256, salt: bytes32, value_: ui
11221148
assert HexBytes(res) == create2_address_of(deployer.address, salt, initcode)
11231149
assert env.get_code(res) == runtime
11241150
assert env.get_balance(res) == value
1151+
1152+
1153+
# test vararg and kwarg order of evaluation
1154+
# test fails because `value` gets evaluated
1155+
# before the 1st vararg which doesn't follow
1156+
# source code order
1157+
@pytest.mark.xfail(raises=AssertionError)
1158+
def test_raw_create_eval_order(get_contract):
1159+
code = """
1160+
a: public(uint256)
1161+
1162+
@deploy
1163+
def __init__():
1164+
initcode: Bytes[100] = b"a"
1165+
res: address = raw_create(
1166+
initcode, self.test1(), value=self.test2(), revert_on_failure=False
1167+
)
1168+
1169+
@internal
1170+
def test1() -> uint256:
1171+
self.a = 1
1172+
return 1
1173+
1174+
@internal
1175+
def test2() -> uint256:
1176+
self.a = 2
1177+
return 2
1178+
"""
1179+
c = get_contract(code)
1180+
assert c.a() == 2

vyper/builtins/functions.py

+8
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,14 @@ def _build_create_IR(self, expr, args, context, value, salt, revert_on_failure):
16021602
initcode = args[0]
16031603
ctor_args = args[1:]
16041604

1605+
if any(potential_overlap(initcode, other) for other in ctor_args + [value, salt]):
1606+
# value or salt could be expressions which trample the initcode
1607+
# buffer. cf. test_raw_create_memory_overlap
1608+
# note that potential_overlap is overly conservative, since it
1609+
# checks for the existence of calls (which are not applicable
1610+
# here, since `initcode` is guaranteed to be in memory).
1611+
initcode = create_memory_copy(initcode, context)
1612+
16051613
# encode the varargs
16061614
to_encode = ir_tuple_from_args(ctor_args)
16071615
type_size_bound = to_encode.typ.abi_type.size_bound()

0 commit comments

Comments
 (0)