auditor

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

commit 584dc047e8fdfb53d28b2190d5d2f755f1967b57
parent 278bc2d33847f190c2a5b10c1d0955db70428eb5
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 11 Feb 2026 20:41:13 +0400

feat: track stack slot kinds to preserve pointer/scalar across spills (IMPL14)

Extends TaintState with tsStackKind (IntMap RegKind) so pointer vs scalar
classification survives spills and reloads.

Changes:
- Add tsStackKind field to TaintState
- Update storeToStack/storePairToStack to record source register kinds
- Update setTaintLoadStack to restore kinds from stack map
- Update clearStackMap to clear tsStackKind on SP modification
- Update joinTaintState to join stack kinds
- Add tests for pointer spill/reload and scalar spill/reload

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

Diffstat:
Mlib/Audit/AArch64/Taint.hs | 63++++++++++++++++++++++++++++++++++++++++++---------------------
Aplans/ARCH14.md | 26++++++++++++++++++++++++++
Aplans/IMPL14.md | 33+++++++++++++++++++++++++++++++++
Mtest/Main.hs | 37+++++++++++++++++++++++++++++++++++++
4 files changed, 138 insertions(+), 21 deletions(-)

diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -76,6 +76,8 @@ data TaintState = TaintState -- ^ Stack slot provenance (keyed by SP offset) , tsKind :: !(Map Reg RegKind) -- ^ Register kinds (pointer vs scalar) + , tsStackKind :: !(IntMap RegKind) + -- ^ Stack slot kinds (keyed by SP offset) } deriving (Eq, Show) -- | GHC 9.10.3 AArch64 public root registers. @@ -111,6 +113,7 @@ emptyTaintState = TaintState , tsProv = Map.empty , tsStackProv = IM.empty , tsKind = Map.empty + , tsStackKind = IM.empty } -- | Initial taint state with public roots marked. @@ -122,6 +125,7 @@ initTaintState = TaintState , tsProv = Map.fromList [(r, ProvPublic) | r <- publicRoots] , tsStackProv = IM.empty , tsKind = Map.fromList [(r, KindPtr) | r <- publicRoots] + , tsStackKind = IM.empty } -- | Seed argument registers according to policy. @@ -414,28 +418,32 @@ setTaintLoad dst st isPublicRoot r = r `elem` publicRoots -- | Set taint for a loaded value from a known stack slot. --- If we have tracked taint/provenance at this offset, use it; else Unknown. --- Loaded values are KindScalar unless they're public roots (which are Ptr). +-- If we have tracked taint/provenance/kind at this offset, use it; else Unknown. +-- Public roots always get Public/ProvPublic/KindPtr. setTaintLoadStack :: Reg -> Int -> TaintState -> TaintState setTaintLoadStack dst offset st | isPublicRoot dst = setTaintProvKind dst Public ProvPublic KindPtr st | otherwise = let taint = IM.findWithDefault Unknown offset (tsStack st) prov = IM.findWithDefault ProvUnknown offset (tsStackProv st) - in setTaintProvKind dst taint prov KindScalar st + kind = IM.findWithDefault KindUnknown offset (tsStackKind st) + in setTaintProvKind dst taint prov kind st where isPublicRoot r = r `elem` publicRoots --- | Store both taint and provenance to a stack slot. -setStackTaintProv :: Int -> Taint -> Provenance -> TaintState -> TaintState -setStackTaintProv offset t p st = st +-- | Store taint, provenance, and kind to a stack slot. +setStackTaintProvKind :: Int -> Taint -> Provenance -> RegKind -> TaintState + -> TaintState +setStackTaintProvKind offset t p k st = st { tsStack = IM.insert offset t (tsStack st) , tsStackProv = IM.insert offset p (tsStackProv st) + , tsStackKind = IM.insert offset k (tsStackKind st) } --- | Clear all stack slot taints and provenance (when SP is modified). +-- | Clear all stack slot taints, provenance, and kinds (when SP is modified). clearStackMap :: TaintState -> TaintState -clearStackMap st = st { tsStack = IM.empty, tsStackProv = IM.empty } +clearStackMap st = st + { tsStack = IM.empty, tsStackProv = IM.empty, tsStackKind = IM.empty } -- | Update register taint, provenance, and kind, clearing stack map if SP. updateWithSpCheckProvKind :: Reg -> Taint -> Provenance -> RegKind @@ -450,15 +458,24 @@ storeToStack :: Reg -> AddrMode -> TaintState -> TaintState storeToStack src addr st = case addr of BaseImm SP imm -> let off = fromInteger imm - in setStackTaintProv off (getTaint src st) (getProvenance src st) st + t = getTaint src st + p = getProvenance src st + k = getKind src st + in setStackTaintProvKind off t p k st PreIndex SP imm -> -- Store at sp+imm, then SP changes let off = fromInteger imm - st' = setStackTaintProv off (getTaint src st) (getProvenance src st) st + t = getTaint src st + p = getProvenance src st + k = getKind src st + st' = setStackTaintProvKind off t p k st in clearStackMap st' PostIndex SP _ -> -- Store at sp, then SP changes (offset 0) - let st' = setStackTaintProv 0 (getTaint src st) (getProvenance src st) st + let t = getTaint src st + p = getProvenance src st + k = getKind src st + st' = setStackTaintProvKind 0 t p k st in clearStackMap st' _ -> st -- Non-SP stores don't affect stack tracking @@ -469,22 +486,24 @@ storePairToStack :: Reg -> Reg -> AddrMode -> TaintState -> TaintState storePairToStack src1 src2 addr st = case addr of BaseImm SP imm -> let off = fromInteger imm - st' = setStackTaintProv off (getTaint src1 st) (getProvenance src1 st) - (setStackTaintProv (off + 8) (getTaint src2 st) - (getProvenance src2 st) st) - in st' + t1 = getTaint src1 st; p1 = getProvenance src1 st; k1 = getKind src1 st + t2 = getTaint src2 st; p2 = getProvenance src2 st; k2 = getKind src2 st + in setStackTaintProvKind off t1 p1 k1 + (setStackTaintProvKind (off + 8) t2 p2 k2 st) PreIndex SP imm -> -- Store at sp+imm and sp+imm+8, then SP changes let off = fromInteger imm - st' = setStackTaintProv off (getTaint src1 st) (getProvenance src1 st) - (setStackTaintProv (off + 8) (getTaint src2 st) - (getProvenance src2 st) st) + t1 = getTaint src1 st; p1 = getProvenance src1 st; k1 = getKind src1 st + t2 = getTaint src2 st; p2 = getProvenance src2 st; k2 = getKind src2 st + st' = setStackTaintProvKind off t1 p1 k1 + (setStackTaintProvKind (off + 8) t2 p2 k2 st) in clearStackMap st' PostIndex SP _ -> -- Store at sp and sp+8, then SP changes - let st' = setStackTaintProv 0 (getTaint src1 st) (getProvenance src1 st) - (setStackTaintProv 8 (getTaint src2 st) - (getProvenance src2 st) st) + let t1 = getTaint src1 st; p1 = getProvenance src1 st; k1 = getKind src1 st + t2 = getTaint src2 st; p2 = getProvenance src2 st; k2 = getKind src2 st + st' = setStackTaintProvKind 0 t1 p1 k1 + (setStackTaintProvKind 8 t2 p2 k2 st) in clearStackMap st' _ -> st @@ -623,6 +642,7 @@ joinTaintState a b = TaintState , tsProv = Map.unionWith joinProvenance (tsProv a) (tsProv b) , tsStackProv = IM.unionWith joinProvenance (tsStackProv a) (tsStackProv b) , tsKind = Map.unionWith joinKind (tsKind a) (tsKind b) + , tsStackKind = IM.unionWith joinKind (tsStackKind a) (tsStackKind b) } -- | Run forward dataflow analysis over a CFG. @@ -683,6 +703,7 @@ initSummary = FuncSummary $ TaintState , tsProv = Map.fromList [ (r, ProvUnknown) | r <- callerSavedRegs ] , tsStackProv = IM.empty , tsKind = Map.fromList [ (r, KindUnknown) | r <- callerSavedRegs ] + , tsStackKind = IM.empty } -- | Caller-saved registers per AArch64 ABI. diff --git a/plans/ARCH14.md b/plans/ARCH14.md @@ -0,0 +1,26 @@ +# ARCH14: Stack Spill Kind Tracking (Stage 2) + +## Goal + +Preserve pointer vs scalar kind information across spills and reloads by +tracking kind for stack slots. + +## Scope + +- Extend taint state with `tsStackKind :: IntMap RegKind`. +- Update stack store/load logic to propagate kind. + +## Kind Propagation Rules (Stage 2) + +- Store to `[sp, #imm]`: record source register kind in stack kind map. +- Load from `[sp, #imm]`: restore kind from stack map; default Unknown. +- Any SP adjustment clears stack kind map (same as taint/provenance). + +## Address Checks + +- Base/index checks remain pointer-aware using `RegKind` for registers. + +## Risks + +- Stack offset tracking is approximate; incorrect SP adjustments can + lead to stale kinds. The existing SP-clear heuristic helps. diff --git a/plans/IMPL14.md b/plans/IMPL14.md @@ -0,0 +1,33 @@ +# IMPL14: Implement Stack Kind Tracking (Stage 2) + +## Summary + +Extend taint state with stack slot kinds so pointer/scalar classification +survives spills and reloads. + +## Steps + +1) Extend TaintState +- Add `tsStackKind :: IntMap RegKind`. +- Initialize empty; clear on SP modification. + +2) Stack store/load updates +- `storeToStack`/`storePairToStack`: record source kinds. +- `loadFromStack`/`loadPairFromStack`: restore kinds for destinations. + +3) Clear on SP changes +- Ensure `clearStackMap` clears stack kinds too. + +4) Tests +- Add a test where a pointer is spilled and reloaded; ensure kind is + preserved. +- Add a test where a scalar spill does not upgrade to pointer. + +## Files to Touch + +- `lib/Audit/AArch64/Taint.hs` +- `test/` + +## Validation + +- `cabal test` diff --git a/test/Main.hs b/test/Main.hs @@ -366,6 +366,43 @@ auditTests = testGroup "Audit" [ other -> assertFailure $ "wrong reason: " ++ show other _ -> assertFailure "expected one violation" + , testCase "good: pointer spill/reload preserves kind" $ do + -- A pointer (from adrp) spilled and reloaded should stay KindPtr + -- and be usable as a base via provenance upgrade + let src = T.unlines + [ "foo:" + , " adrp x8, _const@PAGE" -- x8 = Public, ProvPublic, KindPtr + , " str x8, [sp, #16]" -- Spill pointer to stack + , " ldr x9, [sp, #16]" -- Reload: Unknown, ProvPublic, KindPtr + , " ldr x0, [x9]" -- Use as base - allowed via provenance + , " ret" + ] + case audit "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> assertEqual "no violations" 0 (length (arViolations ar)) + + , testCase "bad: scalar spill/reload doesn't upgrade kind" $ do + -- A scalar (loaded value) spilled and reloaded should stay KindScalar + -- even with public provenance, should not be usable as base + let src = T.unlines + [ "foo:" + , " ldr x8, [x20]" -- x8 = Unknown, ProvUnknown, KindScalar + , " add x8, x20, x8" -- x8 = Unknown, ProvPublic, KindScalar + , " str x8, [sp, #16]" -- Spill scalar to stack + , " ldr x9, [sp, #16]" -- Reload: Unknown, ProvPublic, KindScalar + , " ldr x0, [x9]" -- Use as base - violation! + , " 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 X9 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + , testCase "good: GOT/PLT pattern" $ do -- GOTPAGEOFF pattern: adrp + ldr from GOT should produce public let src = T.unlines