vestigia

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

Symbol.hs (4882B)


      1 {-# OPTIONS_HADDOCK prune #-}
      2 {-# LANGUAGE OverloadedStrings #-}
      3 
      4 -- |
      5 -- Module: Vestigia.Symbol
      6 -- Copyright: (c) 2026 Jared Tobin
      7 -- License: MIT
      8 -- Maintainer: Jared Tobin <jared@ppad.tech>
      9 --
     10 -- Naming an instruction by its source: an addr2line over an
     11 -- object's debug information, inlined frames included, at the
     12 -- instruction's address in the object (where the object's program
     13 -- headers place it, as its disassembly does). LLVM's names each
     14 -- inlined frame by its own function, and an address no symbol covers
     15 -- by none, where binutils' gives the innermost frame the name of the
     16 -- function it was inlined into, and code without debug information
     17 -- (a GHC program's) the name of whatever function precedes it. LLVM
     18 -- leaves a legacy Rust symbol as an Itanium demangler does, its
     19 -- escapes and hash in place, which are undone here.
     20 
     21 module Vestigia.Symbol (
     22     Place(..)
     23   , symbolize
     24   , parseAddr2line
     25   , rustLegacy
     26   ) where
     27 
     28 import Control.Exception (IOException, try)
     29 import Data.Char (chr, isHexDigit)
     30 import qualified Data.Map.Strict as M
     31 import qualified Data.Text as T
     32 import Numeric (readHex, showHex)
     33 import System.Process (readProcess)
     34 import Text.Read (readMaybe)
     35 
     36 -- | One frame of a source location: the function and, where the
     37 --   debug information has it, the file and line. The innermost
     38 --   frame comes first.
     39 data Place = Place {
     40     plFunction :: !(Maybe T.Text)
     41   , plFile :: !(Maybe T.Text)
     42   , plLine :: !(Maybe Int)
     43   } deriving (Eq, Show)
     44 
     45 -- | The source of each address, per object, by one addr2line call
     46 --   per object. An object addr2line cannot read names nothing.
     47 symbolize
     48   :: FilePath -> [(T.Text, Integer)]
     49   -> IO (M.Map (T.Text, Integer) [Place])
     50 symbolize addr2line keys = do
     51   let byObject = M.fromListWith (<>) [ (o, [ off ]) | (o, off) <- keys ]
     52   parts <- traverse one (M.toList byObject)
     53   pure (M.unions parts)
     54   where
     55     one (obj, offs) = do
     56       r <- try (readProcess addr2line
     57              [ "-a", "-f", "-i", "-C", "-e", T.unpack obj ]
     58              (unlines [ "0x" <> showHex o "" | o <- offs ]))
     59              :: IO (Either IOException String)
     60       pure $ case r of
     61         Left _ -> M.empty
     62         Right out -> M.fromList
     63           [ ((obj, a), ps) | (a, ps) <- parseAddr2line (T.pack out) ]
     64 
     65 -- | addr2line's output under @-a -f -i@: an address line, then a
     66 --   function line and a location line per frame.
     67 --
     68 -- >>> parseAddr2line "0x20\n??\n??:0\n"
     69 -- [(32,[Place Nothing Nothing Nothing])]
     70 parseAddr2line :: T.Text -> [(Integer, [Place])]
     71 parseAddr2line = go . T.lines
     72   where
     73     go ls = case ls of
     74       (a : rest) | Just addr <- address a ->
     75         let (body, more) = break (\l -> address l /= Nothing) rest
     76         in (addr, frames body) : go more
     77       (_ : rest) -> go rest
     78       [] -> []
     79 
     80     address :: T.Text -> Maybe Integer
     81     address l = case T.stripPrefix "0x" l of
     82       Just h | not (T.null h), [ (v, "") ] <- readHex (T.unpack h) ->
     83         Just v
     84       _ -> Nothing
     85 
     86     frames ls = case ls of
     87       (f : loc : rest) -> place f loc : frames rest
     88       _ -> []
     89 
     90     place f loc =
     91       let (file, line) = location loc
     92       in Place {
     93              plFunction = if f == "??" then Nothing else Just (rustLegacy f)
     94            , plFile = file
     95            , plLine = line
     96            }
     97 
     98     -- "path:line", "path:line (discriminator n)", or "??:0"
     99     location loc =
    100       let l = T.takeWhile (/= ' ') loc
    101           (p, n) = T.breakOnEnd ":" l
    102           path = T.dropEnd 1 p
    103       in ( if T.null path || path == "??" then Nothing else Just path
    104          , case readMaybe (T.unpack n) of
    105              Just 0 -> Nothing
    106              other -> other )
    107 
    108 -- | A legacy Rust symbol, as an Itanium demangler leaves it, as
    109 --   rustc-demangle gives it: its hash dropped, each path segment's
    110 --   escapes undone. Any other name is left as it is.
    111 --
    112 -- >>> rustLegacy "a::_$LT$impl$u20$b..C$GT$::f::h346fee1681a4f34f"
    113 -- "a::<impl b::C>::f"
    114 rustLegacy :: T.Text -> T.Text
    115 rustLegacy t = case T.breakOnEnd "::" t of
    116   (pre, h) | not (T.null pre), hash h ->
    117     T.intercalate "::"
    118       (fmap segment (T.splitOn "::" (T.dropEnd 2 pre)))
    119   _ -> t
    120   where
    121     hash h = case T.uncons h of
    122       Just ('h', ds) -> T.length ds == 16 && T.all isHexDigit ds
    123       _ -> False
    124     segment g = T.pack (unescape (T.unpack
    125       (maybe g ("$" <>) (T.stripPrefix "_$" g))))
    126     unescape cs = case cs of
    127       '$' : rest
    128         | (code, '$' : more) <- break (== '$') rest
    129         , Just r <- escape code -> r <> unescape more
    130       '.' : '.' : more -> "::" <> unescape more
    131       c : more -> c : unescape more
    132       [] -> []
    133     escape code = case code of
    134       "SP" -> Just "@"
    135       "BP" -> Just "*"
    136       "RF" -> Just "&"
    137       "LT" -> Just "<"
    138       "GT" -> Just ">"
    139       "LP" -> Just "("
    140       "RP" -> Just ")"
    141       "C" -> Just ","
    142       'u' : h | [ (n, "") ] <- readHex h -> Just [ chr n ]
    143       _ -> Nothing