vestigia

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

commit 9b367ba45596268931c1e3728400053b8fe333ee
parent d8e416b049070463033e5564ee2b3f88cead8a69
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 21 Oct 2026 08:53:39 +0400

plugin: an instruction named by its address in its object

The plugin named an instruction by its offset in the file it was
mapped from, and diff passed that offset to addr2line and reported it
as the instruction's place in its object. The two coincide in the
executable segments of a shared library as GNU ld lays them out, but
not in an executable linked at a fixed address: a GHC harness loads
at 0x400000, so its instructions' offsets named nothing, and every
origin in the spikes' GHC harnesses was reported without a function.
They need not coincide in a library either.

At exit the plugin now reads each mapped file's program headers and
writes the address its loadable segments give the instruction, which
is what the file's disassembly and line tables name it by, and what a
memcheck site's offset (its address less the load bias) already was.
A file whose program headers cannot be read names its instructions
by no object, and is said to. The probe check's harness is now
linked at a fixed address; with the old plugin its origins come back
unnamed and the check fails.

Diffstat:
MREADME.md | 3+--
Mlib/Vestigia/Diff.hs | 5+++--
Mlib/Vestigia/Symbol.hs | 10++++------
Mnix/probe.nix | 6+++++-
Mplugin/vestigia-qemu.c | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
5 files changed, 91 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md @@ -141,8 +141,7 @@ ppad-poly1305, and ppad-secp256k1. `diff` must differ only in the secret, argument lengths included. * A GHC harness wants `-with-rtsopts=-xr1G -V0`, against the runtime's terabyte reservation and its interval timer. -* Source lines need line tables, and sites assume an object's file - offsets are its virtual addresses, as GNU ld and lld emit them. +* Source lines need line tables. ## Development diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs @@ -81,7 +81,8 @@ import Numeric (readHex, showHex) import Vestigia.Json import Vestigia.Symbol (Place(..)) --- | One instruction of a run. +-- | One instruction of a run: its run-time address, and its object +-- and address there, where the object's program headers place it. data Insn = Insn { inPc :: !Integer , inCount :: !Integer @@ -130,7 +131,7 @@ readDump t = go (T.lines t) M.empty M.empty _ -> Left ("not hexadecimal: " <> T.unpack x) -- | Where a run loaded each object it executed: any of its --- instructions' addresses less its offset in the object. +-- instructions' run-time addresses less its address in the object. loadBases :: Dump -> M.Map T.Text Integer loadBases d = M.fromList [ (o, inPc i - inOffset i) diff --git a/lib/Vestigia/Symbol.hs b/lib/Vestigia/Symbol.hs @@ -8,11 +8,9 @@ -- Maintainer: Jared Tobin <jared@ppad.tech> -- -- Naming an instruction by its source: binutils' addr2line over an --- object's debug information, inlined frames included. An object's --- file offset is taken as its virtual address, which holds for the --- executable segments of what GNU ld and lld emit; an object that --- does not satisfy it names its sites wrongly, not silently: its --- functions will not match its disassembly. +-- object's debug information, inlined frames included, at the +-- instruction's address in the object (where the object's program +-- headers place it, as its disassembly does). module Vestigia.Symbol ( Place(..) @@ -36,7 +34,7 @@ data Place = Place { , plLine :: !(Maybe Int) } deriving (Eq, Show) --- | The source of each offset, per object, by one addr2line call +-- | The source of each address, per object, by one addr2line call -- per object. An object addr2line cannot read names nothing. symbolize :: FilePath -> [(T.Text, Integer)] diff --git a/nix/probe.nix b/nix/probe.nix @@ -6,13 +6,17 @@ 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 ]; + hardeningDisable = [ "fortify" ]; } '' mkdir -p $out/bin - cc -O0 -g -fPIE -pie -I${vestigia.include}/include \ + cc -O0 -g -fno-pie -no-pie -I${vestigia.include}/include \ -o $out/bin/probe ${../etc/probe/harness.c} \ ${vestigia.include}/include/vestigia.c + readelf -h $out/bin/probe | grep -q 'Type: *EXEC' ''; secrets = [ "00000005" "0000003a" "000000c3" ]; diff --git a/plugin/vestigia-qemu.c b/plugin/vestigia-qemu.c @@ -20,14 +20,19 @@ * * At exit it writes, to the file named by its `out` argument: * - * I <pc> <count> <accesses> <fingerprint> <order> <object> <offset> + * I <pc> <count> <accesses> <fingerprint> <order> <object> <address> * E <from> <to> <count> * * in hex, where <object> is the file the instruction's bytes were - * mapped from ("-" if none) and <offset> its offset in that file. + * mapped from ("-" if none, or if the file's program headers cannot + * be read) and <address> its address in that file: where the file's + * program headers place it, as its disassembly and line tables name + * it. In a shared library that is often the instruction's offset in + * the file; in an executable linked at a fixed address it is not. * * A single-threaded guest is assumed: the state below is unlocked. */ +#include <elf.h> #include <glib.h> #include <inttypes.h> #include <stdio.h> @@ -158,6 +163,70 @@ static const Region *region_of(GArray *rs, uintptr_t a) { return NULL; } +/* a file's loadable segments, by which a file offset is placed at + the address the file's own disassembly gives it */ +typedef struct Segments { + guint n; + Elf64_Phdr *load; +} Segments; + +static Segments *read_segments(const char *path) { + FILE *f = fopen(path, "rb"); + Elf64_Ehdr eh; + Segments *s = NULL; + if (f == NULL) return NULL; + /* a little-endian ELF64 object, as x86_64 and aarch64 build */ + if (fread(&eh, sizeof eh, 1, f) != 1 + || memcmp(eh.e_ident, ELFMAG, SELFMAG) != 0 + || eh.e_ident[EI_CLASS] != ELFCLASS64 + || eh.e_ident[EI_DATA] != ELFDATA2LSB + || eh.e_phentsize != sizeof(Elf64_Phdr) + || fseek(f, (long)eh.e_phoff, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + s = g_new0(Segments, 1); + s->load = g_new0(Elf64_Phdr, eh.e_phnum); + for (guint k = 0; k < eh.e_phnum; k++) { + Elf64_Phdr ph; + if (fread(&ph, sizeof ph, 1, f) != 1) { + g_free(s->load); + g_free(s); + fclose(f); + return NULL; + } + if (ph.p_type == PT_LOAD) s->load[s->n++] = ph; + } + fclose(f); + return s; +} + +/* the segments of the file at a path, read once; NULL if they cannot + be read, which is said once */ +static Segments *segments_of(GHashTable *cache, const char *path) { + gpointer v; + if (g_hash_table_lookup_extended(cache, path, NULL, &v)) return v; + Segments *s = read_segments(path); + if (s == NULL) + fprintf(stderr, "vestigia-qemu: cannot read the program headers " + "of %s; its instructions are named by no object\n", path); + g_hash_table_insert(cache, g_strdup(path), s); + return s; +} + +/* where a file's segments place the byte at a file offset */ +static gboolean address_in(const Segments *s, uint64_t off, + uint64_t *addr) { + for (guint k = 0; k < s->n; k++) { + const Elf64_Phdr *p = &s->load[k]; + if (off >= p->p_offset && off - p->p_offset < p->p_filesz) { + *addr = p->p_vaddr + (off - p->p_offset); + return TRUE; + } + } + return FALSE; +} + static void at_exit(qemu_plugin_id_t id, void *p) { (void)id; (void)p; @@ -167,17 +236,21 @@ static void at_exit(qemu_plugin_id_t id, void *p) { return; } GArray *rs = read_maps(); + GHashTable *segs = g_hash_table_new(g_str_hash, g_str_equal); GHashTableIter it; gpointer key, val; g_hash_table_iter_init(&it, insns); while (g_hash_table_iter_next(&it, &key, &val)) { Insn *i = val; const Region *r = region_of(rs, i->haddr); - if (r != NULL) + Segments *s = r != NULL ? segments_of(segs, r->path) : NULL; + uint64_t addr; + if (s != NULL + && address_in(s, (uint64_t)(i->haddr - r->start) + r->offset, + &addr)) fprintf(out, "I %" PRIx64 " %" PRIx64 " %" PRIx64 " %016" PRIx64 " %016" PRIx64 " %s %" PRIx64 "\n", i->pc, i->count, - i->accesses, i->fingerprint, i->order, r->path, - (uint64_t)(i->haddr - r->start) + r->offset); + i->accesses, i->fingerprint, i->order, r->path, addr); else fprintf(out, "I %" PRIx64 " %" PRIx64 " %" PRIx64 " %016" PRIx64 " %016" PRIx64 " - 0\n", i->pc, i->count, i->accesses,