auditor

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

commit 11d6acaa0021242ebee68efaa5b7204d086f9b7e
parent b8a7cd8dfb9b5cc032fd3ebb6bd1128f6125d51f
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 10 Feb 2026 13:26:20 +0400

test: add call boundary tests for IMPL3

Adds tests verifying intra-procedural call semantics:
- bl does not propagate taint to callee blocks
- caller-saved registers (x0-x17) invalidated after calls
- callee-saved registers (x19+) preserved across calls

Clarifies README that callees in the same file are analyzed
independently.

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

Diffstat:
MREADME.md | 6++++--
Mtest/Main.hs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 60 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md @@ -16,7 +16,8 @@ timing side-channels. It flags loads and stores where: The tool assumes GHC's calling convention, where certain registers (X19-X22, SP) are treated as public (stack/heap pointers), and tracks -how taint propagates through arithmetic and data movement instructions. +how taint propagates through arithmetic and data movement instructions +across basic blocks. ## Usage @@ -42,7 +43,8 @@ Use `-q` for quiet mode (violations only) or `-j` for JSON output. This is an early-stage tool with known limitations: - **No inter-procedural analysis**: Function calls reset taint state - for caller-saved registers; callee behaviour is not analyzed + for caller-saved registers; callees are analyzed independently even + when defined in the same file - **Conservative**: Unknown taint is treated as potentially secret These limitations mean the tool may over-report violations. Manual diff --git a/test/Main.hs b/test/Main.hs @@ -211,4 +211,60 @@ auditTests = testGroup "Audit" [ UnknownBase X8 -> pure () other -> assertFailure $ "wrong reason: " ++ show other _ -> assertFailure "expected one violation" + + , testCase "call: no taint propagation to callee" $ do + -- Taint set before bl should NOT flow into the callee block + -- The callee starts fresh with public roots only + let src = T.unlines + [ "caller:" + , " adrp x8, _const@PAGE" -- x8 = public + , " bl callee" + , " ret" + , "callee:" + , " ldr x0, [x8]" -- x8 unknown here (fresh block) + , " ret" + ] + case audit "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + -- Should have 1 violation: x8 unknown in callee + assertEqual "one violation" 1 (length (arViolations ar)) + case arViolations ar of + [v] -> do + assertEqual "violation in callee" "callee" (vSymbol v) + case vReason v of + UnknownBase X8 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + + , testCase "call: caller-saved invalidation" $ do + -- x0 is public before call, unknown after (caller-saved) + let src = T.unlines + [ "foo:" + , " adrp x0, _const@PAGE" -- x0 = public + , " bl bar" + , " ldr x1, [x0]" -- x0 unknown after call (caller-saved) + , " ret" + ] + case audit "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] -> case vReason v of + UnknownBase X0 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + + , testCase "call: callee-saved preserved" $ do + -- x19 is callee-saved, stays public across call + let src = T.unlines + [ "foo:" + , " bl bar" + , " ldr x0, [x19]" -- x19 public (callee-saved) + , " ret" + ] + case audit "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> assertEqual "no violations" 0 (length (arViolations ar)) ]