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