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