Skip to content

Commit dd8c479

Browse files
authored
Merge pull request #667 from ethereum/no-overapprox-when-good-test
New CopySlice rule that helps to avoid symbolic copyslice in case of overapproximation
2 parents c1a8270 + e328852 commit dd8c479

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- Two more simplification rules: `ReadByte` & `ReadWord` when the `CopySlice`
2727
it is reading from is writing after the position being read, so the
2828
`CopySlice` can be ignored
29+
- More simplification rules that help avoid symbolic copyslice in case of
30+
STATICCALL overapproximation
31+
- Test to make sure we don't accidentally overapproximate a working, good STATICCALL
2932

3033
## Fixed
3134
- We now try to simplify expressions fully before trying to cast them to a concrete value

src/EVM/Expr.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,12 @@ simplify e = if (mapExpr go e == e)
954954

955955
-- redundant CopySlice
956956
go (CopySlice (Lit 0x0) (Lit 0x0) (Lit 0x0) _ dst) = dst
957+
-- We write over dst with data from src. As long as we read from where we write, and
958+
-- it's the same size, we can just skip the 2nd CopySlice
959+
go (CopySlice readOff dstOff size2 (CopySlice srcOff writeOff size1 src _) dst) |
960+
size1 == size2 && readOff == writeOff = CopySlice srcOff dstOff size1 src dst
961+
-- overwrite empty buf with a buf, return is the buf
962+
go (CopySlice (Lit 0) (Lit 0) (BufLength src1) src2 (ConcreteBuf "")) | src1 == src2 = src1
957963

958964
-- simplify storage
959965
go (SLoad slot store) = readStorage' slot store

