vestigia

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

commit 2284937a6cab40688bf8f09a15b4062f6cef7e9e
parent 89244a3ad19b085993be5a2f0ca660964b8202b7
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 18 Oct 2026 07:14:07 +0400

diff: what every run executed, per object

Diffstat:
MCHANGELOG | 5+++--
MREADME.md | 6++++--
Mlib/Vestigia/Diff.hs | 28++++++++++++++++++++++++----
Mtest/Main.hs | 13+++++++++++++
4 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -13,7 +13,8 @@ with vestigia's plugin, its leakage trace summarised per instruction, and where runs differing only in the secret diverge (vestigia/diff-v1), set against controls repeating a - secret; runs are recorded with randomisation off and the - guest's entropy seeded. + secret, with what every run executed in each object; runs are + recorded with randomisation off and the guest's entropy + seeded. * `traceOf` and `diffOf` run either as a derivation; `nix/lib.nix` holds them for any package set. diff --git a/README.md b/README.md @@ -103,8 +103,10 @@ runs (a loop whose trip count the secret sets is one) or an instruction run equally often whose addresses differ. Instructions that merely ran a different number of times sit where control had already diverged, and are counted as downstream rather than listed. -`--object` keeps origins in objects whose path contains the given -text. +The report also lists, per object, the offset of every instruction +some run executed: what the chosen secrets exercised, to set against +what the code could execute. `--object` keeps origins, and what was +executed, to objects whose path contains the given text. A difference means the secret only if nothing else differs between runs. `record` turns address-space randomisation off for itself and diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs @@ -34,6 +34,10 @@ -- an instruction that differs between the first run and a control -- differs without the secret (an entropy source, a clock) and is -- `unsteady`, counted and excluded from the origins. +-- +-- What every run executed, per object, is reported beside: the +-- instructions the chosen secrets exercised, and so, against what +-- a function could execute, the part of it the comparison saw. module Vestigia.Diff ( Insn(..) @@ -112,6 +116,8 @@ data Divergence = Divergence { , dvUnsteady :: ![Insn] , dvRuns :: !Int , dvControls :: !Int + -- | the offsets some run, control or not, executed, by object + , dvExecuted :: !(M.Map T.Text (S.Set Integer)) } deriving (Eq, Show) -- | Compare runs, uncontrolled. @@ -134,6 +140,12 @@ diffControlled cs ds = Divergence { , Just i <- [ firstInsn pc ] ] , dvRuns = length ds , dvControls = length cs + , dvExecuted = M.fromListWith S.union + [ (obj, S.singleton (inOffset i)) + | d <- ds <> cs + , i <- M.elems (dpInsns d) + , inCount i > 0 + , Just obj <- [ inObject i ] ] } where -- what differs between the first run and a repeat of its secret @@ -199,14 +211,19 @@ diffControlled cs ds = Divergence { , Just i <- [ firstInsn pc ] , inAccesses i > 0 ] --- | Only the origins in objects whose path contains one of the --- given texts; downstream instructions are kept, to be counted. +-- | Only the origins, and what was executed, in objects whose path +-- contains one of the given texts; downstream instructions are +-- kept, to be counted. restrictTo :: [T.Text] -> Divergence -> Divergence restrictTo ps dv | null ps = dv - | otherwise = dv { dvOrigins = filter (inside . snd) (dvOrigins dv) } + | otherwise = dv { + dvOrigins = filter (inside . inObject . snd) (dvOrigins dv) + , dvExecuted = M.filterWithKey (\o _ -> inside (Just o)) + (dvExecuted dv) + } where - inside i = case inObject i of + inside mo = case mo of Just o -> any (`T.isInfixOf` o) ps Nothing -> False @@ -242,6 +259,9 @@ divergenceJson version places dv = Object | (o, i) <- dvOrigins dv ]) , ("downstream", byObject (dvDownstream dv)) , ("unsteady", byObject (dvUnsteady dv)) + , ("executed", Object + [ (o, Array (fmap (String . hex) (S.toAscList xs))) + | (o, xs) <- M.toList (dvExecuted dv) ]) ] where count o = Number (toInteger (length diff --git a/test/Main.hs b/test/Main.hs @@ -6,6 +6,7 @@ module Main ( import qualified Data.ByteString as BS 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 Test.Tasty @@ -167,6 +168,18 @@ diffTests = testGroup "diff" dvOrigins dv @?= [] dvDownstream dv @?= [] + , testCase "what any run executed is kept per object" $ do + a <- either assertFailure pure (readDump dumpA) + b <- either assertFailure pure (readDump (T.unlines + [ "I 10 4 0 aaaa /lib.so 10", "I 50 1 0 eeee /lib.so 50" + , "I 60 1 0 ffff /libc.so 60", "I 70 0 0 0000 /lib.so 70" ])) + let dv = diffDumps [ a, b ] + dvExecuted dv @?= M.fromList + [ ("/lib.so", S.fromList [ 0x10, 0x20, 0x30, 0x40, 0x50 ]) + , ("/libc.so", S.fromList [ 0x60 ]) ] + M.keys (dvExecuted (restrictTo [ "lib.so" ] dv)) + @?= [ "/lib.so" ] + , testCase "an unreadable line is refused" $ isLeft (readDump "I 10 zz 0 f /lib.so 0\n") @? "accepted" ]