crucible-0.9: src/Lang/Crucible/Simulator/RecordAndReplay.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Lang.Crucible.Simulator.RecordAndReplay (
HasRecordState(..),
RecordState,
mkRecordState,
HasReplayState(..),
ReplayState,
mkReplayState,
recordTraceLength,
replayTraceLength,
RecordedTrace,
getRecordedTrace,
recordFeature,
replayFeature,
initialTrace,
traceGlobal,
emptyRecordedTrace
) where
import Control.Exception qualified as X
import Control.Lens ((%~), (&), (^.))
import Control.Lens qualified as Lens
import Data.Foldable qualified as F
import Data.Kind (Type)
import Data.Text qualified as Text
import Data.Sequence qualified as Seq
import Lang.Crucible.Backend qualified as CB
import Lang.Crucible.CFG.Core qualified as C
import Lang.Crucible.FunctionHandle qualified as C
import Lang.Crucible.Panic (panic)
import Lang.Crucible.Simulator qualified as C
import Lang.Crucible.Simulator.EvalStmt qualified as C
import Lang.Crucible.Simulator.ExecutionTree qualified as C
import Lang.Crucible.Simulator.GlobalState qualified as C
import Lang.Crucible.Simulator.SymSequence qualified as CSSS
import Lang.Crucible.Types qualified as CT
import What4.Interface qualified as W4
import What4.Partial qualified as W4P
-- | A trace consists of the 'W4.ProgramLoc's returned by
-- 'W4.getCurrentProgramLoc' in 'C.RunningState's during symbolic execution.
--
-- Intentionally not part of the API so as to keep the implementation abstract.
type TraceType = CT.SequenceType (CT.StringType W4.Unicode)
-- | Type parameters:
--
-- * @p@: see 'C.cruciblePersonality'
-- * @sym@: instance of 'Lang.Crucible.Backend.IsSymInterface'
-- * @ext@: language extension, see "Lang.Crucible.CFG.Extension"
-- * @rtp@: type of the simulator return value
type RecordState :: Type -> Type -> Type -> Type -> Type
newtype RecordState p sym ext rtp
= RecordState (C.GlobalVar TraceType)
-- ^ constructor intentionally not exported
{- | A trace from 'recordFeature', processed and ready for consumption by
'replayFeature'.
-}
newtype RecordedTrace sym
= RecordedTrace (C.RegValue sym TraceType)
-- | Type parameters:
--
-- * @p@: see 'C.cruciblePersonality'
-- * @sym@: instance of 'Lang.Crucible.Backend.IsSymInterface'
-- * @ext@: language extension, see "Lang.Crucible.CFG.Extension"
-- * @rtp@: type of the simulator return value
type ReplayState :: Type -> Type -> Type -> Type -> Type
data ReplayState p sym ext rtp
= ReplayState
{ _traceGlobal :: (C.GlobalVar TraceType)
, _initialTrace :: (RecordedTrace sym)
}
-- ^ constructor intentionally not exported
Lens.makeLenses ''ReplayState
-- | Constructor for 'RecordState'
mkRecordState ::
C.HandleAllocator -> IO (RecordState p sym ext rtp)
mkRecordState halloc =
RecordState <$> C.freshGlobalVar halloc "recordState" W4.knownRepr
-- | Constructor for 'ReplayState'
mkReplayState ::
C.HandleAllocator -> RecordedTrace sym -> IO (ReplayState p sym ext rtp)
mkReplayState halloc rt =
ReplayState <$> C.freshGlobalVar halloc "replayState" W4.knownRepr <*> pure rt
-- | A class for Crucible personality types @p@ which contain a
-- 'RecordState'. This execution feature is polymorphic over
-- 'RecordState' so that downstream users can supply their own
-- personality types that extend 'RecordState' further.
class HasRecordState p r sym ext rtp | p -> r sym ext rtp where
recordState :: Lens.Lens' p (RecordState r sym ext rtp)
instance HasRecordState (RecordState p sym ext rtp) p sym ext rtp where
recordState = id
{-# INLINE recordState #-}
-- | A class for Crucible personality types @p@ which contain a
-- 'ReplayState'. This execution feature is polymorphic over
-- 'ReplayState' so that downstream users can supply their own
-- personality types that extend 'ReplayState' further.
class HasReplayState p r sym ext rtp | p -> r sym ext rtp where
replayState :: Lens.Lens' p (ReplayState r sym ext rtp)
instance HasReplayState (ReplayState p sym ext rtp) p sym ext rtp where
replayState = id
{-# INLINE replayState #-}
data TraceGlobalNotDefined = TraceGlobalNotDefined
instance Show TraceGlobalNotDefined where
show _ = "record and replay trace global not defined"
instance X.Exception TraceGlobalNotDefined
locAsStr ::
W4.IsExprBuilder sym =>
sym ->
IO (C.RegValue sym (CT.StringType W4.Unicode))
locAsStr sym = do
loc <- W4.getCurrentProgramLoc sym
let txtLoc = Text.pack (show loc)
W4.stringLit sym (W4.UnicodeLiteral txtLoc)
emptyRecordedTrace :: sym -> IO (RecordedTrace sym)
emptyRecordedTrace sym = RecordedTrace <$> CSSS.nilSymSequence sym
getRecordTrace ::
HasRecordState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
Maybe (C.RegValue sym TraceType)
getRecordTrace simState = do
let ctx = simState ^. C.stateContext
let RecordState g = ctx ^. C.cruciblePersonality . recordState
C.lookupGlobal g (simState ^. C.stateGlobals)
-- | Get the length of the currently recorded trace
recordTraceLength ::
W4.IsExprBuilder sym =>
HasRecordState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
IO (Maybe (W4.SymNat sym))
recordTraceLength simState = do
let sym = simState ^. C.stateSymInterface
case getRecordTrace simState of
Nothing -> pure Nothing
Just s -> Just <$> CSSS.lengthSymSequence sym s
getReplayTrace ::
HasReplayState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
Maybe (C.RegValue sym TraceType)
getReplayTrace simState = do
let ctx = simState ^. C.stateContext
let g = ctx ^. C.cruciblePersonality . replayState . traceGlobal
C.lookupGlobal g (simState ^. C.stateGlobals)
-- | Get the length of the trace being replayed
replayTraceLength ::
W4.IsExprBuilder sym =>
HasReplayState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
IO (Maybe (W4.SymNat sym))
replayTraceLength simState = do
let sym = simState ^. C.stateSymInterface
case getReplayTrace simState of
Nothing -> pure Nothing
Just s -> Just <$> CSSS.lengthSymSequence sym s
-- | An 'C.ExecutionFeature' to record traces.
--
-- During execution this logs program locations to a Crucible global variable.
-- After execution, this variable may be read with 'getRecordedTrace' and the
-- 'RecordedTrace' can be passed to 'replayFeature' to \"replay\" it, i.e., to
-- abort all branches that deviate from it.
--
-- If this is not called with 'C.InitialState' before any other 'C.ExecState',
-- it may throw a 'TraceGlobalNotDefined' exception.
recordFeature ::
( HasRecordState p p sym ext rtp
, W4.IsExprBuilder sym
) =>
C.ExecutionFeature p sym ext rtp
recordFeature =
C.ExecutionFeature $
\case
C.InitialState simCtx globals abortHandler retTy cont -> do
globals' <- insertNewTrace simCtx globals
let iState = C.InitialState simCtx globals' abortHandler retTy cont
return $ C.ExecutionFeatureModifiedState iState
C.RunningState runStateInfo st -> do
loc <- locAsStr (st ^. C.stateSymInterface)
st' <- consTrace st loc
let rState = C.RunningState runStateInfo st'
return $ C.ExecutionFeatureModifiedState rState
_ -> pure C.ExecutionFeatureNoChange
where
insertNewTrace ::
HasRecordState p p sym ext rtp =>
C.SimContext p sym ext ->
C.SymGlobalState sym ->
IO (C.SymGlobalState sym)
insertNewTrace simCtx globals = do
let RecordState g = simCtx ^. C.cruciblePersonality . recordState
let sym = simCtx ^. C.ctxSymInterface
nil <- CSSS.nilSymSequence sym
return (C.insertGlobal g nil globals)
getTraceOrThrow ::
HasRecordState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
IO (C.RegValue sym TraceType)
getTraceOrThrow st =
case getRecordTrace st of
Nothing -> X.throw TraceGlobalNotDefined
Just t -> pure t
insertTrace ::
HasRecordState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
C.RegValue sym TraceType ->
C.SimState p sym ext rtp f args
insertTrace st v = do
let simCtx = st ^. C.stateContext
let RecordState g = simCtx ^. C.cruciblePersonality . recordState
st & C.stateGlobals %~ C.insertGlobal g v
consTrace ::
HasRecordState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
C.RegValue sym (CT.StringType W4.Unicode) ->
IO (C.SimState p sym ext rtp f args)
consTrace st v = do
s <- getTraceOrThrow st
let sym = st ^. C.stateSymInterface
s' <- CSSS.consSymSequence sym v s
pure (insertTrace st s')
-- ^ constructor intentionally not exported to keep 'TraceType' out of the
-- API, but it could be exported in the future if necessary.
-- | Obtain a 'RecordedTrace' after execution.
--
-- This currently requires concretizing the trace, because there is no efficient
-- reverse operation for 'CSSS.SymSequence'.
getRecordedTrace ::
W4.IsExprBuilder sym =>
C.SymGlobalState sym ->
RecordState p sym ext rtp ->
sym ->
-- | Evaluation for booleans, usually a 'What4.Expr.GroundEval.GroundEvalFn'
(W4.Pred sym -> IO Bool) ->
IO (RecordedTrace sym)
getRecordedTrace globals (RecordState g) sym evalBool = do
case C.lookupGlobal g globals of
Nothing -> X.throw TraceGlobalNotDefined
Just s -> RecordedTrace <$> concretizeAndReverseTrace s
where
concretizeAndReverseTrace s = do
concretized <- CSSS.concretizeSymSequence evalBool (evalStr sym) s
let reversed = Seq.reverse concretized
symbolized <- mapM (W4.stringLit sym . W4.UnicodeLiteral) reversed
CSSS.fromListSymSequence sym (F.toList symbolized)
evalStr ::
W4.IsExpr (W4.SymExpr sym) =>
sym ->
W4.SymString sym W4.Unicode ->
IO Text.Text
evalStr _sym s =
case W4.asString s of
Just (W4.UnicodeLiteral s') -> pure s'
Nothing -> panic "getRecordedTrace" ["Non-literal trace element?"]
{- | Inserts a recorded trace into the state's replay trace variable
The replay feature will follow this trace if it is enabled
-}
insertReplayTrace ::
(HasReplayState p p sym ext rtp) =>
C.SimState p sym ext rtp f args ->
C.RegValue sym TraceType ->
C.SimState p sym ext rtp f args
insertReplayTrace st v = do
let simCtx = st ^. C.stateContext
let g = simCtx ^. C.cruciblePersonality . replayState . traceGlobal
st & C.stateGlobals %~ C.insertGlobal g v
-- | An 'C.ExecutionFeature' to replay traces recorded with 'recordFeature'.
--
-- Branches that deviate from the given trace will be aborted with
-- 'C.InfeasibleBranch'.
--
-- If this is not called with 'C.InitialState' before any other 'C.ExecState',
-- it may throw a 'TraceGlobalNotDefined' exception.
replayFeature ::
( HasReplayState p p sym ext rtp
, W4.IsExprBuilder sym
) =>
-- | Whether to stop at the end of the trace. If this is 'True' and execution
-- has exhausted the trace, then any further execution will be aborted via
-- 'C.InfeasibleBranch'.
Bool ->
C.ExecutionFeature p sym ext rtp
replayFeature stop =
C.ExecutionFeature $
\case
C.InitialState simCtx globals abortHandler retTy cont -> do
let rstate = simCtx ^. C.cruciblePersonality . replayState
let g = rstate ^. traceGlobal
let RecordedTrace trace = rstate ^. initialTrace
let globals' = C.insertGlobal g trace globals
let iState = C.InitialState simCtx globals' abortHandler retTy cont
return $ C.ExecutionFeatureModifiedState iState
C.RunningState runStateInfo st -> do
let sym = st ^. C.stateSymInterface
s <- getTraceOrThrow st
partExpr <- CSSS.unconsSymSequence sym (W4.stringIte sym) s
let badPath = do
loc <- W4.getCurrentProgramLoc sym
let st' = C.AbortState (CB.InfeasibleBranch loc) st
pure (C.ExecutionFeatureNewState st')
case partExpr of
W4P.Unassigned
| stop -> badPath
| otherwise -> pure C.ExecutionFeatureNoChange
W4P.PE valid (expectedLoc, rest) ->
C.withBackend (st ^. C.stateContext) $ \bak -> do
let msg = "Trace must be valid"
CB.assert bak valid (C.AssertFailureSimError msg "")
currLoc <- locAsStr sym
atExpectedLoc <- W4.stringEq sym currLoc expectedLoc
case W4.asConstantPred atExpectedLoc of
Just False -> badPath
_ -> do
let msg' = "Execution deviated from trace"
CB.assert bak atExpectedLoc (C.AssertFailureSimError msg' "")
let st' = insertReplayTrace st rest
let rState = C.RunningState runStateInfo st'
pure (C.ExecutionFeatureModifiedState rState)
_ -> pure C.ExecutionFeatureNoChange
where
getTraceOrThrow ::
HasReplayState p p sym ext rtp =>
C.SimState p sym ext rtp f args ->
IO (C.RegValue sym TraceType)
getTraceOrThrow st =
case getReplayTrace st of
Nothing -> X.throw TraceGlobalNotDefined
Just t -> pure t