commit 85d6377d5d28913567bfb2226ab1c319717ca399
parent 490a29c43a37c1dfd2c81d8cbe7e163afc6f498e
Author: Jared Tobin <jared@jtobin.io>
Date: Fri, 13 Feb 2026 20:07:08 +0400
feat: add --callers/-c option for reverse reachability
Shows all symbols that can reach a target symbol (reverse of --symbol).
Uses graph transpose for efficient reverse traversal.
Usage:
auditor -l -s SYMBOL -i file.s # callees (default)
auditor -l -s SYMBOL -c -i file.s # callers
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
3 files changed, 55 insertions(+), 11 deletions(-)
diff --git a/app/Main.hs b/app/Main.hs
@@ -8,7 +8,8 @@ import Audit.AArch64
, NctReason(..), NctFinding(..), nctLine, nctInstr, nctReason
, LineMap, isGhcRuntimeFinding
, SymbolScanResult(..)
- , buildCallGraph, allSymbols
+ , buildCallGraph, allSymbols, reachableSymbols, reachingSymbols
+ , symbolExists
, auditFile, auditFileInterProc
, auditFileWithConfig, auditFileInterProcWithConfig
, parseFile, regName, loadTaintConfig
@@ -42,6 +43,7 @@ data Options = Options
, optSymbol :: !(Maybe Text)
, optListSymbols :: !Bool
, optSymbolFilter :: !(Maybe Text)
+ , optCallers :: !Bool
} deriving (Eq, Show)
optParser :: Parser Options
@@ -111,6 +113,11 @@ optParser = Options
<> metavar "PATTERN"
<> help "Filter symbols containing PATTERN (use with --list-symbols)"
))
+ <*> switch
+ ( long "callers"
+ <> short 'c'
+ <> help "Show callers instead of callees (use with --symbol)"
+ )
optInfo :: ParserInfo Options
optInfo = info (optParser <**> helper)
@@ -210,6 +217,7 @@ outputText opts ar = do
else exitFailure
-- | List all function symbols in the assembly file.
+-- With --symbol, lists callees (or callers with --callers).
listSymbols :: Options -> IO ()
listSymbols opts = do
bs <- BS.readFile (optInput opts)
@@ -224,15 +232,37 @@ listSymbols opts = do
exitFailure
Right lns -> do
let cg = buildCallGraph lns
- syms = Set.toAscList (allSymbols cg)
- filtered = case optSymbolFilter opts of
- Nothing -> syms
- Just pat -> filter (T.isInfixOf pat) syms
- mapM_ TIO.putStrLn filtered
- if optQuiet opts
- then pure ()
- else TIO.putStrLn $ "\n" <> T.pack (show (length filtered))
- <> " symbols"
+ case optSymbol opts of
+ Just sym
+ | not (symbolExists sym cg) -> do
+ TIO.putStrLn $ "Error: symbol not found: " <> sym
+ exitFailure
+ | optCallers opts -> do
+ -- Show symbols that can reach this symbol
+ let callers = Set.toAscList (reachingSymbols sym cg)
+ mapM_ TIO.putStrLn callers
+ if optQuiet opts
+ then pure ()
+ else TIO.putStrLn $ "\n" <> T.pack (show (length callers))
+ <> " symbols can reach " <> sym
+ | otherwise -> do
+ -- Show symbols reachable from this symbol
+ let callees = Set.toAscList (reachableSymbols sym cg)
+ mapM_ TIO.putStrLn callees
+ if optQuiet opts
+ then pure ()
+ else TIO.putStrLn $ "\n" <> T.pack (show (length callees))
+ <> " symbols reachable from " <> sym
+ Nothing -> do
+ let syms = Set.toAscList (allSymbols cg)
+ filtered = case optSymbolFilter opts of
+ Nothing -> syms
+ Just pat -> filter (T.isInfixOf pat) syms
+ mapM_ TIO.putStrLn filtered
+ if optQuiet opts
+ then pure ()
+ else TIO.putStrLn $ "\n" <> T.pack (show (length filtered))
+ <> " symbols"
-- | Output NCT scan results for a specific symbol and its callees.
outputNctSymbol :: Options -> SymbolScanResult -> IO ()
diff --git a/lib/Audit/AArch64.hs b/lib/Audit/AArch64.hs
@@ -56,6 +56,7 @@ module Audit.AArch64 (
, buildCallGraph
, allSymbols
, reachableSymbols
+ , reachingSymbols
, symbolExists
-- * Results
@@ -82,6 +83,7 @@ import Audit.AArch64.CallGraph
, buildCallGraph
, allSymbols
, reachableSymbols
+ , reachingSymbols
, symbolExists
)
import qualified Audit.AArch64.CallGraph as CG (buildCallGraph)
diff --git a/lib/Audit/AArch64/CallGraph.hs b/lib/Audit/AArch64/CallGraph.hs
@@ -17,12 +17,13 @@ module Audit.AArch64.CallGraph (
-- * Queries
, allSymbols
, reachableSymbols
+ , reachingSymbols
, symbolExists
) where
import Audit.AArch64.CFG (isFunctionLabel)
import Audit.AArch64.Types (Instr(..), Line(..))
-import Data.Graph (Graph, Vertex, graphFromEdges, reachable)
+import Data.Graph (Graph, Vertex, graphFromEdges, reachable, transposeG)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
@@ -87,6 +88,17 @@ reachableSymbols root cg = case cgVertexFromK cg root of
[sym | v' <- reachable (cgGraph cg) v
, let (_, sym, _) = cgNodeFromV cg v']
+-- | Get all symbols that can reach a target symbol (including the target).
+--
+-- This is the reverse of 'reachableSymbols': finds all potential callers.
+-- Returns empty set if the target symbol doesn't exist.
+reachingSymbols :: Text -> CallGraph -> Set Text
+reachingSymbols target cg = case cgVertexFromK cg target of
+ Nothing -> Set.empty
+ Just v -> Set.fromList
+ [sym | v' <- reachable (transposeG (cgGraph cg)) v
+ , let (_, sym, _) = cgNodeFromV cg v']
+
-- | Get all symbols in the call graph.
allSymbols :: CallGraph -> Set Text
allSymbols = cgSymbols