auditor

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

commit 2f45e0bc6bda9cab4322ee7355359d6d73992d3d
parent ef6f27ce90789a1febf656d063fbfc6d7a7d53ca
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 11 Feb 2026 18:50:43 +0400

feat: add benchmark suites for parser, CFG, and taint analysis (IMPL9)

- Add NFData instances for core types (Reg, Shift, Extend, Operand,
  AddrMode, Instr, Line, BasicBlock, CFG, AuditResult, etc.) to enable
  full evaluation in benchmarks
- Add deepseq dependency to library
- Create bench/Main.hs with criterion benchmarks for parse, cfg, and
  taint (intra/inter-proc) using CurveNCG.s and secp256k1NCG.s fixtures
- Create bench/Weight.hs with weigh benchmarks for allocation tracking
- Add auditor-bench and auditor-weigh benchmark stanzas to cabal file

Run with: cabal bench auditor-bench / cabal bench auditor-weigh

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

Diffstat:
Abench/Main.hs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abench/Weight.hs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Audit/AArch64/CFG.hs | 8++++++--
Mlib/Audit/AArch64/Check.hs | 8++++++--
Mlib/Audit/AArch64/Types.hs | 24+++++++++++++-----------
Mppad-auditor.cabal | 29+++++++++++++++++++++++++++++
6 files changed, 185 insertions(+), 15 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- Module: Main +-- Copyright: (c) 2025 Jared Tobin +-- License: MIT +-- Maintainer: jared@ppad.tech +-- +-- Criterion benchmarks for parser, CFG, and taint analysis. + +module Main where + +import Audit.AArch64.CFG (buildCFG) +import Audit.AArch64.Check (checkCFG, checkCFGInterProc) +import Audit.AArch64.Parser (parseAsm) +import Criterion.Main +import qualified Data.ByteString as BS +import Data.Text (Text) +import Data.Text.Encoding (decodeUtf8) + +main :: IO () +main = do + -- Load fixtures + smallSrc <- loadFixture "etc/CurveNCG.s" + largeSrc <- loadFixture "etc/secp256k1NCG.s" + + -- Pre-parse for CFG and analysis benchmarks + smallLines <- case parseAsm smallSrc of + Right lns -> pure lns + Left e -> error $ "Failed to parse CurveNCG.s: " ++ show e + largeLines <- case parseAsm largeSrc of + Right lns -> pure lns + Left e -> error $ "Failed to parse secp256k1NCG.s: " ++ show e + let smallCFG = buildCFG smallLines + largeCFG = buildCFG largeLines + + defaultMain + [ bgroup "parse" + [ bench "small (CurveNCG)" $ nf parseAsm smallSrc + , bench "large (secp256k1NCG)" $ nf parseAsm largeSrc + ] + , bgroup "cfg" + [ bench "small (CurveNCG)" $ nf buildCFG smallLines + , bench "large (secp256k1NCG)" $ nf buildCFG largeLines + ] + , bgroup "taint" + [ bench "intra-small" $ nf (checkCFG "bench") smallCFG + , bench "intra-large" $ nf (checkCFG "bench") largeCFG + , bench "inter-small" $ nf (checkCFGInterProc "bench") smallCFG + , bench "inter-large" $ nf (checkCFGInterProc "bench") largeCFG + ] + ] + +loadFixture :: FilePath -> IO Text +loadFixture path = decodeUtf8 <$> BS.readFile path diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- Module: Main +-- Copyright: (c) 2025 Jared Tobin +-- License: MIT +-- Maintainer: jared@ppad.tech +-- +-- Weigh benchmarks for allocation tracking. + +module Main where + +import Audit.AArch64.CFG (buildCFG, CFG) +import Audit.AArch64.Check (checkCFG, checkCFGInterProc) +import Audit.AArch64.Parser (parseAsm) +import Audit.AArch64.Types (Line) +import qualified Data.ByteString as BS +import Data.Text (Text) +import Data.Text.Encoding (decodeUtf8) +import System.IO.Unsafe (unsafePerformIO) +import Weigh + +main :: IO () +main = mainWith $ do + setColumns [Case, Allocated, GCs] + + -- Parse benchmarks + wgroup "parse" $ do + func "small (CurveNCG)" parseAsm smallSrc + func "large (secp256k1NCG)" parseAsm largeSrc + + -- CFG benchmarks + wgroup "cfg" $ do + func "small (CurveNCG)" buildCFG smallLines + func "large (secp256k1NCG)" buildCFG largeLines + + -- Taint benchmarks + wgroup "taint-intra" $ do + func "small (CurveNCG)" (checkCFG "bench") smallCFG + func "large (secp256k1NCG)" (checkCFG "bench") largeCFG + + wgroup "taint-inter" $ do + func "small (CurveNCG)" (checkCFGInterProc "bench") smallCFG + func "large (secp256k1NCG)" (checkCFGInterProc "bench") largeCFG + +-- Fixtures loaded at top-level (evaluated once) +{-# NOINLINE smallSrc #-} +smallSrc :: Text +smallSrc = unsafePerformIO $ loadFixture "etc/CurveNCG.s" + +{-# NOINLINE largeSrc #-} +largeSrc :: Text +largeSrc = unsafePerformIO $ loadFixture "etc/secp256k1NCG.s" + +{-# NOINLINE smallLines #-} +smallLines :: [Line] +smallLines = case parseAsm smallSrc of + Right lns -> lns + Left _ -> error "failed to parse small fixture" + +{-# NOINLINE largeLines #-} +largeLines :: [Line] +largeLines = case parseAsm largeSrc of + Right lns -> lns + Left _ -> error "failed to parse large fixture" + +{-# NOINLINE smallCFG #-} +smallCFG :: CFG +smallCFG = buildCFG smallLines + +{-# NOINLINE largeCFG #-} +largeCFG :: CFG +largeCFG = buildCFG largeLines + +loadFixture :: FilePath -> IO Text +loadFixture path = decodeUtf8 <$> BS.readFile path diff --git a/lib/Audit/AArch64/CFG.hs b/lib/Audit/AArch64/CFG.hs @@ -1,4 +1,6 @@ {-# OPTIONS_HADDOCK prune #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -- | @@ -27,26 +29,28 @@ module Audit.AArch64.CFG ( ) where import Audit.AArch64.Types +import Control.DeepSeq (NFData) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Set (Set) import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T +import GHC.Generics (Generic) -- | A basic block: a sequence of instructions with a single entry point. data BasicBlock = BasicBlock { bbLabel :: !(Maybe Text) -- ^ Optional label at block start , bbLines :: ![Line] -- ^ Instructions in the block , bbSuccs :: ![Text] -- ^ Successor block labels - } deriving (Eq, Show) + } deriving (Eq, Show, Generic, NFData) -- | Control flow graph. data CFG = CFG { cfgBlocks :: ![BasicBlock] -- ^ All basic blocks , cfgLabelMap :: !(Map Text Int) -- ^ Label -> block index , cfgEntry :: !Int -- ^ Entry block index - } deriving (Eq, Show) + } deriving (Eq, Show, Generic, NFData) -- | Get all labels defined in the CFG. blockLabels :: CFG -> Set Text diff --git a/lib/Audit/AArch64/Check.hs b/lib/Audit/AArch64/Check.hs @@ -1,4 +1,6 @@ {-# OPTIONS_HADDOCK prune #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -- | @@ -25,17 +27,19 @@ module Audit.AArch64.Check ( import Audit.AArch64.CFG import Audit.AArch64.Taint import Audit.AArch64.Types +import Control.DeepSeq (NFData) import qualified Data.IntMap.Strict as IM import qualified Data.Map.Strict as Map -import Data.Text (Text) import Data.Maybe (fromMaybe) +import Data.Text (Text) +import GHC.Generics (Generic) -- | Result of auditing assembly. data AuditResult = AuditResult { arViolations :: ![Violation] , arLinesChecked :: !Int , arMemoryAccesses :: !Int - } deriving (Eq, Show) + } deriving (Eq, Show, Generic, NFData) instance Semigroup AuditResult where a <> b = AuditResult diff --git a/lib/Audit/AArch64/Types.hs b/lib/Audit/AArch64/Types.hs @@ -1,4 +1,5 @@ {-# OPTIONS_HADDOCK prune #-} +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} @@ -44,6 +45,7 @@ module Audit.AArch64.Types ( , emptyArgPolicy ) where +import Control.DeepSeq (NFData) import Data.Aeson (ToJSON(..), FromJSON(..), (.=), (.:?), object, withObject) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map @@ -84,7 +86,7 @@ data Reg | Q8 | Q9 | Q10 | Q11 | Q12 | Q13 | Q14 | Q15 | Q16 | Q17 | Q18 | Q19 | Q20 | Q21 | Q22 | Q23 | Q24 | Q25 | Q26 | Q27 | Q28 | Q29 | Q30 | Q31 - deriving (Eq, Ord, Show, Generic) + deriving (Eq, Ord, Show, Generic, NFData) instance ToJSON Reg where toJSON = toJSON . regName @@ -147,7 +149,7 @@ data Shift = LSL !Int -- ^ Logical shift left | LSR !Int -- ^ Logical shift right | ASR !Int -- ^ Arithmetic shift right - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON Shift @@ -157,7 +159,7 @@ data Extend | SXTW -- ^ Signed extend word | UXTX -- ^ Unsigned extend doubleword | SXTX -- ^ Signed extend doubleword - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON Extend @@ -169,7 +171,7 @@ data Operand | OpExtendedReg !Reg !Extend -- ^ Extended register | OpLabel !Text -- ^ Label reference | OpAddr !AddrMode -- ^ Memory address - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON Operand @@ -183,7 +185,7 @@ data AddrMode | PreIndex !Reg !Integer -- ^ [xN, #imm]! | PostIndex !Reg !Integer -- ^ [xN], #imm | Literal !Text -- ^ =label or PC-relative - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON AddrMode @@ -271,7 +273,7 @@ data Instr | Svc !Integer -- Other (unparsed but recorded) | Other !Text ![Text] - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON Instr @@ -280,7 +282,7 @@ data Line = Line { lineNum :: !Int , lineLabel :: !(Maybe Text) , lineInstr :: !(Maybe Instr) - } deriving (Eq, Show, Generic) + } deriving (Eq, Show, Generic, NFData) instance ToJSON Line @@ -289,7 +291,7 @@ data Taint = Public -- ^ Known to be public (derived from stack/heap pointers) | Secret -- ^ Known or assumed to be secret | Unknown -- ^ Not yet determined - deriving (Eq, Ord, Show, Generic) + deriving (Eq, Ord, Show, Generic, NFData) instance ToJSON Taint @@ -307,7 +309,7 @@ joinTaint _ _ = Unknown -- Public+Unknown or Unknown+Unknown data Provenance = ProvPublic -- ^ Derived from public root or constant | ProvUnknown -- ^ Unknown origin (e.g., loaded from memory) - deriving (Eq, Ord, Show, Generic) + deriving (Eq, Ord, Show, Generic, NFData) instance ToJSON Provenance @@ -323,7 +325,7 @@ data ViolationReason | UnknownBase !Reg -- ^ Base register taint unknown | UnknownIndex !Reg -- ^ Index register taint unknown | NonConstOffset -- ^ Non-constant offset without masking - deriving (Eq, Show, Generic) + deriving (Eq, Show, Generic, NFData) instance ToJSON ViolationReason where toJSON (SecretBase r) = @@ -343,7 +345,7 @@ data Violation = Violation , vLine :: !Int , vInstr :: !Instr , vReason :: !ViolationReason - } deriving (Eq, Show, Generic) + } deriving (Eq, Show, Generic, NFData) instance ToJSON Violation where toJSON v = object diff --git a/ppad-auditor.cabal b/ppad-auditor.cabal @@ -34,6 +34,7 @@ library base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 , containers >= 0.6 && < 0.8 + , deepseq >= 1.4 && < 1.6 , megaparsec >= 9.0 && < 10 , text >= 1.2 && < 2.2 , aeson >= 2.0 && < 2.3 @@ -69,3 +70,31 @@ test-suite auditor-tests , tasty , tasty-hunit , text + +benchmark auditor-bench + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: bench + main-is: Main.hs + ghc-options: + -rtsopts -Wall -O2 + build-depends: + base + , bytestring + , criterion >= 1.5 && < 1.7 + , ppad-auditor + , text + +benchmark auditor-weigh + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: bench + main-is: Weight.hs + ghc-options: + -rtsopts -Wall -O2 + build-depends: + base + , bytestring + , ppad-auditor + , text + , weigh >= 0.0.16 && < 0.1