commit c226539fea1c99fb6df434406eae0419d39aea00
parent e48ce759831e9ac6983f51def00e72b451739be0
Author: Jared Tobin <jared@jtobin.io>
Date: Wed, 11 Feb 2026 23:14:12 +0400
feat: add static non-constant-time instruction scanner (IMPL21)
Add parser-only scan mode that flags instructions with potential
timing variability, grouped by function symbol. No dataflow analysis;
purely syntactic inspection.
- New Audit.AArch64.NCT module with scanNct function
- NctReason: CondBranch, IndirectBranch, Div, MulOp, VarShift, RegIndexAddr
- CLI: --scan-nct for scan mode, --nct-detail for per-instruction output
- 17 new tests covering all reason categories and symbol grouping
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
7 files changed, 563 insertions(+), 0 deletions(-)
diff --git a/app/Main.hs b/app/Main.hs
@@ -5,10 +5,13 @@ module Main where
import Audit.AArch64
( AuditResult(..), Violation(..), ViolationReason(..)
, TaintConfig(..)
+ , NctReason(..), NctFinding(..)
, auditFile, auditFileInterProc
, auditFileWithConfig, auditFileInterProcWithConfig
, parseFile, regName, loadTaintConfig
+ , scanNctFile
)
+import Audit.AArch64.Types (Instr)
import Data.Aeson (encode)
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.Map.Strict as Map
@@ -26,6 +29,8 @@ data Options = Options
, optParseOnly :: !Bool
, optTaintConfig :: !(Maybe FilePath)
, optDisplayUnknown :: !Bool
+ , optScanNct :: !Bool
+ , optNctDetail :: !Bool
} deriving (Eq, Show)
optParser :: Parser Options
@@ -66,6 +71,14 @@ optParser = Options
<> short 'u'
<> help "Display unknown violations (only secret shown by default)"
)
+ <*> switch
+ ( long "scan-nct"
+ <> help "Scan for non-constant-time instructions (no taint analysis)"
+ )
+ <*> switch
+ ( long "nct-detail"
+ <> help "Show per-instruction details in NCT scan mode"
+ )
optInfo :: ParserInfo Options
optInfo = info (optParser <**> helper)
@@ -87,6 +100,15 @@ main = do
Right n -> do
TIO.putStrLn $ "Parsed " <> T.pack (show n) <> " lines"
exitSuccess
+ else if optScanNct opts
+ then do
+ result <- scanNctFile (optInput opts)
+ case result of
+ Left err -> do
+ TIO.putStrLn $ "Error: " <> err
+ exitFailure
+ Right findings ->
+ outputNct opts findings
else do
-- Load taint config if provided
mcfg <- case optTaintConfig opts of
@@ -144,6 +166,48 @@ outputText opts ar = do
then exitSuccess
else exitFailure
+-- | Output NCT scan results.
+outputNct :: Options -> Map.Map Text [NctFinding] -> IO ()
+outputNct opts findings = do
+ let syms = Map.toList findings
+ total = sum (map (length . snd) syms)
+ if optNctDetail opts
+ then mapM_ printNctDetail syms
+ else mapM_ printNctSummary syms
+ if optQuiet opts
+ then pure ()
+ else do
+ TIO.putStrLn ""
+ TIO.putStrLn $ "Functions scanned: " <> T.pack (show (length syms))
+ TIO.putStrLn $ "NCT findings: " <> T.pack (show total)
+ if total == 0
+ then exitSuccess
+ else exitFailure
+
+printNctSummary :: (Text, [NctFinding]) -> IO ()
+printNctSummary (sym, fs) =
+ TIO.putStrLn $ sym <> ": " <> T.pack (show (length fs))
+
+printNctDetail :: (Text, [NctFinding]) -> IO ()
+printNctDetail (sym, fs) = mapM_ (printFinding sym) fs
+
+printFinding :: Text -> NctFinding -> IO ()
+printFinding sym f = TIO.putStrLn $
+ sym <> ":" <> T.pack (show (nctLine f)) <> ": "
+ <> nctReasonText (nctReason f) <> ": " <> instrText (nctInstr f)
+
+nctReasonText :: NctReason -> Text
+nctReasonText r = case r of
+ CondBranch -> "cond-branch"
+ IndirectBranch -> "indirect-branch"
+ Div -> "div"
+ MulOp -> "mul"
+ VarShift -> "var-shift"
+ RegIndexAddr -> "reg-index"
+
+instrText :: Instr -> Text
+instrText instr = T.pack (show instr)
+
-- | Filter violations based on options.
-- By default, only secret violations are shown.
filterViolations :: Options -> [Violation] -> [Violation]
diff --git a/lib/Audit/AArch64.hs b/lib/Audit/AArch64.hs
@@ -39,6 +39,12 @@ module Audit.AArch64 (
, auditFileInterProcWithConfig
, parseFile
+ -- * NCT scanner
+ , scanNct
+ , scanNctFile
+ , NctReason(..)
+ , NctFinding(..)
+
-- * Results
, AuditResult(..)
, Violation(..)
@@ -59,6 +65,7 @@ module Audit.AArch64 (
import Audit.AArch64.CFG
import Audit.AArch64.Check
+import Audit.AArch64.NCT (NctReason(..), NctFinding(..), scanNct)
import Audit.AArch64.Parser
import Audit.AArch64.Types
( Reg, Violation(..), ViolationReason(..), regName
@@ -66,6 +73,7 @@ import Audit.AArch64.Types
)
import Data.Aeson (eitherDecodeStrict')
import qualified Data.ByteString as BS
+import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8')
@@ -146,3 +154,14 @@ loadTaintConfig path = do
case eitherDecodeStrict' bs of
Left err -> pure (Left (T.pack err))
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
+ 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))
diff --git a/lib/Audit/AArch64/NCT.hs b/lib/Audit/AArch64/NCT.hs
@@ -0,0 +1,161 @@
+{-# OPTIONS_HADDOCK prune #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Module: Audit.AArch64.NCT
+-- Copyright: (c) 2025 Jared Tobin
+-- License: MIT
+-- Maintainer: jared@ppad.tech
+--
+-- Static non-constant-time instruction scanner for AArch64 assembly.
+-- Flags instructions that typically introduce timing variability.
+
+module Audit.AArch64.NCT (
+ -- * Types
+ NctReason(..)
+ , NctFinding(..)
+ -- * Scanner
+ , scanNct
+ ) where
+
+import Audit.AArch64.CFG (isFunctionLabel)
+import Audit.AArch64.Types
+import Control.DeepSeq (NFData)
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+-- | Reason for flagging an instruction as non-constant-time.
+data NctReason
+ = CondBranch -- ^ Conditional branch (b.<cond>, cbz, cbnz, tbz, tbnz)
+ | IndirectBranch -- ^ Indirect branch (br, blr)
+ | Div -- ^ Division (udiv, sdiv)
+ | MulOp -- ^ Multiplication (mul, madd, msub, umulh, smulh)
+ | VarShift -- ^ Variable shift (lsl, lsr, asr, ror with reg operand)
+ | RegIndexAddr -- ^ Register-indexed memory access
+ deriving (Eq, Ord, Show, Generic, NFData)
+
+-- | A non-constant-time finding.
+data NctFinding = NctFinding
+ { nctLine :: !Int -- ^ Source line number
+ , nctInstr :: !Instr -- ^ The flagged instruction
+ , nctReason :: !NctReason -- ^ Why it was flagged
+ } deriving (Eq, Show, Generic, NFData)
+
+-- | Scan parsed lines for non-constant-time instructions.
+-- Returns findings grouped by function symbol.
+scanNct :: [Line] -> Map Text [NctFinding]
+scanNct = finalize . foldl step (unknownSym, Map.empty)
+ where
+ unknownSym = "<unknown>"
+
+ step (curSym, acc) ln =
+ let sym' = case lineLabel ln of
+ Just lbl | isFunctionLabel lbl -> lbl
+ _ -> curSym
+ findings = classifyLine ln
+ acc' = case findings of
+ [] -> acc
+ _ -> Map.insertWith (++) sym' findings acc
+ in (sym', acc')
+
+ finalize (_, acc) = fmap reverse acc
+
+-- | Classify a line, returning any NCT findings.
+classifyLine :: Line -> [NctFinding]
+classifyLine ln = case lineInstr ln of
+ Nothing -> []
+ Just instr -> case classifyInstr instr of
+ Nothing -> []
+ Just reason -> [NctFinding (lineNum ln) instr reason]
+
+-- | Classify an instruction for NCT concerns.
+classifyInstr :: Instr -> Maybe NctReason
+classifyInstr instr = case instr of
+ -- Conditional branches
+ BCond _ _ -> Just CondBranch
+ Cbz _ _ -> Just CondBranch
+ Cbnz _ _ -> Just CondBranch
+ Tbz _ _ _ -> Just CondBranch
+ Tbnz _ _ _ -> Just CondBranch
+
+ -- Indirect branches
+ Br _ -> Just IndirectBranch
+ Blr _ -> Just IndirectBranch
+
+ -- Division
+ Udiv _ _ _ -> Just Div
+ Sdiv _ _ _ -> Just Div
+
+ -- Multiplication
+ Mul _ _ _ -> Just MulOp
+ Madd _ _ _ _ -> Just MulOp
+ Msub _ _ _ _ -> Just MulOp
+ Umulh _ _ _ -> Just MulOp
+ Smulh _ _ _ -> Just MulOp
+
+ -- Variable shifts (only when shift amount is register)
+ Lsl _ _ op -> checkVarShiftOp op
+ Lsr _ _ op -> checkVarShiftOp op
+ Asr _ _ op -> checkVarShiftOp op
+ Ror _ _ op -> checkVarShiftOp op
+
+ -- Load/store with register index
+ Ldr _ addr -> checkRegIndexAddr addr
+ Ldrb _ addr -> checkRegIndexAddr addr
+ Ldrh _ addr -> checkRegIndexAddr addr
+ Ldrsb _ addr -> checkRegIndexAddr addr
+ Ldrsh _ addr -> checkRegIndexAddr addr
+ Ldrsw _ addr -> checkRegIndexAddr addr
+ Ldur _ addr -> checkRegIndexAddr addr
+ Str _ addr -> checkRegIndexAddr addr
+ Strb _ addr -> checkRegIndexAddr addr
+ Strh _ addr -> checkRegIndexAddr addr
+ Stur _ addr -> checkRegIndexAddr addr
+ Ldp _ _ addr -> checkRegIndexAddr addr
+ Stp _ _ addr -> checkRegIndexAddr addr
+
+ -- Acquire/release loads
+ Ldar _ addr -> checkRegIndexAddr addr
+ Ldarb _ addr -> checkRegIndexAddr addr
+ Ldarh _ addr -> checkRegIndexAddr addr
+ Stlr _ addr -> checkRegIndexAddr addr
+ Stlrb _ addr -> checkRegIndexAddr addr
+ Stlrh _ addr -> checkRegIndexAddr addr
+
+ -- Exclusive loads/stores
+ Ldxr _ addr -> checkRegIndexAddr addr
+ Ldxrb _ addr -> checkRegIndexAddr addr
+ Ldxrh _ addr -> checkRegIndexAddr addr
+ Stxr _ _ addr -> checkRegIndexAddr addr
+ Stxrb _ _ addr -> checkRegIndexAddr addr
+ Stxrh _ _ addr -> checkRegIndexAddr addr
+
+ -- Acquire-exclusive loads and release-exclusive stores
+ Ldaxr _ addr -> checkRegIndexAddr addr
+ Ldaxrb _ addr -> checkRegIndexAddr addr
+ Ldaxrh _ addr -> checkRegIndexAddr addr
+ Stlxr _ _ addr -> checkRegIndexAddr addr
+ Stlxrb _ _ addr -> checkRegIndexAddr addr
+ Stlxrh _ _ addr -> checkRegIndexAddr addr
+
+ _ -> Nothing
+
+-- | Check if operand indicates variable shift (register-based).
+checkVarShiftOp :: Operand -> Maybe NctReason
+checkVarShiftOp op = case op of
+ OpReg _ -> Just VarShift
+ OpShiftedReg _ _ -> Just VarShift
+ OpExtendedReg _ _ -> Just VarShift
+ _ -> Nothing
+
+-- | Check if address mode uses register indexing.
+checkRegIndexAddr :: AddrMode -> Maybe NctReason
+checkRegIndexAddr addr = case addr of
+ BaseReg _ _ -> Just RegIndexAddr
+ BaseRegShift _ _ _ -> Just RegIndexAddr
+ BaseRegExtend _ _ _ -> Just RegIndexAddr
+ _ -> Nothing
diff --git a/plans/ARCH21.md b/plans/ARCH21.md
@@ -0,0 +1,55 @@
+# ARCH21: Static Non-Constant-Time Instruction Scanner
+
+## Goal
+
+Add a parser-only scan mode that flags non-constant-time instructions
+in AArch64 assembly and groups findings by function symbol.
+
+## Motivation
+
+When full taint tracking is too complex or under-specified, a coarse
+instruction-level scanner still provides actionable signals. This mode
+should be fast, require no dataflow, and rely only on the existing
+parser and symbol labeling.
+
+## Scope
+
+- New scan pass over parsed `Line` list.
+- Group results by function label (symbol) using the same label
+ heuristics as CFG (`isFunctionLabel`).
+- No CFG or taint analysis; strictly syntactic inspection.
+
+## Heuristic: Non-Constant-Time Instruction Set
+
+Flag instructions that typically introduce secret-dependent control
+flow or memory timing when operands are data-dependent. Proposed set:
+
+- Conditional branches: `b.<cond>`, `cbz`, `cbnz`, `tbz`, `tbnz`
+- Indirect branches: `br`, `blr` (control flow depends on register)
+- Variable-latency arithmetic:
+ - `udiv`, `sdiv`
+ - `mul`, `madd`, `msub`, `umull`, `smull`, `umulh`, `smulh`
+- Variable-latency shift/rotate when shift amount is register:
+ - `lsl`, `lsr`, `asr`, `ror` with `OpReg`/`OpShiftedReg` operands
+- Table/indirect memory access patterns:
+ - Any load/store with `BaseReg`/`BaseRegShift`/`BaseRegExtend`
+ (indexing by register rather than immediate)
+
+Note: This is deliberately conservative and does not prove
+non-constant-time behavior; it highlights likely sources.
+
+## Output
+
+- A summary listing count of flagged instructions per function.
+- Optional detail mode listing line numbers and instruction text.
+
+## Integration
+
+- Add a new CLI flag: `--scan-nct` (or `--nct`) to run this mode.
+- Implement scanner in a new module `Audit.AArch64.NCT`.
+- Reuse `isFunctionLabel` to group by symbol while traversing lines.
+
+## Risks
+
+- High false positive rate by design.
+- Requires maintenance of the opcode list as parser expands.
diff --git a/plans/IMPL21.md b/plans/IMPL21.md
@@ -0,0 +1,43 @@
+# IMPL21: Static Non-Constant-Time Instruction Scanner
+
+## Changes
+
+1. Add `Audit.AArch64.NCT` module:
+ - `scanNct :: [Line] -> Map Text [NctFinding]`
+ - `NctFinding` holds line number, instruction, and reason.
+ - Maintain current function label while walking lines:
+ - On label line, if `isFunctionLabel`, update current function.
+ - Default symbol name to input file base or `"<unknown>"`.
+
+2. Define `NctReason` enumeration:
+ - `CondBranch`, `IndirectBranch`, `Div`, `Mul`, `VarShift`,
+ `RegIndexAddr`.
+ - Extendable in the future.
+
+3. Implement instruction classifier:
+ - Branches: `BCond`, `Cbz`, `Cbnz`, `Tbz`, `Tbnz` -> `CondBranch`.
+ - `Br`, `Blr` -> `IndirectBranch`.
+ - `Udiv`, `Sdiv` -> `Div`.
+ - `Mul`, `Madd`, `Msub`, `Umulh`, `Smulh` -> `Mul`.
+ - `Lsl`, `Lsr`, `Asr`, `Ror` -> `VarShift` when operand is
+ `OpReg`, `OpShiftedReg`, or `OpExtendedReg`.
+ - Loads/stores with `AddrMode` `BaseReg`, `BaseRegShift`,
+ `BaseRegExtend` -> `RegIndexAddr`.
+
+4. CLI integration (`app/Main.hs`):
+ - Add `--scan-nct` flag to select scan mode.
+ - If set, parse and run scanner instead of taint audit.
+ - Add optional `--nct-detail` to print per-instruction details.
+
+5. Output formatting:
+ - Summary: `symbol: count`.
+ - Detail lines: `symbol:line: reason: instr`.
+
+6. Tests (`test/Main.hs`):
+ - Parsing a small snippet and verifying findings grouped by symbol.
+ - One positive per reason to ensure coverage.
+
+## Notes
+
+- Keep lines under 80 chars.
+- No new dependencies.
diff --git a/ppad-auditor.cabal b/ppad-auditor.cabal
@@ -30,6 +30,7 @@ library
Audit.AArch64.CFG
Audit.AArch64.Taint
Audit.AArch64.Check
+ Audit.AArch64.NCT
build-depends:
base >= 4.9 && < 5
, bytestring >= 0.9 && < 0.13
diff --git a/test/Main.hs b/test/Main.hs
@@ -22,6 +22,7 @@ main = defaultMain $ testGroup "ppad-auditor" [
, interprocTests
, provenanceTests
, taintConfigTests
+ , nctTests
]
-- Parser tests
@@ -1077,3 +1078,222 @@ taintConfigTests = testGroup "TaintConfig" [
-- Two violations: unknown base x20, unknown index x8
assertEqual "two violations" 2 (length (arViolations ar))
]
+
+-- NCT scanner tests
+
+nctTests :: TestTree
+nctTests = testGroup "NCT" [
+ testCase "cond branch: bcond" $ do
+ let src = T.unlines
+ [ "_foo:"
+ , " cmp x0, x1"
+ , " b.eq target"
+ , "target:"
+ , " ret"
+ ]
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "_foo" m of
+ Nothing -> assertFailure "no findings for _foo"
+ Just fs -> do
+ assertEqual "one finding" 1 (length fs)
+ case fs of
+ [f] -> assertEqual "reason" CondBranch (nctReason f)
+ _ -> assertFailure "expected one finding"
+
+ , testCase "cond branch: cbz" $ do
+ let src = "foo:\n cbz x0, target\ntarget:\n ret\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" CondBranch (nctReason f)
+ _ -> assertFailure $ "expected 1 finding, got " ++ show (length fs)
+
+ , testCase "cond branch: tbz" $ do
+ let src = "foo:\n tbz x0, #5, target\ntarget:\n ret\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" CondBranch (nctReason f)
+ _ -> assertFailure $ "expected 1 finding"
+
+ , testCase "indirect branch: br" $ do
+ let src = "foo:\n br x8\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" IndirectBranch (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "indirect branch: blr" $ do
+ let src = "foo:\n blr x8\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" IndirectBranch (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "div: udiv" $ do
+ let src = "foo:\n udiv x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" Div (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "div: sdiv" $ do
+ let src = "foo:\n sdiv x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" Div (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "mul: mul" $ do
+ let src = "foo:\n mul x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" MulOp (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "mul: madd" $ do
+ let src = "foo:\n madd x0, x1, x2, x3\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" MulOp (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "mul: umulh" $ do
+ let src = "foo:\n umulh x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" MulOp (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "var-shift: lsl with reg" $ do
+ let src = "foo:\n lsl x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" VarShift (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "var-shift: lsr with reg" $ do
+ let src = "foo:\n lsr x0, x1, x2\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" VarShift (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "no finding: lsl with imm" $ do
+ -- lsl x0, x1, #5 should NOT flag (immediate shift)
+ let src = "foo:\n lsl x0, x1, #5\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ assertEqual "no findings" Nothing (Map.lookup "foo" m)
+
+ , testCase "reg-index: ldr [xN, xM]" $ do
+ let src = "foo:\n ldr x0, [x1, x2]\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" RegIndexAddr (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "reg-index: str [xN, xM, lsl #3]" $ do
+ let src = "foo:\n str x0, [x1, x2, lsl #3]\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ case Map.lookup "foo" m of
+ Nothing -> assertFailure "no findings"
+ Just fs -> case fs of
+ [f] -> assertEqual "reason" RegIndexAddr (nctReason f)
+ _ -> assertFailure "expected 1 finding"
+
+ , testCase "no finding: ldr [xN, #imm]" $ do
+ -- Immediate offset is safe
+ let src = "foo:\n ldr x0, [x1, #8]\n"
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ assertEqual "no findings" Nothing (Map.lookup "foo" m)
+
+ , testCase "grouping by function symbol" $ do
+ let src = T.unlines
+ [ "_foo:"
+ , " cbz x0, L1"
+ , "L1:"
+ , " ret"
+ , "_bar:"
+ , " udiv x0, x1, x2"
+ , " br x8"
+ , " ret"
+ ]
+ case parseAsm src of
+ Left e -> assertFailure $ "parse failed: " ++ show e
+ Right lns -> do
+ let m = scanNct lns
+ assertEqual "foo: 1 finding" (Just 1) (fmap length (Map.lookup "_foo" m))
+ assertEqual "bar: 2 findings" (Just 2) (fmap length (Map.lookup "_bar" m))
+ -- L1 is local, findings should stay with _foo
+ assertEqual "L1 not a key" Nothing (Map.lookup "L1" m)
+ ]