auditor

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

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:
Mapp/Main.hs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Audit/AArch64.hs | 19+++++++++++++++++++
Alib/Audit/AArch64/NCT.hs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplans/ARCH21.md | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplans/IMPL21.md | 43+++++++++++++++++++++++++++++++++++++++++++
Mppad-auditor.cabal | 1+
Mtest/Main.hs | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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) + ]