From bce1e1d3d7974d372b7e44ba786f65a4bdc2b8b7 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Wed, 21 Aug 2024 14:27:46 -0600 Subject: [PATCH] Fuzzing related findings A collection of fuzzing related updates to the test * Activate functions validation tests, and move to 4750 dir * Add terminal opcode to some tests so they have only one error * Tests found via fuzzing coverage * subcontainer wrong size * subcontainer wrong eof version * EOFCREATE nonexistant container * Many CALLF test covered by inactive 4750 checks Signed-off-by: Danno Ferrin --- .../evmone_exceptions.py | 3 + src/ethereum_test_exceptions/exceptions.py | 4 + .../code_validation_function.py | 189 ------------------ .../eip3540_eof_v1/test_code_validation.py | 101 +--------- .../eip4200_relative_jumps/test_rjump.py | 50 ++++- .../eip4200_relative_jumps/test_rjumpi.py | 4 +- .../eip4750_functions/test_code_validation.py | 161 +++++++++++++++ .../eip6206_jumpf/test_jumpf_execution.py | 6 +- .../eip7620_eof_create/test_eofcreate.py | 21 ++ .../test_subcontainer_validation.py | 49 +++++ 10 files changed, 296 insertions(+), 292 deletions(-) delete mode 100644 tests/prague/eip7692_eof_v1/eip3540_eof_v1/code_validation_function.py create mode 100644 tests/prague/eip7692_eof_v1/eip4750_functions/test_code_validation.py diff --git a/src/ethereum_test_exceptions/evmone_exceptions.py b/src/ethereum_test_exceptions/evmone_exceptions.py index 0a166b80f4..a23e27bf85 100644 --- a/src/ethereum_test_exceptions/evmone_exceptions.py +++ b/src/ethereum_test_exceptions/evmone_exceptions.py @@ -85,6 +85,9 @@ class EvmoneExceptionMapper: ), ExceptionMessage(EOFException.STACK_HEIGHT_MISMATCH, "err: stack_height_mismatch"), ExceptionMessage(EOFException.TOO_MANY_CONTAINERS, "err: too_many_container_sections"), + ExceptionMessage( + EOFException.INVALID_CODE_SECTION_INDEX, "err: invalid_code_section_index" + ), ) def __init__(self) -> None: diff --git a/src/ethereum_test_exceptions/exceptions.py b/src/ethereum_test_exceptions/exceptions.py index a4195b7783..7311dce930 100644 --- a/src/ethereum_test_exceptions/exceptions.py +++ b/src/ethereum_test_exceptions/exceptions.py @@ -730,6 +730,10 @@ class EOFException(ExceptionBase): """ EOF container header has too many sub-containers. """ + INVALID_CODE_SECTION_INDEX = auto() + """ + CALLF Operation referes to a non-existent code section + """ """ diff --git a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/code_validation_function.py b/tests/prague/eip7692_eof_v1/eip3540_eof_v1/code_validation_function.py deleted file mode 100644 index b8026563ed..0000000000 --- a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/code_validation_function.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Code validation of CALLF, RETF opcodes tests -""" - -from typing import List - -from ethereum_test_tools.eof.v1 import Container, Section -from ethereum_test_tools.eof.v1.constants import MAX_CODE_SECTIONS -from ethereum_test_tools.vm.opcode import Opcodes as Op - -VALID: List[Container] = [ - Container( - name="retf_code_input_output", - sections=[ - Section.Code( - code=Op.PUSH0 + Op.CALLF[1] + Op.POP + Op.POP + Op.STOP, - code_inputs=0, - code_outputs=0, - max_stack_height=2, - ), - Section.Code( - code=Op.PUSH0 + Op.RETF, - code_inputs=1, - code_outputs=2, - max_stack_height=2, - ), - ], - ), - Container( - name="stack_height_equal_code_outputs_retf_zero_stop", - sections=[ - Section.Code( - code=Op.CALLF[1] + Op.POP + Op.STOP, - code_inputs=0, - code_outputs=0, - max_stack_height=1, - ), - Section.Code( - code=( - Op.RJUMPI[len(Op.PUSH0) + len(Op.RETF)](Op.ORIGIN) - + Op.PUSH0 - + Op.RETF - + Op.STOP - ), - code_inputs=0, - code_outputs=1, - max_stack_height=1, - ), - ], - ), - Container( - name="callf_max_code_sections_1", - sections=[ - Section.Code( - code=(sum(Op.CALLF[i] for i in range(1, MAX_CODE_SECTIONS)) + Op.STOP), - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - ] - + ( - [ - Section.Code( - code=Op.RETF, - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - ] - * (MAX_CODE_SECTIONS - 1) - ), - ), - Container( - name="callf_max_code_sections_2", - sections=[ - Section.Code( - code=(Op.CALLF[i + 1] + Op.RETF), - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - for i in range(MAX_CODE_SECTIONS - 1) - ] - + [ - Section.Code( - code=Op.RETF, - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - ], - ), -] - -INVALID: List[Container] = [ - Container( - name="function_underflow", - sections=[ - Section.Code( - code=(Op.PUSH0 + Op.CALLF[1] + Op.STOP), - code_inputs=0, - code_outputs=0, - max_stack_height=2, - ), - Section.Code( - code=(Op.POP + Op.POP + Op.RETF), - code_inputs=1, - code_outputs=0, - max_stack_height=2, - ), - ], - validity_error="StackUnderflow", - ), - Container( - name="stack_higher_than_code_outputs", - sections=[ - Section.Code( - code=(Op.STOP), - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ), - Section.Code( - code=(Op.PUSH0 + Op.RETF), - code_inputs=0, - code_outputs=0, - max_stack_height=1, - ), - ], - validity_error="InvalidRetf", - ), - Container( - name="stack_shorter_than_code_outputs", - sections=[ - Section.Code( - code=(Op.STOP), - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ), - Section.Code( - code=(Op.PUSH0 + Op.RETF), - code_inputs=0, - code_outputs=2, - max_stack_height=1, - ), - ], - validity_error="InvalidRetf", - ), - Container( - name="oob_callf_1", - sections=[ - Section.Code( - code=(Op.PUSH0 + Op.CALLF[2] + Op.STOP), - code_inputs=0, - code_outputs=0, - max_stack_height=1, - ), - Section.Code( - code=(Op.POP + Op.POP + Op.RETF), - code_inputs=1, - code_outputs=0, - max_stack_height=2, - ), - ], - validity_error="StackUnderflow", - ), - Container( - name="overflow_code_sections_1", - sections=[ - Section.Code( - code=(Op.CALLF[i + 1] + Op.RETF), - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - for i in range(MAX_CODE_SECTIONS) - ] - + [ - Section.Code( - code=Op.RETF, - code_inputs=0, - code_outputs=0, - max_stack_height=0, - ) - ], - validity_error="InvalidTypeSize", - ), -] diff --git a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py b/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py index 127dd46985..3af9e07cc9 100644 --- a/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py +++ b/tests/prague/eip7692_eof_v1/eip3540_eof_v1/test_code_validation.py @@ -2,41 +2,13 @@ EOF V1 Code Validation tests """ -from typing import Dict, List - import pytest -from ethereum_test_tools import ( - EOA, - Account, - Address, - Alloc, - Environment, - EOFTestFiller, - Transaction, - compute_eofcreate_address, -) -from ethereum_test_tools.eof.v1 import Container, Initcode +from ethereum_test_tools import EOFTestFiller +from ethereum_test_tools.eof.v1 import Container from .. import EOF_FORK_NAME - -# from .code_validation import INVALID as INVALID_CODE -# from .code_validation import VALID as VALID_CODE -# from .code_validation_function import INVALID as INVALID_FN -# from .code_validation_function import VALID as VALID_FN -# from .code_validation_jump import INVALID as INVALID_RJUMP -# from .code_validation_jump import VALID as VALID_RJUMP -from .container import INVALID as INVALID_CONTAINERS -from .container import VALID as VALID_CONTAINERS - -# from .tests_execution_function import VALID as VALID_EXEC_FN - -ALL_VALID = VALID_CONTAINERS -ALL_INVALID = INVALID_CONTAINERS -# ALL_VALID = ( -# VALID_CONTAINERS + VALID_CODE + VALID_RJUMP + VALID_FN + VALID_EXEC_FN -# ) -# ALL_INVALID = INVALID_CONTAINERS + INVALID_CODE + INVALID_RJUMP + INVALID_FN +from .container import INVALID, VALID REFERENCE_SPEC_GIT_PATH = "EIPS/eip-3540.md" REFERENCE_SPEC_VERSION = "8dcb0a8c1c0102c87224308028632cc986a61183" @@ -44,69 +16,6 @@ pytestmark = pytest.mark.valid_from(EOF_FORK_NAME) -@pytest.fixture -def env(): # noqa: D103 - return Environment() - - -@pytest.fixture -def sender(pre: Alloc): # noqa: D103 - return pre.fund_eoa() - - -@pytest.fixture -def create3_init_container(container: Container) -> Initcode: # noqa: D103 - return Initcode(deploy_container=container) - - -@pytest.fixture -def create3_opcode_contract_address( # noqa: D103 - pre: Alloc, - create3_init_container: Initcode, -) -> Address: - return pre.deploy_contract(create3_init_container, address=Address(0x300)) - - -@pytest.fixture -def txs( # noqa: D103 - sender: EOA, - create3_opcode_contract_address: Address, -) -> List[Transaction]: - return [ - Transaction( - to=create3_opcode_contract_address, - gas_limit=100000000, - gas_price=10, - # data=initcode, - protected=False, - sender=sender, - ) - ] - - -@pytest.fixture -def post( # noqa: D103 - create3_init_container: Initcode, - container: Container, - create3_opcode_contract_address: Address, -) -> Dict[Address, Account]: - create_opcode_created_contract_address = compute_eofcreate_address( - create3_opcode_contract_address, - 0, - bytes(create3_init_container.init_container), - ) - - new_account = Account(code=container) - - # Do not expect to create account if it is invalid - if hasattr(new_account, "code") and container.validity_error != "": - return {} - else: - return { - create_opcode_created_contract_address: new_account, - } - - def container_name(c: Container): """ Return the name of the container for use in pytest ids. @@ -119,7 +28,7 @@ def container_name(c: Container): @pytest.mark.parametrize( "container", - ALL_VALID, + VALID, ids=container_name, ) def test_legacy_initcode_valid_eof_v1_contract( @@ -140,7 +49,7 @@ def test_legacy_initcode_valid_eof_v1_contract( @pytest.mark.parametrize( "container", - ALL_INVALID, + INVALID, ids=container_name, ) def test_legacy_initcode_invalid_eof_v1_contract( diff --git a/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjump.py b/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjump.py index 2e0f765500..76210ec841 100644 --- a/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjump.py +++ b/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjump.py @@ -408,7 +408,7 @@ def test_rjump_into_push_1(eof_test: EOFTestFiller, jump: JumpDirection): """EOF1I4200_0011 (Invalid) EOF code containing RJUMP with target PUSH1 immediate""" code = ( Op.PUSH1[1] + Op.RJUMP[-4] if jump == JumpDirection.BACKWARD else Op.RJUMP[1] + Op.PUSH1[1] - ) + ) + Op.STOP eof_test( data=Container( sections=[ @@ -471,7 +471,7 @@ def test_rjump_into_push_n( data_portion_length = int.from_bytes(opcode, byteorder="big") - 0x5F if jump == JumpDirection.FORWARD: offset = data_portion_length if data_portion_end else 1 - code = Op.RJUMP[offset] + opcode[0] + code = Op.RJUMP[offset] + opcode[0] + Op.STOP else: offset = -4 if data_portion_end else -4 - data_portion_length + 1 code = opcode[0] + Op.RJUMP[offset] @@ -696,3 +696,49 @@ def test_rjump_backwards_reference_only( data=container, expect_exception=EOFException.UNREACHABLE_INSTRUCTIONS, ) + + +def test_rjump_backwards_illegal_stack_height( + eof_test: EOFTestFiller, +): + """ + Invalid backward jump, found via fuzzing coverage + """ + eof_test( + data=Container.Code( + code=( + Op.PUSH0 + + Op.RJUMPI[3] + + Op.RJUMP(7) + + Op.PUSH2(0x2015) + + Op.PUSH3(0x015500) + + Op.RJUMP[-10] + ), + max_stack_height=0x24, + ), + expect_exception=EOFException.STACK_HEIGHT_MISMATCH, + ) + + +def test_rjump_backwards_infinite_loop( + eof_test: EOFTestFiller, +): + """ + Infinite loop code containing RJUMP immediate + """ + eof_test( + data=Container( + name="infinite_loop", + sections=[ + Section.Code( + code=Op.PUSH0 + + Op.RJUMPI[3] + + Op.RJUMP[7] + + Op.SSTORE(1, 0x2015) + + Op.STOP + + Op.RJUMP[-10] + ), + Section.Data(data="0xdeadbeef"), + ], + ), + ) diff --git a/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjumpi.py b/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjumpi.py index e8f7d2fc6c..10cfa17521 100644 --- a/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjumpi.py +++ b/tests/prague/eip7692_eof_v1/eip4200_relative_jumps/test_rjumpi.py @@ -605,10 +605,10 @@ def test_rjumpi_into_push_n( data_portion_length = int.from_bytes(opcode, byteorder="big") - 0x5F if jump == JumpDirection.FORWARD: offset = data_portion_length if data_portion_end else 1 - code = Op.PUSH1(1) + Op.RJUMPI[offset] + opcode[0] + code = Op.PUSH1(1) + Op.RJUMPI[offset] + opcode[0] + Op.STOP else: offset = -4 if data_portion_end else -4 - data_portion_length + 1 - code = opcode[0] + Op.RJUMPI[offset] + code = opcode[0] + Op.RJUMPI[offset] + Op.STOP eof_test( data=Container( sections=[ diff --git a/tests/prague/eip7692_eof_v1/eip4750_functions/test_code_validation.py b/tests/prague/eip7692_eof_v1/eip4750_functions/test_code_validation.py new file mode 100644 index 0000000000..c6298f31b1 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip4750_functions/test_code_validation.py @@ -0,0 +1,161 @@ +""" +Code validation of CALLF, RETF opcodes tests +""" + +from typing import List + +import pytest + +from ethereum_test_tools import EOFException, EOFTestFiller +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import MAX_CODE_SECTIONS +from ethereum_test_tools.vm.opcode import Opcodes as Op + +VALID: List[Container] = [ + Container( + name="retf_code_input_output", + sections=[ + Section.Code(code=Op.PUSH0 + Op.CALLF[1] + Op.POP + Op.POP + Op.STOP), + Section.Code( + code=Op.PUSH0 + Op.RETF, + code_outputs=1, + ), + ], + ), + Container( + name="callf_max_code_sections_1", + sections=[ + Section.Code(code=(sum(Op.CALLF[i] for i in range(1, MAX_CODE_SECTIONS)) + Op.STOP)) + ] + + ( + [ + Section.Code( + code=Op.RETF, + code_outputs=0, + ) + ] + * (MAX_CODE_SECTIONS - 1) + ), + ), + Container( + name="callf_max_code_sections_2", + sections=[Section.Code(code=(Op.CALLF[1] + Op.STOP))] + + [ + Section.Code( + code=(Op.CALLF[i + 2] + Op.RETF), + code_outputs=0, + ) + for i in range(MAX_CODE_SECTIONS - 2) + ] + + [ + Section.Code( + code=Op.RETF, + code_outputs=0, + ) + ], + ), +] + +INVALID: List[Container] = [ + Container( + name="function_underflow", + sections=[ + Section.Code(code=(Op.PUSH0 + Op.CALLF[1] + Op.STOP)), + Section.Code( + code=(Op.POP + Op.POP + Op.RETF), + code_inputs=1, + code_outputs=0, + ), + ], + validity_error=EOFException.STACK_UNDERFLOW, + ), + Container( + name="stack_higher_than_code_outputs", + sections=[ + Section.Code( + code=(Op.CALLF[1] + Op.STOP), + ), + Section.Code( + code=(Op.PUSH0 + Op.RETF), + code_outputs=0, + ), + ], + validity_error=EOFException.STACK_HIGHER_THAN_OUTPUTS, + ), + Container( + name="stack_shorter_than_code_outputs", + sections=[ + Section.Code( + code=(Op.CALLF[1] + Op.STOP), + ), + Section.Code( + code=(Op.PUSH0 + Op.RETF), + code_outputs=2, + max_stack_height=1, + ), + ], + validity_error=EOFException.INVALID_MAX_STACK_HEIGHT, + ), + Container( + name="oob_callf_1", + sections=[ + Section.Code( + code=(Op.CALLF[2] + Op.STOP), + ), + Section.Code( + code=(Op.RETF), + code_outputs=0, + ), + ], + validity_error=EOFException.INVALID_CODE_SECTION_INDEX, + ), + Container( + name="overflow_code_sections_1", + sections=[ + Section.Code( + code=(Op.CALLF[1] + Op.STOP), + ) + ] + + [ + Section.Code( + code=(Op.CALLF[i + 2] + Op.RETF), + code_outputs=0, + ) + for i in range(MAX_CODE_SECTIONS) + ] + + [ + Section.Code( + code=Op.RETF, + code_outputs=0, + ) + ], + validity_error=EOFException.TOO_MANY_CODE_SECTIONS, + ), +] + + +def container_name(c: Container): + """ + Return the name of the container for use in pytest ids. + """ + if hasattr(c, "name"): + return c.name + else: + return c.__class__.__name__ + + +@pytest.mark.parametrize( + "container", + [*VALID, *INVALID], + ids=container_name, +) +def test_eof_validity( + eof_test: EOFTestFiller, + container: Container, +): + """ + Test EOF container validaiton for features around EIP-4750 / Functions / Code Sections + """ + eof_test( + data=bytes(container), + ) diff --git a/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py index 84d41c6ec1..611d528c62 100644 --- a/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py +++ b/tests/prague/eip7692_eof_v1/eip6206_jumpf/test_jumpf_execution.py @@ -96,7 +96,7 @@ def test_jumpf_too_large( code=Op.JUMPF[1025], ) ], - validity_error=EOFException.UNDEFINED_EXCEPTION, + validity_error=EOFException.INVALID_CODE_SECTION_INDEX, ), ) @@ -112,7 +112,7 @@ def test_jumpf_way_too_large( code=Op.JUMPF[0xFFFF], ) ], - validity_error=EOFException.UNDEFINED_EXCEPTION, + validity_error=EOFException.INVALID_CODE_SECTION_INDEX, ), ) @@ -128,7 +128,7 @@ def test_jumpf_to_nonexistent_section( code=Op.JUMPF[5], ) ], - validity_error=EOFException.UNDEFINED_EXCEPTION, + validity_error=EOFException.INVALID_CODE_SECTION_INDEX, ), ) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py index 0bb9ae237f..ba67774519 100644 --- a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_eofcreate.py @@ -4,6 +4,8 @@ import pytest +from ethereum_test_exceptions import EOFException +from ethereum_test_specs import EOFTestFiller from ethereum_test_tools import ( Account, Alloc, @@ -569,3 +571,22 @@ def test_eofcreate_revert_eof_returndata( ) state_test(env=env, pre=pre, post=post, tx=tx) + + +@pytest.mark.parametrize("index", [1, 255], ids=lambda x: x) +def test_eofcreate_invalid_index( + eof_test: EOFTestFiller, + index: int, +): + """Referring to non-existent container section index""" + eof_test( + data=Container( + sections=[ + Section.Code( + code=Op.EOFCREATE[index](0, 0, 0, 0) + Op.STOP, + ), + Section.Container(container=Container(sections=[Section.Code(code=Op.INVALID)])), + ], + ), + expect_exception=EOFException.INVALID_CONTAINER_SECTION_INDEX, + ) diff --git a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py index 1b5e586ca4..6170fe5133 100644 --- a/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py +++ b/tests/prague/eip7692_eof_v1/eip7620_eof_create/test_subcontainer_validation.py @@ -471,6 +471,55 @@ def test_container_both_kinds_different_sub(eof_test: EOFTestFiller): ) +@pytest.mark.parametrize("version", [0, 255], ids=lambda x: x) +def test_subcontainer_wrong_eof_version( + eof_test: EOFTestFiller, + version: int, +): + """Test multiple kinds of subcontainer at the same level""" + eof_test( + data=Container( + sections=[ + Section.Code( + code=Op.EOFCREATE[0](0, 0, 0, 0) + Op.STOP, + ), + Section.Container( + container=Container(version=[version], sections=[Section.Code(code=Op.STOP)]) + ), + ], + kind=ContainerKind.RUNTIME, + ), + expect_exception=EOFException.INVALID_VERSION, + ) + + +@pytest.mark.parametrize("delta", [-1, 1], ids=["smaller", "larger"]) +@pytest.mark.parametrize("kind", [ContainerKind.RUNTIME, ContainerKind.INITCODE]) +def test_subcontainer_wrong_size( + eof_test: EOFTestFiller, + delta: int, + kind: ContainerKind, +): + """Test multiple kinds of subcontainer at the same level""" + eof_test( + data=Container( + sections=[ + Section.Code( + code=(Op.EOFCREATE[0](0, 0, 0, 0) + Op.STOP) + if kind == ContainerKind.RUNTIME + else (Op.RETURNCONTRACT[0](0, 0)), + ), + Section.Container( + container=Container(sections=[Section.Code(code=Op.STOP)]), + custom_size=len(stop_sub_container.data) + delta, + ), + ], + kind=kind, + ), + expect_exception=EOFException.INVALID_SECTION_BODIES_SIZE, + ) + + @pytest.mark.parametrize( ["deepest_container", "exception"], [