Memcheck.hs (5216B)
1 {-# OPTIONS_HADDOCK prune #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 4 -- | 5 -- Module: Vestigia.Memcheck 6 -- Copyright: (c) 2026 Jared Tobin 7 -- License: MIT 8 -- Maintainer: Jared Tobin <jared@ppad.tech> 9 -- 10 -- What a memcheck run reported, read from its XML (protocol 11 -- version 4 and later): each error context with its kind, its 12 -- stack, the origin of the undefined value where memcheck tracked 13 -- one, and how many times it occurred. 14 15 module Vestigia.Memcheck ( 16 Frame(..) 17 , Origin(..) 18 , MemError(..) 19 , Memcheck(..) 20 , readMemcheck 21 ) where 22 23 import qualified Data.Map.Strict as M 24 import Data.Maybe (fromMaybe) 25 import qualified Data.Text as T 26 import Numeric (readHex) 27 import Text.Read (readMaybe) 28 29 import Vestigia.Xml 30 31 -- | One stack frame: the instruction address, and what debug 32 -- information says of it. 33 data Frame = Frame { 34 frIp :: !Integer 35 , frObj :: !(Maybe T.Text) 36 , frFn :: !(Maybe T.Text) 37 , frFile :: !(Maybe T.Text) 38 , frLine :: !(Maybe Int) 39 } deriving (Eq, Show) 40 41 -- | Where an undefined value came from: a client request (the 42 -- harness's mark on a secret), something else memcheck named 43 -- (an allocation, an uninitialised stack slot), or nothing 44 -- tracked. 45 data Origin = 46 ClientRequest ![Frame] 47 | OtherOrigin !T.Text ![Frame] 48 | NoOrigin 49 deriving (Eq, Show) 50 51 -- | One error context. 52 data MemError = MemError { 53 meKind :: !T.Text 54 , meWhat :: !T.Text 55 , meStack :: ![Frame] 56 , meOrigin :: !Origin 57 , meCount :: !Int 58 } deriving (Eq, Show) 59 60 -- | A run: the tool's version, the command it ran, whether it 61 -- finished, and its errors in the order reported. 62 data Memcheck = Memcheck { 63 mcVersion :: !(Maybe T.Text) 64 , mcCommand :: ![T.Text] 65 , mcFinished :: !Bool 66 , mcErrors :: ![MemError] 67 } deriving (Eq, Show) 68 69 -- | Read a memcheck XML document. 70 readMemcheck :: T.Text -> Either String Memcheck 71 readMemcheck t = do 72 root <- parseDocument t 73 case root of 74 Element "valgrindoutput" _ -> pure () 75 _ -> Left "not a valgrind XML document" 76 case fmap textOf (child "protocoltool" root) of 77 Just "memcheck" -> pure () 78 other -> Left ("not a memcheck run: " <> show other) 79 let counts = M.fromList 80 [ (textOf u, n) 81 | ec <- children "errorcounts" root 82 , p <- children "pair" ec 83 , Just u <- [ child "unique" p ] 84 , Just c <- [ child "count" p ] 85 , Just n <- [ readMaybe (T.unpack (textOf c)) ] 86 ] 87 errs <- traverse (memError counts) (children "error" root) 88 pure Memcheck { 89 mcVersion = version root 90 , mcCommand = 91 [ textOf x 92 | as <- children "args" root 93 , av <- children "argv" as 94 , x <- elementsOf av ] 95 , mcFinished = any ((== "FINISHED") . textOf) 96 [ s | st <- children "status" root 97 , s <- children "state" st ] 98 , mcErrors = errs 99 } 100 where 101 elementsOf n = case n of 102 Element _ cs -> [ c | c@(Element m _) <- cs 103 , m == "exe" || m == "arg" ] 104 Text _ -> [] 105 106 -- "Using Valgrind-3.26.0 and LibVEX; ..." 107 version root = case 108 [ v 109 | pre <- children "preamble" root 110 , l <- children "line" pre 111 , let w = textOf l 112 , "Using Valgrind-" `T.isPrefixOf` w 113 , let v = T.takeWhile (/= ' ') (T.drop 15 w) ] of 114 (v : _) -> Just v 115 [] -> Nothing 116 117 memError :: M.Map T.Text Int -> Node -> Either String MemError 118 memError counts n = do 119 kind <- need "kind" 120 let what = maybe "" textOf (child "what" n) 121 stacks = children "stack" n 122 unique = maybe "" textOf (child "unique" n) 123 stack <- case stacks of 124 (s : _) -> traverse frame (children "frame" s) 125 [] -> pure [] 126 -- the origin is the auxwhat that names it, and the stack that 127 -- follows it 128 origin <- case (originText, drop 1 stacks) of 129 (Just w, (s : _)) -> do 130 fs <- traverse frame (children "frame" s) 131 pure (if "client request" `T.isInfixOf` w 132 then ClientRequest fs 133 else OtherOrigin w fs) 134 (Just w, []) -> pure (OtherOrigin w []) 135 (Nothing, _) -> pure NoOrigin 136 pure MemError { 137 meKind = kind 138 , meWhat = what 139 , meStack = stack 140 , meOrigin = origin 141 , meCount = fromMaybe 1 (M.lookup unique counts) 142 } 143 where 144 need k = maybe (Left ("an error without " <> T.unpack k)) (pure 145 . textOf) (child k n) 146 originText = case 147 [ w | a <- children "auxwhat" n 148 , let w = textOf a 149 , "was created by" `T.isInfixOf` w ] of 150 (w : _) -> Just w 151 [] -> Nothing 152 153 frame :: Node -> Either String Frame 154 frame n = do 155 ip <- case fmap textOf (child "ip" n) of 156 Just t | Just v <- hex t -> pure v 157 other -> Left ("a frame without an address: " <> show other) 158 pure Frame { 159 frIp = ip 160 , frObj = field "obj" 161 , frFn = field "fn" 162 , frFile = case (field "dir", field "file") of 163 (Just d, Just f) -> Just (d <> "/" <> f) 164 (Nothing, Just f) -> Just f 165 _ -> Nothing 166 , frLine = field "line" >>= readMaybe . T.unpack 167 } 168 where 169 field k = fmap textOf (child k n) 170 hex t = case readHex (T.unpack (stripPrefix0x t)) of 171 [ (v, "") ] -> Just v 172 _ -> Nothing 173 stripPrefix0x t = fromMaybe t (T.stripPrefix "0x" t)