vestigia

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

commit 32a90ed1c2548dfe6e6bb9368fbf484bfd544cdf
parent 164ae07e7fb7c0ebb37c7cfdce5662bf1eac4130
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 21 Oct 2026 09:08:36 +0400

record: QEMU chosen by the harness's architecture

record ran the QEMU for the host's ISA ($VESTIGIA_QEMU, which the
wrapper set to the host's), though the QEMU vestigia builds runs both
x86_64 and aarch64 guests, so an aarch64 harness could not be traced
on an x86_64 builder without --qemu. And addr2line was binutils for
the host alone, which cannot read another ISA's objects, so such a
trace's origins would have come back unnamed.

record now reads the program's ELF header and runs the QEMU for its
machine: --qemu, else $VESTIGIA_QEMU_X86_64 or $VESTIGIA_QEMU_AARCH64,
which the wrapper sets, else qemu-x86_64 or qemu-aarch64 on PATH; a
program of another kind is refused. The wrapper's addr2line is
binutils for all targets. The probe check builds its harness for
aarch64 as well and, on an x86_64 host, holds that harness's
differential origins to the native ones, named.

Diffstat:
MCHANGELOG | 22+++++++++++-----------
MREADME.md | 3++-
Mapp/Main.hs | 32++++++++++++++++++++++++++++----
Mnix/lib.nix | 22+++++++++-------------
Mnix/probe.nix | 35++++++++++++++++++++++++-----------
5 files changed, 74 insertions(+), 40 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -19,17 +19,17 @@ 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 - randomisation off (or not at all) and the guest's entropy, - its getrandom included, seeded, and QEMU's own allocations kept - off the guest's mappings, which under load can otherwise take - the room of a guest library or heap; `diff` refuses runs that - loaded an object at different addresses, runs of another - program or with arguments or an environment of another size - than the first run's, and a control that does not repeat the - first run's arguments; a guest that starts a second thread is - stopped and its dump refused. A taint run that marked no - secret is refused. + runs are recorded, an x86_64 or aarch64 harness on either + host, with randomisation off (or not at all) and the guest's + entropy, its getrandom included, seeded, and QEMU's own + allocations kept off the guest's mappings, which under load + can otherwise take the room of a guest library or heap; `diff` + refuses runs that loaded an object at different addresses, + runs of another program or with arguments or an environment + of another size than the first run's, and a control that does + not repeat the first run's arguments; a guest that starts a + second thread is stopped and its dump refused. A taint run + that marked no secret is refused. * `traceOf` and `diffOf` run either as a derivation; `nix/lib.nix` holds them for any package set, with the version the command reports. diff --git a/README.md b/README.md @@ -143,7 +143,8 @@ ppad-poly1305, and ppad-secp256k1. * What a function is meant to reveal (a comparison's result, a public key) must be marked public, or it is reported. * valgrind and QEMU's user mode run on Linux; elsewhere vestigia only - reads their reports. QEMU traces other ISAs differentially, but + reads their reports. `record` runs an x86_64 or aarch64 harness on + either host, choosing QEMU by the program's ELF header, but memcheck needs a native host. * The plugin records one guest thread: a guest that starts a second is stopped, and its dump refused. Runs compared by `diff` must diff --git a/app/Main.hs b/app/Main.hs @@ -16,6 +16,7 @@ module Main ( import Control.Exception (IOException, try) import Control.Monad (unless, when) import qualified Data.ByteString as BS +import Data.Char (toUpper) import Data.Foldable (for_) import Data.Version (showVersion) import qualified GHC.Foreign as GF @@ -29,7 +30,8 @@ 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.IO + (IOMode(..), hClose, hPutStrLn, openTempFile, stderr, withBinaryFile) import System.Process (proc, waitForProcess, withCreateProcess) import qualified System.Process as P @@ -109,8 +111,10 @@ usage e = do , "--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." + , "QEMU is the one for PROGRAM's architecture: --qemu, else" + , "$VESTIGIA_QEMU_X86_64 or $VESTIGIA_QEMU_AARCH64, else" + , "qemu-x86_64 or qemu-aarch64 on PATH. The plugin and addr2line" + , "default to $VESTIGIA_PLUGIN and $VESTIGIA_ADDR2LINE." ] exitWith (ExitFailure 2) @@ -241,7 +245,10 @@ record o prog progArgs = do hPutStrLn stderr "vestigia: address randomisation could not be \ \turned off; runs would differ in layout, and so everywhere" exitWith (ExitFailure 2) - qemu <- tool (oQemu o) "VESTIGIA_QEMU" "qemu-x86_64" + -- QEMU's user mode runs the programs of one architecture + isa <- machineOf prog + qemu <- tool (oQemu o) ("VESTIGIA_QEMU_" <> fmap toUpper isa) + ("qemu-" <> isa) plugin <- tool (oPlugin o) "VESTIGIA_PLUGIN" "libvestigia-qemu.so" -- QEMU maps the guest's libraries and heap in its own address -- space, wherever its own allocations leave room, and those vary @@ -279,6 +286,23 @@ record o prog progArgs = do (others <> [ (tunables, v) | Just v <- [ own ] ]) c exitWith c +-- the architecture an ELF program is built for, as QEMU names its +-- user-mode emulators: the header's machine, at byte 18 +machineOf :: FilePath -> IO String +machineOf prog = do + r <- try (withBinaryFile prog ReadMode (\h -> BS.hGet h 20)) + case fmap BS.unpack r of + Right (0x7f : 0x45 : 0x4c : 0x46 : _ : 1 : rest) + | [ 0x3e, 0 ] <- drop 12 rest -> pure "x86_64" + | [ 0xb7, 0 ] <- drop 12 rest -> pure "aarch64" + Right _ -> unknown "not an x86_64 or aarch64 ELF program" + Left e -> unknown (show (e :: IOException)) + where + unknown why = do + hPutStrLn stderr ("vestigia: " <> prog <> ": " <> why + <> "; QEMU's user mode runs one") + exitWith (ExitFailure 2) + -- append a run's record to its dump (Vestigia.Diff.readDump): its -- arguments' bytes, its environment's variables and the bytes they -- take, and its exit status diff --git a/nix/lib.nix b/nix/lib.nix @@ -21,19 +21,16 @@ let 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: user mode alone, its guest's getrandom - # drawn from the generator -seed fixes (qemu-getrandom.patch) + # instruction, and the QEMU that runs a harness under it, for + # either ISA, whatever the host's: user mode alone, its guest's + # getrandom drawn from the generator -seed fixes + # (qemu-getrandom.patch) qemu = (pkgs.qemu.override { userOnly = true; hostCpuTargets = [ "x86_64-linux-user" "aarch64-linux-user" ]; }).overrideAttrs (o: { patches = (o.patches or [ ]) ++ [ ./qemu-getrandom.patch ]; }); - 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 ]; @@ -43,9 +40,6 @@ let $(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" { } '' @@ -54,7 +48,8 @@ let cp ${src}/include/vestigia.c $out/include/vestigia.c ''; - # the command, with the valgrind it was built against baked in + # the command, with the valgrind it was built against baked in, + # and an addr2line that reads either ISA's objects vestigia = pkgs.runCommand "vestigia" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' @@ -62,11 +57,12 @@ let 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_QEMU_X86_64 ${qemu}/bin/qemu-x86_64" + "--set-default VESTIGIA_QEMU_AARCH64 ${qemu}/bin/qemu-aarch64" "--set-default VESTIGIA_PLUGIN" "${plugin}/lib/libvestigia-qemu.so" "--set-default VESTIGIA_ADDR2LINE" - "${pkgs.binutils-unwrapped}/bin/addr2line" + "${pkgs.binutils-unwrapped-all-targets}/bin/addr2line" ])} ln -s ${include}/include $out/include ''; diff --git a/nix/probe.nix b/nix/probe.nix @@ -1,24 +1,31 @@ # The probe check: etc/probe/harness.c under both instruments, each # site and origin it should show found where it should be, and -# nothing else in the harness. +# nothing else in the harness. On an x86_64 host, the same harness +# built for aarch64 is traced differentially too, under QEMU; memcheck +# needs a native host. { pkgs, vestigia }: let lib = pkgs.lib; - # linked at a fixed address, where an instruction's address in the - # file is not its offset there - probe = pkgs.runCommandCC "vestigia-probe" { - buildInputs = [ vestigia.valgrindDev ]; + # the harness, built by a package set's C compiler and linked at a + # fixed address, where an instruction's address in the file is not + # its offset there + probeOf = ps: ps.runCommandCC "vestigia-probe" { hardeningDisable = [ "fortify" ]; } '' mkdir -p $out/bin - cc -O0 -g -fno-pie -no-pie -I${vestigia.include}/include \ + $CC -O0 -g -fno-pie -no-pie -I${vestigia.include}/include \ + -I${vestigia.valgrindDev}/include \ -o $out/bin/probe ${../etc/probe/harness.c} \ ${vestigia.include}/include/vestigia.c - readelf -h $out/bin/probe | grep -q 'Type: *EXEC' + $READELF -h $out/bin/probe | grep -q 'Type: *EXEC' ''; + probe = probeOf pkgs; + cross = probeOf pkgs.pkgsCross.aarch64-multiplatform; + x86_64 = pkgs.stdenv.hostPlatform.isx86_64; + threads = pkgs.runCommandCC "vestigia-probe-threads" { } '' mkdir -p $out/bin cc -O0 -pthread -o $out/bin/threads ${../etc/probe/threads.c} @@ -34,12 +41,13 @@ let inherit objects; }; - diff = vestigia.diffOf { - name = "probe"; - harness = "${probe}/bin/probe"; + diffOf = name: harness: vestigia.diffOf { + inherit name objects; + harness = "${harness}/bin/probe"; runs = map (s: [ s ]) secrets; - inherit objects; }; + diff = diffOf "probe" probe; + diffCross = diffOf "probe-aarch64" cross; # each report's (class, function) pairs in the harness, sorted expected = { @@ -63,6 +71,11 @@ pkgs.runCommand "vestigia-probe-check" { check ${diff}/diff.json \ '[.origins[] | [.class, .frames[0].function]] | unique' \ '${builtins.toJSON expected.diff}' + ${lib.optionalString x86_64 '' + check ${diffCross}/diff.json \ + '[.origins[] | [.class, .frames[0].function]] | unique' \ + '${builtins.toJSON expected.diff}' + ''} # each run as it was made: the declared, then the control check ${diff}/diff.json \