commit f036a025212aa897e86f97b4036534a6318e5687
parent 628e38440d5b27024c7bfaa7720117f8be29d69d
Author: Jared Tobin <jared@jtobin.io>
Date: Tue, 27 Oct 2026 12:00:16 +0400
watch: list what lies outside the contract once, for every run
Making the watch list reads every object under audit, which costs
more than recording a short run. vestigia watch writes it, record
--watch reads it, and diffOf lists it once, kept as watch, for all
its runs.
Diffstat:
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} \