Skip to content

Commit 636ef29

Browse files
Update hevm to support dynamic argument inputs in symbolic mode (#1580)
* Fix suitableForSymExec precedence inversion `not $ null m.inputs && ...` parsed as `not (null m.inputs && ...)`, negating the whole conjunction: the dynamic-argument guard never fired (so bytes/string/dynamic-array methods were handed to the symbolic encoder, which throws). Bind `not` to the emptiness check alone so the filter means what it says: explore methods that have arguments, none dynamic, not opted out. * updated to new hevm * dynamic array handling * Update hevm in flake.nix --------- Co-authored-by: gustavo-grieco <gustavo.grieco+github@gmail.com>
1 parent 6d856e0 commit 636ef29

7 files changed

Lines changed: 28 additions & 9 deletions

File tree

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262
(pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
6363
owner = "argotorg";
6464
repo = "hevm";
65-
rev = "8b2cb6266413f7f964e49053a278a895a21dc507";
66-
sha256 = "sha256-wHkH26sfGLEL6XFYl+qZUPdWRZLnoftTqpufy5ASX7k=";
65+
rev = "408bf3100f1edbfc489b21b5218332e583e503a7";
66+
sha256 = "sha256-iJwO0tXuHe5dbPuzIdLHMVW8U4TwAqrC0VKmZcuz9S4=";
6767
}) { secp256k1 = pkgs.secp256k1; })
6868
([
6969
pkgs.haskell.lib.compose.dontCheck

lib/Echidna/SymExec/Common.hs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Data.Text qualified as T
1717
import Optics.Core ((.~), (%), (%~))
1818

1919
import EVM (loadContract, resetState, symbolify)
20-
import EVM.ABI (abiKind, AbiKind(Dynamic), Sig(..), decodeBuf, AbiVals(..), selector, encodeAbiValue, AbiValue(..))
20+
import EVM.ABI (Sig(..), decodeBuf, AbiVals(..), selector, encodeAbiValue, AbiValue(..))
2121
import EVM.Effects (TTY, ReadConfig)
2222
import EVM.Expr qualified
2323
import EVM.Fetch qualified as Fetch
@@ -83,8 +83,12 @@ extractErrors = mapMaybe (\case
8383
_ -> Nothing)
8484

8585
suitableForSymExec :: Method -> Bool
86-
suitableForSymExec m = not $ null m.inputs
87-
&& null (filter (\(_, t) -> abiKind t == Dynamic) m.inputs)
86+
suitableForSymExec m =
87+
-- the method must take arguments (otherwise there is nothing to solve for)
88+
-- and must not opt out via a `_no_symexec` name. Dynamic ABI types
89+
-- (bytes/string/dynamic arrays) are now supported: hevm concretizes them to a
90+
-- bounded length (see maxDynSize), so they no longer disqualify a method.
91+
not (null m.inputs)
8892
&& not (T.isInfixOf "_no_symexec" m.name)
8993

9094

@@ -168,8 +172,11 @@ exploreMethod :: (MonadUnliftIO m, ReadConfig m, TTY m) =>
168172
Method -> SolcContract -> SourceCache -> EVM.Types.VM Concrete -> Addr -> EConfig -> VeriOpts -> SolverGroup -> Fetch.RpcInfo -> Fetch.Session -> m ([TxOrError], PartialsLogs)
169173

170174
exploreMethod method _contract _sources vm defaultSender conf veriOpts solvers rpcInfo session = do
171-
calldataSym@(_, constraints) <- mkCalldata (Just (Sig method.methodSignature (snd <$> method.inputs))) []
175+
-- hevm's mkCalldata now returns ((calldata, constraints), caveats); the
176+
-- caveats (e.g. bounded dynamic args) are not surfaced through echidna yet.
177+
(calldataSym, _caveats) <- mkCalldata (Just (Sig method.methodSignature (snd <$> method.inputs))) []
172178
let
179+
constraints = snd calldataSym
173180
cd = fst calldataSym
174181
fetcher = Fetch.oracle solvers (Just session) rpcInfo
175182
dst = conf.solConf.contractAddr

lib/Echidna/SymExec/Exploration.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ exploreContract contract method vm = do
9898
resultChan <- liftIO newEmptyMVar
9999
let isNonInteractive = conf.uiConf.operationMode == NonInteractive Text
100100
let iterConfig = IterConfig { maxIter = maxIters, askSmtIters = askSmtIters, loopHeuristic = StackBased}
101-
let hevmConfig = defaultConfig { maxWidth = 5, maxDepth = maxExplore, maxBufSize = 12, promiseNoReent = False, onlyDeployed = True, debug = isNonInteractive, dumpQueries = False }
101+
let hevmConfig = defaultConfig { maxWidth = 5, maxDepth = maxExplore, maxBufSize = 12, maxDynSize = 128, promiseNoReent = False, onlyDeployed = True, debug = isNonInteractive, dumpQueries = False }
102102
let veriOpts = VeriOpts {iterConf = iterConfig, rpcInfo = rpcInfo}
103103
let runtimeEnv = defaultEnv { config = hevmConfig }
104104
session <- asks (.fetchSession)

lib/Echidna/SymExec/Verification.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ verifyMethod method contract vm = do
6262
resultChan <- liftIO newEmptyMVar
6363
let isNonInteractive = conf.uiConf.operationMode == NonInteractive Text
6464
let iterConfig = IterConfig { maxIter = maxIters, askSmtIters = askSmtIters, loopHeuristic = StackBased}
65-
let hevmConfig = defaultConfig { maxWidth = 5, maxDepth = maxExplore, dumpExprs = True, maxBufSize = 12, promiseNoReent = False, onlyDeployed = True, debug = isNonInteractive }
65+
let hevmConfig = defaultConfig { maxWidth = 5, maxDepth = maxExplore, dumpExprs = True, maxBufSize = 12, maxDynSize = 128, promiseNoReent = False, onlyDeployed = True, debug = isNonInteractive }
6666
let veriOpts = VeriOpts {iterConf = iterConfig, rpcInfo = rpcInfo}
6767
let runtimeEnv = defaultEnv { config = hevmConfig }
6868
session <- asks (.fetchSession)

src/test/Tests/Symbolic.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ symbolicTests = testGroup "Symbolic tests" $
1818
]
1919
) ["symbolic/verify.yaml", "symbolic/verify.bitwuzla.yaml"]
2020
++ ([
21+
-- dynamic (bytes) calldata: hevm concretizes it up to maxDynSize (128 bytes),
22+
-- so the symbolic worker can solve it. Run under bitwuzla only.
23+
testContract' "symbolic/verify.sol" (Just "VulnerableContract") (Just (>= solcV (0,6,9))) (Just "symbolic/verify.bitwuzla.yaml") True SymbolicWorker
24+
[ ("dynamic passed", solved "dynamic") ]
2125
-- This test is commented out because it requires a specific setup where both the FuzzWorker and SymbolicWorker are used.
2226
-- If you run the symbolic worker alone, it will hang indefinitely.
2327
--, testContract' "symbolic/explore.sol" Nothing Nothing (Just "symbolic/explore.yaml") True SymbolicWorker

stack.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ packages:
66

77
extra-deps:
88
- git: https://github.com/argotorg/hevm.git
9-
commit: 8b2cb6266413f7f964e49053a278a895a21dc507
9+
commit: 408bf3100f1edbfc489b21b5218332e583e503a7
1010

1111
- smt2-parser-0.1.0.1@sha256:1e1a4565915ed851c13d1e6b8bb5185cf5d454da3b43170825d53e221f753d77,1421
1212
- spawn-0.3@sha256:b91e01d8f2b076841410ae284b32046f91471943dc799c1af77d666c72101f02,1162

tests/solidity/symbolic/verify.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ contract VulnerableContract {
3939
assert(y != 42);
4040
}
4141

42+
// dynamic (bytes) input: hevm concretizes it up to maxDynSize (128 bytes).
43+
// Solvable, since the bug only needs the first two bytes to be 0xde 0xad.
44+
function dynamic(bytes calldata data) public {
45+
if (data.length >= 2) {
46+
assert(data[0] != 0xde || data[1] != 0xad); // BUG
47+
}
48+
}
49+
4250
}

0 commit comments

Comments
 (0)