test/test.hs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ tests = testGroup "hevm"
504504
let e = ReadByte (Lit 0x0) (WriteWord (Lit 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd) (Lit 0x0) (ConcreteBuf "\255\255\255\255"))
505505
b <- checkEquiv e (Expr.simplify e)
506506
assertBoolM "Simplifier failed" b
507+
, test "copyslice-simps" $ do
508+
let e a b = CopySlice (Lit 0) (Lit 0) (BufLength (AbstractBuf "buff")) (CopySlice (Lit 0) (Lit 0) (BufLength (AbstractBuf "buff")) (AbstractBuf "buff") (ConcreteBuf a)) (ConcreteBuf b)
509+
expr1 = e "" ""
510+
expr2 = e "" "aasdfasdf"
511+
expr3 = e "9832478932" ""
512+
expr4 = e "9832478932" "aasdfasdf"
513+
assertEqualM "Not full simp" (Expr.simplify expr1) (AbstractBuf "buff")
514+
assertEqualM "Not full simp" (Expr.simplify expr2) $ CopySlice (Lit 0x0) (Lit 0x0) (BufLength (AbstractBuf "buff")) (AbstractBuf "buff") (ConcreteBuf "aasdfasdf")
515+
assertEqualM "Not full simp" (Expr.simplify expr3) (AbstractBuf "buff")
516+
assertEqualM "Not full simp" (Expr.simplify expr4) $ CopySlice (Lit 0x0) (Lit 0x0) (BufLength (AbstractBuf "buff")) (AbstractBuf "buff") (ConcreteBuf "aasdfasdf")
507517
, test "buffer-length-copy-slice-beyond-source1" $ do
508518
let e = BufLength (CopySlice (Lit 0x2) (Lit 0x2) (Lit 0x1) (ConcreteBuf "a") (ConcreteBuf ""))
509519
b <- checkEquiv e (Expr.simplify e)
@@ -2290,6 +2300,99 @@ tests = testGroup "hevm"
22902300
assertEqualM "number of counterexamples" 1 numCexes
22912301
assertEqualM "number of errors" 0 numErrs
22922302
assertEqualM "number of qed-s" 0 numQeds
2303+
, test "copyslice-symbolic-ok" $ do
2304+
Just c <- solcRuntime "C"
2305+
[i|
2306+
contract Target {
2307+
function get(address addr) external view returns (uint256) {
2308+
return 55;
2309+
}
2310+
}
2311+
contract C {
2312+
function retFor(address addr) public returns (uint256) {
2313+
Target mm = new Target();
2314+
uint256 ret = mm.get(addr);
2315+
assert(ret == 4);
2316+
return ret;
2317+
}
2318+
}
2319+
|]
2320+
let sig2 = Just (Sig "retFor(address)" [AbiAddressType])
2321+
(expr, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig2 [] defaultVeriOpts
2322+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches expr) <> " paths"
2323+
assertBoolM "The expression is NOT error" $ not $ any isError ret
2324+
-- NOTE: below used to be symbolic copyslice copy error before new copyslice
2325+
-- simplifications in Expr.simplify
2326+
, test "overapproximates-undeployed-contract" $ do
2327+
Just c <- solcRuntime "C"
2328+
[i|
2329+
contract Target {
2330+
function get(address addr) external view returns (uint256) {
2331+
return 55;
2332+
}
2333+
}
2334+
contract C {
2335+
Target mm;
2336+
function retFor(address addr) public returns (uint256) {
2337+
// NOTE: this is symbolic execution, and no setUp has been ran
2338+
// hence, this below calls unknown code! So it overapproximates.
2339+
// mm is actually 0 here, which we may want to give a warning for
2340+
uint256 ret = mm.get(addr);
2341+
assert(ret == 4);
2342+
return ret;
2343+
}
2344+
}
2345+
|]
2346+
let sig2 = Just (Sig "retFor(address)" [AbiAddressType])
2347+
(expr, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig2 [] defaultVeriOpts
2348+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches expr) <> " paths"
2349+
assertBoolM "The expression is NOT error" $ not $ any isError ret
2350+
let numCexes = sum $ map (fromEnum . isCex) ret
2351+
-- There are 2 CEX-es
2352+
-- This is because with one CEX, the return DATA
2353+
-- is empty, and in the other, the return data is non-empty (but symbolic)
2354+
assertEqualM "number of counterexamples" 2 numCexes
2355+
, test "staticcall-no-overapprox-2" $ do
2356+
Just c <- solcRuntime "C"
2357+
[i|
2358+
contract Target {
2359+
function add(uint256 x, uint256 y) external pure returns (uint256) {
2360+
unchecked {
2361+
return x + y;
2362+
}
2363+
}
2364+
}
2365+
contract C {
2366+
function checkval(uint256 x, uint256 y) public {
2367+
Target t = new Target();
2368+
address realAddr = address(t);
2369+
bytes memory data = abi.encodeWithSignature("add(uint256,uint256)", x, y);
2370+
(bool success, bytes memory returnData) = realAddr.staticcall(data);
2371+
assert(success);
2372+
assert(returnData.length == 32);
2373+
2374+
// Decode the return value
2375+
uint256 result = abi.decode(returnData, (uint256));
2376+
2377+
// Assert that the result is equal to x + y
2378+
unchecked {
2379+
assert(result == x + y);
2380+
}
2381+
}
2382+
}
2383+
|]
2384+
let sig = Just (Sig "checkval(uint256,uint256)" [AbiUIntType 256, AbiUIntType 256])
2385+
(res, ret) <- withDefaultSolver $ \s -> checkAssert s defaultPanicCodes c sig [] defaultVeriOpts
2386+
putStrLnM $ "successfully explored: " <> show (Expr.numBranches res) <> " paths"
2387+
assertBoolM "The expression is NOT partial" $ not $ Expr.containsNode isPartial res
2388+
assertBoolM "The expression is NOT unknown" $ not $ any isUnknown ret
2389+
assertBoolM "The expression is NOT error" $ not $ any isError ret
2390+
let numCexes = sum $ map (fromEnum . isCex) ret
2391+
let numErrs = sum $ map (fromEnum . isError) ret
2392+
let numQeds = sum $ map (fromEnum . isQed) ret
2393+
assertEqualM "number of counterexamples" 0 numCexes
2394+
assertEqualM "number of errors" 0 numErrs
2395+
assertEqualM "number of qed-s" 1 numQeds
22932396
, test "staticcall-check-symbolic1" $ do
22942397
Just c <- solcRuntime "C"
22952398
[i|

0 commit comments

Comments
 (0)