vestigia

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

commit d977ab84713434e4cca65e08c1741fe79aea5513
parent f319ea2c4c6186ac2f244dd58ba9d8125081cb96
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 18 Oct 2026 12:03:29 +0400

diff: runs designed for a path, compared for control and coverage alone

Diffstat:
MCHANGELOG | 7++++---
MREADME.md | 7+++++++
Mapp/Main.hs | 14++++++++++----
Mlib/Vestigia/Diff.hs | 57++++++++++++++++++++++++++++++++++++++++++++-------------
Mnix/lib.nix | 22+++++++++++++++++-----
Mtest/Main.hs | 19+++++++++++++++++++
6 files changed, 101 insertions(+), 25 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -13,8 +13,9 @@ 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, with what every run executed in each object; runs are - recorded with randomisation off and the guest's entropy - seeded. + 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 + 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 @@ -108,6 +108,13 @@ 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. +`--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 +counts toward the control origins (a branch whose outcomes shift +under it chose by the secret) and what was executed, never toward +the address origins, which only runs alike in all but the secret can +show. + A difference means the secret only if nothing else differs between runs. `record` turns address-space randomisation off for itself and QEMU, and seeds the guest's entropy, so runs lay memory out alike diff --git a/app/Main.hs b/app/Main.hs @@ -30,7 +30,7 @@ import qualified System.Process as P import Paths_vestigia (version) import Vestigia.Diff - (diffControlled, divergenceJson, divergenceSummary, dvOrigins, + (diffDesigned, divergenceJson, divergenceSummary, dvOrigins, inObject, inOffset, readDump, restrictTo) import Vestigia.Json (encode) import Vestigia.Layout (fixLayout) @@ -49,6 +49,7 @@ data Opts = Opts { , oAddr2line :: !(Maybe FilePath) , oObjects :: ![T.Text] , oControls :: ![FilePath] + , oDesigned :: ![FilePath] } main :: IO () @@ -85,7 +86,8 @@ usage e = do , " vestigia record [--qemu PATH] [--plugin PATH] --dump FILE \ \-- PROGRAM [ARG...]" , " vestigia diff [--out FILE] [--addr2line PATH] \ - \[--object TEXT]... [--control DUMP]... DUMP DUMP..." + \[--object TEXT]... [--control DUMP]... [--designed DUMP]... \ + \DUMP DUMP..." , "" , "run executes PROGRAM under memcheck, which reports every" , "branch and address that depended on bytes the program marked" @@ -107,7 +109,7 @@ usage e = do -- options before the program; "--" ends them options :: [String] -> Either String (Opts, [String]) options = go (Opts Nothing Nothing Nothing Nothing Nothing Nothing - Nothing [] []) + Nothing [] [] []) where go o xs = case xs of ("--" : rest) -> Right (o, rest) @@ -122,6 +124,8 @@ options = go (Opts Nothing Nothing Nothing Nothing Nothing Nothing go o { oObjects = oObjects o <> [ T.pack t ] } rest ("--control" : p : rest) -> go o { oControls = oControls o <> [ p ] } rest + ("--designed" : p : rest) -> + go o { oDesigned = oDesigned o <> [ p ] } rest (x : _) | take 2 x == "--" -> Left ("unknown option: " <> x) _ -> Right (o, xs) @@ -239,7 +243,9 @@ diff o paths = do addr2line <- tool (oAddr2line o) "VESTIGIA_ADDR2LINE" "addr2line" dumps <- traverse readOne paths controls <- traverse readOne (oControls o) - let dv = restrictTo (oObjects o) (diffControlled controls dumps) + designed <- traverse readOne (oDesigned o) + let dv = restrictTo (oObjects o) + (diffDesigned controls dumps designed) places <- symbolize addr2line [ (obj, inOffset i) | (_, i) <- dvOrigins dv , Just obj <- [ inObject i ] ] diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs @@ -35,6 +35,14 @@ -- differs without the secret (an entropy source, a clock) and is -- `unsteady`, counted and excluded from the origins. -- +-- Runs designed to reach a particular path are compared beside these +-- but not alike: they differ from the others in where they go, not +-- only in the secret, so an instruction's addresses differing in one +-- of them says nothing about the secret. A designed run counts toward +-- the control origins (a branch whose outcomes shift under it chose +-- by the secret) and what was executed, never toward the address +-- origins, which only runs alike in all but the secret can show. +-- -- 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. @@ -47,6 +55,7 @@ module Vestigia.Diff ( , Divergence(..) , diffDumps , diffControlled + , diffDesigned , restrictTo , divergenceJson , divergenceSummary @@ -116,6 +125,7 @@ data Divergence = Divergence { , dvUnsteady :: ![Insn] , dvRuns :: !Int , dvControls :: !Int + , dvDesigned :: !Int -- | the offsets some run, control or not, executed, by object , dvExecuted :: !(M.Map T.Text (S.Set Integer)) } deriving (Eq, Show) @@ -129,7 +139,13 @@ diffDumps = diffControlled [] -- instruction at which some two runs differ, and which no control -- shows differing without the secret. diffControlled :: [Dump] -> [Dump] -> Divergence -diffControlled cs ds = Divergence { +diffControlled cs ds = diffDesigned cs ds [] + +-- | Compare runs, given controls and runs designed to reach particular +-- paths. The address origins are the declared runs' alone; the +-- 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) ] @@ -140,9 +156,10 @@ diffControlled cs ds = Divergence { , Just i <- [ firstInsn pc ] ] , dvRuns = length ds , dvControls = length cs + , dvDesigned = length xs , dvExecuted = M.fromListWith S.union [ (obj, S.singleton (inOffset i)) - | d <- ds <> cs + | d <- ds <> xs <> cs , i <- M.elems (dpInsns d) , inCount i > 0 , Just obj <- [ inObject i ] ] @@ -163,26 +180,34 @@ diffControlled cs ds = Divergence { , fmap sig ia /= fmap sig ib || ta /= tb ] sig i = (inCount i, inFingerprint i) - pcs = S.unions (fmap (M.keysSet . dpInsns) ds) - firstInsn pc = case [ i | d <- ds + -- every run, the declared first + runs = ds <> xs + + pcs = S.unions (fmap (M.keysSet . dpInsns) runs) + firstInsn pc = case [ i | d <- runs , Just i <- [ M.lookup pc (dpInsns d) ] ] of (i : _) -> Just i [] -> Nothing -- run counts per run, absent counted as never run - countsAt pc = [ maybe 0 inCount (M.lookup pc (dpInsns d)) | d <- ds ] - alike xs = case xs of + countsIn rs pc = + [ maybe 0 inCount (M.lookup pc (dpInsns d)) | d <- rs ] + alike ys = case ys of [] -> True - (x : rest) -> all (== x) rest + (y : rest) -> all (== y) rest - steady = S.filter (alike . countsAt) pcs + -- run equally often in every run, and in every declared run + steady = S.filter (alike . countsIn runs) pcs + steadyDeclared = S.filter (alike . countsIn ds) + (S.unions (fmap (M.keysSet . dpInsns) ds)) -- an instruction's run count and transfers, in each run that ran -- it ranAt pc = [ (inCount i, M.findWithDefault M.empty pc (dpTransfers d)) - | d <- ds, Just i <- [ M.lookup pc (dpInsns d) ], inCount i > 0 ] - transferSources = S.unions (fmap (M.keysSet . dpTransfers) ds) + | d <- runs, Just i <- [ M.lookup pc (dpInsns d) ] + , inCount i > 0 ] + transferSources = S.unions (fmap (M.keysSet . dpTransfers) runs) -- outcomes in the same proportion: each target taken as often, -- relative to the run count, in both @@ -195,7 +220,9 @@ diffControlled cs ds = Divergence { [] -> False controlPcs = S.filter divergent transferSources - downstream = (pcs `S.difference` steady) `S.difference` controlPcs + addressPcs = S.fromList [ inPc i | (_, i) <- addresses ] + downstream = ((pcs `S.difference` steady) `S.difference` controlPcs) + `S.difference` addressPcs controls = [ (ControlOrigin, i) @@ -204,7 +231,7 @@ diffControlled cs ds = Divergence { addresses = [ (AddressOrigin, i) - | pc <- S.toAscList steady + | pc <- S.toAscList steadyDeclared , let fps = [ fmap inFingerprint (M.lookup pc (dpInsns d)) | d <- ds ] , not (alike fps) @@ -241,6 +268,7 @@ divergenceJson version places dv = Object , ("version", String version) ]) , ("runs", Number (toInteger (dvRuns dv))) , ("controls", Number (toInteger (dvControls dv))) + , ("designed", Number (toInteger (dvDesigned dv))) , ("summary", Object [ ("control", count ControlOrigin) , ("address", count AddressOrigin) @@ -291,7 +319,10 @@ divergenceSummary places dv = T.unlines $ , maybe "-" base (inObject i) ] | (o, i) <- dvOrigins dv ] <> [ T.pack (show (length (dvOrigins dv))) <> " origin(s) over " - <> T.pack (show (dvRuns dv)) <> " run(s); " + <> T.pack (show (dvRuns dv)) <> " run(s)" + <> (if dvDesigned dv == 0 then "" + else " and " <> T.pack (show (dvDesigned dv)) <> " designed") + <> "; " <> T.pack (show (length (dvDownstream dv))) <> " instruction(s) downstream; " <> T.pack (show (length (dvUnsteady dv))) diff --git a/nix/lib.nix b/nix/lib.nix @@ -93,9 +93,11 @@ let # keeps origins in objects whose path contains one of them. The # first run is repeated as a control, so that what differs # without the secret is set aside rather than taken for it. - # A run's own failure leaves its status; a dump vestigia - # cannot read fails the build. - diffOf = { name, harness, runs, objects ? [ ] }: + # `designed` runs, aimed at particular paths, are compared beside + # them for control and coverage but not for addresses (vestigia + # diff --designed). A run's own failure leaves its status; a dump + # vestigia cannot read fails the build. + diffOf = { name, harness, runs, objects ? [ ], designed ? [ ] }: pkgs.runCommand "vestigia-diff-${name}" { } '' mkdir -p $out start=$(date +%s%N) @@ -106,7 +108,16 @@ let > $out/stdout-${toString k} 2> $out/stderr-${toString k} echo $? > $out/status-${toString k} set -e - '') runs} + '') runs}${lib.concatImapStrings (k: args: '' + set +e + ${vestigia}/bin/vestigia record \ + --dump $out/designed-${toString k} \ + -- ${harness} ${lib.escapeShellArgs args} \ + > $out/stdout-designed-${toString k} \ + 2> $out/stderr-designed-${toString k} + echo $? > $out/status-designed-${toString k} + set -e + '') designed} set +e ${vestigia}/bin/vestigia record --dump $out/control-1 \ -- ${harness} ${lib.escapeShellArgs (builtins.head runs)} \ @@ -115,7 +126,8 @@ let set -e ${vestigia}/bin/vestigia diff --out $out/diff.json \ ${lib.concatMapStrings (o: "--object ${lib.escapeShellArg o} ") - objects} \ + objects}${lib.concatImapStrings (k: _: + "--designed $out/designed-${toString k} ") designed} \ --control $out/control-1 \ $out/dump-* 2> $out/summary echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds diff --git a/test/Main.hs b/test/Main.hs @@ -162,6 +162,25 @@ diffTests = testGroup "diff" @?= [ (ControlOrigin, 0x10) ] fmap inPc (dvUnsteady dv) @?= [ 0x20 ] + , testCase "a designed run moves control, not addresses" $ do + a <- either assertFailure pure (readDump dumpA) + b <- either assertFailure pure (readDump dumpB) + -- a run designed onto another path: 0x10 taken every time, and + -- 0x20's addresses its own + 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" + , "I 50 1 0 eeee /lib.so 50" ])) + let dv = diffDesigned [] [ a, a ] [ x ] + fmap (\(o, i) -> (o, inPc i)) (dvOrigins dv) + @?= [ (ControlOrigin, 0x10) ] + dvDesigned dv @?= 1 + M.lookup "/lib.so" (dvExecuted dv) + @?= Just (S.fromList [ 0x10, 0x20, 0x30, 0x40, 0x50 ]) + -- a declared run differing there is still an address origin + fmap (\(o, i) -> (o, inPc i)) (dvOrigins (diffDesigned [] [ a, b ] + [ x ])) @?= [ (ControlOrigin, 0x10), (AddressOrigin, 0x20) ] + , testCase "identical runs have no origins" $ do a <- either assertFailure pure (readDump dumpA) let dv = diffDumps [ a, a ]