auditor

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

commit 9bb1c0547562a533bf8e7259310cda9115c33fdf
parent c71f0b539b1e57c0c618fceafd2a506dcbd86ec9
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 27 Feb 2026 18:11:06 +0400

feat: preserve callee-saved taint across external calls/jumps

Per AArch64 ABI, registers X19-X28 are callee-saved. External functions
must preserve them, so taint in these registers survives calls.

Changes:
- Use invalidateCallerSaved for external tail calls and indirect jumps
  (br), preserving X19-X28 taint
- Remove unused invalidateStgArgRegs function
- Add tests for callee-saved preservation across external bl and blr

Note: mul_wnaf still shows 0 secret violations because the secret is in
a boxed Integer closure. The secret_pointee config points to the closure,
but the first load [ptr] returns the info pointer, not the secret value.
Type-aware analysis would be needed to handle boxed types properly.

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

Diffstat:
Mlib/Audit/AArch64/Taint.hs | 20++++----------------
Mtest/Main.hs | 33++++++++++++++++++++++++++++-----
2 files changed, 32 insertions(+), 21 deletions(-)

diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -1010,18 +1010,6 @@ invalidateCallerSaved st = st , X16, X17 ] --- | Invalidate STG argument registers for unknown tail call targets. --- STG calling convention: X22=R1 (closure), X23-X27=R2-R6 (args). --- Used when we can't determine the tail call target (br xN or unknown symbol). -invalidateStgArgRegs :: TaintState -> TaintState -invalidateStgArgRegs st = st - { tsRegs = foldr (\r -> Map.insert r Unknown) (tsRegs st) stgArgRegs - , tsProv = foldr (\r -> Map.insert r ProvUnknown) (tsProv st) stgArgRegs - , tsKind = foldr (\r -> Map.insert r KindUnknown) (tsKind st) stgArgRegs - } - where - stgArgRegs = [X22, X23, X24, X25, X26, X27] - -- | Join two taint states (element-wise join). -- For registers in both, take the join. For registers in only one, keep. -- Stack slots (SP and STG), provenance, kinds, and heap bucket are also joined. @@ -1225,7 +1213,7 @@ analyzeBlockWithSummaries bb st0 summaries = foldl' go st0 (bbLines bb) -- | Transfer with summary application for calls and tail calls. -- For in-file tail calls, state flows via CFG edges - no summary application. --- For external tail calls, we conservatively invalidate STG arg registers. +-- For external calls/jumps, we preserve callee-saved registers per ABI. transferWithSummary :: Instr -> TaintState -> Map Text FuncSummary -> TaintState transferWithSummary instr st summaries = case instr of Bl target -> @@ -1238,9 +1226,9 @@ transferWithSummary instr st summaries = case instr of | isFunctionLabel target -> case Map.lookup target summaries of Just _ -> st -- In-file: CFG edge propagates state - Nothing -> invalidateStgArgRegs st -- External: conservative - -- Indirect jumps: conservative treatment - Br _ -> invalidateStgArgRegs st + Nothing -> invalidateCallerSaved st -- External: ABI preserves X19-X28 + -- Indirect jumps (STG closure evaluation): ABI preserves callee-saved + Br _ -> invalidateCallerSaved st _ -> transfer instr st -- | Run inter-procedural fixpoint analysis. diff --git a/test/Main.hs b/test/Main.hs @@ -1564,20 +1564,25 @@ tailCallTests = testGroup "TailCall" [ assertEqual "secret violation" 1 (length $ filter isSecretViolation (map vReason (arViolations ar))) - -- Indirect jump handling - , testCase "indirect jump invalidates STG regs" $ do + -- Indirect call preserves callee-saved (ABI requirement) + , testCase "indirect call preserves callee-saved taint" $ do let src = T.unlines [ "_foo:" - , " mov x22, x0" + , " mov x23, x0" , " ldr x8, [x19]" - , " br x8" + , " blr x8" + , " ldr x1, [x20, x23]" + , " ret" ] cfg = TaintConfig (Map.singleton "_foo" (ArgPolicy (Set.singleton X0) Set.empty Set.empty Set.empty Set.empty)) True case auditInterProcWithConfig cfg "test" src of Left e -> assertFailure $ "parse failed: " ++ show e - Right ar -> assertEqual "no violations" 0 (length (arViolations ar)) + Right ar -> + -- x23 is callee-saved, so taint preserved across blr + assertEqual "x23 secret preserved" 1 + (length $ filter isSecretViolation (map vReason (arViolations ar))) -- Tail call to unknown function , testCase "tail call to unknown function is conservative" $ do @@ -1674,4 +1679,22 @@ tailCallTests = testGroup "TailCall" [ assertEqual "x0 secret preserved" 1 (length $ filter isSecretViolation (map vReason (arViolations ar))) + -- Callee-saved registers preserved across external bl calls + , testCase "external bl preserves callee-saved taint" $ do + let src = T.unlines + [ "_foo:" + , " mov x23, x0" + , " bl _external_func" + , " ldr x1, [x20, x23]" + , " ret" + ] + cfg = TaintConfig (Map.singleton "_foo" + (ArgPolicy (Set.singleton X0) Set.empty Set.empty + Set.empty Set.empty)) True + case auditInterProcWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> + assertEqual "x23 secret preserved across external call" 1 + (length $ filter isSecretViolation (map vReason (arViolations ar))) + ]