Skip to content

Missing primitive for unmasking uninterruptibility of async exceptions?

Motivation

I'm looking at a bug where async is called in an uninterruptibleMask, causing a deadlock as the withAsync cannot complete because the Async cannot be canceled due to spawning in the uninterruptibleMask state.

The local solution is to write:

asyncWithUnmask $ \unmask -> unmask $ do
  mask $ \restore -> ...

However, this leaves us in a slightly vulnerable state - that unmask lambda parameter is provided as unsafeUnmask, which removes all masking and then throws the async exception.

Looking into the code, it doesn't appear that there's a primitive that goes from MaskedUninterruptible to MaskedInterruptible. We have unmaskAsyncException#, which seems to remove all masking. Indeed, it seems like the first thing it does is throw any async exceptions on the thread - so the unmask $ mask pattern is immediately vulnerable.

The issue is located here: https://github.com/iand675/hs-opentelemetry/pull/42

Proposal

-- | Removes the uninterruptibility of the masked action.
-- 
-- Has no effect if we are in an unmasked or interruptibly masked state.
-- Insert appropriate unsafety caveats here.
unsafeUnUninterruptibleMask :: IO a -> IO a
unsafeUnUninterruptibleMask (IO io) = IO (unsafeUnUninterruptibleMask# io)
Edited by parsonsmatt
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information