auditor

An aarch64 constant-time memory access auditing tool.
git clone git://git.ppad.tech/auditor.git
Log | Files | Refs | README | LICENSE

commit 4e45a8f45f3bc928db14b206a961df11ba506a75
parent 1024a263bdbf80cc9451e5171b376aa104d6c4be
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 13 Feb 2026 07:44:12 +0400

feat: improve NCT scanner accuracy and add GHC runtime filtering

- Remove DIT instructions from NCT flagging (mul, madd, msub, umulh,
  smulh, variable shifts) per ARM architecture docs
- Add --filter-ghc-runtime flag to filter common GHC runtime patterns:
  - Heap checks (cmp <r>, x28 followed by conditional branch)
  - Tag checks (tst <r>, #7 followed by conditional branch)
  - Closure entry (ldr x8, [...] followed by blr x8)
- Show flagged instructions indented under each function symbol
- Reduces false positives from 332 to 56 on typical GHC output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mapp/Main.hs | 7++++++-
Mlib/Audit/AArch64.hs | 20++++++++++++++++----
Mlib/Audit/AArch64/NCT.hs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 96 insertions(+), 5 deletions(-)

diff --git a/app/Main.hs b/app/Main.hs @@ -31,6 +31,7 @@ data Options = Options , optDisplayUnknown :: !Bool , optScanNct :: !Bool , optNctDetail :: !Bool + , optFilterGhc :: !Bool } deriving (Eq, Show) optParser :: Parser Options @@ -79,6 +80,10 @@ optParser = Options ( long "nct-detail" <> help "Show per-instruction details in NCT scan mode" ) + <*> switch + ( long "filter-ghc-runtime" + <> help "Filter GHC runtime patterns (heap/tag checks) in NCT scan" + ) optInfo :: ParserInfo Options optInfo = info (optParser <**> helper) @@ -102,7 +107,7 @@ main = do exitSuccess else if optScanNct opts then do - result <- scanNctFile (optInput opts) + result <- scanNctFile (optFilterGhc opts) (optInput opts) case result of Left err -> do TIO.putStrLn $ "Error: " <> err diff --git a/lib/Audit/AArch64.hs b/lib/Audit/AArch64.hs @@ -42,6 +42,7 @@ module Audit.AArch64 ( -- * NCT scanner , scanNct , scanNctFile + , filterGhcRuntime , NctReason(..) , NctFinding(..) @@ -65,7 +66,8 @@ module Audit.AArch64 ( import Audit.AArch64.CFG import Audit.AArch64.Check -import Audit.AArch64.NCT (NctReason(..), NctFinding(..), scanNct) +import Audit.AArch64.NCT + (NctReason(..), NctFinding(..), scanNct, filterGhcRuntime) import Audit.AArch64.Parser import Audit.AArch64.Types ( Reg, Violation(..), ViolationReason(..), regName @@ -156,12 +158,22 @@ loadTaintConfig path = do Right cfg -> pure (Right cfg) -- | Scan an assembly file for non-constant-time instructions. -scanNctFile :: FilePath -> IO (Either Text (Map.Map Text [NctFinding])) -scanNctFile path = do +-- When the first argument is True, filters out common GHC runtime patterns +-- (heap checks, tag checks, closure entry) that are not CT-relevant. +scanNctFile + :: Bool -- ^ Filter GHC runtime patterns + -> FilePath + -> IO (Either Text (Map.Map Text [NctFinding])) +scanNctFile filterGhc path = do bs <- BS.readFile path case decodeUtf8' bs of Left err -> pure (Left (T.pack (show err))) Right src -> case parseAsm src of Left err -> pure (Left (T.pack (showParseError err))) - Right lns -> pure (Right (scanNct lns)) + Right lns -> + let findings = scanNct lns + filtered = if filterGhc + then filterGhcRuntime lns findings + else findings + in pure (Right filtered) diff --git a/lib/Audit/AArch64/NCT.hs b/lib/Audit/AArch64/NCT.hs @@ -18,11 +18,15 @@ module Audit.AArch64.NCT ( , NctFinding(..) -- * Scanner , scanNct + -- * Filtering + , filterGhcRuntime ) where import Audit.AArch64.CFG (isFunctionLabel) import Audit.AArch64.Types import Control.DeepSeq (NFData) +import Data.IntMap.Strict (IntMap) +import qualified Data.IntMap.Strict as IntMap import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Text (Text) @@ -136,3 +140,73 @@ checkRegIndexAddr addr = case addr of BaseRegShift _ _ _ -> Just RegIndexAddr BaseRegExtend _ _ _ -> Just RegIndexAddr _ -> Nothing + +-- | Filter out GHC runtime patterns from NCT findings. +-- +-- Recognizes and removes: +-- +-- * Heap checks: @cmp <reg>, x28@ followed by conditional branch +-- (x28 is GHC's heap limit register) +-- +-- * Tag checks: @tst <reg>, #0x7@ followed by conditional branch +-- (pointer tagging for evaluation state) +-- +-- * Closure entry: @ldr x8, [<reg>]@ followed by @blr x8@ +-- (entering closure via info pointer) +filterGhcRuntime :: [Line] -> Map Text [NctFinding] -> Map Text [NctFinding] +filterGhcRuntime lns findings = Map.mapMaybe filterFindings findings + where + -- Build line number -> Line map for O(1) lookup + lineMap :: IntMap Line + lineMap = IntMap.fromList [(lineNum l, l) | l <- lns] + + filterFindings :: [NctFinding] -> Maybe [NctFinding] + filterFindings fs = + let fs' = filter (not . isGhcRuntime) fs + in if null fs' then Nothing else Just fs' + + isGhcRuntime :: NctFinding -> Bool + isGhcRuntime f = case nctReason f of + CondBranch -> isHeapCheck f || isTagCheck f + IndirectBranch -> isClosureEntry f + _ -> False + + -- Check if conditional branch is a heap check (prev: cmp <r>, x28) + isHeapCheck :: NctFinding -> Bool + isHeapCheck f = case prevInstr f of + Just (Cmp _ (OpReg X28)) -> True + _ -> False + + -- Check if conditional branch is a tag check (prev: tst <r>, #7) + isTagCheck :: NctFinding -> Bool + isTagCheck f = case prevInstr f of + Just (Tst _ (OpImm 7)) -> True + _ -> False + + -- Check if blr x8 is closure entry (recent: ldr/ldur x8, [...]) + -- Look back up to 3 instructions to account for intervening loads + isClosureEntry :: NctFinding -> Bool + isClosureEntry f = case nctInstr f of + Blr X8 -> any isX8Load (recentInstrs 3 f) + _ -> False + + isX8Load :: Instr -> Bool + isX8Load instr = case instr of + Ldr X8 _ -> True + Ldur X8 _ -> True + _ -> False + + -- Get instruction from preceding line + prevInstr :: NctFinding -> Maybe Instr + prevInstr f = do + prevLine <- IntMap.lookup (nctLine f - 1) lineMap + lineInstr prevLine + + -- Get up to n recent instructions before finding + recentInstrs :: Int -> NctFinding -> [Instr] + recentInstrs n f = + [ instr + | offset <- [1..n] + , Just ln <- [IntMap.lookup (nctLine f - offset) lineMap] + , Just instr <- [lineInstr ln] + ]