vestigia

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

commit 70a30ecfce7bf56a2deed0510e93d8aefbfab62e
parent 83a5e59a7681a797cd5315696d88f163c43dfbad
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 30 Oct 2026 18:35:40 +0400

read: objects resolved to the files they name

A harness reports each object as its loader reached it, which for a
library linked by soname is the soname's link (libfoo.so.1), while
memcheck names a frame's object by the file the link leads to
(libfoo.so.1.2.3). Neither the path nor the file name matched, so
every site in such a library went without an offset, and a consumer
placing sites by offset could not place it. read resolves the
reported paths before locating frames; one that names nothing where
read runs is kept as reported.

Diffstat:
MCHANGELOG | 3+++
Mapp/Main.hs | 5+++--
Mlib/Vestigia/Trace.hs | 20++++++++++++++++++++
Mtest/Main.hs | 19+++++++++++++++++++
Mvestigia.cabal | 3+++
5 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -18,6 +18,9 @@ * `record` appends the guest's ISA to its dump. * `diffOf` takes a `contract`; the command finds LLVM's `llvm-objdump` through `$VESTIGIA_OBJDUMP`. + * `read` resolves the objects a harness reports to the files they + name, as memcheck names a frame's object, so a library reached by + its soname's link has its sites placed by offset. - 0.1.0 (2026-10-22) * Initial version, with two instruments over a harness that marks diff --git a/app/Main.hs b/app/Main.hs @@ -54,7 +54,8 @@ import Vestigia.Layout (fixLayout) import Vestigia.Memcheck (Memcheck, readMemcheck) import Vestigia.Symbol (symbolize) import Vestigia.Trace - (Taint(..), readObjects, sitesIn, trace, traceJson, traceSummary) + (Taint(..), readObjects, resolveObjects, sitesIn, trace, traceJson, + traceSummary) data Opts = Opts { oValgrind :: !(Maybe FilePath) @@ -289,7 +290,7 @@ markedObjects p = do \no secret (vestigia_secret was never called on any bytes); \ \its run says nothing") exitWith (ExitFailure 2) - Right os -> pure os + Right os -> resolveObjects os readXml :: FilePath -> IO Memcheck readXml xml = do diff --git a/lib/Vestigia/Trace.hs b/lib/Vestigia/Trace.hs @@ -33,16 +33,19 @@ module Vestigia.Trace ( , trace , sitesIn , readObjects + , resolveObjects , objectOffset , traceJson , traceSummary , classText ) where +import Control.Exception (IOException, try) import qualified Data.List as L import qualified Data.Map.Strict as M import qualified Data.Text as T import Numeric (readHex, showHex) +import System.Directory (canonicalizePath, doesPathExist) import Vestigia.Json import Vestigia.Memcheck @@ -189,6 +192,23 @@ readObjects = traverse line . filter (not . T.null . T.strip) . T.lines -- | A frame's address in its object, where the harness reported the -- object: by its path, else by its file name. +-- | A run's objects, each path resolved to the file it names, as +-- memcheck names a frame's object: a loader reaches a library by +-- the link its soname names, memcheck by the file the link leads +-- to. A path that names nothing here, as in a report made +-- elsewhere, is kept as reported. +resolveObjects :: [(Integer, T.Text)] -> IO [(Integer, T.Text)] +resolveObjects = traverse resolve + where + resolve (b, p) = do + let path = T.unpack p + here <- doesPathExist path + if not here then pure (b, p) else do + r <- try (canonicalizePath path) + pure (b, either (keep p) T.pack r) + keep :: T.Text -> IOException -> T.Text + keep p _ = p + objectOffset :: [(Integer, T.Text)] -> Frame -> Maybe Integer objectOffset os f = do obj <- frObj f diff --git a/test/Main.hs b/test/Main.hs @@ -9,6 +9,8 @@ import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Encoding as TE +import System.Directory +import System.FilePath ((</>)) import Test.Tasty import Test.Tasty.HUnit @@ -108,6 +110,23 @@ traceTests = testGroup "trace" @?= Just 0x401000 objectOffset os (f "/lib/libc.so.6" 0x7000) @?= Nothing + , testCase "an object reached by a link is named by its file" $ do + tmp <- getTemporaryDirectory + let dir = tmp </> "vestigia-resolve-objects" + file = dir </> "liba.so.1.2.3" + link = dir </> "liba.so.1" + removePathForcibly dir + createDirectoryIfMissing True dir + writeFile file "" + createFileLink "liba.so.1.2.3" link + real <- T.pack <$> canonicalizePath file + os <- resolveObjects + [ (0x4000000, T.pack link), (0, "/nowhere/bin/harness") ] + removePathForcibly dir + os @?= [ (0x4000000, real), (0, "/nowhere/bin/harness") ] + objectOffset os (Frame 0x4000010 (Just real) Nothing Nothing Nothing) + @?= Just 0x10 + , testCase "a site is kept to the objects named" $ do tr <- one <$> fixture "probe.xml" length (trSites (sitesIn [ "probe" ] tr)) @?= 2 diff --git a/vestigia.cabal b/vestigia.cabal @@ -44,6 +44,7 @@ library base >= 4.9 && < 5 , bytestring , containers + , directory , process , text @@ -78,6 +79,8 @@ test-suite vestigia-tests base , bytestring , containers + , directory + , filepath , tasty , tasty-hunit , text