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