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:
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))
]