auditor

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

commit 4921ff38f9e774b90501559ba42ed121ee15133f
parent d6cebeeb19325432dc131a5cfede78dc89a2ae43
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 10 Feb 2026 12:26:49 +0400

fix: parser and taint analysis bugs

Parser fixes:
- Add try for register parsers to allow backtracking (sp vs s0)
- Fix pre-index addressing mode (! after ] not inside)
- Restructure line parsing to avoid infinite loop on empty lines
- Use hspace instead of space1 to preserve newlines

Taint analysis fix:
- Correct join lattice: Public+Unknown -> Unknown (not Public)
- This ensures secret-derived values are properly flagged

Tests pass. Fixtures validate correctly:
- good.s: 0 violations (10 memory accesses)
- bad.s: 4 violations detected

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

Diffstat:
Aflake.lock | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Audit/AArch64.hs | 4+++-
Mlib/Audit/AArch64/Check.hs | 1-
Mlib/Audit/AArch64/Parser.hs | 90++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mlib/Audit/AArch64/Taint.hs | 2+-
Mlib/Audit/AArch64/Types.hs | 12++++++------
Mppad-auditor.cabal | 3++-
Mtest/Main.hs | 2+-
Mtest/fixtures/bad.s | 16++++++++--------
9 files changed, 154 insertions(+), 64 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -0,0 +1,88 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766840161, + "narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "ppad-nixpkgs": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1766932084, + "narHash": "sha256-GvVsbTfW+B7IQ9K/QP2xcXJAm1lhBin1jYZWNjOzT+o=", + "ref": "master", + "rev": "353e61763b959b960a55321a85423501e3e9ed7a", + "revCount": 2, + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + } + }, + "root": { + "inputs": { + "flake-utils": [ + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": "ppad-nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/Audit/AArch64.hs b/lib/Audit/AArch64.hs @@ -36,6 +36,8 @@ module Audit.AArch64 ( , AuditResult(..) , Violation(..) , ViolationReason(..) + , Reg + , regName -- * Re-exports , ParseError @@ -44,7 +46,7 @@ module Audit.AArch64 ( import Audit.AArch64.CFG import Audit.AArch64.Check import Audit.AArch64.Parser -import Audit.AArch64.Types +import Audit.AArch64.Types (Reg, Violation(..), ViolationReason(..), regName) import qualified Data.ByteString as BS import Data.Text (Text) import qualified Data.Text as T diff --git a/lib/Audit/AArch64/Check.hs b/lib/Audit/AArch64/Check.hs @@ -23,7 +23,6 @@ import Audit.AArch64.CFG import Audit.AArch64.Taint import Audit.AArch64.Types import Data.Text (Text) -import qualified Data.Text as T -- | Result of auditing assembly. data AuditResult = AuditResult diff --git a/lib/Audit/AArch64/Parser.hs b/lib/Audit/AArch64/Parser.hs @@ -16,11 +16,11 @@ module Audit.AArch64.Parser ( import Audit.AArch64.Types import Control.Monad (void) -import Data.Char (isAlphaNum, isDigit, isSpace) +import Data.Char (isAlphaNum) import Data.Text (Text) import qualified Data.Text as T import Data.Void (Void) -import Text.Megaparsec +import Text.Megaparsec hiding (ParseError) import Text.Megaparsec.Char import qualified Text.Megaparsec.Char.Lexer as L @@ -31,10 +31,10 @@ type ParseError = ParseErrorBundle Text Void parseAsm :: Text -> Either ParseError [Line] parseAsm = parse (many pLine <* eof) "<asm>" --- Space consumer (skips whitespace and comments). +-- Space consumer (skips horizontal whitespace and comments, not newlines). sc :: Parser () sc = L.space - space1 + hspace1 (L.skipLineComment "//") (L.skipBlockComment "/*" "*/") @@ -42,26 +42,23 @@ sc = L.space lexeme :: Parser a -> Parser a lexeme = L.lexeme sc --- Symbol (alphanumeric + underscore + dot + dollar). -symbol :: Text -> Parser Text -symbol = L.symbol sc - -- Parse a single line. pLine :: Parser Line pLine = do pos <- getSourcePos let ln = unPos (sourceLine pos) - skipMany (satisfy isSpace) + hspace -- Skip leading horizontal whitespace only choice [ pDirectiveLine ln - , pLabelOrInstrLine ln + , pLabelLine ln + , pInstrLine ln , pEmptyLine ln ] pEmptyLine :: Int -> Parser Line pEmptyLine ln = do void (optional pComment) - void (optional eol) + void eol -- Must consume actual newline pure (Line ln Nothing Nothing) pComment :: Parser () @@ -76,15 +73,23 @@ pDirectiveLine ln = do void (optional eol) pure (Line ln Nothing Nothing) -pLabelOrInstrLine :: Int -> Parser Line -pLabelOrInstrLine ln = do - mLabel <- optional (try pLabel) - skipMany (satisfy isSpace) +pLabelLine :: Int -> Parser Line +pLabelLine ln = do + lbl <- try pLabel + hspace mInstr <- optional pInstr - skipMany (satisfy isSpace) + hspace + void (optional pComment) + void (optional eol) + pure (Line ln (Just lbl) mInstr) + +pInstrLine :: Int -> Parser Line +pInstrLine ln = do + instr <- pInstr + hspace void (optional pComment) void (optional eol) - pure (Line ln mLabel mInstr) + pure (Line ln Nothing (Just instr)) pLabel :: Parser Text pLabel = lexeme $ do @@ -242,7 +247,7 @@ pComma = void (lexeme (char ',')) pReg :: Parser Reg pReg = lexeme $ choice - [ pXReg, pWReg, pDReg, pSReg, pQReg, pSpecialReg ] + [ try pXReg, try pWReg, try pDReg, try pSReg, try pQReg, pSpecialReg ] pXReg :: Parser Reg pXReg = do @@ -377,10 +382,10 @@ pShiftAmt = do pSymbolRef :: Parser Text pSymbolRef = lexeme $ do -- Handle =literal, @PAGE, @PAGEOFF suffixes, etc. - prefix <- optional (char '=') + hasPrefix <- option False (True <$ char '=') name <- pIdentifier suffix <- optional pSymbolSuffix - let base = maybe name (T.cons '=') prefix <> name + let base = if hasPrefix then T.cons '=' name else name pure (maybe base (base <>) suffix) pSymbolSuffix :: Parser Text @@ -407,39 +412,34 @@ pBracketAddr = lexeme $ do sc base <- pReg sc - mode <- optional (pComma *> pAddrModeRest base) + innerMode <- optional (pComma *> pAddrModeInner) sc void (char ']') - -- Check for pre/post index + -- Check for pre-index ! or post-index offset + preIdx <- option False (True <$ char '!') postIdx <- optional pPostIndex - case (mode, postIdx) of - (Nothing, Nothing) -> pure (BaseImm base 0) - (Nothing, Just imm) -> pure (PostIndex base imm) - (Just (Left imm, True), Nothing) -> pure (PreIndex base imm) - (Just (Left imm, False), Nothing) -> pure (BaseImm base imm) - (Just (Right (r, Nothing), _), Nothing) -> pure (BaseReg base r) - (Just (Right (r, Just (Left sh)), _), Nothing) -> pure (BaseRegShift base r sh) - (Just (Right (r, Just (Right ext)), _), Nothing) -> pure (BaseRegExtend base r ext) + case (innerMode, preIdx, postIdx) of + (Nothing, False, Nothing) -> pure (BaseImm base 0) + (Nothing, False, Just imm) -> pure (PostIndex base imm) + (Just (Left imm), True, Nothing) -> pure (PreIndex base imm) + (Just (Left imm), False, Nothing) -> pure (BaseImm base imm) + (Just (Right (r, Nothing)), False, Nothing) -> pure (BaseReg base r) + (Just (Right (r, Just (Left sh))), False, Nothing) -> pure (BaseRegShift base r sh) + (Just (Right (r, Just (Right ext))), False, Nothing) -> pure (BaseRegExtend base r ext) _ -> fail "invalid addressing mode" -pAddrModeRest :: Reg -> Parser (Either Integer (Reg, Maybe (Either Shift Extend)), Bool) -pAddrModeRest _ = do - choice - [ try pImmOffset - , pRegOffset - ] - -pImmOffset :: Parser (Either Integer (Reg, Maybe (Either Shift Extend)), Bool) -pImmOffset = do - imm <- pImm - preIdx <- optional (char '!') - pure (Left imm, maybe False (const True) preIdx) +-- Parse the inner part of [base, ...] after the comma +pAddrModeInner :: Parser (Either Integer (Reg, Maybe (Either Shift Extend))) +pAddrModeInner = choice + [ Left <$> pImm + , Right <$> pRegWithModifier + ] -pRegOffset :: Parser (Either Integer (Reg, Maybe (Either Shift Extend)), Bool) -pRegOffset = do +pRegWithModifier :: Parser (Reg, Maybe (Either Shift Extend)) +pRegWithModifier = do r <- pReg modifier <- optional (pComma *> pShiftOrExtend) - pure (Right (r, modifier), False) + pure (r, modifier) pShiftOrExtend :: Parser (Either Shift Extend) pShiftOrExtend = choice diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -80,7 +80,7 @@ transfer instr st = case instr of -- Move: destination gets source taint Mov dst op -> setTaint dst (operandTaint op st) st Movz dst _ _ -> setTaint dst Public st -- Immediate is public - Movk dst _ _ -> st -- Keeps existing value, modifies bits + Movk _ _ _ -> st -- Keeps existing value, modifies bits Movn dst _ _ -> setTaint dst Public st -- Immediate is public -- Arithmetic: result is join of operand taints diff --git a/lib/Audit/AArch64/Types.hs b/lib/Audit/AArch64/Types.hs @@ -225,13 +225,13 @@ data Taint instance ToJSON Taint -- | Join operation for taint lattice (least upper bound). --- Unknown < Public < Secret +-- For CT safety: Public only if both are Public. +-- Order: Public < Unknown < Secret joinTaint :: Taint -> Taint -> Taint -joinTaint Secret _ = Secret -joinTaint _ Secret = Secret -joinTaint Public _ = Public -joinTaint _ Public = Public -joinTaint Unknown Unknown = Unknown +joinTaint Public Public = Public +joinTaint Secret _ = Secret +joinTaint _ Secret = Secret +joinTaint _ _ = Unknown -- Public+Unknown or Unknown+Unknown -- | Reason for a violation. data ViolationReason diff --git a/ppad-auditor.cabal b/ppad-auditor.cabal @@ -45,7 +45,8 @@ executable auditor ghc-options: -Wall -O2 -threaded build-depends: - base + aeson + , base , bytestring , optparse-applicative >= 0.16 && < 0.19 , ppad-auditor diff --git a/test/Main.hs b/test/Main.hs @@ -97,7 +97,7 @@ taintTests = testGroup "Taint" [ -- X0 = X20 (public) + X1 (unknown) -> unknown l = Line 1 Nothing (Just (Add X0 X20 (OpReg X1))) st' = analyzeLine l st - assertEqual "result is public" Public (getTaint X0 st') + assertEqual "result is unknown" Unknown (getTaint X0 st') , testCase "load makes unknown" $ do let st = initTaintState diff --git a/test/fixtures/bad.s b/test/fixtures/bad.s @@ -11,20 +11,20 @@ _unsafe_function: mov x29, sp ; Load a secret value - ldr x0, [x20] ; x0 = secret from memory + ldr x0, [x20] - ; BAD: Use secret as base address (cache-timing leak) - ldr x1, [x0] ; violation: unknown base x0 + ; BAD: Use secret as base address + ldr x1, [x0] - ; BAD: Use secret as index (cache-timing leak) - ldr x2, [x21, x0] ; violation: unknown index x0 + ; BAD: Use secret as index + ldr x2, [x21, x0] ; BAD: Secret-derived address via arithmetic - add x3, x0, #256 ; x3 = secret + offset - ldr x4, [x3] ; violation: unknown base x3 + add x3, x0, #256 + ldr x4, [x3] ; BAD: Secret used in shifted index - ldr x5, [x21, x0, lsl #3] ; violation: unknown index x0 + ldr x5, [x21, x0, lsl #3] ; Frame teardown ldp x29, x30, [sp], #16