vestigia

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

commit 8b69178513d4f42a3f2645d66adf23d4a7b6b713
parent 200dfd9496267c8d8bae5dd3b45116676348eccf
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 19 Oct 2026 13:42:14 +0400

diff: each origin names the runs alike at it (diff-v2)

The diff report becomes vestigia/diff-v2. Each origin now says how the
runs behaved there: the runs that behaved alike, grouped, and the runs
that never reached it, each named by its dump's file name. A reader can
then tell which secrets the execution told apart, and so whether two
secrets a function accepted differed at a site, or only an accepted and
a refused one.

What ran a different number of times only downstream of an origin is
listed per object rather than counted, and --object restricts it as it
restricts origins. diffOf passes its dumps in run order, not by glob.

Diffstat:
MCHANGELOG | 4+++-
MREADME.md | 19+++++++++++--------
Mapp/Main.hs | 7+++++--
Mlib/Vestigia/Diff.hs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mnix/lib.nix | 3++-
Mtest/Main.hs | 30++++++++++++++++++++++++++++++
Mvestigia.cabal | 1+
7 files changed, 121 insertions(+), 24 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -12,7 +12,9 @@ * `vestigia record` and `vestigia diff`: the harness under QEMU 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 + diverge (vestigia/diff-v2: each origin with the runs alike + there and the runs that never reached it, and what ran + downstream listed per object), set against controls repeating a secret, with what every run executed in each object, and runs designed to reach particular paths compared for control and coverage alone (`--designed`); runs are recorded with diff --git a/README.md b/README.md @@ -100,13 +100,16 @@ address 0x152eb Fq::sqrt_ratio_zeta invsqrt.rs:117 ... An *origin* is a branch whose outcomes differ in proportion between 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. -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. +instruction run equally often whose addresses differ. Each origin +names the runs that behaved alike there, grouped, and the runs that +never reached it, each run by its dump's file name: which secrets +the execution told apart. Instructions that merely ran a different +number of times sit where control had already diverged, and are +listed per object as downstream. 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, what ran downstream, and what +was executed, to objects whose path contains the given text. `--designed DUMP` names a run aimed at a particular path. It differs from the others in where it goes, not only in the secret, so it @@ -144,7 +147,7 @@ vestigia.lib.x86_64-linux.diffOf { ``` Each run's report is `trace.json` (`vestigia/trace-v1`) or -`diff.json` (`vestigia/diff-v1`), beside the raw memcheck XML or +`diff.json` (`vestigia/diff-v2`), beside the raw memcheck XML or plugin dumps, what the harness printed, and the run's wall time. ## Two instruments diff --git a/app/Main.hs b/app/Main.hs @@ -24,6 +24,7 @@ import qualified Data.Text.IO as TIO import System.Directory (getTemporaryDirectory, removeFile) import System.Environment (getArgs, getEnvironment, lookupEnv) import System.Exit (ExitCode(..), exitWith) +import System.FilePath (takeFileName) import System.IO (hClose, hPutStrLn, openTempFile, stderr) import System.Process (proc, waitForProcess, withCreateProcess) import qualified System.Process as P @@ -249,8 +250,10 @@ diff o paths = do places <- symbolize addr2line [ (obj, inOffset i) | (_, i) <- dvOrigins dv , Just obj <- [ inObject i ] ] - let doc = encode - (divergenceJson (T.pack (showVersion version)) places dv) + -- a run is named by its dump's file name, the declared first + let names = fmap (T.pack . takeFileName) (paths <> oDesigned o) + doc = encode + (divergenceJson (T.pack (showVersion version)) names places dv) maybe (TIO.putStr doc) (\p -> TIO.writeFile p doc) (oOut o) TIO.hPutStr stderr (divergenceSummary places dv) where diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs @@ -46,6 +46,13 @@ -- 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. +-- +-- Each origin names how the runs behaved there: the runs alike at +-- it, grouped (taking each transfer in the same proportion, at a +-- control origin; touching the same addresses, at an address +-- origin), and the runs that never reached it. Whether two runs +-- that differed there both had their secrets accepted is the +-- reader's to say, from what each run returned. module Vestigia.Diff ( Insn(..) @@ -121,6 +128,10 @@ data Origin = -- different number of times. data Divergence = Divergence { dvOrigins :: ![(Origin, Insn)] + -- | at each origin, the runs alike there, grouped, and the runs + -- that never reached it, as indices into the declared runs + -- then the designed (an address origin's, the declared alone) + , dvBehaviour :: !(M.Map (Origin, Integer) ([[Int]], [Int])) , dvDownstream :: ![Insn] , dvUnsteady :: ![Insn] , dvRuns :: !Int @@ -146,9 +157,9 @@ diffControlled cs ds = diffDesigned cs ds [] -- control origins and what was executed are every run's. diffDesigned :: [Dump] -> [Dump] -> [Dump] -> Divergence diffDesigned cs ds xs = Divergence { - dvOrigins = L.sortOn (inPc . snd) - [ o | o@(_, i) <- controls <> addresses - , not (inPc i `S.member` unsteady) ] + dvOrigins = L.sortOn (inPc . snd) origins + , dvBehaviour = M.fromList + [ ((o, inPc i), behaviourAt o (inPc i)) | (o, i) <- origins ] , dvDownstream = [ i | pc <- S.toAscList downstream , not (pc `S.member` unsteady) , Just i <- [ firstInsn pc ] ] @@ -180,9 +191,33 @@ diffDesigned cs ds xs = Divergence { , fmap sig ia /= fmap sig ib || ta /= tb ] sig i = (inCount i, inFingerprint i) + origins = [ o | o@(_, i) <- controls <> addresses + , not (inPc i `S.member` unsteady) ] + -- every run, the declared first runs = ds <> xs + -- how the runs behaved at an origin: those that ran it, grouped + -- by what they did there, and those that never ran it. Only the + -- declared runs speak to an address. + behaviourAt o pc = case o of + ControlOrigin -> + ( classes proportional + [ (k, (inCount i, M.findWithDefault M.empty pc + (dpTransfers d))) + | (k, d, i) <- ranIn runs ] + , absentIn runs ) + AddressOrigin -> + ( classes (==) [ (k, inFingerprint i) | (k, _, i) <- ranIn ds ] + , absentIn ds ) + where + ranIn rs = [ (k, d, i) | (k, d) <- zip [ 0 .. ] rs + , Just i <- [ M.lookup pc (dpInsns d) ] + , inCount i > 0 ] + absentIn rs = [ k | (k, d) <- zip [ 0 .. ] rs + , maybe True ((== 0) . inCount) + (M.lookup pc (dpInsns d)) ] + pcs = S.unions (fmap (M.keysSet . dpInsns) runs) firstInsn pc = case [ i | d <- runs , Just i <- [ M.lookup pc (dpInsns d) ] ] of @@ -238,14 +273,14 @@ diffDesigned cs ds xs = Divergence { , Just i <- [ firstInsn pc ] , inAccesses i > 0 ] --- | Only the origins, and what was executed, in objects whose path --- contains one of the given texts; downstream instructions are --- kept, to be counted. +-- | Only the origins, what was executed, and what ran downstream, in +-- objects whose path contains one of the given texts. restrictTo :: [T.Text] -> Divergence -> Divergence restrictTo ps dv | null ps = dv | otherwise = dv { dvOrigins = filter (inside . inObject . snd) (dvOrigins dv) + , dvDownstream = filter (inside . inObject) (dvDownstream dv) , dvExecuted = M.filterWithKey (\o _ -> inside (Just o)) (dvExecuted dv) } @@ -259,11 +294,13 @@ originText o = case o of ControlOrigin -> "control" AddressOrigin -> "address" --- | The report: @vestigia/diff-v1@. +-- | The report: @vestigia/diff-v2@. Runs are named as given, the +-- declared then the designed. divergenceJson - :: T.Text -> M.Map (T.Text, Integer) [Place] -> Divergence -> Value -divergenceJson version places dv = Object - [ ("schema", String "vestigia/diff-v1") + :: T.Text -> [T.Text] -> M.Map (T.Text, Integer) [Place] -> Divergence + -> Value +divergenceJson version names places dv = Object + [ ("schema", String "vestigia/diff-v2") , ("tool", Object [ ("name", String "vestigia") , ("version", String version) ]) , ("runs", Number (toInteger (dvRuns dv))) @@ -283,9 +320,17 @@ divergenceJson version places dv = Object , ("offset", String (hex (inOffset i))) , ("count", Number (inCount i)) , ("frames", Array (fmap placeJson (placesOf i))) + , ("groups", Array (fmap (Array . fmap named) groups)) + , ("absent", Array (fmap named absent)) ] - | (o, i) <- dvOrigins dv ]) - , ("downstream", byObject (dvDownstream dv)) + | (o, i) <- dvOrigins dv + , let (groups, absent) = M.findWithDefault ([], []) (o, inPc i) + (dvBehaviour dv) ]) + , ("downstream", Object + [ (obj, Array (fmap (String . hex) (S.toAscList offs))) + | (obj, offs) <- M.toList (M.fromListWith S.union + [ (obj, S.singleton (inOffset i)) + | i <- dvDownstream dv, Just obj <- [ inObject i ] ]) ]) , ("unsteady", byObject (dvUnsteady dv)) , ("executed", Object [ (o, Array (fmap (String . hex) (S.toAscList xs))) @@ -294,6 +339,9 @@ divergenceJson version places dv = Object where count o = Number (toInteger (length [ () | (o', _) <- dvOrigins dv, o' == o ])) + named k = case drop k names of + (n : _) -> String n + [] -> String (T.pack (show k)) byObject is = Object [ (o, Number (toInteger n)) | (o, n) <- M.toList (M.fromListWith (+) @@ -344,3 +392,12 @@ divergenceSummary places dv = T.unlines $ hex :: Integer -> T.Text hex n = "0x" <> T.pack (showHex n "") + +-- keys grouped by an equivalence on their values, each group in +-- order and the groups by their first member +classes :: (a -> a -> Bool) -> [(Int, a)] -> [[Int]] +classes eq ys = case ys of + [] -> [] + ((k, y) : rest) -> + let (same, other) = L.partition (eq y . snd) rest + in (k : fmap fst same) : classes eq other diff --git a/nix/lib.nix b/nix/lib.nix @@ -129,7 +129,8 @@ let objects}${lib.concatImapStrings (k: _: "--designed $out/designed-${toString k} ") designed} \ --control $out/control-1 \ - $out/dump-* 2> $out/summary + ${lib.concatImapStrings (k: _: "$out/dump-${toString k} ") + runs}2> $out/summary echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds ''; in diff --git a/test/Main.hs b/test/Main.hs @@ -181,6 +181,36 @@ diffTests = testGroup "diff" fmap (\(o, i) -> (o, inPc i)) (dvOrigins (diffDesigned [] [ a, b ] [ x ])) @?= [ (ControlOrigin, 0x10), (AddressOrigin, 0x20) ] + , testCase "an origin names the runs alike at it" $ do + a <- either assertFailure pure (readDump dumpA) + b <- either assertFailure pure (readDump dumpB) + x <- either assertFailure pure (readDump (T.unlines + [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 4" + , "I 20 4 4 9999 /lib.so 20", "I 30 4 0 cccc /lib.so 30" ])) + -- at the branch, the first and third runs alike; the designed + -- run is a control origin's to group, and the address origin + -- speaks only of the declared runs + let dv = diffDesigned [] [ a, b, a ] [ x ] + M.lookup (ControlOrigin, 0x10) (dvBehaviour dv) + @?= Just ([ [ 0, 2 ], [ 1 ], [ 3 ] ], []) + M.lookup (AddressOrigin, 0x20) (dvBehaviour dv) + @?= Just ([ [ 0, 2 ], [ 1 ] ], []) + -- the designed run never reaches 0x40 + M.lookup (ControlOrigin, 0x10) + (dvBehaviour (diffDesigned [] [ a, a ] [ x ])) + @?= Just ([ [ 0, 1 ], [ 2 ] ], []) + + , testCase "downstream is kept to the objects under audit" $ do + a <- either assertFailure pure (readDump dumpA) + b <- either assertFailure pure (readDump (T.unlines + [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 3" + , "I 20 4 4 bbbb /lib.so 20", "I 30 3 0 cccc /lib.so 30" + , "I 40 1 1 dddd /lib.so 40", "I 60 2 0 ffff /libc.so 60" ])) + let dv = diffDumps [ a, b ] + fmap inPc (dvDownstream dv) @?= [ 0x30, 0x60 ] + fmap inPc (dvDownstream (restrictTo [ "lib.so" ] dv)) + @?= [ 0x30 ] + , testCase "identical runs have no origins" $ do a <- either assertFailure pure (readDump dumpA) let dv = diffDumps [ a, a ] diff --git a/vestigia.cabal b/vestigia.cabal @@ -56,6 +56,7 @@ executable vestigia base , bytestring , directory + , filepath , process , text , vestigia