Skip to content

Commit 4974580

Browse files
authored
Improve identification of CBOR bytecode metadata (#1506)
* Improve identification of CBOR bytecode metadata * pick the last prefix if several are present, to work in bytecode-in-bytecode scenarios (e.g., a contract that deploys other contracts) * identify CBOR length and drop other trailing data (typically constructor arguments) * Try CBOR metadata matches from end backwards, pick first valid In bytecode-in-bytecode scenarios (e.g. a contract deploying another), the child contract's CBOR prefix can appear in the middle of the parent bytecode. Previously we only tried the last prefix match and fell back to full bytecode if it was invalid. Now we try all matches from end to start and return the first one with a valid CBOR length.
1 parent e899c10 commit 4974580

File tree

1 file changed

+52
-5
lines changed

1 file changed

+52
-5
lines changed

lib/Echidna/SourceMapping.hs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
module Echidna.SourceMapping where
22

33
import Control.Applicative ((<|>))
4+
import Data.Bits (shiftL, (.|.))
45
import Data.ByteString (ByteString)
56
import Data.ByteString qualified as BS
67
import Data.IORef (IORef, readIORef, atomicModifyIORef')
7-
import Data.List (find)
8+
import Data.List (find, sortBy)
9+
import Data.Ord (Down(..))
810
import Data.Map.Strict (Map)
911
import Data.Map.Strict qualified as Map
1012
import Data.Maybe (mapMaybe)
1113
import Data.Vector qualified as V
14+
import Data.Word (Word16)
1215

1316
import EVM.Dapp (DappInfo(..), findSrc)
1417
import EVM.Expr (maybeLitByteSimp)
@@ -88,12 +91,56 @@ findSrcByMetadata contr dapp = find compareMetadata (snd <$> Map.elems dapp.solc
8891
findSrcForReal :: DappInfo -> Contract -> Maybe SolcContract
8992
findSrcForReal dapp contr = findSrc contr dapp <|> findSrcByMetadata contr dapp
9093

94+
-- | Find the position of CBOR length indicator after a metadata prefix.
95+
-- The length indicator is 2 bytes that encode the distance from prefix start to that position.
96+
-- Returns the position of the length indicator (not including the 2 bytes themselves).
97+
findCBORLength :: ByteString -> Int -> Maybe Int
98+
findCBORLength metadata prefixPos = go (prefixPos + 1)
99+
where
100+
go currentPos
101+
| currentPos + 2 > BS.length metadata = Nothing
102+
| otherwise = case readWord16BE metadata currentPos of
103+
Nothing -> Nothing
104+
Just lengthValue ->
105+
let distanceFromPrefix = currentPos - prefixPos
106+
in if fromIntegral lengthValue == distanceFromPrefix
107+
then Just currentPos
108+
else go (currentPos + 1)
109+
-- | Read 2 bytes at given position as big-endian Word16
110+
readWord16BE :: ByteString -> Int -> Maybe Word16
111+
readWord16BE bs pos
112+
| pos + 1 < BS.length bs =
113+
let b1 = fromIntegral (BS.index bs pos) :: Word16
114+
b2 = fromIntegral (BS.index bs (pos + 1)) :: Word16
115+
in Just $ (b1 `shiftL` 8) .|. b2
116+
| otherwise = Nothing
117+
118+
-- | Find all occurrences of any of the given prefixes in the bytecode,
119+
-- sorted by position descending (from end to start).
120+
findAllPrefixes :: ByteString -> [ByteString] -> [(Int, ByteString)]
121+
findAllPrefixes bs prefixes =
122+
sortBy (\a b -> compare (Down (fst a)) (Down (fst b))) $ concatMap findAll prefixes
123+
where
124+
findAll prefix = go 0
125+
where
126+
go offset = case BS.breakSubstring prefix (BS.drop offset bs) of
127+
(_, rest) | BS.null rest -> []
128+
(before, _) ->
129+
let pos = offset + BS.length before
130+
in (pos, prefix) : go (pos + 1)
131+
91132
getBytecodeMetadata :: ByteString -> ByteString
92133
getBytecodeMetadata bs =
93-
let stripCandidates = flip BS.breakSubstring bs <$> knownBzzrPrefixes in
94-
case find ((/= mempty) . snd) stripCandidates of
95-
Nothing -> bs -- if no metadata is found, return the complete bytecode
96-
Just (_, m) -> m
134+
case firstValid (findAllPrefixes bs knownBzzrPrefixes) of
135+
Nothing -> bs -- if no valid metadata is found, return the complete bytecode
136+
Just metadata -> metadata
137+
where
138+
firstValid [] = Nothing
139+
firstValid ((pos, _prefix):rest) =
140+
case findCBORLength bs pos of
141+
Just lengthPos ->
142+
Just $ BS.drop pos (BS.take (lengthPos + 2) bs)
143+
Nothing -> firstValid rest
97144

98145
knownBzzrPrefixes :: [ByteString]
99146
knownBzzrPrefixes =

0 commit comments

Comments
 (0)