Trace.hs (11538B)
1 {-# OPTIONS_HADDOCK prune #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 4 -- | 5 -- Module: Vestigia.Trace 6 -- Copyright: (c) 2026 Jared Tobin 7 -- License: MIT 8 -- Maintainer: Jared Tobin <jared@ppad.tech> 9 -- 10 -- A run's leakage, read from what memcheck reported. An error 11 -- whose undefined value was created by a client request descends 12 -- from a byte the harness marked secret, so it is a site where the 13 -- execution depended on the secret: a branch (`control`), an 14 -- address computed from it (`address`), an integer division's 15 -- operand (`operand`, from vestigia's patched memcheck), or a 16 -- system call handed it (`syscall`). Every other uninitialised read 17 -- is kept apart as `unrelated`, and memory errors as `faults`: 18 -- neither says anything about the secret. Leak reports say nothing 19 -- about the execution and are dropped. 20 -- 21 -- memcheck sees the path a run took, so runs of one harness on 22 -- several secrets are read together: a site is one instruction and 23 -- class however many runs reached it, with the runs that did. Where 24 -- a run's objects are known, a site is named by its address in its 25 -- object, so runs that loaded an object apart still agree on it. 26 27 module Vestigia.Trace ( 28 Class(..) 29 , Taint(..) 30 , Located(..) 31 , Site(..) 32 , Trace(..) 33 , trace 34 , sitesIn 35 , readObjects 36 , resolveObjects 37 , objectOffset 38 , traceJson 39 , traceSummary 40 , classText 41 ) where 42 43 import Control.Exception (IOException, try) 44 import qualified Data.List as L 45 import qualified Data.Map.Strict as M 46 import qualified Data.Text as T 47 import Numeric (readHex, showHex) 48 import System.Directory (canonicalizePath, doesPathExist) 49 50 import Vestigia.Json 51 import Vestigia.Memcheck 52 53 -- | How an execution depended on a secret at a site. 54 data Class = 55 Control 56 | Address 57 | Operand 58 | Syscall 59 deriving (Eq, Ord, Show) 60 61 -- | One run under memcheck: its name, what memcheck reported, where 62 -- the harness's loader put each object (its load bias and path, 63 -- as the harness reported them; none for a report made 64 -- elsewhere), and the harness's exit status, where known. 65 data Taint = Taint { 66 tnName :: !T.Text 67 , tnMemcheck :: !Memcheck 68 , tnObjects :: ![(Integer, T.Text)] 69 , tnExit :: !(Maybe Int) 70 } deriving (Eq, Show) 71 72 -- | A frame, with its address in its object where its run's objects 73 -- say. 74 data Located = Located { 75 lcFrame :: !Frame 76 , lcOffset :: !(Maybe Integer) 77 } deriving (Eq, Show) 78 79 -- | One instruction that depended on a secret: where, as the first 80 -- run to reach it saw it; how many times, over every run; the runs 81 -- that reached it, as indices into the runs; and every stack that 82 -- did. 83 data Site = Site { 84 siClass :: !Class 85 , siAt :: !Located 86 , siCount :: !Int 87 , siRuns :: ![Int] 88 , siStacks :: ![[Located]] 89 } deriving (Eq, Show) 90 91 -- | Runs' sites, in the order first reported, and what memcheck 92 -- reported that says nothing about the secret, each with the 93 -- index of the run that reported it. 94 data Trace = Trace { 95 trRuns :: ![Taint] 96 , trSites :: ![Site] 97 , trUnrelated :: ![(Int, MemError)] 98 , trFaults :: ![(Int, MemError)] 99 } deriving (Eq, Show) 100 101 classText :: Class -> T.Text 102 classText c = case c of 103 Control -> "control" 104 Address -> "address" 105 Operand -> "operand" 106 Syscall -> "syscall" 107 108 -- what a secret-originated error says about the execution 109 classify :: MemError -> Maybe Class 110 classify e = case (meOrigin e, meKind e) of 111 (ClientRequest _, "UninitCondition") -> Just Control 112 (ClientRequest _, "UninitValue") -> Just Address 113 (ClientRequest _, "UninitOperand") -> Just Operand 114 (ClientRequest _, "SyscallParam") -> Just Syscall 115 _ -> Nothing 116 117 uninitialised :: MemError -> Bool 118 uninitialised e = meKind e `elem` 119 [ "UninitCondition", "UninitValue", "UninitOperand", "SyscallParam" ] 120 121 -- | The runs' leakage. Sites are one per instruction and class: 122 -- memcheck reports one context per distinct stack, and the 123 -- instruction is what depended on the secret, whatever called it 124 -- and in whichever run. 125 trace :: [Taint] -> Trace 126 trace ts = Trace { 127 trRuns = ts 128 , trSites = 129 [ Site { 130 siClass = c 131 , siAt = at 132 , siCount = sum [ meCount e | (_, e, _) <- xs ] 133 , siRuns = L.nub [ k | (k, _, _) <- xs ] 134 , siStacks = L.nubBy (\a b -> fmap ident a == fmap ident b) 135 [ st | (_, _, st) <- xs ] 136 } 137 | key@(c, _) <- L.nub (fmap fst tagged) 138 , Just xs@((_, _, at : _) : _) <- [ M.lookup key grouped ] ] 139 , trUnrelated = 140 [ (k, e) | (k, t) <- runs, e <- mcErrors (tnMemcheck t) 141 , uninitialised e, classify e == Nothing ] 142 , trFaults = 143 [ (k, e) | (k, t) <- runs, e <- mcErrors (tnMemcheck t) 144 , not (uninitialised e) 145 , not ("Leak_" `T.isPrefixOf` meKind e) ] 146 } 147 where 148 runs = zip [ 0 .. ] ts 149 tagged = 150 [ ((c, ident top), (k, e, st)) 151 | (k, t) <- runs 152 , e <- mcErrors (tnMemcheck t) 153 , Just c <- [ classify e ] 154 , let st = fmap (locate (tnObjects t)) (meStack e) 155 , (top : _) <- [ st ] ] 156 grouped = M.fromListWith (flip (<>)) 157 [ (key, [ v ]) | (key, v) <- tagged ] 158 -- an instruction is its address in its object where that is 159 -- known, else its address in the run 160 ident (Located f off) = 161 ( frObj f, maybe (Left (frIp f)) Right off, frFn f, frFile f 162 , frLine f ) 163 164 locate :: [(Integer, T.Text)] -> Frame -> Located 165 locate os f = Located f (objectOffset os f) 166 167 -- | Only the sites in objects whose path contains one of the given 168 -- texts, as a differential run keeps its origins; every site when 169 -- none is given. 170 sitesIn :: [T.Text] -> Trace -> Trace 171 sitesIn ts tr 172 | null ts = tr 173 | otherwise = tr { trSites = filter inside (trSites tr) } 174 where 175 inside s = case frObj (lcFrame (siAt s)) of 176 Just o -> any (`T.isInfixOf` o) ts 177 Nothing -> False 178 179 -- | The objects file the harness's first mark writes: a load bias 180 -- (hex) and a path per line. 181 -- 182 -- >>> readObjects "7f0000 /lib/libc.so.6\n400000 /bin/harness\n" 183 -- Right [(8323072,"/lib/libc.so.6"),(4194304,"/bin/harness")] 184 readObjects :: T.Text -> Either String [(Integer, T.Text)] 185 readObjects = traverse line . filter (not . T.null . T.strip) . T.lines 186 where 187 line l = case T.breakOn " " l of 188 (b, p) | not (T.null p) 189 , [ (v, "") ] <- readHex (T.unpack b) -> 190 Right (v, T.drop 1 p) 191 _ -> Left ("an unreadable object line: " <> T.unpack l) 192 193 -- | A frame's address in its object, where the harness reported the 194 -- object: by its path, else by its file name. 195 -- | A run's objects, each path resolved to the file it names, as 196 -- memcheck names a frame's object: a loader reaches a library by 197 -- the link its soname names, memcheck by the file the link leads 198 -- to. A path that names nothing here, as in a report made 199 -- elsewhere, is kept as reported. 200 resolveObjects :: [(Integer, T.Text)] -> IO [(Integer, T.Text)] 201 resolveObjects = traverse resolve 202 where 203 resolve (b, p) = do 204 let path = T.unpack p 205 here <- doesPathExist path 206 if not here then pure (b, p) else do 207 r <- try (canonicalizePath path) 208 pure (b, either (keep p) T.pack r) 209 keep :: T.Text -> IOException -> T.Text 210 keep p _ = p 211 212 objectOffset :: [(Integer, T.Text)] -> Frame -> Maybe Integer 213 objectOffset os f = do 214 obj <- frObj f 215 base <- case [ b | (b, p) <- os, p == obj ] of 216 (b : _) -> Just b 217 [] -> case [ b | (b, p) <- os, fileName p == fileName obj ] of 218 (b : _) -> Just b 219 [] -> Nothing 220 if frIp f >= base then Just (frIp f - base) else Nothing 221 where 222 fileName p = case reverse (T.splitOn "/" p) of 223 (x : _) -> x 224 [] -> p 225 226 -- | The report: @vestigia/trace-v2@. 227 traceJson :: T.Text -> Trace -> Value 228 traceJson version tr = Object 229 [ ("schema", String "vestigia/trace-v2") 230 , ("tool", Object [ ("name", String "vestigia") 231 , ("version", String version) ]) 232 , ("runs", Array 233 [ Object 234 [ ("name", String (tnName t)) 235 , ("valgrind", maybe Null String (mcVersion mc)) 236 , ("command", Array (fmap String (mcCommand mc))) 237 , ("finished", Bool (mcFinished mc)) 238 , ("exit", maybe Null (Number . toInteger) (tnExit t)) 239 , ("objects", Array 240 [ Object [ ("base", String (hex b)), ("path", String p) ] 241 | (b, p) <- tnObjects t ]) 242 ] 243 | t <- trRuns tr, let mc = tnMemcheck t ]) 244 , ("summary", Object 245 [ ("control", count Control) 246 , ("address", count Address) 247 , ("operand", count Operand) 248 , ("syscall", count Syscall) 249 , ("unrelated", Number (toInteger (length (trUnrelated tr)))) 250 , ("faults", Number (toInteger (length (trFaults tr)))) 251 ]) 252 , ("sites", Array (fmap siteJson (trSites tr))) 253 , ("unrelated", Array (fmap errorJson (trUnrelated tr))) 254 , ("faults", Array (fmap errorJson (trFaults tr))) 255 ] 256 where 257 count c = Number (toInteger (length 258 [ () | s <- trSites tr, siClass s == c ])) 259 name k = case drop k (trRuns tr) of 260 (t : _) -> String (tnName t) 261 [] -> String (T.pack (show k)) 262 objectsOf k = case drop k (trRuns tr) of 263 (t : _) -> tnObjects t 264 [] -> [] 265 siteJson s = Object $ 266 [ ("class", String (classText (siClass s))) ] 267 <> locatedFields (siAt s) 268 <> [ ("count", Number (toInteger (siCount s))) 269 , ("runs", Array (fmap name (siRuns s))) 270 , ("stacks", Array [ Array (fmap (Object . locatedFields) st) 271 | st <- siStacks s ]) 272 ] 273 errorJson (k, e) = Object 274 [ ("run", name k) 275 , ("kind", String (meKind e)) 276 , ("what", String (meWhat e)) 277 , ("origin", case meOrigin e of 278 ClientRequest _ -> String "client request" 279 OtherOrigin w _ -> String w 280 NoOrigin -> Null) 281 , ("count", Number (toInteger (meCount e))) 282 , ("stack", Array (fmap (Object . locatedFields 283 . locate (objectsOf k)) (meStack e))) 284 ] 285 286 locatedFields :: Located -> [(T.Text, Value)] 287 locatedFields (Located f off) = 288 [ ("ip", String (hex (frIp f))) 289 , ("object", maybe Null String (frObj f)) 290 , ("offset", maybe Null (String . hex) off) 291 , ("function", maybe Null String (frFn f)) 292 , ("file", maybe Null String (frFile f)) 293 , ("line", maybe Null (Number . toInteger) (frLine f)) 294 ] 295 296 hex :: Integer -> T.Text 297 hex n = "0x" <> T.pack (showHex n "") 298 299 -- | One line per site, for a reader at a terminal: with how many runs 300 -- reached it, where there were several. 301 traceSummary :: Trace -> T.Text 302 traceSummary tr = T.unlines $ 303 [ T.intercalate " " $ 304 [ classText (siClass s) 305 , hex (frIp f) 306 , maybe "?" id (frFn f) 307 , location f 308 , "x" <> T.pack (show (siCount s)) ] 309 <> [ T.pack (show (length (siRuns s))) <> "/" 310 <> T.pack (show runs) <> " runs" 311 | runs > 1 ] 312 | s <- trSites tr, let f = lcFrame (siAt s) ] 313 <> [ T.pack (show (length (trSites tr))) <> " secret-dependent \ 314 \site(s) over " <> T.pack (show runs) <> " run(s); " 315 <> T.pack (show (length (trUnrelated tr))) 316 <> " unrelated; " <> T.pack (show (length (trFaults tr))) 317 <> " fault(s)" ] 318 where 319 runs = length (trRuns tr) 320 location f = case (frFile f, frLine f) of 321 (Just p, Just l) -> base p <> ":" <> T.pack (show l) 322 (Just p, Nothing) -> base p 323 _ -> maybe "?" id (frObj f) 324 base p = last' p (T.splitOn "/" p) 325 last' d xs = case reverse xs of 326 (x : _) -> x 327 [] -> d