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:
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 \