auditor

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

Main.hs (64419B)


      1 {-# LANGUAGE OverloadedStrings #-}
      2 
      3 module Main where
      4 
      5 import Audit.AArch64
      6 import Audit.AArch64.CallGraph
      7 import Audit.AArch64.Parser
      8 import Audit.AArch64.Runtime.GHC (ghcRuntime)
      9 import Audit.AArch64.Taint
     10 import Audit.AArch64.Types
     11 import Data.Aeson (eitherDecodeStrict')
     12 import qualified Data.ByteString.Char8 as B8
     13 import qualified Data.Map.Strict as Map
     14 import qualified Data.Set as Set
     15 import qualified Data.Text as T
     16 import Test.Tasty
     17 import Test.Tasty.HUnit
     18 
     19 -- | Runtime config used throughout tests.
     20 rt :: RuntimeConfig
     21 rt = ghcRuntime
     22 
     23 -- | Check if a violation is secret-derived
     24 -- (not unknown or structural).
     25 isSecretViolation :: ViolationReason -> Bool
     26 isSecretViolation r = case r of
     27   SecretBase _     -> True
     28   SecretIndex _    -> True
     29   UnknownBase _    -> False
     30   UnknownIndex _   -> False
     31   NonConstOffset   -> False
     32 
     33 main :: IO ()
     34 main = defaultMain $ testGroup "ppad-auditor" [
     35     parserTests
     36   , taintTests
     37   , auditTests
     38   , interprocTests
     39   , provenanceTests
     40   , taintConfigTests
     41   , nctTests
     42   , callGraphTests
     43   , tailCallTests
     44   ]
     45 
     46 -- Parser tests
     47 
     48 parserTests :: TestTree
     49 parserTests = testGroup "Parser" [
     50     testCase "parse label" $ do
     51       let src = "foo:\n"
     52       case parseAsm src of
     53         Left _ -> assertFailure "parse failed"
     54         Right lns -> do
     55           assertEqual "line count" 1 (length lns)
     56           assertEqual "label" (Just "foo")
     57             (lineLabel (head lns))
     58 
     59   , testCase "parse ldr base+imm" $ do
     60       let src = "ldr x0, [x20, #8]\n"
     61       case parseAsm src of
     62         Left _ -> assertFailure "parse failed"
     63         Right lns -> case lineInstr (head lns) of
     64           Just (Ldr X0 (BaseImm X20 8)) -> pure ()
     65           other -> assertFailure $
     66             "unexpected: " ++ show other
     67 
     68   , testCase "parse str base+reg" $ do
     69       let src = "str x1, [x21, x2]\n"
     70       case parseAsm src of
     71         Left _ -> assertFailure "parse failed"
     72         Right lns -> case lineInstr (head lns) of
     73           Just (Str X1 (BaseReg X21 X2)) -> pure ()
     74           other -> assertFailure $
     75             "unexpected: " ++ show other
     76 
     77   , testCase "parse add" $ do
     78       let src = "add x0, x1, x2\n"
     79       case parseAsm src of
     80         Left _ -> assertFailure "parse failed"
     81         Right lns -> case lineInstr (head lns) of
     82           Just (Add X0 X1 (OpReg X2)) -> pure ()
     83           other -> assertFailure $
     84             "unexpected: " ++ show other
     85 
     86   , testCase "parse adrp" $ do
     87       let src = "adrp x5, _symbol@PAGE\n"
     88       case parseAsm src of
     89         Left _ -> assertFailure "parse failed"
     90         Right lns -> case lineInstr (head lns) of
     91           Just (Adrp X5 _) -> pure ()
     92           other -> assertFailure $
     93             "unexpected: " ++ show other
     94 
     95   , testCase "skip directives" $ do
     96       let src = ".section __TEXT\n\
     97                 \.globl _foo\n_foo:\n  ret\n"
     98       case parseAsm src of
     99         Left e -> assertFailure $
    100           "parse failed: " ++ show e
    101         Right lns -> do
    102           let instrs = filter
    103                 ((/= Nothing) . lineInstr) lns
    104           assertEqual "instruction count"
    105             1 (length instrs)
    106 
    107   , testCase "parse ldr base+symbol" $ do
    108       let src = "ldr x8, [x8, _foo@GOTPAGEOFF]\n"
    109       case parseAsm src of
    110         Left e -> assertFailure $
    111           "parse failed: " ++ show e
    112         Right lns ->
    113           case lineInstr (safeHead lns) of
    114             Just (Ldr X8
    115               (BaseSymbol X8 "_foo@GOTPAGEOFF"))
    116               -> pure ()
    117             other -> assertFailure $
    118               "unexpected: " ++ show other
    119 
    120   , testCase "parse shifted register operand" $ do
    121       let src = "add x0, x1, x2, lsl #3\n"
    122       case parseAsm src of
    123         Left e -> assertFailure $
    124           "parse failed: " ++ show e
    125         Right lns ->
    126           case lineInstr (safeHead lns) of
    127             Just (Add X0 X1
    128               (OpShiftedReg X2 (LSL 3))) -> pure ()
    129             other -> assertFailure $
    130               "unexpected: " ++ show other
    131 
    132   , testCase "parse plain register operand" $ do
    133       let src = "add x0, x1, x2\n"
    134       case parseAsm src of
    135         Left e -> assertFailure $
    136           "parse failed: " ++ show e
    137         Right lns ->
    138           case lineInstr (safeHead lns) of
    139             Just (Add X0 X1 (OpReg X2)) -> pure ()
    140             other -> assertFailure $
    141               "unexpected: " ++ show other
    142 
    143   , testCase "parse literal address" $ do
    144       let src = "ldr x0, =foo\n"
    145       case parseAsm src of
    146         Left e -> assertFailure $
    147           "parse failed: " ++ show e
    148         Right lns ->
    149           case lineInstr (safeHead lns) of
    150             Just (Ldr X0 (Literal "foo")) -> pure ()
    151             other -> assertFailure $
    152               "unexpected: " ++ show other
    153 
    154   , testCase "parse all register types" $ do
    155       let src = T.unlines
    156             [ "mov x0, x1"
    157             , "mov w0, w1"
    158             , "mov d0, d1"
    159             , "mov s0, s1"
    160             , "mov q0, q1"
    161             , "mov x0, sp"
    162             , "mov x0, xzr"
    163             , "mov w0, wzr"
    164             , "mov x0, fp"
    165             , "mov x0, lr"
    166             ]
    167       case parseAsm src of
    168         Left e -> assertFailure $
    169           "parse failed: " ++ show e
    170         Right lns ->
    171           assertEqual "parsed all lines"
    172             10 (length lns)
    173 
    174   , testCase "parse label with instruction" $ do
    175       let src = "foo: mov x0, x1\n"
    176       case parseAsm src of
    177         Left e -> assertFailure $
    178           "parse failed: " ++ show e
    179         Right lns -> case safeHead lns of
    180           Line _ (Just "foo")
    181             (Just (Mov X0 (OpReg X1))) -> pure ()
    182           other -> assertFailure $
    183             "unexpected: " ++ show other
    184 
    185   , testCase "parse instruction without label" $ do
    186       let src = "  mov x0, x1\n"
    187       case parseAsm src of
    188         Left e -> assertFailure $
    189           "parse failed: " ++ show e
    190         Right lns -> case safeHead lns of
    191           Line _ Nothing
    192             (Just (Mov X0 (OpReg X1))) -> pure ()
    193           other -> assertFailure $
    194             "unexpected: " ++ show other
    195 
    196   -- Acquire/release and exclusive memory ops
    197   , testCase "parse ldar" $ do
    198       let src = "ldar x0, [x1]\n"
    199       case parseAsm src of
    200         Left e -> assertFailure $
    201           "parse failed: " ++ show e
    202         Right lns ->
    203           case lineInstr (safeHead lns) of
    204             Just (Ldar X0 (BaseImm X1 0)) ->
    205               pure ()
    206             other -> assertFailure $
    207               "unexpected: " ++ show other
    208 
    209   , testCase "parse stlr" $ do
    210       let src = "stlr x0, [x1]\n"
    211       case parseAsm src of
    212         Left e -> assertFailure $
    213           "parse failed: " ++ show e
    214         Right lns ->
    215           case lineInstr (safeHead lns) of
    216             Just (Stlr X0 (BaseImm X1 0)) ->
    217               pure ()
    218             other -> assertFailure $
    219               "unexpected: " ++ show other
    220 
    221   , testCase "parse stxr" $ do
    222       let src = "stxr w0, x1, [x2]\n"
    223       case parseAsm src of
    224         Left e -> assertFailure $
    225           "parse failed: " ++ show e
    226         Right lns ->
    227           case lineInstr (safeHead lns) of
    228             Just (Stxr W0 X1 (BaseImm X2 0)) ->
    229               pure ()
    230             other -> assertFailure $
    231               "unexpected: " ++ show other
    232 
    233   , testCase "parse ldaxr" $ do
    234       let src = "ldaxr x0, [x1]\n"
    235       case parseAsm src of
    236         Left e -> assertFailure $
    237           "parse failed: " ++ show e
    238         Right lns ->
    239           case lineInstr (safeHead lns) of
    240             Just (Ldaxr X0 (BaseImm X1 0)) ->
    241               pure ()
    242             other -> assertFailure $
    243               "unexpected: " ++ show other
    244 
    245   , testCase "parse stlxr" $ do
    246       let src = "stlxr w0, x1, [x2]\n"
    247       case parseAsm src of
    248         Left e -> assertFailure $
    249           "parse failed: " ++ show e
    250         Right lns ->
    251           case lineInstr (safeHead lns) of
    252             Just (Stlxr W0 X1 (BaseImm X2 0)) ->
    253               pure ()
    254             other -> assertFailure $
    255               "unexpected: " ++ show other
    256   ]
    257 
    258 safeHead :: [a] -> a
    259 safeHead (x:_) = x
    260 safeHead []    = error "safeHead: empty list"
    261 
    262 -- Taint tests
    263 
    264 taintTests :: TestTree
    265 taintTests = testGroup "Taint" [
    266     testCase "public roots" $ do
    267       let st = initTaintState rt
    268       assertEqual "X20 is public"
    269         Public (getTaint X20 st)
    270       assertEqual "X21 is public"
    271         Public (getTaint X21 st)
    272       assertEqual "SP is public"
    273         Public (getTaint SP st)
    274 
    275   , testCase "unknown register" $ do
    276       let st = initTaintState rt
    277       assertEqual "X0 is unknown"
    278         Unknown (getTaint X0 st)
    279 
    280   , testCase "mov propagates taint" $ do
    281       let st = initTaintState rt
    282           l = Line 1 Nothing
    283                 (Just (Mov X0 (OpReg X20)))
    284           st' = analyzeLine rt l st
    285       assertEqual "X0 becomes public"
    286         Public (getTaint X0 st')
    287 
    288   , testCase "add joins taints" $ do
    289       let st = initTaintState rt
    290           l = Line 1 Nothing
    291                 (Just (Add X0 X20 (OpReg X1)))
    292           st' = analyzeLine rt l st
    293       assertEqual "result is unknown"
    294         Unknown (getTaint X0 st')
    295 
    296   , testCase "load makes unknown" $ do
    297       let st = initTaintStateWith rt False
    298           l = Line 1 Nothing
    299                 (Just (Ldr X0 (BaseImm X20 0)))
    300           st' = analyzeLine rt l st
    301       assertEqual "loaded value is unknown"
    302         Unknown (getTaint X0 st')
    303   ]
    304 
    305 -- Audit tests
    306 
    307 auditTests :: TestTree
    308 auditTests = testGroup "Audit" [
    309     testCase "good: public base" $ do
    310       let src = T.unlines
    311             [ "foo:"
    312             , "  ldr x0, [x20, #8]"
    313             , "  str x0, [x21]"
    314             , "  ret"
    315             ]
    316       case audit rt "test" src of
    317         Left e -> assertFailure $
    318           "parse failed: " ++ show e
    319         Right ar -> assertEqual "no violations"
    320           0 (length (arViolations ar))
    321 
    322   , testCase "bad: unknown base" $ do
    323       let src = T.unlines
    324             [ "foo:"
    325             , "  ldr x0, [x0]"
    326             , "  ret"
    327             ]
    328       case audit rt "test" src of
    329         Left e -> assertFailure $
    330           "parse failed: " ++ show e
    331         Right ar -> do
    332           assertEqual "one violation"
    333             1 (length (arViolations ar))
    334           case arViolations ar of
    335             [v] -> case vReason v of
    336               UnknownBase X0 -> pure ()
    337               other -> assertFailure $
    338                 "wrong reason: " ++ show other
    339             _ -> assertFailure
    340               "expected one violation"
    341 
    342   , testCase "bad: secret-derived index" $ do
    343       let src = T.unlines
    344             [ "foo:"
    345             , "  ldr x0, [x21]"
    346             , "  ldr x1, [x20, x0]"
    347             , "  ret"
    348             ]
    349       case audit rt "test" src of
    350         Left e -> assertFailure $
    351           "parse failed: " ++ show e
    352         Right ar -> do
    353           assertEqual "one violation"
    354             1 (length (arViolations ar))
    355           case arViolations ar of
    356             [v] -> case vReason v of
    357               UnknownIndex X0 -> pure ()
    358               other -> assertFailure $
    359                 "wrong reason: " ++ show other
    360             _ -> assertFailure
    361               "expected one violation"
    362 
    363   , testCase "good: adrp+add pattern" $ do
    364       let src = T.unlines
    365             [ "foo:"
    366             , "  adrp x8, _const@PAGE"
    367             , "  add x8, x8, _const@PAGEOFF"
    368             , "  ldr x0, [x8]"
    369             , "  ret"
    370             ]
    371       case audit rt "test" src of
    372         Left e -> assertFailure $
    373           "parse failed: " ++ show e
    374         Right ar -> assertEqual "no violations"
    375           0 (length (arViolations ar))
    376 
    377   , testCase "good: cross-block public taint" $ do
    378       let src = T.unlines
    379             [ "blockA:"
    380             , "  adrp x8, _const@PAGE"
    381             , "  cbz x0, blockB"
    382             , "blockB:"
    383             , "  ldr x1, [x8]"
    384             , "  ret"
    385             ]
    386       case audit rt "test" src of
    387         Left e -> assertFailure $
    388           "parse failed: " ++ show e
    389         Right ar -> assertEqual "no violations"
    390           0 (length (arViolations ar))
    391 
    392   , testCase "bad: cross-block unknown taint" $ do
    393       let src = T.unlines
    394             [ "blockA:"
    395             , "  ldr x8, [x21]"
    396             , "  cbz x0, blockB"
    397             , "blockB:"
    398             , "  ldr x1, [x8]"
    399             , "  ret"
    400             ]
    401       case audit rt "test" src of
    402         Left e -> assertFailure $
    403           "parse failed: " ++ show e
    404         Right ar -> do
    405           assertEqual "one violation"
    406             1 (length (arViolations ar))
    407           case arViolations ar of
    408             [v] -> case vReason v of
    409               UnknownBase X8 -> pure ()
    410               other -> assertFailure $
    411                 "wrong reason: " ++ show other
    412             _ -> assertFailure
    413               "expected one violation"
    414 
    415   , testCase "call: no taint propagation to callee" $ do
    416       let src = T.unlines
    417             [ "caller:"
    418             , "  adrp x8, _const@PAGE"
    419             , "  bl callee"
    420             , "  ret"
    421             , "callee:"
    422             , "  ldr x0, [x8]"
    423             , "  ret"
    424             ]
    425       case audit rt "test" src of
    426         Left e -> assertFailure $
    427           "parse failed: " ++ show e
    428         Right ar -> do
    429           assertEqual "one violation"
    430             1 (length (arViolations ar))
    431           case arViolations ar of
    432             [v] -> do
    433               assertEqual "violation in callee"
    434                 "callee" (vSymbol v)
    435               case vReason v of
    436                 UnknownBase X8 -> pure ()
    437                 other -> assertFailure $
    438                   "wrong reason: " ++ show other
    439             _ -> assertFailure
    440               "expected one violation"
    441 
    442   , testCase "call: caller-saved invalidation" $ do
    443       let src = T.unlines
    444             [ "foo:"
    445             , "  adrp x0, _const@PAGE"
    446             , "  bl bar"
    447             , "  ldr x1, [x0]"
    448             , "  ret"
    449             ]
    450       case audit rt "test" src of
    451         Left e -> assertFailure $
    452           "parse failed: " ++ show e
    453         Right ar -> do
    454           assertEqual "one violation"
    455             1 (length (arViolations ar))
    456           case arViolations ar of
    457             [v] -> case vReason v of
    458               UnknownBase X0 -> pure ()
    459               other -> assertFailure $
    460                 "wrong reason: " ++ show other
    461             _ -> assertFailure
    462               "expected one violation"
    463 
    464   , testCase "call: callee-saved preserved" $ do
    465       let src = T.unlines
    466             [ "foo:"
    467             , "  bl bar"
    468             , "  ldr x0, [x19]"
    469             , "  ret"
    470             ]
    471       case audit rt "test" src of
    472         Left e -> assertFailure $
    473           "parse failed: " ++ show e
    474         Right ar -> assertEqual "no violations"
    475           0 (length (arViolations ar))
    476 
    477   , testCase "good: stack spill/reload preserves public" $ do
    478       let src = T.unlines
    479             [ "foo:"
    480             , "  stp x20, x21, [sp]"
    481             , "  ldr x8, [sp]"
    482             , "  ldr x9, [sp, #8]"
    483             , "  ldr x0, [x8]"
    484             , "  ldr x1, [x9]"
    485             , "  ret"
    486             ]
    487       case audit rt "test" src of
    488         Left e -> assertFailure $
    489           "parse failed: " ++ show e
    490         Right ar -> assertEqual "no violations"
    491           0 (length (arViolations ar))
    492 
    493   , testCase "bad: stack spill/reload preserves unknown" $ do
    494       let src = T.unlines
    495             [ "foo:"
    496             , "  ldr x8, [x21]"
    497             , "  str x8, [sp, #16]"
    498             , "  ldr x9, [sp, #16]"
    499             , "  ldr x0, [x9]"
    500             , "  ret"
    501             ]
    502       case audit rt "test" src of
    503         Left e -> assertFailure $
    504           "parse failed: " ++ show e
    505         Right ar -> do
    506           assertEqual "one violation"
    507             1 (length (arViolations ar))
    508           case arViolations ar of
    509             [v] -> case vReason v of
    510               UnknownBase X9 -> pure ()
    511               other -> assertFailure $
    512                 "wrong reason: " ++ show other
    513             _ -> assertFailure
    514               "expected one violation"
    515 
    516   , testCase "good: pointer spill/reload preserves kind" $ do
    517       let src = T.unlines
    518             [ "foo:"
    519             , "  adrp x8, _const@PAGE"
    520             , "  str x8, [sp, #16]"
    521             , "  ldr x9, [sp, #16]"
    522             , "  ldr x0, [x9]"
    523             , "  ret"
    524             ]
    525       case audit rt "test" src of
    526         Left e -> assertFailure $
    527           "parse failed: " ++ show e
    528         Right ar -> assertEqual "no violations"
    529           0 (length (arViolations ar))
    530 
    531   , testCase "bad: scalar spill/reload doesn't upgrade kind" $ do
    532       let src = T.unlines
    533             [ "foo:"
    534             , "  ldr x8, [x21]"
    535             , "  add x8, x20, x8"
    536             , "  str x8, [sp, #16]"
    537             , "  ldr x9, [sp, #16]"
    538             , "  ldr x0, [x9]"
    539             , "  ret"
    540             ]
    541       case audit rt "test" src of
    542         Left e -> assertFailure $
    543           "parse failed: " ++ show e
    544         Right ar -> do
    545           assertEqual "one violation"
    546             1 (length (arViolations ar))
    547           case arViolations ar of
    548             [v] -> case vReason v of
    549               UnknownBase X9 -> pure ()
    550               other -> assertFailure $
    551                 "wrong reason: " ++ show other
    552             _ -> assertFailure
    553               "expected one violation"
    554 
    555   -- STG stack (x20-relative) spill/reload tests
    556   , testCase "good: STG stack spill/reload preserves public" $ do
    557       let src = T.unlines
    558             [ "foo:"
    559             , "  adrp x8, _const@PAGE"
    560             , "  str x8, [x20, #8]"
    561             , "  ldr x9, [x20, #8]"
    562             , "  ldr x0, [x9]"
    563             , "  ret"
    564             ]
    565       case audit rt "test" src of
    566         Left e -> assertFailure $
    567           "parse failed: " ++ show e
    568         Right ar -> assertEqual "no violations"
    569           0 (length (arViolations ar))
    570 
    571   , testCase "bad: STG stack spill/reload preserves unknown" $ do
    572       let src = T.unlines
    573             [ "foo:"
    574             , "  ldr x8, [x21]"
    575             , "  str x8, [x20, #16]"
    576             , "  ldr x9, [x20, #16]"
    577             , "  ldr x0, [x9]"
    578             , "  ret"
    579             ]
    580       case audit rt "test" src of
    581         Left e -> assertFailure $
    582           "parse failed: " ++ show e
    583         Right ar -> do
    584           assertEqual "one violation"
    585             1 (length (arViolations ar))
    586           case arViolations ar of
    587             [v] -> case vReason v of
    588               UnknownBase X9 -> pure ()
    589               other -> assertFailure $
    590                 "wrong reason: " ++ show other
    591             _ -> assertFailure
    592               "expected one violation"
    593 
    594   , testCase "good: STG stack pointer spill preserves kind" $ do
    595       let src = T.unlines
    596             [ "foo:"
    597             , "  adrp x8, _const@PAGE"
    598             , "  str x8, [x20, #24]"
    599             , "  ldr x9, [x20, #24]"
    600             , "  ldr x0, [x9]"
    601             , "  ret"
    602             ]
    603       case audit rt "test" src of
    604         Left e -> assertFailure $
    605           "parse failed: " ++ show e
    606         Right ar -> assertEqual "no violations"
    607           0 (length (arViolations ar))
    608 
    609   , testCase "good: STG stack pre-indexed load preserves taint" $ do
    610       let src = T.unlines
    611             [ "foo:"
    612             , "  adrp x8, _const@PAGE"
    613             , "  str x8, [x20, #16]"
    614             , "  ldr x9, [x20, #16]!"
    615             , "  ldr x0, [x9]"
    616             , "  ret"
    617             ]
    618       case audit rt "test" src of
    619         Left e -> assertFailure $
    620           "parse failed: " ++ show e
    621         Right ar -> assertEqual "no violations"
    622           0 (length (arViolations ar))
    623 
    624   , testCase "good: GOT/PLT pattern" $ do
    625       let src = T.unlines
    626             [ "foo:"
    627             , "  adrp x8, _sym@GOTPAGE"
    628             , "  ldr x8, [x8, _sym@GOTPAGEOFF]"
    629             , "  ldr x0, [x8]"
    630             , "  ret"
    631             ]
    632       case audit rt "test" src of
    633         Left e -> assertFailure $
    634           "parse failed: " ++ show e
    635         Right ar -> assertEqual "no violations"
    636           0 (length (arViolations ar))
    637 
    638   -- Heap taint propagation tests
    639   , testCase "bad: heap store propagates secret to later load" $ do
    640       let src = T.unlines
    641             [ "_foo:"
    642             , "  str x0, [x21]"
    643             , "  ldr x9, [x21, #8]"
    644             , "  ldr x1, [x20, x9]"
    645             , "  ret"
    646             ]
    647           cfg = TaintConfig (Map.singleton "_foo"
    648             (ArgPolicy (Set.singleton X0)
    649               Set.empty Set.empty
    650               Set.empty Set.empty)) True
    651       case auditWithConfig rt cfg "test" src of
    652         Left e -> assertFailure $
    653           "parse failed: " ++ show e
    654         Right ar -> do
    655           assertEqual "one violation"
    656             1 (length (arViolations ar))
    657           case arViolations ar of
    658             [v] -> case vReason v of
    659               SecretIndex X9 -> pure ()
    660               other -> assertFailure $
    661                 "wrong reason: " ++ show other
    662             _ -> assertFailure
    663               "expected one violation"
    664 
    665   , testCase "bad: heap taints join on multiple stores" $ do
    666       let src = T.unlines
    667             [ "_foo:"
    668             , "  adrp x8, _const@PAGE"
    669             , "  str x8, [x21]"
    670             , "  str x0, [x21, #8]"
    671             , "  ldr x9, [x21, #16]"
    672             , "  ldr x1, [x20, x9]"
    673             , "  ret"
    674             ]
    675           cfg = TaintConfig (Map.singleton "_foo"
    676             (ArgPolicy (Set.singleton X0)
    677               Set.empty Set.empty
    678               Set.empty Set.empty)) True
    679       case auditWithConfig rt cfg "test" src of
    680         Left e -> assertFailure $
    681           "parse failed: " ++ show e
    682         Right ar -> do
    683           assertEqual "one violation"
    684             1 (length (arViolations ar))
    685           case arViolations ar of
    686             [v] -> case vReason v of
    687               SecretIndex X9 -> pure ()
    688               other -> assertFailure $
    689                 "wrong reason: " ++ show other
    690             _ -> assertFailure
    691               "expected one violation"
    692 
    693   -- Refined heap slot tests
    694   , testCase "good: refined heap slot preserves public at different offset" $ do
    695       let src = T.unlines
    696             [ "_foo:"
    697             , "  adrp x8, _const@PAGE"
    698             , "  str x0, [x21]"
    699             , "  str x8, [x21, #8]"
    700             , "  ldr x9, [x21, #8]"
    701             , "  ldr x10, [x9]"
    702             , "  ret"
    703             ]
    704           cfg = TaintConfig (Map.singleton "_foo"
    705             (ArgPolicy (Set.singleton X0)
    706               Set.empty Set.empty
    707               Set.empty Set.empty)) True
    708       case auditWithConfig rt cfg "test" src of
    709         Left e -> assertFailure $
    710           "parse failed: " ++ show e
    711         Right ar -> assertEqual "no violations"
    712           0 (length (arViolations ar))
    713 
    714   , testCase "bad: refined heap slot preserves secret at its offset" $ do
    715       let src = T.unlines
    716             [ "_foo:"
    717             , "  adrp x8, _const@PAGE"
    718             , "  str x8, [x21]"
    719             , "  str x0, [x21, #8]"
    720             , "  ldr x9, [x21, #8]"
    721             , "  ldr x10, [x20, x9]"
    722             , "  ret"
    723             ]
    724           cfg = TaintConfig (Map.singleton "_foo"
    725             (ArgPolicy (Set.singleton X0)
    726               Set.empty Set.empty
    727               Set.empty Set.empty)) True
    728       case auditWithConfig rt cfg "test" src of
    729         Left e -> assertFailure $
    730           "parse failed: " ++ show e
    731         Right ar -> do
    732           assertEqual "one violation"
    733             1 (length (arViolations ar))
    734           case arViolations ar of
    735             [v] -> case vReason v of
    736               SecretIndex X9 -> pure ()
    737               other -> assertFailure $
    738                 "wrong reason: " ++ show other
    739             _ -> assertFailure
    740               "expected one violation"
    741 
    742   , testCase "bad: refined heap slot falls back for unknown offset" $ do
    743       let src = T.unlines
    744             [ "_foo:"
    745             , "  str x0, [x21]"
    746             , "  ldr x9, [x21, #16]"
    747             , "  ldr x10, [x20, x9]"
    748             , "  ret"
    749             ]
    750           cfg = TaintConfig (Map.singleton "_foo"
    751             (ArgPolicy (Set.singleton X0)
    752               Set.empty Set.empty
    753               Set.empty Set.empty)) True
    754       case auditWithConfig rt cfg "test" src of
    755         Left e -> assertFailure $
    756           "parse failed: " ++ show e
    757         Right ar -> do
    758           assertEqual "one violation"
    759             1 (length (arViolations ar))
    760           case arViolations ar of
    761             [v] -> case vReason v of
    762               SecretIndex X9 -> pure ()
    763               other -> assertFailure $
    764                 "wrong reason: " ++ show other
    765             _ -> assertFailure
    766               "expected one violation"
    767 
    768   -- Acquire/release and exclusive memory ops
    769   , testCase "bad: ldar from unknown base" $ do
    770       let src = T.unlines
    771             [ "_foo:"
    772             , "  ldar x8, [x0]"
    773             , "  ret"
    774             ]
    775       case audit rt "test" src of
    776         Left e -> assertFailure $
    777           "parse failed: " ++ show e
    778         Right ar -> do
    779           assertEqual "one violation"
    780             1 (length (arViolations ar))
    781           case arViolations ar of
    782             [v] -> case vReason v of
    783               UnknownBase X0 -> pure ()
    784               other -> assertFailure $
    785                 "wrong reason: " ++ show other
    786             _ -> assertFailure
    787               "expected one violation"
    788 
    789   , testCase "good: stlr to stack preserves taint" $ do
    790       let src = T.unlines
    791             [ "_foo:"
    792             , "  adrp x8, _const@PAGE"
    793             , "  stlr x8, [sp]"
    794             , "  ldr x9, [sp]"
    795             , "  ldr x10, [x9]"
    796             , "  ret"
    797             ]
    798       case audit rt "test" src of
    799         Left e -> assertFailure $
    800           "parse failed: " ++ show e
    801         Right ar -> assertEqual "no violations"
    802           0 (length (arViolations ar))
    803 
    804   , testCase "good: stxr status register is public" $ do
    805       let src = T.unlines
    806             [ "_foo:"
    807             , "  stxr w0, x1, [x21]"
    808             , "  ldr x8, [x20, w0]"
    809             , "  ret"
    810             ]
    811       case audit rt "test" src of
    812         Left e -> assertFailure $
    813           "parse failed: " ++ show e
    814         Right ar -> assertEqual "no violations"
    815           0 (length (arViolations ar))
    816 
    817   , testCase "good: stlxr status register is public" $ do
    818       let src = T.unlines
    819             [ "_foo:"
    820             , "  stlxr w0, x1, [x21]"
    821             , "  ldr x8, [x20, w0]"
    822             , "  ret"
    823             ]
    824       case audit rt "test" src of
    825         Left e -> assertFailure $
    826           "parse failed: " ++ show e
    827         Right ar -> assertEqual "no violations"
    828           0 (length (arViolations ar))
    829   ]
    830 
    831 -- Inter-procedural tests
    832 
    833 interprocTests :: TestTree
    834 interprocTests = testGroup "InterProc" [
    835     testCase "interproc: callee sets x0 public" $ do
    836       let src = T.unlines
    837             [ "_callee:"
    838             , "  adrp x0, _const@PAGE"
    839             , "  ret"
    840             , "_caller:"
    841             , "  bl _callee"
    842             , "  ldr x1, [x0]"
    843             , "  ret"
    844             ]
    845       case audit rt "test" src of
    846         Left e -> assertFailure $
    847           "parse failed: " ++ show e
    848         Right ar -> assertEqual "default: 1 violation"
    849           1 (length (arViolations ar))
    850       case auditInterProc rt "test" src of
    851         Left e -> assertFailure $
    852           "parse failed: " ++ show e
    853         Right ar -> assertEqual "interproc: 0 violations"
    854           0 (length (arViolations ar))
    855 
    856   , testCase "interproc: callee leaves x0 unknown" $ do
    857       let src = T.unlines
    858             [ "_callee:"
    859             , "  ldr x0, [x21]"
    860             , "  ret"
    861             , "_caller:"
    862             , "  bl _callee"
    863             , "  ldr x1, [x0]"
    864             , "  ret"
    865             ]
    866       case audit rt "test" src of
    867         Left e -> assertFailure $
    868           "parse failed: " ++ show e
    869         Right ar -> assertEqual "default: 1 violation"
    870           1 (length (arViolations ar))
    871       case auditInterProc rt "test" src of
    872         Left e -> assertFailure $
    873           "parse failed: " ++ show e
    874         Right ar -> assertEqual "interproc: 1 violation"
    875           1 (length (arViolations ar))
    876 
    877   , testCase "interproc: default mode unchanged" $ do
    878       let src = T.unlines
    879             [ "foo:"
    880             , "  adrp x8, _const@PAGE"
    881             , "  bl bar"
    882             , "  ldr x0, [x8]"
    883             , "  ret"
    884             ]
    885       case audit rt "test" src of
    886         Left e -> assertFailure $
    887           "parse failed: " ++ show e
    888         Right ar -> assertEqual "1 violation"
    889           1 (length (arViolations ar))
    890   ]
    891 
    892 -- Provenance tests
    893 
    894 provenanceTests :: TestTree
    895 provenanceTests = testGroup "Provenance" [
    896     testCase "good: mov from public root preserves provenance" $ do
    897       let src = T.unlines
    898             [ "foo:"
    899             , "  mov x8, x20"
    900             , "  ldr x0, [x8]"
    901             , "  ret"
    902             ]
    903       case audit rt "test" src of
    904         Left e -> assertFailure $
    905           "parse failed: " ++ show e
    906         Right ar -> assertEqual "no violations"
    907           0 (length (arViolations ar))
    908 
    909   , testCase "good: add #imm preserves provenance" $ do
    910       let src = T.unlines
    911             [ "foo:"
    912             , "  add x8, x20, #16"
    913             , "  ldr x0, [x8]"
    914             , "  ret"
    915             ]
    916       case audit rt "test" src of
    917         Left e -> assertFailure $
    918           "parse failed: " ++ show e
    919         Right ar -> assertEqual "no violations"
    920           0 (length (arViolations ar))
    921 
    922   , testCase "bad: load clears provenance" $ do
    923       let src = T.unlines
    924             [ "foo:"
    925             , "  mov x8, x20"
    926             , "  ldr x8, [x8]"
    927             , "  ldr x0, [x8]"
    928             , "  ret"
    929             ]
    930       case audit rt "test" src of
    931         Left e -> assertFailure $
    932           "parse failed: " ++ show e
    933         Right ar -> do
    934           assertEqual "one violation"
    935             1 (length (arViolations ar))
    936           case arViolations ar of
    937             [v] -> case vReason v of
    938               UnknownBase X8 -> pure ()
    939               other -> assertFailure $
    940                 "wrong reason: " ++ show other
    941             _ -> assertFailure
    942               "expected one violation"
    943 
    944   , testCase "good: orr with xzr preserves provenance" $ do
    945       let src = T.unlines
    946             [ "foo:"
    947             , "  mov x8, x20"
    948             , "  orr x9, x8, xzr"
    949             , "  ldr x0, [x9]"
    950             , "  ret"
    951             ]
    952       case audit rt "test" src of
    953         Left e -> assertFailure $
    954           "parse failed: " ++ show e
    955         Right ar -> assertEqual "no violations"
    956           0 (length (arViolations ar))
    957 
    958   , testCase "good: pointer untagging preserves public provenance" $ do
    959       let src = T.unlines
    960             [ "foo:"
    961             , "  mov x8, x20"
    962             , "  and x8, x8, #0xfffffffffffffff8"
    963             , "  ldr x0, [x8]"
    964             , "  ret"
    965             ]
    966       case audit rt "test" src of
    967         Left e -> assertFailure $
    968           "parse failed: " ++ show e
    969         Right ar -> assertEqual "no violations"
    970           0 (length (arViolations ar))
    971 
    972   , testCase "bad: pointer untagging doesn't upgrade unknown provenance" $ do
    973       let src = T.unlines
    974             [ "foo:"
    975             , "  ldr x8, [x21]"
    976             , "  and x8, x8, #-8"
    977             , "  ldr x0, [x8]"
    978             , "  ret"
    979             ]
    980       case audit rt "test" src of
    981         Left e -> assertFailure $
    982           "parse failed: " ++ show e
    983         Right ar -> assertEqual "one violation"
    984           1 (length (arViolations ar))
    985 
    986   , testCase "bad: scalar cannot be laundered via provenance" $ do
    987       let src = T.unlines
    988             [ "foo:"
    989             , "  ldr x8, [x21]"
    990             , "  add x9, x20, x8"
    991             , "  ldr x0, [x9]"
    992             , "  ret"
    993             ]
    994       case audit rt "test" src of
    995         Left e -> assertFailure $
    996           "parse failed: " ++ show e
    997         Right ar -> do
    998           assertEqual "one violation"
    999             1 (length (arViolations ar))
   1000           case arViolations ar of
   1001             [v] -> case vReason v of
   1002               UnknownBase X9 -> pure ()
   1003               other -> assertFailure $
   1004                 "wrong reason: " ++ show other
   1005             _ -> assertFailure
   1006               "expected one violation"
   1007   ]
   1008 
   1009 -- Taint config tests
   1010 
   1011 taintConfigTests :: TestTree
   1012 taintConfigTests = testGroup "TaintConfig" [
   1013     testCase "parse valid config" $ do
   1014       let json = B8.pack
   1015             "{\"foo\": {\"secret\": [\"X0\"], \
   1016             \\"public\": [\"X1\"]}}"
   1017       case eitherDecodeStrict' json
   1018              :: Either String TaintConfig of
   1019         Left e -> assertFailure $
   1020           "parse failed: " ++ e
   1021         Right cfg -> do
   1022           let policy =
   1023                 Map.lookup "foo" (tcPolicies cfg)
   1024           case policy of
   1025             Nothing -> assertFailure
   1026               "missing policy for 'foo'"
   1027             Just p -> do
   1028               assertEqual "secret regs"
   1029                 (Set.singleton X0) (apSecret p)
   1030               assertEqual "public regs"
   1031                 (Set.singleton X1) (apPublic p)
   1032 
   1033   , testCase "parse invalid register" $ do
   1034       let json = B8.pack
   1035             "{\"foo\": {\"secret\": [\"INVALID\"]}}"
   1036       case eitherDecodeStrict' json
   1037              :: Either String TaintConfig of
   1038         Left _ -> pure ()
   1039         Right _ -> assertFailure
   1040           "should have failed on invalid register"
   1041 
   1042   , testCase "seedArgs marks secret" $ do
   1043       let policy = ArgPolicy (Set.singleton X0)
   1044             Set.empty Set.empty
   1045             Set.empty Set.empty
   1046           st = seedArgs rt policy (initTaintState rt)
   1047       assertEqual "X0 is secret"
   1048         Secret (getTaint X0 st)
   1049 
   1050   , testCase "seedArgs marks public" $ do
   1051       let policy = ArgPolicy Set.empty
   1052             (Set.singleton X0) Set.empty
   1053             Set.empty Set.empty
   1054           st = seedArgs rt policy (initTaintState rt)
   1055       assertEqual "X0 is public"
   1056         Public (getTaint X0 st)
   1057 
   1058   , testCase "secret takes precedence over public" $ do
   1059       let policy = ArgPolicy (Set.singleton X0)
   1060             (Set.singleton X0) Set.empty
   1061             Set.empty Set.empty
   1062           st = seedArgs rt policy (initTaintState rt)
   1063       assertEqual "X0 is secret"
   1064         Secret (getTaint X0 st)
   1065 
   1066   , testCase "secret_pointee load with imm offset produces Secret" $ do
   1067       let src = T.unlines
   1068             [ "_foo:"
   1069             , "  ldr x1, [x0, #8]"
   1070             , "  ldr x2, [x21, x1]"
   1071             , "  ret"
   1072             ]
   1073           cfg = TaintConfig (Map.singleton "_foo"
   1074             (ArgPolicy Set.empty Set.empty
   1075               (Set.singleton X0)
   1076               Set.empty Set.empty)) True
   1077       case auditWithConfig rt cfg "test" src of
   1078         Left e -> assertFailure $
   1079           "parse failed: " ++ show e
   1080         Right ar -> do
   1081           assertEqual "one violation"
   1082             1 (length (arViolations ar))
   1083           case arViolations ar of
   1084             [v] -> assertEqual "secret index"
   1085               (SecretIndex X1) (vReason v)
   1086             _ -> assertFailure
   1087               "expected exactly one violation"
   1088 
   1089   , testCase "secret_pointee load with reg offset produces Secret" $ do
   1090       let src = T.unlines
   1091             [ "_foo:"
   1092             , "  ldr x2, [x0, x1]"
   1093             , "  ldr x3, [x21, x2]"
   1094             , "  ret"
   1095             ]
   1096           cfg = TaintConfig (Map.singleton "_foo"
   1097             (ArgPolicy Set.empty Set.empty
   1098               (Set.singleton X0)
   1099               Set.empty Set.empty)) True
   1100       case auditWithConfig rt cfg "test" src of
   1101         Left e -> assertFailure $
   1102           "parse failed: " ++ show e
   1103         Right ar -> do
   1104           assertEqual "two violations"
   1105             2 (length (arViolations ar))
   1106           let reasons = map vReason (arViolations ar)
   1107           assertBool "has secret index x2"
   1108             (SecretIndex X2 `elem` reasons)
   1109 
   1110   , testCase "secret takes precedence over secret_pointee" $ do
   1111       let policy = ArgPolicy (Set.singleton X0)
   1112             Set.empty (Set.singleton X0)
   1113             Set.empty Set.empty
   1114           st = seedArgs rt policy (initTaintState rt)
   1115       assertEqual "X0 is secret"
   1116         Secret (getTaint X0 st)
   1117       assertEqual "X0 provenance"
   1118         ProvUnknown (getProvenance X0 st)
   1119 
   1120   , testCase "non-pointer arithmetic invalidates secret_pointee" $ do
   1121       let src = T.unlines
   1122             [ "_foo:"
   1123             , "  mul x0, x0, x1"
   1124             , "  ldr x2, [x0, #0]"
   1125             , "  ldr x3, [x21, x2]"
   1126             , "  ret"
   1127             ]
   1128           cfg = TaintConfig (Map.singleton "_foo"
   1129             (ArgPolicy Set.empty Set.empty
   1130               (Set.singleton X0)
   1131               Set.empty Set.empty)) True
   1132       case auditWithConfig rt cfg "test" src of
   1133         Left e -> assertFailure $
   1134           "parse failed: " ++ show e
   1135         Right ar ->
   1136           assertEqual "no secret violations" 0
   1137             (length $ filter isSecretViolation
   1138               (map vReason (arViolations ar)))
   1139 
   1140   , testCase "pointer arithmetic preserves secret_pointee" $ do
   1141       let src = T.unlines
   1142             [ "_foo:"
   1143             , "  add x0, x0, #8"
   1144             , "  ldr x1, [x0, #0]"
   1145             , "  ldr x2, [x21, x1]"
   1146             , "  ret"
   1147             ]
   1148           cfg = TaintConfig (Map.singleton "_foo"
   1149             (ArgPolicy Set.empty Set.empty
   1150               (Set.singleton X0)
   1151               Set.empty Set.empty)) True
   1152       case auditWithConfig rt cfg "test" src of
   1153         Left e -> assertFailure $
   1154           "parse failed: " ++ show e
   1155         Right ar ->
   1156           assertEqual "one secret violation" 1
   1157             (length $ filter isSecretViolation
   1158               (map vReason (arViolations ar)))
   1159 
   1160   , testCase "secret arg causes violation on indexed access" $ do
   1161       let src = T.unlines
   1162             [ "_mul_wnaf:"
   1163             , "  ldr x8, [x20, x0]"
   1164             , "  ret"
   1165             ]
   1166           cfg = TaintConfig (Map.singleton "_mul_wnaf"
   1167             (ArgPolicy (Set.singleton X0)
   1168               Set.empty Set.empty
   1169               Set.empty Set.empty)) True
   1170       case auditWithConfig rt cfg "test" src of
   1171         Left e -> assertFailure $
   1172           "parse failed: " ++ show e
   1173         Right ar -> do
   1174           assertEqual "one violation"
   1175             1 (length (arViolations ar))
   1176           case arViolations ar of
   1177             [v] -> case vReason v of
   1178               SecretIndex X0 -> pure ()
   1179               other -> assertFailure $
   1180                 "wrong reason: " ++ show other
   1181             _ -> assertFailure
   1182               "expected one violation"
   1183 
   1184   , testCase "public arg allows indexed access" $ do
   1185       let src = T.unlines
   1186             [ "_foo:"
   1187             , "  ldr x8, [x20, x0]"
   1188             , "  ret"
   1189             ]
   1190           cfg = TaintConfig (Map.singleton "_foo"
   1191             (ArgPolicy Set.empty (Set.singleton X0)
   1192               Set.empty Set.empty Set.empty)) True
   1193       case auditWithConfig rt cfg "test" src of
   1194         Left e -> assertFailure $
   1195           "parse failed: " ++ show e
   1196         Right ar -> assertEqual "no violations"
   1197           0 (length (arViolations ar))
   1198 
   1199   , testCase "interproc with config" $ do
   1200       let src = T.unlines
   1201             [ "_mul_wnaf:"
   1202             , "  ldr x8, [x20, x0]"
   1203             , "  ret"
   1204             ]
   1205           cfg = TaintConfig (Map.singleton "_mul_wnaf"
   1206             (ArgPolicy (Set.singleton X0)
   1207               Set.empty Set.empty
   1208               Set.empty Set.empty)) True
   1209       case auditInterProcWithConfig rt cfg "test" src of
   1210         Left e -> assertFailure $
   1211           "parse failed: " ++ show e
   1212         Right ar ->
   1213           assertEqual "one violation"
   1214             1 (length (arViolations ar))
   1215 
   1216   -- STG stack slot seeding tests
   1217   , testCase "parse stg_secret/stg_public in config" $ do
   1218       let json = B8.pack $ unlines
   1219             [ "{"
   1220             , "  \"_foo\": {"
   1221             , "    \"secret\": [\"X0\"],"
   1222             , "    \"stg_secret\": [8, 152],"
   1223             , "    \"stg_public\": [24]"
   1224             , "  }"
   1225             , "}"
   1226             ]
   1227       case eitherDecodeStrict' json
   1228              :: Either String TaintConfig of
   1229         Left e -> assertFailure $
   1230           "parse failed: " ++ e
   1231         Right cfg -> do
   1232           let policy =
   1233                 Map.lookup "_foo" (tcPolicies cfg)
   1234           case policy of
   1235             Nothing -> assertFailure
   1236               "missing policy for '_foo'"
   1237             Just p -> do
   1238               assertEqual "stg_secret"
   1239                 (Set.fromList [8, 152])
   1240                 (apStgSecret p)
   1241               assertEqual "stg_public"
   1242                 (Set.singleton 24)
   1243                 (apStgPublic p)
   1244 
   1245   , testCase "stg_secret seeds STG stack slot as secret" $ do
   1246       let src = T.unlines
   1247             [ "_foo:"
   1248             , "  ldr x8, [x20, #8]"
   1249             , "  ldr x9, [x21, x8]"
   1250             , "  ret"
   1251             ]
   1252           cfg = TaintConfig (Map.singleton "_foo"
   1253             (ArgPolicy Set.empty Set.empty Set.empty
   1254               (Set.singleton 8) Set.empty)) True
   1255       case auditWithConfig rt cfg "test" src of
   1256         Left e -> assertFailure $
   1257           "parse failed: " ++ show e
   1258         Right ar -> do
   1259           assertEqual "one violation"
   1260             1 (length (arViolations ar))
   1261           case arViolations ar of
   1262             [v] -> case vReason v of
   1263               SecretIndex X8 -> pure ()
   1264               other -> assertFailure $
   1265                 "wrong reason: " ++ show other
   1266             _ -> assertFailure
   1267               "expected one violation"
   1268 
   1269   , testCase "stg_public seeds STG stack slot as public" $ do
   1270       let src = T.unlines
   1271             [ "_foo:"
   1272             , "  ldr x8, [x20, #16]"
   1273             , "  ldr x9, [x21, x8]"
   1274             , "  ret"
   1275             ]
   1276           cfg = TaintConfig (Map.singleton "_foo"
   1277             (ArgPolicy Set.empty Set.empty Set.empty
   1278               Set.empty (Set.singleton 16))) True
   1279       case auditWithConfig rt cfg "test" src of
   1280         Left e -> assertFailure $
   1281           "parse failed: " ++ show e
   1282         Right ar -> assertEqual "no violations"
   1283           0 (length (arViolations ar))
   1284 
   1285   , testCase "stg_secret takes precedence over stg_public" $ do
   1286       let src = T.unlines
   1287             [ "_foo:"
   1288             , "  ldr x8, [x20, #8]"
   1289             , "  ldr x9, [x21, x8]"
   1290             , "  ret"
   1291             ]
   1292           cfg = TaintConfig (Map.singleton "_foo"
   1293             (ArgPolicy Set.empty Set.empty Set.empty
   1294               (Set.singleton 8)
   1295               (Set.singleton 8))) True
   1296       case auditWithConfig rt cfg "test" src of
   1297         Left e -> assertFailure $
   1298           "parse failed: " ++ show e
   1299         Right ar -> do
   1300           assertEqual "one violation"
   1301             1 (length (arViolations ar))
   1302           case arViolations ar of
   1303             [v] -> case vReason v of
   1304               SecretIndex X8 -> pure ()
   1305               other -> assertFailure $
   1306                 "wrong reason: " ++ show other
   1307             _ -> assertFailure
   1308               "expected one violation"
   1309 
   1310   -- STG stack delta tracking tests
   1311   , testCase "stg_secret preserved after sub x20" $ do
   1312       let src = T.unlines
   1313             [ "_foo:"
   1314             , "  sub x20, x20, #16"
   1315             , "  ldr x8, [x20, #24]"
   1316             , "  ldr x9, [x21, x8]"
   1317             , "  ret"
   1318             ]
   1319           cfg = TaintConfig (Map.singleton "_foo"
   1320             (ArgPolicy Set.empty Set.empty Set.empty
   1321               (Set.singleton 8) Set.empty)) True
   1322       case auditWithConfig rt cfg "test" src of
   1323         Left e -> assertFailure $
   1324           "parse failed: " ++ show e
   1325         Right ar -> do
   1326           assertEqual "one violation"
   1327             1 (length (arViolations ar))
   1328           case arViolations ar of
   1329             [v] -> case vReason v of
   1330               SecretIndex X8 -> pure ()
   1331               other -> assertFailure $
   1332                 "wrong reason: " ++ show other
   1333             _ -> assertFailure
   1334               "expected one violation"
   1335 
   1336   , testCase "stg_secret preserved after add x20" $ do
   1337       let src = T.unlines
   1338             [ "_foo:"
   1339             , "  add x20, x20, #16"
   1340             , "  ldr x8, [x20, #8]"
   1341             , "  ldr x9, [x21, x8]"
   1342             , "  ret"
   1343             ]
   1344           cfg = TaintConfig (Map.singleton "_foo"
   1345             (ArgPolicy Set.empty Set.empty Set.empty
   1346               (Set.singleton 24) Set.empty)) True
   1347       case auditWithConfig rt cfg "test" src of
   1348         Left e -> assertFailure $
   1349           "parse failed: " ++ show e
   1350         Right ar -> do
   1351           assertEqual "one violation"
   1352             1 (length (arViolations ar))
   1353           case arViolations ar of
   1354             [v] -> case vReason v of
   1355               SecretIndex X8 -> pure ()
   1356               other -> assertFailure $
   1357                 "wrong reason: " ++ show other
   1358             _ -> assertFailure
   1359               "expected one violation"
   1360 
   1361   , testCase "stg slot cleared on non-constant x20 update" $ do
   1362       let src = T.unlines
   1363             [ "_foo:"
   1364             , "  add x20, x20, x1"
   1365             , "  ldr x8, [x20, #8]"
   1366             , "  ldr x9, [x21, x8]"
   1367             , "  ret"
   1368             ]
   1369           cfg = TaintConfig (Map.singleton "_foo"
   1370             (ArgPolicy Set.empty Set.empty Set.empty
   1371               (Set.singleton 8) Set.empty)) True
   1372       case auditWithConfig rt cfg "test" src of
   1373         Left e -> assertFailure $
   1374           "parse failed: " ++ show e
   1375         Right ar ->
   1376           assertEqual "one violation"
   1377             1 (length (arViolations ar))
   1378   ]
   1379 
   1380 -- NCT scanner tests
   1381 
   1382 nctTests :: TestTree
   1383 nctTests = testGroup "NCT" [
   1384     testCase "cond branch: bcond" $ do
   1385       let src = T.unlines
   1386             [ "_foo:"
   1387             , "  cmp x0, x1"
   1388             , "  b.eq target"
   1389             , "target:"
   1390             , "  ret"
   1391             ]
   1392       case parseAsm src of
   1393         Left e -> assertFailure $
   1394           "parse failed: " ++ show e
   1395         Right lns -> do
   1396           let m = scanNct rt lns
   1397           case Map.lookup "_foo" m of
   1398             Nothing -> assertFailure
   1399               "no findings for _foo"
   1400             Just fs -> do
   1401               assertEqual "one finding"
   1402                 1 (length fs)
   1403               case fs of
   1404                 [f] -> assertEqual "reason"
   1405                   CondBranch (nctReason f)
   1406                 _ -> assertFailure
   1407                   "expected one finding"
   1408 
   1409   , testCase "cond branch: cbz" $ do
   1410       let src = "foo:\n  cbz x0, target\n\
   1411                 \target:\n  ret\n"
   1412       case parseAsm src of
   1413         Left e -> assertFailure $
   1414           "parse failed: " ++ show e
   1415         Right lns -> do
   1416           let m = scanNct rt lns
   1417           case Map.lookup "foo" m of
   1418             Nothing -> assertFailure "no findings"
   1419             Just fs -> case fs of
   1420               [f] -> assertEqual "reason"
   1421                 CondBranch (nctReason f)
   1422               _ -> assertFailure $
   1423                 "expected 1 finding, got "
   1424                 ++ show (length fs)
   1425 
   1426   , testCase "cond branch: tbz" $ do
   1427       let src = "foo:\n  tbz x0, #5, target\n\
   1428                 \target:\n  ret\n"
   1429       case parseAsm src of
   1430         Left e -> assertFailure $
   1431           "parse failed: " ++ show e
   1432         Right lns -> do
   1433           let m = scanNct rt lns
   1434           case Map.lookup "foo" m of
   1435             Nothing -> assertFailure "no findings"
   1436             Just fs -> case fs of
   1437               [f] -> assertEqual "reason"
   1438                 CondBranch (nctReason f)
   1439               _ -> assertFailure "expected 1 finding"
   1440 
   1441   , testCase "indirect branch: br" $ do
   1442       let src = "foo:\n  br x8\n"
   1443       case parseAsm src of
   1444         Left e -> assertFailure $
   1445           "parse failed: " ++ show e
   1446         Right lns -> do
   1447           let m = scanNct rt lns
   1448           case Map.lookup "foo" m of
   1449             Nothing -> assertFailure "no findings"
   1450             Just fs -> case fs of
   1451               [f] -> assertEqual "reason"
   1452                 IndirectBranch (nctReason f)
   1453               _ -> assertFailure "expected 1 finding"
   1454 
   1455   , testCase "indirect branch: blr" $ do
   1456       let src = "foo:\n  blr x8\n"
   1457       case parseAsm src of
   1458         Left e -> assertFailure $
   1459           "parse failed: " ++ show e
   1460         Right lns -> do
   1461           let m = scanNct rt lns
   1462           case Map.lookup "foo" m of
   1463             Nothing -> assertFailure "no findings"
   1464             Just fs -> case fs of
   1465               [f] -> assertEqual "reason"
   1466                 IndirectBranch (nctReason f)
   1467               _ -> assertFailure "expected 1 finding"
   1468 
   1469   , testCase "div: udiv" $ do
   1470       let src = "foo:\n  udiv x0, x1, x2\n"
   1471       case parseAsm src of
   1472         Left e -> assertFailure $
   1473           "parse failed: " ++ show e
   1474         Right lns -> do
   1475           let m = scanNct rt lns
   1476           case Map.lookup "foo" m of
   1477             Nothing -> assertFailure "no findings"
   1478             Just fs -> case fs of
   1479               [f] -> assertEqual "reason"
   1480                 Div (nctReason f)
   1481               _ -> assertFailure "expected 1 finding"
   1482 
   1483   , testCase "div: sdiv" $ do
   1484       let src = "foo:\n  sdiv x0, x1, x2\n"
   1485       case parseAsm src of
   1486         Left e -> assertFailure $
   1487           "parse failed: " ++ show e
   1488         Right lns -> do
   1489           let m = scanNct rt lns
   1490           case Map.lookup "foo" m of
   1491             Nothing -> assertFailure "no findings"
   1492             Just fs -> case fs of
   1493               [f] -> assertEqual "reason"
   1494                 Div (nctReason f)
   1495               _ -> assertFailure "expected 1 finding"
   1496 
   1497   , testCase "no finding: mul (DIT)" $ do
   1498       let src = "foo:\n  mul x0, x1, x2\n"
   1499       case parseAsm src of
   1500         Left e -> assertFailure $
   1501           "parse failed: " ++ show e
   1502         Right lns -> do
   1503           let m = scanNct rt lns
   1504           assertEqual "no findings" Nothing
   1505             (Map.lookup "foo" m)
   1506 
   1507   , testCase "no finding: madd (DIT)" $ do
   1508       let src = "foo:\n  madd x0, x1, x2, x3\n"
   1509       case parseAsm src of
   1510         Left e -> assertFailure $
   1511           "parse failed: " ++ show e
   1512         Right lns -> do
   1513           let m = scanNct rt lns
   1514           assertEqual "no findings" Nothing
   1515             (Map.lookup "foo" m)
   1516 
   1517   , testCase "no finding: umulh (DIT)" $ do
   1518       let src = "foo:\n  umulh x0, x1, x2\n"
   1519       case parseAsm src of
   1520         Left e -> assertFailure $
   1521           "parse failed: " ++ show e
   1522         Right lns -> do
   1523           let m = scanNct rt lns
   1524           assertEqual "no findings" Nothing
   1525             (Map.lookup "foo" m)
   1526 
   1527   , testCase "no finding: lsl with reg (DIT)" $ do
   1528       let src = "foo:\n  lsl x0, x1, x2\n"
   1529       case parseAsm src of
   1530         Left e -> assertFailure $
   1531           "parse failed: " ++ show e
   1532         Right lns -> do
   1533           let m = scanNct rt lns
   1534           assertEqual "no findings" Nothing
   1535             (Map.lookup "foo" m)
   1536 
   1537   , testCase "no finding: lsr with reg (DIT)" $ do
   1538       let src = "foo:\n  lsr x0, x1, x2\n"
   1539       case parseAsm src of
   1540         Left e -> assertFailure $
   1541           "parse failed: " ++ show e
   1542         Right lns -> do
   1543           let m = scanNct rt lns
   1544           assertEqual "no findings" Nothing
   1545             (Map.lookup "foo" m)
   1546 
   1547   , testCase "no finding: lsl with imm" $ do
   1548       let src = "foo:\n  lsl x0, x1, #5\n"
   1549       case parseAsm src of
   1550         Left e -> assertFailure $
   1551           "parse failed: " ++ show e
   1552         Right lns -> do
   1553           let m = scanNct rt lns
   1554           assertEqual "no findings" Nothing
   1555             (Map.lookup "foo" m)
   1556 
   1557   , testCase "reg-index: ldr [xN, xM]" $ do
   1558       let src = "foo:\n  ldr x0, [x1, x2]\n"
   1559       case parseAsm src of
   1560         Left e -> assertFailure $
   1561           "parse failed: " ++ show e
   1562         Right lns -> do
   1563           let m = scanNct rt lns
   1564           case Map.lookup "foo" m of
   1565             Nothing -> assertFailure "no findings"
   1566             Just fs -> case fs of
   1567               [f] -> assertEqual "reason"
   1568                 RegIndexAddr (nctReason f)
   1569               _ -> assertFailure "expected 1 finding"
   1570 
   1571   , testCase "reg-index: str [xN, xM, lsl #3]" $ do
   1572       let src = "foo:\n  str x0, [x1, x2, lsl #3]\n"
   1573       case parseAsm src of
   1574         Left e -> assertFailure $
   1575           "parse failed: " ++ show e
   1576         Right lns -> do
   1577           let m = scanNct rt lns
   1578           case Map.lookup "foo" m of
   1579             Nothing -> assertFailure "no findings"
   1580             Just fs -> case fs of
   1581               [f] -> assertEqual "reason"
   1582                 RegIndexAddr (nctReason f)
   1583               _ -> assertFailure "expected 1 finding"
   1584 
   1585   , testCase "no finding: ldr [xN, #imm]" $ do
   1586       let src = "foo:\n  ldr x0, [x1, #8]\n"
   1587       case parseAsm src of
   1588         Left e -> assertFailure $
   1589           "parse failed: " ++ show e
   1590         Right lns -> do
   1591           let m = scanNct rt lns
   1592           assertEqual "no findings" Nothing
   1593             (Map.lookup "foo" m)
   1594 
   1595   , testCase "grouping by function symbol" $ do
   1596       let src = T.unlines
   1597             [ "_foo:"
   1598             , "  cbz x0, L1"
   1599             , "L1:"
   1600             , "  ret"
   1601             , "_bar:"
   1602             , "  udiv x0, x1, x2"
   1603             , "  br x8"
   1604             , "  ret"
   1605             ]
   1606       case parseAsm src of
   1607         Left e -> assertFailure $
   1608           "parse failed: " ++ show e
   1609         Right lns -> do
   1610           let m = scanNct rt lns
   1611           assertEqual "foo: 1 finding" (Just 1)
   1612             (fmap length (Map.lookup "_foo" m))
   1613           assertEqual "bar: 2 findings" (Just 2)
   1614             (fmap length (Map.lookup "_bar" m))
   1615           assertEqual "L1 not a key" Nothing
   1616             (Map.lookup "L1" m)
   1617   ]
   1618 
   1619 -- Call graph tests
   1620 
   1621 callGraphTests :: TestTree
   1622 callGraphTests = testGroup "CallGraph" [
   1623     testCase "symbolExists: existing symbol" $ do
   1624       let src = T.unlines
   1625             [ "_foo:"
   1626             , "  ret"
   1627             ]
   1628       case parseAsm src of
   1629         Left e -> assertFailure $
   1630           "parse failed: " ++ show e
   1631         Right lns -> do
   1632           let cg = buildCallGraph rt lns
   1633           assertEqual "symbol exists"
   1634             True (symbolExists "_foo" cg)
   1635 
   1636   , testCase "symbolExists: missing symbol" $ do
   1637       let src = T.unlines
   1638             [ "_foo:"
   1639             , "  ret"
   1640             ]
   1641       case parseAsm src of
   1642         Left e -> assertFailure $
   1643           "parse failed: " ++ show e
   1644         Right lns -> do
   1645           let cg = buildCallGraph rt lns
   1646           assertEqual "symbol missing"
   1647             False (symbolExists "_bar" cg)
   1648 
   1649   , testCase "reachableSymbols: direct call" $ do
   1650       let src = T.unlines
   1651             [ "_foo:"
   1652             , "  bl _bar"
   1653             , "  ret"
   1654             , "_bar:"
   1655             , "  ret"
   1656             ]
   1657       case parseAsm src of
   1658         Left e -> assertFailure $
   1659           "parse failed: " ++ show e
   1660         Right lns -> do
   1661           let cg = buildCallGraph rt lns
   1662               reachable =
   1663                 reachableSymbols "_foo" cg
   1664           assertEqual "foo reachable" True
   1665             (Set.member "_foo" reachable)
   1666           assertEqual "bar reachable" True
   1667             (Set.member "_bar" reachable)
   1668           assertEqual "count" 2
   1669             (Set.size reachable)
   1670 
   1671   , testCase "reachableSymbols: transitive call" $ do
   1672       let src = T.unlines
   1673             [ "_foo:"
   1674             , "  bl _bar"
   1675             , "  ret"
   1676             , "_bar:"
   1677             , "  bl _baz"
   1678             , "  ret"
   1679             , "_baz:"
   1680             , "  ret"
   1681             ]
   1682       case parseAsm src of
   1683         Left e -> assertFailure $
   1684           "parse failed: " ++ show e
   1685         Right lns -> do
   1686           let cg = buildCallGraph rt lns
   1687               reachable =
   1688                 reachableSymbols "_foo" cg
   1689           assertEqual "baz reachable from foo" True
   1690             (Set.member "_baz" reachable)
   1691           assertEqual "count" 3
   1692             (Set.size reachable)
   1693 
   1694   , testCase "reachableSymbols: no callees" $ do
   1695       let src = T.unlines
   1696             [ "_foo:"
   1697             , "  ret"
   1698             , "_bar:"
   1699             , "  ret"
   1700             ]
   1701       case parseAsm src of
   1702         Left e -> assertFailure $
   1703           "parse failed: " ++ show e
   1704         Right lns -> do
   1705           let cg = buildCallGraph rt lns
   1706               reachable =
   1707                 reachableSymbols "_foo" cg
   1708           assertEqual "only foo reachable"
   1709             (Set.singleton "_foo") reachable
   1710 
   1711   , testCase "reachableSymbols: missing root returns empty" $ do
   1712       let src = T.unlines
   1713             [ "_foo:"
   1714             , "  ret"
   1715             ]
   1716       case parseAsm src of
   1717         Left e -> assertFailure $
   1718           "parse failed: " ++ show e
   1719         Right lns -> do
   1720           let cg = buildCallGraph rt lns
   1721               reachable =
   1722                 reachableSymbols "_missing" cg
   1723           assertEqual "empty set"
   1724             Set.empty reachable
   1725   ]
   1726 
   1727 -- Tail call inter-procedural tests
   1728 
   1729 tailCallTests :: TestTree
   1730 tailCallTests = testGroup "TailCall" [
   1731 
   1732     testCase "tail call propagates unknown taint" $ do
   1733       let src = T.unlines
   1734             [ "_caller:"
   1735             , "  ldr x23, [x21]"
   1736             , "  b _callee_info"
   1737             , "_callee_info:"
   1738             , "  ldr x0, [x20, x23]"
   1739             , "  ret"
   1740             ]
   1741       case auditInterProc rt "test" src of
   1742         Left e -> assertFailure $
   1743           "parse failed: " ++ show e
   1744         Right ar -> assertEqual "one violation"
   1745           1 (length (arViolations ar))
   1746 
   1747   , testCase "tail call propagates secret taint" $ do
   1748       let src = T.unlines
   1749             [ "_caller:"
   1750             , "  mov x22, x0"
   1751             , "  b _callee_info"
   1752             , "_callee_info:"
   1753             , "  ldr x1, [x20, x22]"
   1754             , "  ret"
   1755             ]
   1756           cfg = TaintConfig (Map.singleton "_caller"
   1757             (ArgPolicy (Set.singleton X0)
   1758               Set.empty Set.empty
   1759               Set.empty Set.empty)) True
   1760       case auditInterProcWithConfig rt cfg "test" src of
   1761         Left e -> assertFailure $
   1762           "parse failed: " ++ show e
   1763         Right ar ->
   1764           assertEqual "secret violation" 1
   1765             (length $ filter isSecretViolation
   1766               (map vReason (arViolations ar)))
   1767 
   1768   , testCase "local branch is intra-procedural" $ do
   1769       let src = T.unlines
   1770             [ "_foo:"
   1771             , "  mov x0, x1"
   1772             , "  b Llocal"
   1773             , "Llocal:"
   1774             , "  ldr x2, [x20, x0]"
   1775             , "  ret"
   1776             ]
   1777       case auditInterProc rt "test" src of
   1778         Left e -> assertFailure $
   1779           "parse failed: " ++ show e
   1780         Right ar -> assertEqual "one unknown violation"
   1781           1 (length (arViolations ar))
   1782 
   1783   , testCase "NCG local branch (Lc prefix) is intra-procedural" $ do
   1784       let src = T.unlines
   1785             [ "_foo:"
   1786             , "  mov x22, x0"
   1787             , "  b LcaB"
   1788             , "LcaB:"
   1789             , "  ldr x1, [x20, x22]"
   1790             , "  ret"
   1791             ]
   1792           cfg = TaintConfig (Map.singleton "_foo"
   1793             (ArgPolicy (Set.singleton X0)
   1794               Set.empty Set.empty
   1795               Set.empty Set.empty)) True
   1796       case auditInterProcWithConfig rt cfg "test" src of
   1797         Left e -> assertFailure $
   1798           "parse failed: " ++ show e
   1799         Right ar ->
   1800           assertEqual "secret violation" 1
   1801             (length $ filter isSecretViolation
   1802               (map vReason (arViolations ar)))
   1803 
   1804   , testCase "chain of tail calls propagates taint" $ do
   1805       let src = T.unlines
   1806             [ "_a:"
   1807             , "  mov x22, x0"
   1808             , "  b _b_info"
   1809             , "_b_info:"
   1810             , "  mov x23, x22"
   1811             , "  b _c_info"
   1812             , "_c_info:"
   1813             , "  ldr x1, [x20, x23]"
   1814             , "  ret"
   1815             ]
   1816           cfg = TaintConfig (Map.singleton "_a"
   1817             (ArgPolicy (Set.singleton X0)
   1818               Set.empty Set.empty
   1819               Set.empty Set.empty)) True
   1820       case auditInterProcWithConfig rt cfg "test" src of
   1821         Left e -> assertFailure $
   1822           "parse failed: " ++ show e
   1823         Right ar ->
   1824           assertEqual "secret violation" 1
   1825             (length $ filter isSecretViolation
   1826               (map vReason (arViolations ar)))
   1827 
   1828   , testCase "indirect call preserves callee-saved taint" $ do
   1829       let src = T.unlines
   1830             [ "_foo:"
   1831             , "  mov x23, x0"
   1832             , "  ldr x8, [x19]"
   1833             , "  blr x8"
   1834             , "  ldr x1, [x20, x23]"
   1835             , "  ret"
   1836             ]
   1837           cfg = TaintConfig (Map.singleton "_foo"
   1838             (ArgPolicy (Set.singleton X0)
   1839               Set.empty Set.empty
   1840               Set.empty Set.empty)) True
   1841       case auditInterProcWithConfig rt cfg "test" src of
   1842         Left e -> assertFailure $
   1843           "parse failed: " ++ show e
   1844         Right ar ->
   1845           assertEqual "x23 secret preserved" 1
   1846             (length $ filter isSecretViolation
   1847               (map vReason (arViolations ar)))
   1848 
   1849   , testCase "tail call to unknown function is conservative" $ do
   1850       let src = T.unlines
   1851             [ "_foo:"
   1852             , "  mov x22, x0"
   1853             , "  b _unknown_info"
   1854             ]
   1855           cfg = TaintConfig (Map.singleton "_foo"
   1856             (ArgPolicy (Set.singleton X0)
   1857               Set.empty Set.empty
   1858               Set.empty Set.empty)) True
   1859       case auditInterProcWithConfig rt cfg "test" src of
   1860         Left e -> assertFailure $
   1861           "parse failed: " ++ show e
   1862         Right ar -> assertEqual "no violations"
   1863           0 (length (arViolations ar))
   1864 
   1865   , testCase "secret_pointee propagates across tail call" $ do
   1866       let src = T.unlines
   1867             [ "_caller:"
   1868             , "  mov x22, x0"
   1869             , "  b _callee_info"
   1870             , "_callee_info:"
   1871             , "  ldr x1, [x22, #0]"
   1872             , "  ldr x2, [x20, x1]"
   1873             , "  ret"
   1874             ]
   1875           cfg = TaintConfig (Map.singleton "_caller"
   1876             (ArgPolicy Set.empty Set.empty
   1877               (Set.singleton X0)
   1878               Set.empty Set.empty)) True
   1879       case auditInterProcWithConfig rt cfg "test" src of
   1880         Left e -> assertFailure $
   1881           "parse failed: " ++ show e
   1882         Right ar ->
   1883           assertEqual "secret violation" 1
   1884             (length $ filter isSecretViolation
   1885               (map vReason (arViolations ar)))
   1886 
   1887   , testCase "mutual tail calls reach fixpoint" $ do
   1888       let src = T.unlines
   1889             [ "_a_info:"
   1890             , "  cbnz x0, Lskip"
   1891             , "  b _b_info"
   1892             , "Lskip:"
   1893             , "  ret"
   1894             , "_b_info:"
   1895             , "  ldr x1, [x20, x22]"
   1896             , "  b _a_info"
   1897             ]
   1898           cfg = TaintConfig (Map.singleton "_a_info"
   1899             (ArgPolicy (Set.singleton X22)
   1900               Set.empty Set.empty
   1901               Set.empty Set.empty)) True
   1902       case auditInterProcWithConfig rt cfg "test" src of
   1903         Left e -> assertFailure $
   1904           "parse failed: " ++ show e
   1905         Right ar ->
   1906           assertEqual "secret violation in cycle" 1
   1907             (length $ filter isSecretViolation
   1908               (map vReason (arViolations ar)))
   1909 
   1910   , testCase "bl followed by tail call" $ do
   1911       let src = T.unlines
   1912             [ "_caller:"
   1913             , "  mov x22, x0"
   1914             , "  bl _helper"
   1915             , "  b _callee_info"
   1916             , "_helper:"
   1917             , "  ret"
   1918             , "_callee_info:"
   1919             , "  ldr x1, [x20, x22]"
   1920             , "  ret"
   1921             ]
   1922           cfg = TaintConfig (Map.singleton "_caller"
   1923             (ArgPolicy (Set.singleton X0)
   1924               Set.empty Set.empty
   1925               Set.empty Set.empty)) True
   1926       case auditInterProcWithConfig rt cfg "test" src of
   1927         Left e -> assertFailure $
   1928           "parse failed: " ++ show e
   1929         Right ar ->
   1930           assertEqual "secret violation" 1
   1931             (length $ filter isSecretViolation
   1932               (map vReason (arViolations ar)))
   1933 
   1934   , testCase "tail call preserves x0 argument taint" $ do
   1935       let src = T.unlines
   1936             [ "_caller:"
   1937             , "  b _callee_info"
   1938             , "_callee_info:"
   1939             , "  ldr x1, [x20, x0]"
   1940             , "  ret"
   1941             ]
   1942           cfg = TaintConfig (Map.singleton "_caller"
   1943             (ArgPolicy (Set.singleton X0)
   1944               Set.empty Set.empty
   1945               Set.empty Set.empty)) True
   1946       case auditInterProcWithConfig rt cfg "test" src of
   1947         Left e -> assertFailure $
   1948           "parse failed: " ++ show e
   1949         Right ar ->
   1950           assertEqual "x0 secret preserved" 1
   1951             (length $ filter isSecretViolation
   1952               (map vReason (arViolations ar)))
   1953 
   1954   , testCase "external bl preserves callee-saved taint" $ do
   1955       let src = T.unlines
   1956             [ "_foo:"
   1957             , "  mov x23, x0"
   1958             , "  bl _external_func"
   1959             , "  ldr x1, [x20, x23]"
   1960             , "  ret"
   1961             ]
   1962           cfg = TaintConfig (Map.singleton "_foo"
   1963             (ArgPolicy (Set.singleton X0)
   1964               Set.empty Set.empty
   1965               Set.empty Set.empty)) True
   1966       case auditInterProcWithConfig rt cfg "test" src of
   1967         Left e -> assertFailure $
   1968           "parse failed: " ++ show e
   1969         Right ar ->
   1970           assertEqual
   1971             "x23 secret preserved across external call"
   1972             1
   1973             (length $ filter isSecretViolation
   1974               (map vReason (arViolations ar)))
   1975 
   1976   ]