Skip to content

Commit a16fbc1

Browse files
committed
feat(tests): add dynamic-destination JUMP/JUMPI invalid-target tests
Port the behaviors of the legacy VMTests dynamic-jump family (DynamicJump*, BlockNumberDynamicJump*, JDfromStorageDynamicJump*, bad_indirect_jump2, jumpi1) that had no EEST equivalent: jumps with runtime-computed destinations landing inside PUSH immediate data (on 0x5B and non-0x5B bytes) or far beyond code size, and a not-taken JUMPI with an invalid static destination.
1 parent a9abd46 commit a16fbc1

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Tests for JUMP and JUMPI with runtime-computed destinations."""
2+
3+
import pytest
4+
from execution_testing import (
5+
Account,
6+
Alloc,
7+
Fork,
8+
Hash,
9+
Op,
10+
StateTestFiller,
11+
Storage,
12+
Transaction,
13+
)
14+
15+
REFERENCE_SPEC_GIT_PATH = "N/A"
16+
REFERENCE_SPEC_VERSION = "N/A"
17+
18+
GAS_LIMIT = 100_000
19+
20+
21+
@pytest.mark.valid_from("Frontier")
22+
@pytest.mark.parametrize("opcode", [Op.JUMP, Op.JUMPI])
23+
@pytest.mark.parametrize(
24+
"dest_kind",
25+
["push_data_jumpdest", "push_data_non_jumpdest", "beyond_code"],
26+
)
27+
def test_dynamic_jump_invalid_destination(
28+
state_test: StateTestFiller,
29+
pre: Alloc,
30+
fork: Fork,
31+
opcode: Op,
32+
dest_kind: str,
33+
) -> None:
34+
"""
35+
Jump to an invalid destination taken from calldata and verify the
36+
exceptional halt: storage stays empty and all gas is consumed.
37+
38+
The destination comes from calldata, so implementations that
39+
pre-analyze static PUSH+JUMP pairs must still validate it at
40+
execution time. Covers landing inside PUSH immediate data (on a 0x5B
41+
byte and on a non-0x5B byte) and landing far beyond the code size at
42+
a destination whose 64-bit truncation is a valid JUMPDEST.
43+
"""
44+
storage = Storage()
45+
if opcode == Op.JUMP:
46+
dispatch = Op.JUMP(Op.CALLDATALOAD(0))
47+
else:
48+
dispatch = Op.JUMPI(Op.CALLDATALOAD(0), 1)
49+
fallthrough = (
50+
Op.SSTORE(storage.store_next(0, "jump not halted"), 1) + Op.STOP
51+
)
52+
# PUSH2 data holds a 0x5B byte that must not count as a JUMPDEST.
53+
island = Op.PUSH2(0x5B00) + Op.POP
54+
code = (
55+
dispatch
56+
+ fallthrough
57+
+ island
58+
+ Op.JUMPDEST
59+
+ Op.SSTORE(storage.store_next(0, "invalid destination taken"), 2)
60+
+ Op.STOP
61+
)
62+
push_data = len(dispatch + fallthrough) + 1
63+
jumpdest = len(dispatch + fallthrough + island)
64+
assert bytes(code)[jumpdest] == Op.JUMPDEST.int()
65+
dest = {
66+
"push_data_jumpdest": push_data,
67+
"push_data_non_jumpdest": push_data + 1,
68+
# An implementation truncating the destination to 64 bits would
69+
# land on the valid JUMPDEST and store 2 instead of halting.
70+
"beyond_code": 2**64 + jumpdest,
71+
}[dest_kind]
72+
73+
contract = pre.deploy_contract(code)
74+
tx = Transaction(
75+
sender=pre.fund_eoa(),
76+
to=contract,
77+
data=Hash(dest),
78+
gas_limit=GAS_LIMIT,
79+
expected_receipt={"cumulative_gas_used": GAS_LIMIT},
80+
protected=fork.supports_protected_txs(),
81+
)
82+
83+
state_test(pre=pre, post={contract: Account(storage=storage)}, tx=tx)
84+
85+
86+
@pytest.mark.valid_from("Frontier")
87+
def test_jumpi_not_taken_invalid_destination(
88+
state_test: StateTestFiller,
89+
pre: Alloc,
90+
fork: Fork,
91+
) -> None:
92+
"""
93+
Verify that a false-condition JUMPI whose static destination is
94+
invalid does not fail and falls through.
95+
96+
The condition comes from calldata, so implementations that
97+
eagerly validate a static JUMPI destination must still skip the
98+
validation when the branch is not taken.
99+
"""
100+
storage = Storage()
101+
fallthrough = Op.SSTORE(storage.store_next(1, "fell through"), 1) + Op.STOP
102+
dispatch = Op.JUMPI(0, Op.CALLDATALOAD(0))
103+
stop_offset = len(dispatch + fallthrough) - 1
104+
dispatch = Op.JUMPI(stop_offset, Op.CALLDATALOAD(0))
105+
code = dispatch + fallthrough
106+
assert bytes(code)[stop_offset] == Op.STOP.int()
107+
108+
contract = pre.deploy_contract(code)
109+
tx = Transaction(
110+
sender=pre.fund_eoa(),
111+
to=contract,
112+
data=Hash(0),
113+
protected=fork.supports_protected_txs(),
114+
)
115+
116+
state_test(pre=pre, post={contract: Account(storage=storage)}, tx=tx)

0 commit comments

Comments
 (0)