Restoring a continuation containing a catch# frame can result in incorrect async exception masking
The following program produces an incorrect result on GHC 9.6.2:
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
import Control.Exception
import Data.IORef
import GHC.Prim
import GHC.Types
data PromptTag a = PromptTag (PromptTag# a)
newPromptTag :: IO (PromptTag a)
newPromptTag = IO (\s -> case newPromptTag# s of
(# s', tag #) -> (# s, PromptTag tag #))
prompt :: PromptTag a -> IO a -> IO a
prompt (PromptTag tag) (IO m) = IO (prompt# tag m)
control0 :: PromptTag a -> ((IO b -> IO a) -> IO a) -> IO b
control0 (PromptTag tag) f =
IO (control0# tag (\k -> case f (\(IO a) -> IO (k a)) of IO b -> b))
data E = E deriving (Show)
instance Exception E
main :: IO ()
main = do
tag <- newPromptTag
ref <- newIORef Nothing
mask_ $
prompt tag $
handle (\E -> pure ()) $
control0 tag $ \k ->
writeIORef ref (Just k)
Just k <- readIORef ref
k (throwIO E)
print =<< getMaskingState
The program should print Unmasked, but it prints MaskedInterruptible.
The root cause is an oversight in the implementation of continuation restore. Continuation restore already takes care to patch the restored stack frames when mask/unmask frames are involved, as described in Note [Continuations and async exception masking] in rts/Continuation.c. However, it turns out that catch# frames also store the async exception masking state, so we need to patch those frames in a similar way (but currently do not).