vestigia

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

commit 7440899be79afbd1839f87c905c68515643be54f
parent f87fdf896e5a6dda1ed834261a44b398bc60cfa6
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 17 Oct 2026 22:23:36 +0400

diff: controls, seeded entropy, and nix/lib.nix for consumers

Diffstat:
MCHANGELOG | 7+++++--
MREADME.md | 24+++++++++++++++++++-----
Mapp/Main.hs | 39+++++++++++++++++++++------------------
Metc/spike/penumbra/RESULTS.md | 5++++-
Metc/spike/poly1305/RESULTS.md | 9++++++---
Mflake.nix | 112+++++--------------------------------------------------------------------------
Mlib/Vestigia/Diff.hs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Alib/Vestigia/Layout.hs | 27+++++++++++++++++++++++++++
Anix/lib.nix | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/Main.hs | 14++++++++++++++
Mvestigia.cabal | 5+++--
11 files changed, 285 insertions(+), 146 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -11,5 +11,8 @@ * `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). - * `traceOf` and `diffOf` run either as a derivation. + diverge (vestigia/diff-v1), set against controls repeating a + secret; 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 @@ -85,12 +85,15 @@ in the secret, and `diff` reports where the runs diverge: $ for s in 1 2 3; do > vestigia record --dump run-$s -- ./harness public_key 2 $s > done -$ vestigia diff --object libpenumbra_audit --out diff.json run-* +$ vestigia record --dump control -- ./harness public_key 2 1 +$ vestigia diff --object libpenumbra_audit --control control \ +> --out diff.json run-* control 0xe9d6 Element::vartime_compress_to_field sign.rs:11 ... address 0x152c8 Fq::sqrt_ratio_zeta invsqrt.rs:113 ... address 0x152eb Fq::sqrt_ratio_zeta invsqrt.rs:117 ... ... -44 origin(s) over 3 run(s); 1383 instruction(s) downstream +44 origin(s) over 3 run(s); 1383 instruction(s) downstream; 0 unsteady +over 1 control(s) ``` An *origin* is a branch whose outcomes differ in proportion between @@ -99,15 +102,26 @@ 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. `record` turns address-space randomisation off for itself and -QEMU, since runs to be compared must lay memory out alike. +text. + +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 +and a hash table seeded from the system's randomness is laid out +alike too. `--control DUMP` names a run repeating the first run's +secret: whatever differs between the two (a clock read, a +scheduler's choice) is *unsteady*, counted and kept out of the +origins. ### As derivations The flake's `lib.<system>` holds `traceOf` and `diffOf`, which run a harness under either instrument as a derivation: a run for `x86_64-linux` then executes on whatever builds for that system, and -its report is cached like any other output. +its report is cached like any other output. `diffOf` repeats the +first run as its control. `nix/lib.nix` holds the same definitions +for one package set, for a consumer that pins vestigia and builds +traces with its own nixpkgs. ```nix vestigia.lib.x86_64-linux.diffOf { diff --git a/app/Main.hs b/app/Main.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE OverloadedStrings #-} -- | @@ -15,8 +14,7 @@ module Main ( ) where import Control.Exception (IOException, try) -import Control.Monad (when) -import Foreign.C.Types (CInt(..)) +import Control.Monad (unless) import qualified Data.ByteString as BS import Data.Version (showVersion) import qualified Data.Text as T @@ -31,18 +29,14 @@ import System.Process (proc, waitForProcess, withCreateProcess) import Paths_vestigia (version) import Vestigia.Diff - (diffDumps, divergenceJson, divergenceSummary, dvOrigins, inObject, - inOffset, readDump, restrictTo) + (diffControlled, divergenceJson, divergenceSummary, dvOrigins, + inObject, inOffset, readDump, restrictTo) import Vestigia.Json (encode) +import Vestigia.Layout (fixLayout) import Vestigia.Memcheck (readMemcheck) import Vestigia.Symbol (symbolize) import Vestigia.Trace (trace, traceJson, traceSummary) --- address-space randomisation off, for this process and its --- children; nonzero where it cannot be (cbits/norandom.c) -foreign import ccall unsafe "vestigia_no_randomize" - c_noRandomize :: IO CInt - data Opts = Opts { oValgrind :: !(Maybe FilePath) , oXml :: !(Maybe FilePath) @@ -52,6 +46,7 @@ data Opts = Opts { , oDump :: !(Maybe FilePath) , oAddr2line :: !(Maybe FilePath) , oObjects :: ![T.Text] + , oControls :: ![FilePath] } main :: IO () @@ -88,7 +83,7 @@ usage e = do , " vestigia record [--qemu PATH] [--plugin PATH] --dump FILE \ \-- PROGRAM [ARG...]" , " vestigia diff [--out FILE] [--addr2line PATH] \ - \[--object TEXT]... DUMP DUMP..." + \[--object TEXT]... [--control DUMP]... DUMP DUMP..." , "" , "run executes PROGRAM under memcheck, which reports every" , "branch and address that depended on bytes the program marked" @@ -99,7 +94,9 @@ usage e = do , "dumps its leakage trace per instruction; diff compares dumps" , "of runs that differ only in a secret and reports where they" , "diverge (vestigia/diff-v1), naming sources with addr2line." - , "--object keeps origins in objects whose path contains TEXT." + , "--object keeps origins in objects whose path contains TEXT;" + , "--control names a run repeating the first run's secret, and" + , "what differs in it is unsteady, not an origin." , "qemu, the plugin, and addr2line default to $VESTIGIA_QEMU," , "$VESTIGIA_PLUGIN, and $VESTIGIA_ADDR2LINE." ] @@ -108,7 +105,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) @@ -121,6 +118,8 @@ options = go (Opts Nothing Nothing Nothing Nothing Nothing Nothing ("--addr2line" : p : rest) -> go o { oAddr2line = Just p } rest ("--object" : t : rest) -> go o { oObjects = oObjects o <> [ T.pack t ] } rest + ("--control" : p : rest) -> + go o { oControls = oControls o <> [ p ] } rest (x : _) | take 2 x == "--" -> Left ("unknown option: " <> x) _ -> Right (o, xs) @@ -196,15 +195,18 @@ tool opt var dflt = do record :: Opts -> FilePath -> [String] -> IO () record o prog progArgs = do dump <- maybe (usage "record: --dump FILE is needed") pure (oDump o) - -- runs to be compared must lay memory out alike - fixed <- c_noRandomize - when (fixed /= 0) $ hPutStrLn stderr + -- runs to be compared must lay memory out alike, and draw the + -- same entropy: a hash table seeded from the operating system's + -- randomness is laid out anew in every process + fixed <- fixLayout + unless fixed $ hPutStrLn stderr "vestigia: address randomisation is still on; runs may differ \ \in layout, and so everywhere" qemu <- tool (oQemu o) "VESTIGIA_QEMU" "qemu-x86_64" plugin <- tool (oPlugin o) "VESTIGIA_PLUGIN" "libvestigia-qemu.so" r <- try (withCreateProcess - (proc qemu ([ "-plugin", plugin <> ",out=" <> dump, prog ] + (proc qemu ([ "-seed", "0" + , "-plugin", plugin <> ",out=" <> dump, prog ] <> progArgs)) (\_ _ _ ph -> waitForProcess ph)) case r of @@ -218,7 +220,8 @@ diff :: Opts -> [FilePath] -> IO () diff o paths = do addr2line <- tool (oAddr2line o) "VESTIGIA_ADDR2LINE" "addr2line" dumps <- traverse readOne paths - let dv = restrictTo (oObjects o) (diffDumps dumps) + controls <- traverse readOne (oControls o) + let dv = restrictTo (oObjects o) (diffControlled controls dumps) places <- symbolize addr2line [ (obj, inOffset i) | (_, i) <- dvOrigins dv , Just obj <- [ inObject i ] ] diff --git a/etc/spike/penumbra/RESULTS.md b/etc/spike/penumbra/RESULTS.md @@ -34,7 +34,10 @@ address): check. Differential traces (44, 45, and 72 origins; about 1380 -instructions downstream): +instructions downstream; with the guest's entropy seeded and a +control repeating the first secret, none unsteady, so the +`invsqrt` lookups are the secret's and not hashbrown's random +seeding): - The double-and-add (bits.rs:25, 32; skip_while.rs:50). - The modulus compares inside every field operation on the point's diff --git a/etc/spike/poly1305/RESULTS.md b/etc/spike/poly1305/RESULTS.md @@ -44,6 +44,9 @@ per three-run diff, on pollux. differential runs. - Address-space randomisation made identical runs differ at 2183 places; `vestigia record` now turns it off for itself and QEMU. -- A seed passed as an argument is decoded by libc, which shows as - origins in libc; differential origins are filtered to the - harness binary, which holds the whole Haskell program. +- Even with layout fixed, a GHC program does not run identically + twice: about 120 instructions in libc and the runtime differ + between runs of the same key. A control run repeating the first + key exposes them, and `diff` sets them aside as unsteady; with + the control, mutants-1's origins are L3's address and L1's + branches, and none of the runtime's. diff --git a/flake.nix b/flake.nix @@ -16,115 +16,17 @@ let pkgs = import nixpkgs { inherit system; }; lib = pkgs.lib; - - hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: { - vestigia = new.callCabal2nix "vestigia" ./. { }; - }); - - # memcheck runs on Linux only; elsewhere the command can still - # read a run's XML + v = import ./nix/lib.nix { inherit pkgs; src = ./.; }; + hpkgs = v.hpkgs; linux = pkgs.stdenv.hostPlatform.isLinux; - valgrind = pkgs.valgrind; - - # the QEMU plugin recording a run's leakage trace per - # instruction, and the QEMU that runs a harness of this - # system's ISA under it - qemu = pkgs.qemu; - qemuFor = { - x86_64 = "${qemu}/bin/qemu-x86_64"; - aarch64 = "${qemu}/bin/qemu-aarch64"; - }; - plugin = pkgs.runCommandCC "vestigia-qemu-plugin" { - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = [ pkgs.glib ]; - } '' - mkdir -p $out/lib - cc -O2 -shared -fPIC -Wall -Werror \ - $(pkg-config --cflags glib-2.0) -I${qemu}/include \ - -o $out/lib/libvestigia-qemu.so ${./plugin/vestigia-qemu.c} - ''; - isa = builtins.head (lib.splitString "-" system); - - # the harness-side header; a harness build also needs - # valgrind's own (valgrindDev) - include = pkgs.runCommand "vestigia-include" { } '' - mkdir -p $out/include - cp ${./include/vestigia.h} $out/include/vestigia.h - cp ${./include/vestigia-ffi.c} $out/include/vestigia-ffi.c - ''; - - # the command, with the valgrind it was built against baked in - vestigia = pkgs.runCommand "vestigia" { - nativeBuildInputs = [ pkgs.makeWrapper ]; - } '' - mkdir -p $out/bin - makeWrapper ${hpkgs.vestigia}/bin/vestigia $out/bin/vestigia \ - ${lib.optionalString linux (lib.concatStringsSep " " [ - "--set-default VESTIGIA_VALGRIND ${valgrind}/bin/valgrind" - "--set-default VESTIGIA_QEMU ${qemuFor.${isa}}" - "--set-default VESTIGIA_PLUGIN" - "${plugin}/lib/libvestigia-qemu.so" - "--set-default VESTIGIA_ADDR2LINE" - "${pkgs.binutils-unwrapped}/bin/addr2line" - ])} - ln -s ${include}/include $out/include - ''; - - # one harness run under memcheck, as a derivation: its report, - # memcheck's XML, and what the harness printed. A harness that - # fails still leaves a report of what ran; a run vestigia could - # not read fails the build. - traceOf = { name, harness, args ? [ ] }: - pkgs.runCommand "vestigia-trace-${name}" { } '' - mkdir -p $out - start=$(date +%s%N) - set +e - ${vestigia}/bin/vestigia run \ - --xml $out/memcheck.xml --out $out/trace.json \ - -- ${harness} ${lib.escapeShellArgs args} \ - > $out/stdout 2> $out/stderr - echo $? > $out/status - set -e - echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds - test -s $out/trace.json || { - cat $out/stderr >&2 - exit 1 - } - ''; - - # runs of one harness that differ only in a secret, each - # recorded under QEMU, and where they diverge: the dumps, and - # the report. `runs` is a list of argument lists; `objects` - # keeps origins in objects whose path contains one of them. - # A run's own failure leaves its status; a dump vestigia - # cannot read fails the build. - diffOf = { name, harness, runs, objects ? [ ] }: - pkgs.runCommand "vestigia-diff-${name}" { } '' - mkdir -p $out - start=$(date +%s%N) - ${lib.concatImapStrings (k: args: '' - set +e - ${vestigia}/bin/vestigia record --dump $out/dump-${toString k} \ - -- ${harness} ${lib.escapeShellArgs args} \ - > $out/stdout-${toString k} 2> $out/stderr-${toString k} - echo $? > $out/status-${toString k} - set -e - '') runs} - ${vestigia}/bin/vestigia diff --out $out/diff.json \ - ${lib.concatMapStrings (o: "--object ${lib.escapeShellArg o} ") - objects} \ - $out/dump-* 2> $out/summary - echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds - ''; in { - packages.default = vestigia; - packages.include = include; - packages.plugin = plugin; + packages.default = v.vestigia; + packages.include = v.include; + packages.plugin = v.plugin; lib = { - inherit traceOf diffOf; - valgrindDev = valgrind.dev; + inherit (v) traceOf diffOf valgrindDev; }; checks.default = hpkgs.vestigia; @@ -134,7 +36,7 @@ buildInputs = [ hpkgs.cabal-install pkgs.stdenv.cc - ] ++ lib.optional linux valgrind; + ] ++ lib.optional linux pkgs.valgrind; shellHook = '' PS1="[vestigia] \w$ " diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs @@ -28,6 +28,12 @@ -- branch and then used as an index differs between runs, though -- no undefined bit ever reached it. It sees only what the chosen -- secrets actually varied. +-- +-- A difference means the secret only if nothing else differs +-- between runs. Controls are runs repeating the first run's secret: +-- 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. module Vestigia.Diff ( Insn(..) @@ -36,6 +42,7 @@ module Vestigia.Diff ( , Origin(..) , Divergence(..) , diffDumps + , diffControlled , restrictTo , divergenceJson , divergenceSummary @@ -102,19 +109,48 @@ data Origin = data Divergence = Divergence { dvOrigins :: ![(Origin, Insn)] , dvDownstream :: ![Insn] + , dvUnsteady :: ![Insn] , dvRuns :: !Int + , dvControls :: !Int } deriving (Eq, Show) --- | Compare runs. Every run is set against every other, so an --- origin is any instruction at which some two runs differ. +-- | Compare runs, uncontrolled. diffDumps :: [Dump] -> Divergence -diffDumps ds = Divergence { - dvOrigins = L.sortOn (inPc . snd) (controls <> addresses) +diffDumps = diffControlled [] + +-- | Compare runs, given controls repeating the first run's secret. +-- Every run is set against every other, so an origin is any +-- instruction at which some two runs differ, and which no control +-- shows differing without the secret. +diffControlled :: [Dump] -> [Dump] -> Divergence +diffControlled cs ds = Divergence { + dvOrigins = L.sortOn (inPc . snd) + [ o | o@(_, i) <- controls <> addresses + , not (inPc i `S.member` unsteady) ] , dvDownstream = [ i | pc <- S.toAscList downstream + , not (pc `S.member` unsteady) , Just i <- [ firstInsn pc ] ] + , dvUnsteady = [ i | pc <- S.toAscList unsteady + , Just i <- [ firstInsn pc ] ] , dvRuns = length ds + , dvControls = length cs } where + -- what differs between the first run and a repeat of its secret + unsteady = case ds of + (d : _) -> S.unions (fmap (differing d) cs) + [] -> S.empty + differing a b = S.fromList + [ pc + | pc <- S.toList (M.keysSet (dpInsns a) + `S.union` M.keysSet (dpInsns b)) + , let ia = M.lookup pc (dpInsns a) + ib = M.lookup pc (dpInsns b) + ta = M.lookup pc (dpTransfers a) + tb = M.lookup pc (dpTransfers b) + , 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 , Just i <- [ M.lookup pc (dpInsns d) ] ] of @@ -187,10 +223,12 @@ divergenceJson version places dv = Object , ("tool", Object [ ("name", String "vestigia") , ("version", String version) ]) , ("runs", Number (toInteger (dvRuns dv))) + , ("controls", Number (toInteger (dvControls dv))) , ("summary", Object [ ("control", count ControlOrigin) , ("address", count AddressOrigin) , ("downstream", Number (toInteger (length (dvDownstream dv)))) + , ("unsteady", Number (toInteger (length (dvUnsteady dv)))) ]) , ("origins", Array [ Object $ @@ -202,15 +240,16 @@ divergenceJson version places dv = Object , ("frames", Array (fmap placeJson (placesOf i))) ] | (o, i) <- dvOrigins dv ]) - , ("downstream", Object - [ (o, Number (toInteger n)) - | (o, n) <- M.toList (M.fromListWith (+) - [ (maybe "-" id (inObject i), 1 :: Int) - | i <- dvDownstream dv ]) ]) + , ("downstream", byObject (dvDownstream dv)) + , ("unsteady", byObject (dvUnsteady dv)) ] where count o = Number (toInteger (length [ () | (o', _) <- dvOrigins dv, o' == o ])) + byObject is = Object + [ (o, Number (toInteger n)) + | (o, n) <- M.toList (M.fromListWith (+) + [ (maybe "-" id (inObject i), 1 :: Int) | i <- is ]) ] placesOf i = case inObject i of Just obj -> M.findWithDefault [] (obj, inOffset i) places Nothing -> [] @@ -234,7 +273,10 @@ divergenceSummary places dv = T.unlines $ <> [ T.pack (show (length (dvOrigins dv))) <> " origin(s) over " <> T.pack (show (dvRuns dv)) <> " run(s); " <> T.pack (show (length (dvDownstream dv))) - <> " instruction(s) downstream" ] + <> " instruction(s) downstream; " + <> T.pack (show (length (dvUnsteady dv))) + <> " unsteady over " <> T.pack (show (dvControls dv)) + <> " control(s)" ] where innermost i = case inObject i of Just obj -> case M.findWithDefault [] (obj, inOffset i) places of diff --git a/lib/Vestigia/Layout.hs b/lib/Vestigia/Layout.hs @@ -0,0 +1,27 @@ +{-# OPTIONS_HADDOCK prune #-} +{-# LANGUAGE ForeignFunctionInterface #-} + +-- | +-- Module: Vestigia.Layout +-- Copyright: (c) 2026 Jared Tobin +-- License: MIT +-- Maintainer: Jared Tobin <jared@ppad.tech> +-- +-- Runs compared to find where a secret mattered must lay memory out +-- alike, or every address differs between them and no difference +-- means anything. + +module Vestigia.Layout ( + fixLayout + ) where + +import Foreign.C.Types (CInt(..)) + +foreign import ccall unsafe "vestigia_no_randomize" + c_noRandomize :: IO CInt + +-- | Turn address-space randomisation off for this process and every +-- process it starts (the personality is inherited across fork and +-- exec). 'False' where it cannot be: off Linux, or refused. +fixLayout :: IO Bool +fixLayout = fmap (== 0) c_noRandomize diff --git a/nix/lib.nix b/nix/lib.nix @@ -0,0 +1,127 @@ +# vestigia for one system: the command (with the valgrind, QEMU, +# plugin, and addr2line it runs baked in where they exist), the QEMU +# plugin, the harness-side headers, and the functions that run a +# harness under either instrument as a derivation. +# +# The flake applies it per system; a consumer pinning vestigia (as +# synthesis does) imports it with its own package set, so a trace is +# built by the same definitions wherever it is built. +{ pkgs, src ? ../. }: + +let + lib = pkgs.lib; + + hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: { + vestigia = new.callCabal2nix "vestigia" src { }; + }); + + # memcheck runs on Linux only; elsewhere the command can still + # read a run's XML + linux = pkgs.stdenv.hostPlatform.isLinux; + valgrind = pkgs.valgrind; + + # the QEMU plugin recording a run's leakage trace per + # instruction, and the QEMU that runs a harness of this + # system's ISA under it + qemu = pkgs.qemu; + qemuFor = { + x86_64 = "${qemu}/bin/qemu-x86_64"; + aarch64 = "${qemu}/bin/qemu-aarch64"; + }; + plugin = pkgs.runCommandCC "vestigia-qemu-plugin" { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.glib ]; + } '' + mkdir -p $out/lib + cc -O2 -shared -fPIC -Wall -Werror \ + $(pkg-config --cflags glib-2.0) -I${qemu}/include \ + -o $out/lib/libvestigia-qemu.so ${src}/plugin/vestigia-qemu.c + ''; + isa = builtins.head + (lib.splitString "-" pkgs.stdenv.hostPlatform.system); + + # the harness-side header; a harness build also needs + # valgrind's own (valgrindDev) + include = pkgs.runCommand "vestigia-include" { } '' + mkdir -p $out/include + cp ${src}/include/vestigia.h $out/include/vestigia.h + cp ${src}/include/vestigia-ffi.c $out/include/vestigia-ffi.c + ''; + + # the command, with the valgrind it was built against baked in + vestigia = pkgs.runCommand "vestigia" { + nativeBuildInputs = [ pkgs.makeWrapper ]; + } '' + mkdir -p $out/bin + makeWrapper ${hpkgs.vestigia}/bin/vestigia $out/bin/vestigia \ + ${lib.optionalString linux (lib.concatStringsSep " " [ + "--set-default VESTIGIA_VALGRIND ${valgrind}/bin/valgrind" + "--set-default VESTIGIA_QEMU ${qemuFor.${isa}}" + "--set-default VESTIGIA_PLUGIN" + "${plugin}/lib/libvestigia-qemu.so" + "--set-default VESTIGIA_ADDR2LINE" + "${pkgs.binutils-unwrapped}/bin/addr2line" + ])} + ln -s ${include}/include $out/include + ''; + + # one harness run under memcheck, as a derivation: its report, + # memcheck's XML, and what the harness printed. A harness that + # fails still leaves a report of what ran; a run vestigia could + # not read fails the build. + traceOf = { name, harness, args ? [ ] }: + pkgs.runCommand "vestigia-trace-${name}" { } '' + mkdir -p $out + start=$(date +%s%N) + set +e + ${vestigia}/bin/vestigia run \ + --xml $out/memcheck.xml --out $out/trace.json \ + -- ${harness} ${lib.escapeShellArgs args} \ + > $out/stdout 2> $out/stderr + echo $? > $out/status + set -e + echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds + test -s $out/trace.json || { + cat $out/stderr >&2 + exit 1 + } + ''; + + # runs of one harness that differ only in a secret, each + # recorded under QEMU, and where they diverge: the dumps, and + # the report. `runs` is a list of argument lists; `objects` + # 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 ? [ ] }: + pkgs.runCommand "vestigia-diff-${name}" { } '' + mkdir -p $out + start=$(date +%s%N) + ${lib.concatImapStrings (k: args: '' + set +e + ${vestigia}/bin/vestigia record --dump $out/dump-${toString k} \ + -- ${harness} ${lib.escapeShellArgs args} \ + > $out/stdout-${toString k} 2> $out/stderr-${toString k} + echo $? > $out/status-${toString k} + set -e + '') runs} + set +e + ${vestigia}/bin/vestigia record --dump $out/control-1 \ + -- ${harness} ${lib.escapeShellArgs (builtins.head runs)} \ + > $out/stdout-control-1 2> $out/stderr-control-1 + echo $? > $out/status-control-1 + set -e + ${vestigia}/bin/vestigia diff --out $out/diff.json \ + ${lib.concatMapStrings (o: "--object ${lib.escapeShellArg o} ") + objects} \ + --control $out/control-1 \ + $out/dump-* 2> $out/summary + echo $(( ($(date +%s%N) - start) / 1000000 )) > $out/milliseconds + ''; +in +{ + inherit hpkgs vestigia plugin include traceOf diffOf; + valgrindDev = valgrind.dev; +} diff --git a/test/Main.hs b/test/Main.hs @@ -134,6 +134,20 @@ diffTests = testGroup "diff" notElem 0x18 (fmap (inPc . snd) (dvOrigins dv)) @? "a branch that always jumps is not secret-dependent" + , testCase "what differs without the secret is unsteady" $ do + a <- either assertFailure pure (readDump dumpA) + b <- either assertFailure pure (readDump dumpB) + -- a repeat of A whose 0x20 touched other addresses: an + -- entropy source, not the secret + c <- either assertFailure pure (readDump (T.unlines + [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 2" + , "I 20 4 4 ffff /lib.so 20", "I 30 2 0 cccc /lib.so 30" + , "I 40 1 1 dddd /lib.so 40" ])) + let dv = diffControlled [ c ] [ a, b ] + fmap (\(o, i) -> (o, inPc i)) (dvOrigins dv) + @?= [ (ControlOrigin, 0x10) ] + fmap inPc (dvUnsteady dv) @?= [ 0x20 ] + , 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 @@ -26,9 +26,12 @@ library hs-source-dirs: lib ghc-options: -Wall -Werror + c-sources: + cbits/norandom.c exposed-modules: Vestigia.Diff , Vestigia.Json + , Vestigia.Layout , Vestigia.Memcheck , Vestigia.Symbol , Vestigia.Trace @@ -45,8 +48,6 @@ executable vestigia main-is: Main.hs ghc-options: -Wall -Werror -rtsopts - c-sources: - cbits/norandom.c other-modules: Paths_vestigia autogen-modules: