vestigia

Detect where execution depends on secrets.
git clone git://git.ppad.tech/vestigia.git
Log | Files | Refs | README | LICENSE

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)