vestigia

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

commit fa5ed7309389266d7f1f60f354fcbf68aef320f3
parent 628e38440d5b27024c7bfaa7720117f8be29d69d
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 27 Oct 2026 12:00:16 +0400

merge impl/watch-once: the watch list made once per comparison

vestigia watch writes the list of instructions outside the contract
for a program, and record --contract NAME --watch FILE reads it, so
the runs of one comparison share it rather than each disassembling
and classifying the objects under audit again. diffOf lists it once,
kept in its output as watch.

Diffstat:
MCHANGELOG | 2++
MREADME.md | 5++++-
Mapp/Main.hs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mnix/lib.nix | 14++++++++++----
4 files changed, 75 insertions(+), 30 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -13,6 +13,8 @@ ran (vestigia/diff-v4, which also names the ISA and contract, and gives an operand origin its text where the watch list named it). Runs recorded under other contracts are refused. + * `vestigia watch` writes the watch list once, for `record --watch` + to read in every run of one program; `diffOf` does so. * `record` appends the guest's ISA to its dump. * `diffOf` takes a `contract`; the command finds LLVM's `llvm-objdump` through `$VESTIGIA_OBJDUMP`. diff --git a/README.md b/README.md @@ -139,7 +139,10 @@ contract says nothing of its time either way, so it qualifies a clean report rather than failing it. The report names the contract and the ISA, and how many instructions outside the contract ran. Runs compared must have been recorded under the same contract. A -division is an operand origin, as without a contract. +division is an operand origin, as without a contract. Making the list +reads every object under audit, so runs of one program can share it: +`vestigia watch` writes it once, and `record --contract NAME --watch +FILE` reads it. ### As derivations diff --git a/app/Main.hs b/app/Main.hs @@ -69,6 +69,7 @@ data Opts = Opts { , oDesigned :: ![FilePath] , oContract :: !(Maybe T.Text) , oObjdump :: !(Maybe FilePath) + , oWatch :: !(Maybe FilePath) } main :: IO () @@ -83,6 +84,10 @@ main = do Right (o, runs@(_ : _)) -> readRuns o runs Right _ -> usage "read: a run or memcheck XML file expected" Left e -> usage e + ("watch" : rest) -> case options rest of + Right (o, prog : progArgs) -> watch o prog progArgs + Right (_, []) -> usage "watch: no program given" + Left e -> usage e ("record" : rest) -> case options rest of Right (o, prog : progArgs) -> record o prog progArgs Right (_, []) -> usage "record: no program given" @@ -103,8 +108,10 @@ usage e = do \[--object TEXT]... -- PROGRAM [ARG...]" , " vestigia read [--out FILE] [--object TEXT]... RUN..." , " vestigia record [--qemu PATH] [--plugin PATH] --dump FILE \ - \[--contract NAME [--objdump PATH] [--object TEXT]...] \ - \-- PROGRAM [ARG...]" + \[--contract NAME [--watch FILE | [--objdump PATH] \ + \[--object TEXT]...]] -- PROGRAM [ARG...]" + , " vestigia watch [--qemu PATH] [--out FILE] --contract NAME \ + \[--objdump PATH] [--object TEXT]... -- PROGRAM [ARG...]" , " vestigia diff [--out FILE] [--addr2line PATH] \ \[--object TEXT]... [--control DUMP]... [--designed DUMP]... \ \DUMP DUMP..." @@ -128,7 +135,9 @@ usage e = do , "the program loads whose path contains an --object TEXT (all," , "with none), read with objdump (--objdump, else" , "$VESTIGIA_OBJDUMP, else llvm-objdump); diff reports those whose" - , "operands differed beside the origins." + , "operands differed beside the origins. watch writes the list of" + , "those instructions once, for record --watch to read in runs of" + , "one program rather than make anew in each." , "--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." @@ -142,7 +151,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 Nothing) + Nothing [] [] [] Nothing Nothing Nothing) where go o xs = case xs of ("--" : rest) -> Right (o, rest) @@ -161,6 +170,7 @@ options = go (Opts Nothing Nothing Nothing Nothing Nothing Nothing go o { oDesigned = oDesigned o <> [ p ] } rest ("--contract" : c : rest) -> go o { oContract = Just (T.pack c) } rest ("--objdump" : p : rest) -> go o { oObjdump = Just p } rest + ("--watch" : p : rest) -> go o { oWatch = Just p } rest (x : _) | take 2 x == "--" -> Left ("unknown option: " <> x) _ -> Right (o, xs) @@ -321,15 +331,22 @@ record o prog progArgs = do ("qemu-" <> isa) plugin <- tool (oPlugin o) "VESTIGIA_PLUGIN" "libvestigia-qemu.so" -- under a contract, the instructions outside it in the objects - -- under audit, for the plugin to watch - contract <- traverse (\name -> case readContract name of - Just c | contractIsa c == isa -> pure c - | otherwise -> refuseRecord (T.unpack name <> " speaks to " - <> contractIsa c <> ", and " <> prog <> " is " <> isa) - Nothing -> refuseRecord ("no contract named " <> T.unpack name - <> "; arm-dit and intel-doitm are known")) - (oContract o) - watch <- traverse (\c -> watchList o qemu c prog progArgs) contract + -- under audit, for the plugin to watch: the list given, made once + -- for runs of one program, or one made here + contract <- traverse (contractFor isa prog) (oContract o) + watching <- case (contract, oWatch o) of + (Just _, Just w) -> pure (Just (w, False)) + (Just c, Nothing) -> do + t <- watchList o qemu c prog progArgs + tmp <- getTemporaryDirectory + (path, h) <- openTempFile tmp "vestigia.watch" + hClose h + TIO.writeFile path t + pure (Just (path, True)) + (Nothing, Just _) -> refuseRecord "a watch list is read under the \ + \contract it was made for: --watch needs --contract" + (Nothing, Nothing) -> pure Nothing + let watchFile = fmap fst watching -- QEMU maps the guest's libraries and heap in its own address -- space, wherever its own allocations leave room, and those vary -- with how its threads run: a second malloc arena, or a large block @@ -350,12 +367,12 @@ record o prog progArgs = do ((proc qemu (guest <> [ "-seed", "0" , "-plugin", plugin <> ",out=" <> dump - <> maybe "" (",watch=" <>) watch + <> maybe "" (",watch=" <>) watchFile , prog ] <> progArgs)) { P.env = Just ((tunables, host) : others) }) (\_ _ _ ph -> waitForProcess ph)) - for_ watch removeFile + for_ watching (\(w, made) -> when made (removeFile w)) case r of Left e -> do hPutStrLn stderr ("vestigia: cannot run " <> qemu <> ": " @@ -374,12 +391,33 @@ refuseRecord e = do hPutStrLn stderr ("vestigia: " <> e) exitWith (ExitFailure 2) --- the plugin's watch list under a contract, written to a temporary --- file: every instruction outside it in the objects the program loads --- (and the program's own) whose path contains an --object text, all --- of them with none +-- the contract named, for a program of the ISA it speaks to +contractFor :: String -> FilePath -> T.Text -> IO Contract +contractFor isa prog name = case readContract name of + Just c | contractIsa c == isa -> pure c + | otherwise -> refuseRecord (T.unpack name <> " speaks to " + <> contractIsa c <> ", and " <> prog <> " is " <> isa) + Nothing -> refuseRecord ("no contract named " <> T.unpack name + <> "; arm-dit and intel-doitm are known") + +-- write a program's watch list under a contract, once, for runs of +-- it to read (record --watch) +watch :: Opts -> FilePath -> [String] -> IO () +watch o prog progArgs = do + isa <- machineOf prog + qemu <- tool (oQemu o) ("VESTIGIA_QEMU_" <> fmap toUpper isa) + ("qemu-" <> isa) + name <- maybe (usage "watch: --contract NAME is needed") pure + (oContract o) + c <- contractFor isa prog name + t <- watchList o qemu c prog progArgs + maybe (TIO.putStr t) (\p -> TIO.writeFile p t) (oOut o) + +-- the plugin's watch list under a contract: every instruction outside +-- it in the objects the program loads (and the program's own) whose +-- path contains an --object text, all of them with none watchList :: Opts -> FilePath -> Contract -> FilePath -> [String] - -> IO FilePath + -> IO T.Text watchList o qemu c prog progArgs = do objdump <- tool (oObjdump o) "VESTIGIA_OBJDUMP" "llvm-objdump" loaded <- loadedObjects qemu prog progArgs @@ -391,11 +429,7 @@ watchList o qemu c prog progArgs = do ls <- disassemble objdump c p pure (watchSection c p ls)) kept - tmp <- getTemporaryDirectory - (path, h) <- openTempFile tmp "vestigia.watch" - hClose h - TIO.writeFile path (T.concat sections) - pure path + pure (T.concat sections) -- the shared objects a program loads, as its dynamic loader resolves -- them, run under QEMU so that any ISA's are found diff --git a/nix/lib.nix b/nix/lib.nix @@ -115,20 +115,26 @@ let # them for control and coverage but not for addresses (vestigia # diff --designed). Under a `contract` (arm-dit or intel-doitm, the # harness's ISA's), every run also fingerprints the operands of the - # instructions outside it in `objects` (vestigia record --contract). + # instructions outside it in `objects`, listed once (vestigia watch, + # kept as `watch`) for every run to read (record --watch). # A run's own failure leaves its status; a dump vestigia cannot read # fails the build. diffOf = { name, harness, runs, objects ? [ ], designed ? [ ] , contract ? null }: let watch = lib.optionalString (contract != null) - ("--contract ${lib.escapeShellArg contract} " - + lib.concatMapStrings (o: "--object ${lib.escapeShellArg o} ") - objects); + "--contract ${lib.escapeShellArg contract} --watch $out/watch "; in pkgs.runCommand "vestigia-diff-${name}" { } '' mkdir -p $out start=$(date +%s%N) + ${lib.optionalString (contract != null) '' + ${vestigia}/bin/vestigia watch --out $out/watch \ + --contract ${lib.escapeShellArg contract} \ + ${lib.concatMapStrings (o: "--object ${lib.escapeShellArg o} ") + objects}-- ${harness} \ + ${lib.escapeShellArgs (builtins.head runs)} + ''} ${lib.concatImapStrings (k: args: '' set +e ${vestigia}/bin/vestigia record --dump $out/dump-${toString k} \