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).