vestigia

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

commit 5ee48803f1f90d28d65229525953692310b59771
parent 93e98d61cb94ee92bb02ba7a555a15405bdebefd
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 21 Oct 2026 10:15:23 +0400

symbol: LLVM's addr2line, which names what it can and no more

With instructions now named by their addresses in their objects,
binutils' addr2line named the origins in the spikes' GHC harnesses,
but wrongly: code with no debug information was given the name of the
nearest function symbol before it, vestigia_public, whatever lay
between. And for an inlined instruction it gave the innermost frame
the name of the function it was inlined into, beside the inlined
function's own line: Penumbra's origin at 0xe9d6 read
vartime_compress_to_field at sign.rs:11, a line of abs<Fq>.

LLVM's addr2line names an address no symbol covers by none, as
memcheck does, and each inlined frame by its own function; it reads
either ISA's objects too, so the wrapper bakes it in where binutils
for all targets was. It leaves a legacy Rust symbol as an Itanium
demangler does, its `$LT$`-style escapes and hash in place, so
Vestigia.Symbol undoes them as rustc-demangle would (rustLegacy).

On pollux, the poly1305 spike's GHC origins now read `?`, as
memcheck's own sites there do, and Penumbra's innermost frames name
the inlined functions the lines are in. Synthesis keys a line by the
symbol its disassembly encloses it in, not by this name.

Diffstat:
MREADME.md | 6+++---
Mlib/Vestigia/Symbol.hs | 51++++++++++++++++++++++++++++++++++++++++++++++++---
Mnix/lib.nix | 5+++--
Mtest/Main.hs | 13+++++++++++++
4 files changed, 67 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md @@ -74,10 +74,10 @@ $ for s in 1 2 3; do $ vestigia record --dump control -- ./harness public_key 2 1 $ vestigia diff --object libpenumbra_audit --control control \ > --out diff.json run-* -control 0xe9d6 Element::vartime_compress_to_field sign.rs:11 ... -address 0x152c8 Fq::sqrt_ratio_zeta invsqrt.rs:113 ... +control 0xe9d6 abs<Fq> sign.rs:11 ... +address 0x152c8 Fq::sqrt_ratio_zeta invsqrt.rs:113 ... ... -44 origin(s) over 3 run(s); 1383 instruction(s) downstream; 0 unsteady ... +40 origin(s) over 3 run(s); 1387 instruction(s) downstream; 0 unsteady ... ``` An *origin* is a branch whose outcomes differ between runs, in diff --git a/lib/Vestigia/Symbol.hs b/lib/Vestigia/Symbol.hs @@ -7,18 +7,26 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> -- --- Naming an instruction by its source: binutils' addr2line over an +-- Naming an instruction by its source: an addr2line over an -- 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). +-- headers place it, as its disassembly does). LLVM's names each +-- inlined frame by its own function, and an address no symbol covers +-- by none, where binutils' gives the innermost frame the name of the +-- function it was inlined into, and code without debug information +-- (a GHC program's) the name of whatever function precedes it. LLVM +-- leaves a legacy Rust symbol as an Itanium demangler does, its +-- escapes and hash in place, which are undone here. module Vestigia.Symbol ( Place(..) , symbolize , parseAddr2line + , rustLegacy ) where import Control.Exception (IOException, try) +import Data.Char (chr, isHexDigit) import qualified Data.Map.Strict as M import qualified Data.Text as T import Numeric (readHex, showHex) @@ -82,7 +90,7 @@ parseAddr2line = go . T.lines place f loc = let (file, line) = location loc in Place { - plFunction = if f == "??" then Nothing else Just f + plFunction = if f == "??" then Nothing else Just (rustLegacy f) , plFile = file , plLine = line } @@ -96,3 +104,40 @@ parseAddr2line = go . T.lines , case readMaybe (T.unpack n) of Just 0 -> Nothing other -> other ) + +-- | A legacy Rust symbol, as an Itanium demangler leaves it, as +-- rustc-demangle gives it: its hash dropped, each path segment's +-- escapes undone. Any other name is left as it is. +-- +-- >>> rustLegacy "a::_$LT$impl$u20$b..C$GT$::f::h346fee1681a4f34f" +-- "a::<impl b::C>::f" +rustLegacy :: T.Text -> T.Text +rustLegacy t = case T.breakOnEnd "::" t of + (pre, h) | not (T.null pre), hash h -> + T.intercalate "::" + (fmap segment (T.splitOn "::" (T.dropEnd 2 pre))) + _ -> t + where + hash h = case T.uncons h of + Just ('h', ds) -> T.length ds == 16 && T.all isHexDigit ds + _ -> False + segment g = T.pack (unescape (T.unpack + (maybe g ("$" <>) (T.stripPrefix "_$" g)))) + unescape cs = case cs of + '$' : rest + | (code, '$' : more) <- break (== '$') rest + , Just r <- escape code -> r <> unescape more + '.' : '.' : more -> "::" <> unescape more + c : more -> c : unescape more + [] -> [] + escape code = case code of + "SP" -> Just "@" + "BP" -> Just "*" + "RF" -> Just "&" + "LT" -> Just "<" + "GT" -> Just ">" + "LP" -> Just "(" + "RP" -> Just ")" + "C" -> Just "," + 'u' : h | [ (n, "") ] <- readHex h -> Just [ chr n ] + _ -> Nothing diff --git a/nix/lib.nix b/nix/lib.nix @@ -52,7 +52,8 @@ let ''; # the command, with the valgrind it was built against baked in, - # and an addr2line that reads either ISA's objects + # and LLVM's addr2line, which reads either ISA's objects and names + # inlined frames by their own functions (Vestigia.Symbol) vestigia = pkgs.runCommand "vestigia" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' @@ -65,7 +66,7 @@ let "--set-default VESTIGIA_PLUGIN" "${plugin}/lib/libvestigia-qemu.so" "--set-default VESTIGIA_ADDR2LINE" - "${pkgs.binutils-unwrapped-all-targets}/bin/addr2line" + "${pkgs.llvm}/bin/llvm-addr2line" ])} ln -s ${include}/include $out/include ''; diff --git a/test/Main.hs b/test/Main.hs @@ -386,6 +386,19 @@ symbolTests = testGroup "symbol" @?= [ (0x1234, [ Place (Just "inner") (Just "/src/a.rs") (Just 12) , Place (Just "outer") (Just "/src/b.rs") (Just 40) ]) , (0x5678, [ Place Nothing Nothing Nothing ]) ] + + , testCase "a legacy Rust name's escapes and hash undone" $ do + rustLegacy "decaf377::ark_curve::encoding::_$LT$impl$u20$decaf377\ + \..ark_curve..element..projective..Element$GT$::vartime\ + \_compress_to_field::h346fee1681a4f34f" + @?= "decaf377::ark_curve::encoding::<impl decaf377::ark_curve::\ + \element::projective::Element>::vartime_compress_to_field" + rustLegacy "_$LT$$RF$T$u20$as$u20$core..fmt..Debug$GT$::fmt::\ + \h0123456789abcdef" + @?= "<&T as core::fmt::Debug>::fmt" + -- a C++ name, or one without a hash, is left as it is + rustLegacy "std::vector<int>::size" @?= "std::vector<int>::size" + rustLegacy "a::b$LT$c" @?= "a::b$LT$c" ] jsonTests :: TestTree