feat: local call/return detection with context-sensitive resolution#222
Draft
feat: local call/return detection with context-sensitive resolution#222
Conversation
Add a pre-fixpoint pass that detects private function call/return patterns and resolves return jumps via context-sensitive graph traversal. Local summaries (no fixpoint needed): - PrivateFunctionCall: block has static JUMP + pushes a surviving JUMPDEST label - PrivateFunctionReturn: block ends with dynamic JUMP using only stack ops Context-sensitive resolution: - Transactional call-string contexts (max depth 8) - On call: push caller to context, follow to callee - On return: pop context to find caller's continuation, add edge - CutToCaller pattern from gigahorse's transactional_shrinking_context.dl Soundness: - Only pure return blocks (JUMPDEST/POP/DUP/SWAP/PUSH/JUMP) are classified as returns, excluding TLOAD/SLOAD/MLOAD-based patterns - Resolutions are only committed when ALL dynamic jumps in the contract are accounted for (no other unresolved dynamic jumps remain) - Dead code excluded from has_dynamic_jumps recomputation Resolves the nested_call_return test case (wrapper return through inner function) that was previously unresolvable by the flat abstract interpreter.
Instead of committing PCR resolutions as a separate pre-pass, the private call/return detection now produces hints that are seeded into the abstract interpreter's fixpoint as discovered edges. This allows the fixpoint to propagate abstract states along PCR-discovered return edges, resolving additional jumps that were previously unreachable. PCR resolutions that the fixpoint cannot independently confirm are used as fallback targets, but remain subject to invalidate_suspect_jumps for soundness. Gains across benchmarks: erc20_transfer +3, fiat_token +3, seaport +1, airdrop +1, curve_stableswap +2.
Merging this PR will degrade performance by 74.2%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | airdrop/compile/translate |
17.7 ms | 21.4 ms | -17.12% |
| ❌ | fiat_token/compile/translate |
57.4 ms | 71.4 ms | -19.62% |
| ❌ | uniswap_v2_pair/compile/translate |
26.8 ms | 31.9 ms | -16.03% |
| ❌ | univ2_router/compile/translate |
50.8 ms | 196.7 ms | -74.2% |
| ❌ | usdc_proxy/compile/translate |
5.5 ms | 7.6 ms | -28.38% |
| ⚡ | weth/compile/jit |
543.5 ms | 379.8 ms | +43.11% |
Comparing dani/local-jump-resolution (91bc0d8) with main (013cfe1)
…ution # Conflicts: # crates/revmc/src/bytecode/passes/block_analysis.rs
- Exclude MULTI_JUMP/INVALID_JUMP from private-call detection (term.data is not a valid callee for those). - Discard all PCR hints on non-convergence (partial exploration can miss valid continuations). - Dedup return targets to avoid duplicate continuations from different contexts. - Only use PCR fallback for Top (reachable but unknown), not Bottom (unreachable). - Bump Context SmallVec from 4 to 8 to match MAX_CONTEXT_DEPTH. - Fix jump-resolution script to skip the initial pre-resolution unresolved count.
Replace the opcode whitelist for private function return detection with a stack provenance simulation. Tracks whether each stack slot originates from the block's entry stack (Input) or was produced in-block (Local). A dynamic JUMP qualifies as a private return if its operand has Input provenance and the block contains no unsafe opcodes (TLOAD/TSTORE, CALL-family, CREATE-family). This resolves weth (0 unresolved), usdc_proxy (0 unresolved), and univ2_router (0 unresolved) by detecting return blocks that contain arithmetic, memory ops, SLOAD, BALANCE, LOG, etc.
8d7410c to
14bf82f
Compare
…ith opaque taint - Extract shared stack-shuffling ops (POP, DUP, SWAP, DUPN, SWAPN, EXCHANGE) into generic apply_stack_shuffle, used by both interpret_block and simulate_provenance. - Remove is_return_safe_block opcode deny-list. Return classification now uses provenance alone. - Add compute_opaque_taint for structural soundness: seeds opaque entries from callee blocks with non-private-call preds (seed A) and all JUMPDEST blocks when unmodeled dynamic jumps exist (seed B). Propagates forward and taints reachable return blocks. - Fix block_analysis_local to commit Invalid jump resolutions so PCR doesn't see them as unmodeled dynamic jumps.
…check Move apply_stack_shuffle from a free function in block_analysis to a method on Bytecode, using get_u8_imm instead of raw code slice. Fix provenance check to use the destination operand position instead of always checking TOS. For JUMP dest is TOS, but for JUMPI dest is second-from-top (TOS is the condition).
- PCR: taint return blocks reached with empty/invalid context during traversal. Context truncation at MAX_CONTEXT_DEPTH can silently drop callers, producing incomplete target sets. - Dedup: restore transitive redirect chain compression between iterations. Without it, chained redirects (A→B, B→C) leave stale intermediate targets in rebuild_cfg and translate.rs. - LLVM: revert AOT mode from thread-local context to owned Box<Context>. The TLS approach was unsound with unsafe impl Send — moving the backend across threads would create a dangling reference. - Add regression tests for all three issues plus JUMPI provenance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a private call/return detection pass that resolves dynamic jump targets
using context-sensitive call-string analysis, integrated into the block analysis
fixpoint.
What
The pass detects two patterns in each basic block:
Private function call: a static JUMP (adjacent PUSH+JUMP) where the block
also pushes a valid JUMPDEST label that survives to exit — the label is the
return address, and the static target is the callee.
Private function return: a dynamic JUMP whose operand has entry-stack
provenance (passed by the caller, not computed in-block from
memory/storage/arithmetic). Detected via a lightweight stack provenance
simulation that tracks whether each value originated from the block's
entry stack or was produced locally.
A worklist-based traversal over (block, call-string) pairs then traces which
callers push which return addresses, resolving return jumps to their
continuation targets (single or multi-target).
How it integrates
Instead of committing resolutions as a separate pre-pass, the PCR results are
fed as seed edges into the abstract interpreter's fixpoint. This lets the
fixpoint propagate abstract states along return edges, discovering additional
resolutions it couldn't find on its own. PCR resolutions that the fixpoint
can't independently confirm are used as fallback targets, subject to the
existing
invalidate_suspect_jumpssoundness check.Pipeline:
static_jump_analysis → mark_dead_code → rebuild_cfg → block_analysis (includes PCR) → mark_dead_code → dedup → rebuild_cfg → sectionsResults
Full resolution stats across all benchmarks:
weth, usdc_proxy, and univ2_router are now fully resolved (0 unresolved). PCR
enables the fixpoint to discover additional resolutions it couldn't find on
its own by seeding return edges into the abstract interpreter.
Soundness
a lightweight stack simulation tracks whether each value originates from the
block's entry stack or was produced locally by computation/memory/storage.
RETURN, REVERT, STOP, INVALID) to prevent false positives on shared utility
blocks.
term.datais not a valid callee).invalidate_suspect_jumps: if unresolvedTop jumps exist, any resolution in a suspect block is invalidated.
invalidation check catches these cases.
has_dynamic_jumpscounted dead-codejumps (added
recompute_has_dynamic_jumps()).