auditor

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

commit 6dd91fa69d98bf078cf11ec505c236699993bd5d
parent 191611b72312b0990f0e4488c2c61ce47bcd1c3a
Author: Jared Tobin <jared@jtobin.io>
Date:   Thu, 26 Feb 2026 17:22:02 +0400

fix: improve secret_pointee handling and add tests

Address reviewer feedback:

- Handle register-offset addressing modes (BaseReg, BaseRegShift,
  BaseRegExtend) for secret_pointee, not just BaseImm
- Require KindPtr and Public taint for isSecretDataPointer, so
  non-pointer arithmetic invalidates the secret pointee property
- Add comprehensive tests:
  - Load through secret_pointee with immediate offset
  - Load through secret_pointee with register offset
  - secret takes precedence over secret_pointee
  - Non-pointer arithmetic (mul) invalidates secret_pointee
  - Pointer arithmetic (add #imm) preserves secret_pointee

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

Diffstat:
Mlib/Audit/AArch64/Taint.hs | 17++++++++++++++++-
Mtest/Main.hs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 111 insertions(+), 1 deletion(-)

diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -618,8 +618,13 @@ isPublicPointer r st = -- | Check if a register is a pointer to secret data. -- Loads through such pointers produce Secret values. +-- Requires the register to be a Public pointer with ProvSecretData provenance. +-- If arithmetic mutates it to a scalar, it's no longer a valid secret pointer. isSecretDataPointer :: Reg -> TaintState -> Bool -isSecretDataPointer r st = getProvenance r st == ProvSecretData +isSecretDataPointer r st = + getTaint r st == Public && + getKind r st == KindPtr && + getProvenance r st == ProvSecretData -- | Set a heap slot for a (base, offset) pair. setHeapSlot :: Reg -> Int -> Taint -> Provenance -> RegKind -> TaintState @@ -845,6 +850,16 @@ loadFromStack dst addr st = case addr of BaseImm base imm | isPublicPointer base st -> setTaintLoadHeapSlot dst base (fromInteger imm) st + -- Secret data pointer with register offset: [base, xM] variants + BaseReg base _idx + | isSecretDataPointer base st -> + setTaintProvKind dst Secret ProvUnknown KindScalar st + BaseRegShift base _idx _shift + | isSecretDataPointer base st -> + setTaintProvKind dst Secret ProvUnknown KindScalar st + BaseRegExtend base _idx _ext + | isSecretDataPointer base st -> + setTaintProvKind dst Secret ProvUnknown KindScalar st -- Other loads: read from coarse heap bucket _ -> setTaintLoadHeap dst st diff --git a/test/Main.hs b/test/Main.hs @@ -15,6 +15,15 @@ import qualified Data.Text as T import Test.Tasty import Test.Tasty.HUnit +-- | Check if a violation reason is secret (not unknown). +isSecretViolation :: ViolationReason -> Bool +isSecretViolation r = case r of + SecretBase _ -> True + SecretIndex _ -> True + UnknownBase _ -> False + UnknownIndex _ -> False + NonConstOffset -> True + main :: IO () main = defaultMain $ testGroup "ppad-auditor" [ parserTests @@ -898,6 +907,92 @@ taintConfigTests = testGroup "TaintConfig" [ st = seedArgs policy initTaintState assertEqual "X0 is secret" Secret (getTaint X0 st) + , testCase "secret_pointee load with imm offset produces Secret" $ do + -- x0 is pointer to secret data; ldr through it produces Secret + let src = T.unlines + [ "_foo:" + , " ldr x1, [x0, #8]" -- Load through secret_pointee + , " ldr x2, [x21, x1]" -- Use as index -> violation + , " ret" + ] + cfg = TaintConfig (Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton X0) + Set.empty Set.empty)) True + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + assertEqual "one violation" 1 (length (arViolations ar)) + case arViolations ar of + [v] -> assertEqual "secret index" (SecretIndex X1) (vReason v) + _ -> assertFailure "expected exactly one violation" + + , testCase "secret_pointee load with reg offset produces Secret" $ do + -- x0 is pointer to secret data; ldr [x0, x1] produces Secret + -- x1 is unknown, so we get: unknown index x1 + secret index x2 + let src = T.unlines + [ "_foo:" + , " ldr x2, [x0, x1]" -- Load through secret_pointee with reg idx + , " ldr x3, [x21, x2]" -- Use as index -> violation + , " ret" + ] + cfg = TaintConfig (Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton X0) + Set.empty Set.empty)) True + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + -- Two violations: unknown x1 index, secret x2 index + assertEqual "two violations" 2 (length (arViolations ar)) + let reasons = map vReason (arViolations ar) + assertBool "has secret index x2" (SecretIndex X2 `elem` reasons) + + , testCase "secret takes precedence over secret_pointee" $ do + -- If both secret and secret_pointee, secret wins (scalar, not pointer) + let policy = ArgPolicy (Set.singleton X0) Set.empty (Set.singleton X0) + Set.empty Set.empty + st = seedArgs policy initTaintState + assertEqual "X0 is secret" Secret (getTaint X0 st) + -- Secret scalar is not a secret_pointee (provenance is ProvUnknown) + assertEqual "X0 provenance" ProvUnknown (getProvenance X0 st) + + , testCase "non-pointer arithmetic invalidates secret_pointee" $ do + -- mul produces KindScalar, so x0 is no longer a valid pointer + -- Loads through it go to coarse heap bucket (Unknown), not Secret + let src = T.unlines + [ "_foo:" + , " mul x0, x0, x1" -- Multiply -> KindScalar + , " ldr x2, [x0, #0]" -- Load from heap bucket (not secret) + , " ldr x3, [x21, x2]" -- Use as index -> unknown, not secret + , " ret" + ] + cfg = TaintConfig (Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton X0) + Set.empty Set.empty)) True + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> + -- No secret violations (x2 is Unknown from heap, not Secret) + assertEqual "no secret violations" 0 + (length $ filter isSecretViolation (map vReason (arViolations ar))) + + , testCase "pointer arithmetic preserves secret_pointee" $ do + -- add with immediate keeps KindPtr, so x0 is still secret_pointee + let src = T.unlines + [ "_foo:" + , " add x0, x0, #8" -- Pointer arithmetic -> KindPtr + , " ldr x1, [x0, #0]" -- Load through secret_pointee -> Secret + , " ldr x2, [x21, x1]" -- Use as index -> secret violation + , " ret" + ] + cfg = TaintConfig (Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton X0) + Set.empty Set.empty)) True + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + assertEqual "one secret violation" 1 + (length $ filter isSecretViolation (map vReason (arViolations ar))) + , testCase "secret arg causes violation on indexed access" $ do let src = T.unlines [ "_mul_wnaf:"