diff --git a/docs/changes/2026-07-28-evm-memory-plan-selector-dse/README.md b/docs/changes/2026-07-28-evm-memory-plan-selector-dse/README.md new file mode 100644 index 00000000..bb75eb7e --- /dev/null +++ b/docs/changes/2026-07-28-evm-memory-plan-selector-dse/README.md @@ -0,0 +1,171 @@ +# Change: Reject incomplete EVM memory facts + +- **Status**: Verified +- **Date**: 2026-07-28 +- **Tier**: Light + +## Overview + +Multipass now publishes opcode-level memory facts only when the analyzer has a +reliable block-entry stack. Blocks with incomplete facts retain their control +flow graph topology and act as conservative barriers for every memory-plan +consumer. This prevents dead-store elimination from removing a selector store +that a subsequent `KECCAK256` observes. + +## Motivation + +Mainnet block 21800000 first diverges at transaction 15 in the 0x Exchange +proxy. The interpreter hashes the selector `0x803ba26d` with the proxy mapping +slot and reads the implementation from storage. Multipass instead hashes a +zero selector, reads an empty slot, and reverts. Commit `da0678e` introduced +the regression; its parent and the same commit with the memory-plan framework +disabled both execute the block correctly. + +## Impact + +The change affects only the facts supplied to multipass EVM memory-plan +consumers. Unreliable blocks lose optimization proofs but retain their CFG +identity. The EVM gas schedule, EVMC host contract, Reth witness, and replay +expectations are unchanged. + +The conservative fallback can reduce memory-plan optimization in blocks whose +entry stack comes from dynamic dispatch. Blocks with a reliable static entry +stack retain the existing facts, prechecks, grouping, load forwarding, and +dead-store proofs. + +## Root cause + +The first bad commit, `da0678e`, passed an unresolved analyzer entry depth of +`-1` to the memory-facts builder as depth zero. The failing proxy block entered +through a dynamic jump with hidden stack values. Simulating `SWAP3` from an +empty abstract stack silently made it a no-op, so the builder recorded both +stores at offset zero and recorded the following `KECCAK256` read at offset 64 +with unknown size. Dead-store elimination consumed those false facts and +removed the selector store even though `KECCAK256` observes it. + +## Design + +Blocks with unresolved, inconsistent, or dynamic-dispatch-tainted entry stack +depth retain their CFG topology but do not publish opcode-level memory facts. +The taint matters even when the analyzer has a non-negative resolved depth: +indirect dispatch can still supply a hidden runtime prefix that the static +depth under-counts. + +The implementation enforces the fallback at four seams: + +- The bytecode visitor accepts an entry stack only when its depth is resolved, + consistent, and not derived from dynamic dispatch. +- An explicit `HasCompleteOpcodeFacts` marker distinguishes an unreliable block + from a reliable block that contains no memory operations. +- `MemoryFactsBuilder::observeOpcode` is a no-op while an incomplete block is + current. It publishes no operation and does not mutate the abstract stack or + value numbering, even if a future caller omits the visitor-side fast path. +- Guaranteed-minimum analysis resets incomplete blocks to zero. Linear-region + grouping treats them as unknown-effect barriers. Precheck, grouping, + dead-store, and load-forwarding consumers therefore cannot derive a proof + from partial facts or across the missing dynamic edge. + +## Verification + +### Regression tests + +The baseline-red variants establish each layer of the contract: + +- The unresolved-entry test fails on the first-bad baseline because it + publishes false memory operations at PCs 65, 71, and 76. +- The two production visitor tests fail on the baseline because dynamic + targets publish memory operations at PCs 7 and 28. +- The two consumer tests fail when the incomplete marker is present without + conservative consumer handling. The invalid guarantees are 160 and 64 bytes. +- The direct builder test fails when `observeOpcode` is permitted to process an + incomplete current block: the block publishes two operations and pollutes the + following complete block. + +The final implementation passes 7/7 focused regressions. They cover the exact +selector-store/two-word-hash motif, unresolved and dynamic-dispatch-tainted +visitor entries, the builder-owned no-op invariant, cross-block +guaranteed-minimum analysis, and the planner's precheck, grouping, dead-store, +and load-forwarding results. + +### Build and suites + +The final library has SHA-256 +`1f71e0d477846785281b1afca6d0ebf52a55898a12dcfc3c14ee2019bcfea787`. +The captured incremental build exits zero and contains no compiler warning +lines. The same source and build pass: + +- 115/115 EVM frontend tests; +- 65/65 interpreter-versus-multipass differential tests; +- 223/223 external-VM unit tests in `dtvm_local_test.sh --auto`; +- 209/209 multipass EVM assembly tests; and +- 12/12 CTest targets. + +The external state-test suite passes 2710/2723 cases and fails 13 cases, so +`dtvm_local_test.sh --auto` exits one. The final library reproduces the same 13 +failing test names seen during hardening. A same-environment filtered +comparison captured during that hardening reproduced the same failing set and +state-root mismatch lines on clean `upstream/main` and the candidate. The +evmone binary and EEST fixtures are external `DTVM-LabPerf` assets rather than +repository-owned test assets; this result is recorded as an +asset-compatibility baseline, not a regression caused by this change. + +### Block 21800000 + +The final builder-hardened library replays the original failing block in +explicit `multipass` mode with exit code zero. The run executes 58 +transactions and validates gas used `22416172`, the receipt root, logs bloom, +and proof-union post-state. The sealed identities are: + +- runner SHA-256: + `26a902e9c6fa77121e5b3f7851afdead2fd85d5f8f03630c553e8eec4a4dcf1f`; +- one-block corpus SHA-256: + `03465cf38697bbb5fdd1f327685fa78048d8d37aba80ec80d3c8b8c9bffee93a`; +- result: + `/home/wyh/Dev/reth-dtvm-20260724T020354Z/evidence/kwrongblockgas-20260728/fix-validation/04-prefix001-final-builder-hardened-attempt-002/result.json`. + +The sealed four-block replay uses the same library SHA-256 +`1f71e0d477846785281b1afca6d0ebf52a55898a12dcfc3c14ee2019bcfea787` +and exits zero. Blocks 21800000 through 21800003 execute 499 transactions with +total gas used `70826847`. The run records `fresh_db`, +`db_state_access_audited`, `header_body_prevalidated`, +`gas_receipt_root_bloom_checked`, `per_block_post_compared`, and +`union_post_compared` as true. + +The four-block result has SHA-256 +`516ad8f95e412fd4c2b86d5a52f9c849df4cdc7800b6baef068503741973f475`; +the exported post-state has SHA-256 +`d3280d389c11c381027f53df44961d1cdaddc6080db52b661306f9e02c3bfe21`. +Both artifacts are stored under +`/home/wyh/Dev/reth-dtvm-20260724T020354Z/evidence/kwrongblockgas-20260728/fix-validation/05-prefix004-final-builder-hardened/`. + +## Quality-gate limitations + +The final `tools/format.sh check`, captured at 2026-07-28 11:14:20 UTC and +bound to library SHA-256 +`1f71e0d477846785281b1afca6d0ebf52a55898a12dcfc3c14ee2019bcfea787`, +exits zero with empty standard output. The changed C++ files also pass their +direct clang-format check, `git diff --check` passes, and `tests/evm_asm` has +no diff from `upstream/main`. + +Test and formatting tools generated ignored outputs and changed six tracked +EVM assembly fixtures during intermediate runs. The ignored outputs were +removed and the tracked fixtures were restored exactly to `upstream/main`; +none of that generated noise remains in the change. + +`tools/check_change_doc.py` is absent from this repository checkout. Both +attempts to run the required declaration check therefore exit 2 with a +file-not-found error; this document does not claim that gate passed. The +missing helper does not invalidate the recorded code, test, or replay evidence, +but the declaration gate remains incomplete. + +## Checklist + +- [x] Implementation complete +- [x] Tests added/updated +- [x] Module spec reviewed; the documented conservative fallback contract is + unchanged +- [x] Final library build and in-repository focused, frontend, differential, + EVM assembly, and CTest suites pass +- [x] Original block 21800000 passes with the final builder-hardened library +- [x] Sealed four-block replay passes with the same library SHA +- [ ] Change-document declaration helper becomes available and passes diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 317e55ab..1946c8da 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -147,12 +147,26 @@ template class EVMByteCodeVisitor { const auto &BlockInfos = Analyzer.getBlockInfos(); for (const auto &[EntryPC, BlockInfo] : BlockInfos) { - const int32_t EntryDepth = std::max(BlockInfo.ResolvedEntryStackDepth, 0); + const bool HasReliableEntryStack = + BlockInfo.ResolvedEntryStackDepth >= 0 && + !BlockInfo.HasInconsistentEntryDepth && + !BlockInfo.EntryDepthMayComeFromDynamicDispatch; + const int32_t EntryDepth = + HasReliableEntryStack ? BlockInfo.ResolvedEntryStackDepth : 0; std::vector EntryValues = EntryAddresses.getEntryValues( EntryPC, static_cast(EntryDepth)); - MemoryFacts.beginBlock(EntryPC, BlockInfo.BodyStartPC, - BlockInfo.BodyEndPC, EntryValues, - BlockInfo.Successors, BlockInfo.Predecessors); + MemoryFacts.beginBlock( + EntryPC, BlockInfo.BodyStartPC, BlockInfo.BodyEndPC, EntryValues, + BlockInfo.Successors, BlockInfo.Predecessors, HasReliableEntryStack); + + // Memory facts model stack-relative addresses. An unresolved, + // inconsistent, or dynamic-dispatch-derived entry depth cannot reliably + // supply the hidden live-ins used by DUP/SWAP, so simulating such a block + // can invent precise but false aliases. Keep every memory-plan consumer + // conservative by retaining only the block topology. + if (!HasReliableEntryStack) { + continue; + } size_t ScanPC = static_cast(BlockInfo.BodyStartPC); const size_t EndPC = std::min(BlockInfo.BodyEndPC, BytecodeSize); diff --git a/src/compiler/evm_frontend/evm_memory_analysis.h b/src/compiler/evm_frontend/evm_memory_analysis.h index eb0b0622..a17bf649 100644 --- a/src/compiler/evm_frontend/evm_memory_analysis.h +++ b/src/compiler/evm_frontend/evm_memory_analysis.h @@ -831,6 +831,11 @@ class MemoryGuaranteedMinBytesAnalysis { return It == EntryBytes.end() ? 0 : It->second; } + uint64_t getGuaranteedMinBytesAtExit(uint64_t EntryPC) const { + auto It = ExitBytes.find(EntryPC); + return It == ExitBytes.end() ? 0 : It->second; + } + uint64_t getGuaranteedMinBytesBeforeOp(uint32_t OpId) const { auto It = BeforeOpBytes.find(OpId); return It == BeforeOpBytes.end() ? 0 : It->second; @@ -904,6 +909,9 @@ class MemoryGuaranteedMinBytesAnalysis { uint64_t transfer(const MemoryBlockFacts &Block, uint64_t EntryBytesValue) const { + if (!Block.HasCompleteOpcodeFacts) { + return 0; + } uint64_t Current = EntryBytesValue; for (size_t I = Block.OpsBegin; I < Block.OpsEnd && I < Facts.Ops.size(); ++I) { @@ -921,6 +929,9 @@ class MemoryGuaranteedMinBytesAnalysis { void recordBeforeOpBytes(const MemoryBlockFacts &Block, uint64_t EntryBytesValue) { + if (!Block.HasCompleteOpcodeFacts) { + return; + } uint64_t Current = EntryBytesValue; bool SeenBarrier = false; for (size_t I = Block.OpsBegin; I < Block.OpsEnd && I < Facts.Ops.size(); @@ -942,6 +953,9 @@ class MemoryGuaranteedMinBytesAnalysis { } uint64_t computeEntryFromPredecessors(const MemoryBlockFacts &Block) const { + if (!Block.HasCompleteOpcodeFacts) { + return 0; + } if (Block.Predecessors.empty()) { return 0; } diff --git a/src/compiler/evm_frontend/evm_memory_facts.h b/src/compiler/evm_frontend/evm_memory_facts.h index 84a79d9c..5f16f82a 100644 --- a/src/compiler/evm_frontend/evm_memory_facts.h +++ b/src/compiler/evm_frontend/evm_memory_facts.h @@ -197,6 +197,7 @@ struct MemoryBlockFacts { uint64_t BodyEndPC = 0; size_t OpsBegin = 0; size_t OpsEnd = 0; + bool HasCompleteOpcodeFacts = true; bool HasBarrier = false; MemoryHardBarrierKind FirstHardBarrierKind = MemoryHardBarrierKind::None; evmc_opcode FirstHardBarrierOpcode = OP_STOP; @@ -251,7 +252,8 @@ class MemoryFactsBuilder { void beginBlock(uint64_t EntryPC, uint64_t BodyStartPC, uint64_t BodyEndPC, const std::vector &EntryValues, const std::vector &Successors = {}, - const std::vector &Predecessors = {}) { + const std::vector &Predecessors = {}, + bool HasCompleteOpcodeFacts = true) { endBlock(); HasCurrentBlock = true; CurrentBlockEntryPC = EntryPC; @@ -262,6 +264,7 @@ class MemoryFactsBuilder { Block.BodyEndPC = BodyEndPC; Block.OpsBegin = Facts.Ops.size(); Block.OpsEnd = Facts.Ops.size(); + Block.HasCompleteOpcodeFacts = HasCompleteOpcodeFacts; Block.Successors = Successors; Block.Predecessors = Predecessors; Facts.Blocks[EntryPC] = std::move(Block); @@ -289,6 +292,12 @@ class MemoryFactsBuilder { void observeOpcode(evmc_opcode Opcode, uint64_t Pc, const uint8_t *Bytecode, size_t BytecodeSize) { + if (HasCurrentBlock) { + auto It = Facts.Blocks.find(CurrentBlockEntryPC); + if (It == Facts.Blocks.end() || !It->second.HasCompleteOpcodeFacts) { + return; + } + } if (Opcode >= OP_PUSH0 && Opcode <= OP_PUSH32) { observePush(Opcode, Pc, Bytecode, BytecodeSize); return; diff --git a/src/compiler/evm_frontend/evm_memory_grouping.h b/src/compiler/evm_frontend/evm_memory_grouping.h index b9100ad9..fef598a9 100644 --- a/src/compiler/evm_frontend/evm_memory_grouping.h +++ b/src/compiler/evm_frontend/evm_memory_grouping.h @@ -362,6 +362,9 @@ class MemoryLinearRegionConsumer final : public MemoryOptimizationPlanProvider { getHardBarrierRejectReason(const MemoryAnalysisView &View, const MemoryBlockFacts &Block, const MemoryFacts &Facts) { + if (!Block.HasCompleteOpcodeFacts) { + return MemoryLinearRegionRejectReason::BarrierUnknownEffect; + } if (Block.HasBarrier) { return mapHardBarrierKind(Block.FirstHardBarrierKind); } diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index c35da3bc..4892a10d 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -253,13 +253,22 @@ collectAnalyzerMemoryFacts(const std::vector &Bytecode) { COMPILER::MemoryFactsBuilder FactsBuilder; const auto &Blocks = Analyzer.getBlockInfos(); for (const auto &[EntryPC, BlockInfo] : Blocks) { - const int32_t EntryDepth = std::max(BlockInfo.ResolvedEntryStackDepth, 0); + const bool HasReliableEntryStack = + BlockInfo.ResolvedEntryStackDepth >= 0 && + !BlockInfo.HasInconsistentEntryDepth && + !BlockInfo.EntryDepthMayComeFromDynamicDispatch; + const int32_t EntryDepth = + HasReliableEntryStack ? BlockInfo.ResolvedEntryStackDepth : 0; std::vector EntryValues = EntryAddresses.getEntryValues(EntryPC, static_cast(EntryDepth)); FactsBuilder.beginBlock(EntryPC, BlockInfo.BodyStartPC, BlockInfo.BodyEndPC, EntryValues, BlockInfo.Successors, - BlockInfo.Predecessors); + BlockInfo.Predecessors, HasReliableEntryStack); + + if (!HasReliableEntryStack) { + continue; + } size_t PC = static_cast(BlockInfo.BodyStartPC); const size_t EndPC = std::min(BlockInfo.BodyEndPC, Bytecode.size()); @@ -281,6 +290,7 @@ struct MemoryFactBlockSpec { uint64_t BodyEndPC = 0; std::vector Successors; std::vector Predecessors; + bool HasCompleteOpcodeFacts = true; }; COMPILER::MemoryFacts @@ -290,7 +300,12 @@ collectManualBlockMemoryFacts(const std::vector &Bytecode, const uint8_t *Data = Bytecode.empty() ? nullptr : Bytecode.data(); for (const MemoryFactBlockSpec &Block : Blocks) { FactsBuilder.beginBlock(Block.EntryPC, Block.BodyStartPC, Block.BodyEndPC, - {}, Block.Successors, Block.Predecessors); + {}, Block.Successors, Block.Predecessors, + Block.HasCompleteOpcodeFacts); + + if (!Block.HasCompleteOpcodeFacts) { + continue; + } size_t PC = static_cast(Block.BodyStartPC); const size_t EndPC = std::min(Block.BodyEndPC, Bytecode.size()); @@ -352,6 +367,62 @@ TEST(EVMMemoryFactsBuilderTest, AttributesOpsToAnalyzerBlocks) { EXPECT_EQ(JumpTarget->MaxConstRequiredSize, 0x60u); } +TEST(EVMMemoryFactsBuilderTest, + IncompleteBlockIgnoresOpcodesWithoutPollutingNextBlock) { + const std::vector Bytecode = { + OP_PUSH1, 0x01, OP_PUSH1, 0x00, OP_MSTORE, + OP_PUSH1, 0x40, OP_PUSH0, OP_SWAP1, OP_KECCAK256, + OP_CALLDATASIZE, // PC10: first opcode in the complete block. + OP_MLOAD, // PC11 + OP_STOP}; + + COMPILER::MemoryFactsBuilder FactsBuilder; + FactsBuilder.beginBlock(0, 0, 10, {}, {}, {}, false); + size_t PC = 0; + while (PC < 10) { + evmc_opcode Opcode = static_cast(Bytecode[PC]); + FactsBuilder.observeOpcode(Opcode, PC, Bytecode.data(), Bytecode.size()); + ++PC; + if (Opcode >= OP_PUSH0 && Opcode <= OP_PUSH32) { + PC += static_cast(Opcode) - static_cast(OP_PUSH0); + } + } + + const COMPILER::MemoryFacts &IncompleteOnlyFacts = FactsBuilder.getFacts(); + const COMPILER::MemoryBlockFacts *IncompleteOnly = + IncompleteOnlyFacts.getBlock(0); + ASSERT_NE(IncompleteOnly, nullptr); + EXPECT_FALSE(IncompleteOnly->HasCompleteOpcodeFacts); + EXPECT_EQ(IncompleteOnly->OpsBegin, IncompleteOnly->OpsEnd); + EXPECT_TRUE(IncompleteOnlyFacts.Ops.empty()); + + FactsBuilder.beginBlock(10, 10, Bytecode.size(), {}); + while (PC < Bytecode.size()) { + evmc_opcode Opcode = static_cast(Bytecode[PC]); + FactsBuilder.observeOpcode(Opcode, PC, Bytecode.data(), Bytecode.size()); + ++PC; + if (Opcode >= OP_PUSH0 && Opcode <= OP_PUSH32) { + PC += static_cast(Opcode) - static_cast(OP_PUSH0); + } + } + + COMPILER::MemoryFacts Facts = FactsBuilder.takeFacts(); + const COMPILER::MemoryBlockFacts *Incomplete = Facts.getBlock(0); + const COMPILER::MemoryBlockFacts *Complete = Facts.getBlock(10); + ASSERT_NE(Incomplete, nullptr); + ASSERT_NE(Complete, nullptr); + EXPECT_FALSE(Incomplete->HasCompleteOpcodeFacts); + EXPECT_EQ(Incomplete->OpsBegin, Incomplete->OpsEnd); + EXPECT_TRUE(Complete->HasCompleteOpcodeFacts); + ASSERT_EQ(Facts.Ops.size(), 1u); + ASSERT_EQ(Facts.Ops[0].Pc, 11u); + ASSERT_EQ(Facts.Ops[0].Kind, COMPILER::MemoryOpKind::MLoad); + ASSERT_EQ(Facts.Ops[0].Reads.size(), 1u); + EXPECT_EQ(Facts.Ops[0].Reads[0].Addr.Kind, + COMPILER::AddressBaseKind::StackValue); + EXPECT_EQ(Facts.Ops[0].Reads[0].Addr.ValueId, 1u); +} + TEST(EVMMemoryEntryAddressAnalysisTest, PropagatesConstAddressAcrossUnconditionalJump) { const std::vector Bytecode = {OP_PUSH1, 0x80, OP_PUSH1, @@ -633,6 +704,31 @@ TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, IgnoresZeroLengthCallDataCopy) { EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0u); } +TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, + IncompleteBlockResetsGuaranteeAndBlocksSuccessorPropagation) { + const std::vector Bytecode = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_STOP, OP_STOP, + OP_STOP, OP_STOP, OP_STOP, OP_PUSH1, 0x00, OP_MLOAD, OP_STOP}; + const std::vector Blocks = { + {0, 0, 5, {5}, {}}, + {5, 5, 10, {10}, {0}, false}, + {10, 10, 14, {}, {5}}, + }; + + COMPILER::MemoryFacts Facts = collectManualBlockMemoryFacts(Bytecode, Blocks); + const COMPILER::MemoryBlockFacts *Incomplete = Facts.getBlock(5); + ASSERT_NE(Incomplete, nullptr); + ASSERT_FALSE(Incomplete->HasCompleteOpcodeFacts); + ASSERT_EQ(Facts.Ops.size(), 2u); + + COMPILER::MemoryGuaranteedMinBytesAnalysis Guaranteed(Facts); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesAtExit(0), 0xa0u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesAtEntry(5), 0u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesAtExit(5), 0u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesAtEntry(10), 0u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0u); +} + TEST(EVMMemoryFactsBuilderTest, RecordsCopyAddressSpaces) { const std::vector Bytecode = {OP_PUSH1, 0x20, OP_PUSH1, 0x04, OP_PUSH1, 0x80, OP_CALLDATACOPY}; @@ -996,6 +1092,73 @@ TEST(EVMMemoryDeadStoreAnalysisTest, RejectsSameBasePartialOverlap) { EXPECT_FALSE(DeadStores.isDeadStore(Facts.Ops[1].Id)); } +TEST(EVMMemoryDeadStoreAnalysisTest, + SelectorStoreObservedByTwoWordKeccakIsNotDead) { + const std::vector Bytecode = { + OP_PUSH0, OP_CALLDATALOAD, OP_PUSH1, 0x20, OP_CALLDATALOAD, OP_PUSH1, + 0x40, OP_CALLDATALOAD, + // Exact selector-to-storage-key stack motif used by the failing proxy. + OP_PUSH32, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, OP_SWAP3, + OP_SWAP1, OP_SWAP3, OP_AND, OP_PUSH0, OP_SWAP1, OP_DUP2, OP_MSTORE, + OP_PUSH1, 0x20, OP_SWAP3, OP_SWAP1, OP_SWAP3, OP_MSTORE, OP_POP, OP_PUSH1, + 0x40, OP_SWAP1, OP_KECCAK256}; + + COMPILER::MemoryFacts Facts = collectMemoryFacts(Bytecode); + COMPILER::MemoryDeadStoreAnalysis DeadStores(Facts); + + ASSERT_GE(Facts.Ops.size(), 6u); + const COMPILER::MemoryOp &SelectorStore = Facts.Ops[Facts.Ops.size() - 3]; + const COMPILER::MemoryOp &SlotStore = Facts.Ops[Facts.Ops.size() - 2]; + const COMPILER::MemoryOp &Keccak = Facts.Ops.back(); + ASSERT_EQ(SelectorStore.Kind, COMPILER::MemoryOpKind::MStore); + ASSERT_EQ(SlotStore.Kind, COMPILER::MemoryOpKind::MStore); + ASSERT_EQ(Keccak.Kind, COMPILER::MemoryOpKind::Keccak); + ASSERT_EQ(SelectorStore.Writes.size(), 1u); + ASSERT_EQ(SlotStore.Writes.size(), 1u); + ASSERT_EQ(Keccak.Reads.size(), 1u); + EXPECT_EQ(SelectorStore.Writes[0].Addr.Offset, 0); + EXPECT_EQ(SlotStore.Writes[0].Addr.Offset, 32); + EXPECT_EQ(Keccak.Reads[0].Addr.Offset, 0); + ASSERT_TRUE(Keccak.Reads[0].Size.Known); + EXPECT_EQ(Keccak.Reads[0].Size.Value, 64u); + EXPECT_FALSE(DeadStores.isDeadStore(SelectorStore.Id)); +} + +TEST(EVMMemoryDeadStoreAnalysisTest, + UnresolvedDynamicEntryDoesNotProduceFalseSelectorAliasFacts) { + const std::vector Bytecode = { + // Two reachable dynamic-JUMP sources enter PC24 with different hidden + // live-in depths, so its absolute entry stack is unresolved. + OP_PUSH0, OP_CALLDATALOAD, OP_PUSH1, 0x0d, OP_JUMPI, OP_PUSH1, 0x11, + OP_PUSH1, 0x22, OP_PUSH1, 0x20, OP_CALLDATALOAD, OP_JUMP, OP_JUMPDEST, + OP_PUSH1, 0x11, OP_PUSH1, 0x22, OP_PUSH1, 0x33, OP_PUSH1, 0x20, + OP_CALLDATALOAD, OP_JUMP, OP_JUMPDEST, + // Exact selector-to-storage-key motif from the failing proxy. + OP_PUSH32, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, OP_SWAP3, + OP_SWAP1, OP_SWAP3, OP_AND, OP_PUSH0, OP_SWAP1, OP_DUP2, OP_MSTORE, + OP_PUSH1, 0x20, OP_SWAP3, OP_SWAP1, OP_SWAP3, OP_MSTORE, OP_POP, OP_PUSH1, + 0x40, OP_SWAP1, OP_KECCAK256, OP_STOP}; + + const EVMAnalyzer Analyzer = analyzeBytecode(Bytecode); + const auto *TargetBlock = findBlock(Analyzer, 24); + ASSERT_NE(TargetBlock, nullptr); + ASSERT_LT(TargetBlock->ResolvedEntryStackDepth, 0); + + COMPILER::MemoryFacts Facts = collectAnalyzerMemoryFacts(Bytecode); + const COMPILER::MemoryBlockFacts *TargetFacts = Facts.getBlock(24); + ASSERT_NE(TargetFacts, nullptr); + EXPECT_FALSE(TargetFacts->HasCompleteOpcodeFacts); + for (const COMPILER::MemoryOp &Op : Facts.Ops) { + EXPECT_NE(Op.Pc, 65u); + EXPECT_NE(Op.Pc, 71u); + EXPECT_NE(Op.Pc, 76u); + } +} + TEST(EVMMemoryLoadForwardingAnalysisTest, FindsExactReachingMStore) { const std::vector Bytecode = { OP_PUSH1, 0x2a, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, 0x80, OP_MLOAD}; @@ -1374,6 +1537,35 @@ TEST(EVMMemoryConsumerFrameworkTest, EXPECT_EQ(Planner.getGuaranteedMinBytesAtEntry(Tail->EntryPC), 0x60u); } +TEST(EVMMemoryConsumerFrameworkTest, LinearRegionDoesNotCrossIncompleteBlock) { + const std::vector Bytecode = { + OP_PUSH1, 0x01, OP_PUSH1, 0x00, OP_MSTORE, OP_STOP, + OP_STOP, OP_STOP, OP_STOP, OP_STOP, OP_PUSH1, 0x02, + OP_PUSH1, 0x20, OP_MSTORE, OP_STOP}; + const std::vector Blocks = { + {0, 0, 5, {5}, {}}, + {5, 5, 10, {10}, {0}, false}, + {10, 10, 16, {}, {5}}, + }; + + COMPILER::MemoryFacts Facts = collectManualBlockMemoryFacts(Bytecode, Blocks); + COMPILER::MemoryAnalysisView View(Facts); + COMPILER::MemoryPrecheckConsumer Prechecks(View); + COMPILER::MemoryGroupingConsumer Grouping(View, Prechecks); + COMPILER::MemoryExpansionPlanner Planner(View); + + EXPECT_FALSE(Prechecks.buildMemoryExpansionPlan(5, 10).has_value()); + EXPECT_FALSE(Grouping.buildMemoryExpansionPlan(5, 10).has_value()); + EXPECT_FALSE(Planner.isDeadStore(5)); + EXPECT_FALSE(Planner.getForwardingStorePC(5).has_value()); + EXPECT_FALSE(Planner.buildMemoryExpansionPlan(0, 5).has_value()); + EXPECT_EQ(Planner.getGuaranteedMinBytesAtEntry(5), 0u); + EXPECT_EQ(Planner.getGuaranteedMinBytesAtEntry(10), 0u); + EXPECT_EQ( + Planner.getLastDiagnostics().LinearRegionRejectedBarrierUnknownEffect, + 1u); +} + TEST(EVMMemoryConsumerFrameworkTest, LinearRegionRejectsBranchingHead) { const std::vector Bytecode = { OP_PUSH1, 0x01, OP_PUSH1, 0x00, OP_MSTORE, @@ -1784,6 +1976,14 @@ class MockEVMBuilder { void finalizeEVMBase() {} + void setMemoryFacts(const COMPILER::MemoryFacts &Facts) { + CapturedMemoryFacts = Facts; + } + + const COMPILER::MemoryFacts &memoryFacts() const { + return CapturedMemoryFacts; + } + bool isOpcodeDefined(evmc_opcode Opcode) const { const auto *InstructionNames = evmc_get_instruction_names_table(EVMC_CANCUN); @@ -2206,6 +2406,7 @@ class MockEVMBuilder { uint64_t LargeStaticWorkspaceLoweringCoveredMStore8Ops = 0; private: + COMPILER::MemoryFacts CapturedMemoryFacts; bool EnableRuntimeStackChecks = false; uint8_t CurrentOpcode = 0xff; std::array Stats = {}; @@ -2362,6 +2563,87 @@ TEST(EVMJITFrontendVisitorTest, TerminatingMemoryHelpersRetainExactOpcodePC) { EXPECT_EQ(RevertBuilder.helperOpcodes()[0].PC, 2u); } +TEST(EVMJITFrontendVisitorTest, + DynamicDispatchTaintedEntryRetainsTopologyWithoutMemoryOps) { + const std::vector Bytecode = { + OP_PUSH0, OP_CALLDATALOAD, + OP_JUMP, + OP_JUMPDEST, // PC3: dynamically dispatched region target. + OP_PUSH1, 0x01, + OP_PUSH0, + OP_MSTORE, // PC7 + OP_STOP}; + + const EVMAnalyzer Analyzer = analyzeBytecode(Bytecode); + const auto *TargetBlock = findBlock(Analyzer, 3); + ASSERT_NE(TargetBlock, nullptr); + ASSERT_EQ(TargetBlock->ResolvedEntryStackDepth, 0); + ASSERT_TRUE(TargetBlock->EntryDepthMayComeFromDynamicDispatch); + + MockEVMBuilder Builder; + ASSERT_TRUE(compileWithMockBuilder(Bytecode, Builder)); + + const COMPILER::MemoryFacts &Facts = Builder.memoryFacts(); + const COMPILER::MemoryBlockFacts *TargetFacts = Facts.getBlock(3); + ASSERT_NE(TargetFacts, nullptr); + EXPECT_FALSE(TargetFacts->HasCompleteOpcodeFacts); + for (const COMPILER::MemoryOp &Op : Facts.Ops) { + EXPECT_NE(Op.Pc, 7u); + } +} + +TEST(EVMJITFrontendVisitorTest, + UnresolvedDynamicEntryRetainsTopologyWithoutMemoryOps) { + const std::vector Bytecode = { + // Two dynamic sources reach PC24 with different hidden live-in depths. + OP_PUSH0, + OP_CALLDATALOAD, + OP_PUSH1, + 0x0d, + OP_JUMPI, + OP_PUSH1, + 0x11, + OP_PUSH1, + 0x22, + OP_PUSH1, + 0x20, + OP_CALLDATALOAD, + OP_JUMP, + OP_JUMPDEST, + OP_PUSH1, + 0x11, + OP_PUSH1, + 0x22, + OP_PUSH1, + 0x33, + OP_PUSH1, + 0x20, + OP_CALLDATALOAD, + OP_JUMP, + OP_JUMPDEST, // PC24: unresolved dynamic-dispatch target. + OP_PUSH1, + 0x01, + OP_PUSH0, + OP_MSTORE, // PC28 + OP_STOP}; + + const EVMAnalyzer Analyzer = analyzeBytecode(Bytecode); + const auto *TargetBlock = findBlock(Analyzer, 24); + ASSERT_NE(TargetBlock, nullptr); + ASSERT_LT(TargetBlock->ResolvedEntryStackDepth, 0); + + MockEVMBuilder Builder; + ASSERT_TRUE(compileWithMockBuilder(Bytecode, Builder)); + + const COMPILER::MemoryFacts &Facts = Builder.memoryFacts(); + const COMPILER::MemoryBlockFacts *TargetFacts = Facts.getBlock(24); + ASSERT_NE(TargetFacts, nullptr); + EXPECT_FALSE(TargetFacts->HasCompleteOpcodeFacts); + for (const COMPILER::MemoryOp &Op : Facts.Ops) { + EXPECT_NE(Op.Pc, 28u); + } +} + TEST(EVMJITFrontendVisitorTest, DynamicJumpConsistencyErrorEscapesVisitor) { const std::vector Bytecode = { 0x60, 0x04, // PC0 PUSH1 0x04 (analyzer resolves a constant destination)