auditor

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

commit 8153ab942cbfd3b0df7cfb4851f91d5ac4ab8a22
parent 7ab24b4c9867f765cc2ff3a52dc42fb177750a0f
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 11 Feb 2026 22:38:05 +0400

feat: preserve STG stack slots across constant x20 adjustments (IMPL19)

Track constant deltas to x20 by shifting STG stack slot maps instead of
clearing them. This preserves seeded stg_secret/stg_public slots across
common prologue patterns like `sub x20, x20, #16`.

Changes:
- Add shiftStgStackMap to shift slot offsets by a signed delta
- Update Add/Sub/Adds/Subs transfer to shift when x20 += imm or x20 -= imm
- Update pre/post-indexed [x20, #imm] handling to shift instead of clear
- Remove unused updateWithSpCheckProvKind helper
- Add tests for delta tracking across add/sub x20

Semantics:
- sub x20, x20, #imm: shift slots by +imm (old offset 8 -> new offset 24)
- add x20, x20, #imm: shift slots by -imm (old offset 24 -> new offset 8)
- Non-constant x20 updates still clear the map

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

Diffstat:
Mlib/Audit/AArch64/Taint.hs | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Aplans/ARCH19.md | 47+++++++++++++++++++++++++++++++++++++++++++++++
Aplans/IMPL19.md | 38++++++++++++++++++++++++++++++++++++++
Mtest/Main.hs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 216 insertions(+), 27 deletions(-)

diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -233,27 +233,28 @@ transfer instr st = case instr of -- Arithmetic: result is join of operand taints/provenances -- Clear stack map if SP is modified (offsets become stale) + -- For X20 with immediate operand, shift STG stack map instead of clearing -- Pointer arithmetic (ptr + imm) preserves pointer kind Add dst r1 op -> let t = join2 (getTaint r1 st) (operandTaint op st) p = provJoin2 (getProvenance r1 st) (operandProv op st) k = pointerArithKind r1 op st - in updateWithSpCheckProvKind dst t p k st + in updateWithX20ShiftAdd dst r1 op t p k st Sub dst r1 op -> let t = join2 (getTaint r1 st) (operandTaint op st) p = provJoin2 (getProvenance r1 st) (operandProv op st) k = pointerArithKind r1 op st - in updateWithSpCheckProvKind dst t p k st + in updateWithX20ShiftSub dst r1 op t p k st Adds dst r1 op -> let t = join2 (getTaint r1 st) (operandTaint op st) p = provJoin2 (getProvenance r1 st) (operandProv op st) k = pointerArithKind r1 op st - in updateWithSpCheckProvKind dst t p k st + in updateWithX20ShiftAdd dst r1 op t p k st Subs dst r1 op -> let t = join2 (getTaint r1 st) (operandTaint op st) p = provJoin2 (getProvenance r1 st) (operandProv op st) k = pointerArithKind r1 op st - in updateWithSpCheckProvKind dst t p k st + in updateWithX20ShiftSub dst r1 op t p k st Adc dst r1 r2 -> let t = join2 (getTaint r1 st) (getTaint r2 st) p = provJoin2 (getProvenance r1 st) (getProvenance r2 st) @@ -520,6 +521,22 @@ clearStgStackMap :: TaintState -> TaintState clearStgStackMap st = st { tsStgStack = IM.empty, tsStgStackProv = IM.empty, tsStgStackKind = IM.empty } +-- | Shift all STG stack slot offsets by a signed delta. +-- Used when x20 is adjusted by a constant: after `sub x20, x20, #16`, the old +-- slot at offset 8 is now at offset 24 relative to the new x20. +-- For `add x20, x20, #imm`, delta should be `-imm`. +-- For `sub x20, x20, #imm`, delta should be `+imm`. +shiftStgStackMap :: Int -> TaintState -> TaintState +shiftStgStackMap delta st + | delta == 0 = st + | otherwise = st + { tsStgStack = shiftIM (tsStgStack st) + , tsStgStackProv = shiftIM (tsStgStackProv st) + , tsStgStackKind = shiftIM (tsStgStackKind st) + } + where + shiftIM im = IM.fromList [(k + delta, v) | (k, v) <- IM.toList im] + -- | Join taint, provenance, and kind into the heap bucket. -- Used for non-stack stores to propagate secrets through heap memory. joinHeapBucket :: Taint -> Provenance -> RegKind -> TaintState -> TaintState @@ -559,12 +576,27 @@ setTaintLoadStgStack dst offset st where isPublicRoot r = r `elem` publicRoots --- | Update register taint, provenance, and kind. --- Clears hardware stack map if SP, STG stack map if X20. -updateWithSpCheckProvKind :: Reg -> Taint -> Provenance -> RegKind - -> TaintState -> TaintState -updateWithSpCheckProvKind dst t p k st +-- | Update for Add instruction, shifting STG stack map when x20 += imm. +-- After `add x20, x20, #imm`, old offset O maps to new offset O - imm. +updateWithX20ShiftAdd :: Reg -> Reg -> Operand -> Taint -> Provenance + -> RegKind -> TaintState -> TaintState +updateWithX20ShiftAdd dst r1 op t p k st | dst == SP = clearStackMap (setTaintProvKind dst t p k st) + | dst == X20, r1 == X20, OpImm imm <- op = + let delta = negate (fromInteger imm) + in shiftStgStackMap delta (setTaintProvKind dst t p k st) + | dst == X20 = clearStgStackMap (setTaintProvKind dst t p k st) + | otherwise = setTaintProvKind dst t p k st + +-- | Update for Sub instruction, shifting STG stack map when x20 -= imm. +-- After `sub x20, x20, #imm`, old offset O maps to new offset O + imm. +updateWithX20ShiftSub :: Reg -> Reg -> Operand -> Taint -> Provenance + -> RegKind -> TaintState -> TaintState +updateWithX20ShiftSub dst r1 op t p k st + | dst == SP = clearStackMap (setTaintProvKind dst t p k st) + | dst == X20, r1 == X20, OpImm imm <- op = + let delta = fromInteger imm + in shiftStgStackMap delta (setTaintProvKind dst t p k st) | dst == X20 = clearStgStackMap (setTaintProvKind dst t p k st) | otherwise = setTaintProvKind dst t p k st @@ -602,20 +634,22 @@ storeToStack src addr st = case addr of k = getKind src st in setStgStackTaintProvKind off t p k st PreIndex X20 imm -> - -- Store at x20+imm, then X20 changes + -- Store at x20+imm, then x20 = x20 + imm let off = fromInteger imm t = getTaint src st p = getProvenance src st k = getKind src st + delta = negate (fromInteger imm) st' = setStgStackTaintProvKind off t p k st - in clearStgStackMap st' - PostIndex X20 _ -> - -- Store at x20, then X20 changes (offset 0) + in shiftStgStackMap delta st' + PostIndex X20 imm -> + -- Store at x20, then x20 = x20 + imm let t = getTaint src st p = getProvenance src st k = getKind src st + delta = negate (fromInteger imm) st' = setStgStackTaintProvKind 0 t p k st - in clearStgStackMap st' + in shiftStgStackMap delta st' -- Refined heap slot: [base, #imm] where base is a public pointer BaseImm base imm | isPublicPointer base st -> @@ -666,20 +700,22 @@ storePairToStack src1 src2 addr st = case addr of in setStgStackTaintProvKind off t1 p1 k1 (setStgStackTaintProvKind (off + 8) t2 p2 k2 st) PreIndex X20 imm -> - -- Store at x20+imm and x20+imm+8, then X20 changes + -- Store at x20+imm and x20+imm+8, then x20 = x20 + imm let off = fromInteger imm t1 = getTaint src1 st; p1 = getProvenance src1 st; k1 = getKind src1 st t2 = getTaint src2 st; p2 = getProvenance src2 st; k2 = getKind src2 st + delta = negate (fromInteger imm) st' = setStgStackTaintProvKind off t1 p1 k1 (setStgStackTaintProvKind (off + 8) t2 p2 k2 st) - in clearStgStackMap st' - PostIndex X20 _ -> - -- Store at x20 and x20+8, then X20 changes + in shiftStgStackMap delta st' + PostIndex X20 imm -> + -- Store at x20 and x20+8, then x20 = x20 + imm 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 + delta = negate (fromInteger imm) st' = setStgStackTaintProvKind 0 t1 p1 k1 (setStgStackTaintProvKind 8 t2 p2 k2 st) - in clearStgStackMap st' + in shiftStgStackMap delta st' -- Refined heap slot: [base, #imm] where base is a public pointer BaseImm base imm | isPublicPointer base st -> @@ -714,11 +750,13 @@ loadFromStack dst addr st = case addr of -- STG stack (X20) BaseImm X20 imm -> setTaintLoadStgStack dst (fromInteger imm) st PreIndex X20 imm -> - -- Load from x20+imm, then x20 changes - clearStgStackMap (setTaintLoadStgStack dst (fromInteger imm) st) + -- Load from x20+imm, then x20 = x20 + imm + let delta = negate (fromInteger imm) + in shiftStgStackMap delta (setTaintLoadStgStack dst (fromInteger imm) st) PostIndex X20 imm -> - -- Load first, then clear (X20 changes after access) - clearStgStackMap (setTaintLoadStgStack dst (fromInteger imm) st) + -- Load first, then x20 = x20 + imm + let delta = negate (fromInteger imm) + in shiftStgStackMap delta (setTaintLoadStgStack dst (fromInteger imm) st) -- Symbol/literal loads BaseSymbol _ _ -> setTaintProv dst Public ProvPublic st -- GOT/PLT entry -> address @@ -755,15 +793,17 @@ loadPairFromStack dst1 dst2 addr st = case addr of let off = fromInteger imm in setTaintLoadStgStack dst1 off (setTaintLoadStgStack dst2 (off + 8) st) PreIndex X20 imm -> - -- Load from x20+imm and x20+imm+8, then x20 changes + -- Load from x20+imm and x20+imm+8, then x20 = x20 + imm let off = fromInteger imm + delta = negate (fromInteger imm) st' = setTaintLoadStgStack dst1 off (setTaintLoadStgStack dst2 (off+8) st) - in clearStgStackMap st' + in shiftStgStackMap delta st' PostIndex X20 imm -> - -- Load first, then clear (X20 changes after access) + -- Load first, then x20 = x20 + imm let off = fromInteger imm + delta = negate (fromInteger imm) st' = setTaintLoadStgStack dst1 off (setTaintLoadStgStack dst2 (off+8) st) - in clearStgStackMap st' + in shiftStgStackMap delta st' -- Refined heap slot: [base, #imm] where base is a public pointer BaseImm base imm | isPublicPointer base st -> diff --git a/plans/ARCH19.md b/plans/ARCH19.md @@ -0,0 +1,47 @@ +# ARCH19: STG Stack Delta Tracking + +## Goal + +Preserve STG stack slot taint across constant adjustments of `x20` +so seeding by offset remains valid after small prologues/epilogues. + +## Context + +We currently clear all `tsStgStack*` maps when `x20` changes. This is +safe but loses information for common prologue patterns like: + + sub x20, x20, #16 + +This wipes seeded `stg_secret` slots before later loads. + +## Approach + +Track constant deltas to `x20` by shifting STG stack slot maps: + +- On `add x20, x20, #imm`: shift all offsets by `-imm`. +- On `sub x20, x20, #imm`: shift all offsets by `+imm`. + +This matches the address mapping: after `sub x20, x20, #16`, the old +slot at offset `8` is now at offset `24` relative to the new `x20`. + +For any non-constant update to `x20`, continue to clear all maps. + +## Scope + +- `tsStgStack`, `tsStgStackProv`, `tsStgStackKind` only. +- Do not change `tsStack*` (SP) logic. +- Keep taint/provenance/kind semantics unchanged for all other regs. + +## Integration Points + +- Add a helper to shift STG stack maps by a signed delta. +- Update transfer cases for `Add/Sub` where `dst == X20` and operand + is an immediate. +- Update pre/post-indexed `[x20, #imm]` load/store handling to shift + maps by `imm` instead of clearing after access. + +## Risks + +- Incorrect sign or shift direction causes false positives/negatives. +- Large deltas could overflow `Int`; treat as best-effort and allow + the existing `Int` semantics. diff --git a/plans/IMPL19.md b/plans/IMPL19.md @@ -0,0 +1,38 @@ +# IMPL19: STG Stack Delta Tracking + +## Changes + +1. Add `shiftStgStackMap :: Int -> TaintState -> TaintState` in + `lib/Audit/AArch64/Taint.hs`: + - If delta is 0, return input. + - Otherwise rebuild `tsStgStack`, `tsStgStackProv`, `tsStgStackKind` + by shifting keys by `delta`. + - Preserve values; drop any collisions by `IM.insert` semantics + (last wins) or use `IM.fromList` (stable order is fine). + +2. Update transfer for `Add/Sub`: + - In `Add dst r1 op` and `Sub dst r1 op`, if `dst == X20`, `r1 == X20`, + and `op` is `OpImm imm`, use `shiftStgStackMap` instead of + `clearStgStackMap` when updating `x20`. + - Preserve existing taint/prov/kind updates for `x20`. + - For any other `x20` update, keep `clearStgStackMap` behavior. + +3. Update pre/post-index handling in load/store helpers: + - In `storeToStack` and `storePairToStack`, for `PreIndex X20 imm` + and `PostIndex X20 imm`, replace `clearStgStackMap` with + `shiftStgStackMap` using `imm` (post-index applies after access, + so shift after storing). + - In `loadFromStack` and `loadPairFromStack`, do the same for + `PreIndex X20 imm` and `PostIndex X20 imm` after loading. + +## Tests + +- Add a small fixture and unit test that seeds `stg_secret` at offset 8, + then processes `sub x20, x20, #16` followed by `ldr x1, [x20, #24]` and + asserts the load is tainted. +- Add a symmetric test for `add x20, x20, #16` shifting the other way. + +## Notes + +- Keep lines under 80 chars. +- Avoid new dependencies. diff --git a/test/Main.hs b/test/Main.hs @@ -914,4 +914,68 @@ taintConfigTests = testGroup "TaintConfig" [ SecretIndex X8 -> pure () other -> assertFailure $ "wrong reason: " ++ show other _ -> assertFailure "expected one violation" + + -- STG stack delta tracking tests (IMPL19) + , testCase "stg_secret preserved after sub x20" $ do + -- Seed secret at offset 8, then sub x20, x20, #16 + -- After sub, old offset 8 maps to new offset 24 + let src = T.unlines + [ "_foo:" + , " sub x20, x20, #16" -- x20 -= 16, shift slots by +16 + , " ldr x8, [x20, #24]" -- Load from new offset 24 (was 8) + , " ldr x9, [x21, x8]" -- Use as index -> violation + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton 8) Set.empty) + case auditWithConfig cfg "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 + SecretIndex X8 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + + , testCase "stg_secret preserved after add x20" $ do + -- Seed secret at offset 24, then add x20, x20, #16 + -- After add, old offset 24 maps to new offset 8 + let src = T.unlines + [ "_foo:" + , " add x20, x20, #16" -- x20 += 16, shift slots by -16 + , " ldr x8, [x20, #8]" -- Load from new offset 8 (was 24) + , " ldr x9, [x21, x8]" -- Use as index -> violation + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton 24) Set.empty) + case auditWithConfig cfg "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 + SecretIndex X8 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + + , testCase "stg slot cleared on non-constant x20 update" $ do + -- Secret at offset 8, then x20 updated non-constantly + -- Should clear slot map, load yields unknown (not secret) + -- Note: x20 itself becomes unknown, causing a base violation too + let src = T.unlines + [ "_foo:" + , " add x20, x20, x1" -- Non-constant update clears map, x20 unknown + , " ldr x8, [x20, #8]" -- Unknown base x20 + load yields unknown + , " ldr x9, [x21, x8]" -- Use as index -> unknown violation + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_foo" + (ArgPolicy Set.empty Set.empty (Set.singleton 8) Set.empty) + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + -- Two violations: unknown base x20, unknown index x8 + assertEqual "two violations" 2 (length (arViolations ar)) ]