Skip to content

Commit 5763a92

Browse files
snoyberghvr
authored andcommitted
Detect asynchronous exceptions via their types #187 (#202)
* Detect asynchronous exceptions via their types #187 This commit uses the same async-exception detection mechanism as is used by the safe-exceptions package, via checking if the given exception is cast to a SomeAsyncException. (On older GHCs without SomeAsyncException, it contains a hard-coded list of async exception types.) It then ensures that: * Throwing via throwChecked always generates a synchronous exception * Catching via catchChecked (et al) never catches an asynchronous exception Unfortunately, I don't currently have a reliable test case to ensure that this fixes the problems described in #187. Hopefully with this patch available we can begin testing cabal-install and Stack against the change and see if it resolves the issues. * Treat Timeout as an async exception too * Remove exceptions not actually considered async
1 parent d91afd3 commit 5763a92

File tree

1 file changed

+39
-2
lines changed
  • hackage-security/src/Hackage/Security/Util

1 file changed

+39
-2
lines changed

hackage-security/src/Hackage/Security/Util/Checked.hs

+39-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
{-# LANGUAGE IncoherentInstances #-}
1010
#endif
1111

12+
{-# LANGUAGE DeriveDataTypeable#-}
13+
1214
-- | Checked exceptions
1315
module Hackage.Security.Util.Checked (
1416
Throws
@@ -25,6 +27,7 @@ module Hackage.Security.Util.Checked (
2527

2628
import Control.Exception (Exception, IOException)
2729
import qualified Control.Exception as Base
30+
import Data.Typeable (Typeable)
2831

2932
#if __GLASGOW_HASKELL__ >= 708
3033
import GHC.Prim (coerce)
@@ -50,14 +53,48 @@ unthrow _ x = unWrap (coerceWrap (Wrap x :: Wrap e a))
5053
Base exceptions
5154
-------------------------------------------------------------------------------}
5255

56+
-- | Determine if an exception is asynchronous, based on its type.
57+
isAsync :: Exception e => e -> Bool
58+
#if MIN_VERSION_base(4, 7, 0)
59+
isAsync e =
60+
case Base.fromException $ Base.toException e of
61+
Just Base.SomeAsyncException{} -> True
62+
Nothing -> False
63+
#else
64+
-- Earlier versions of GHC had no SomeAsyncException. We have to
65+
-- instead make up a list of async exceptions.
66+
isAsync e =
67+
let se = Base.toException e
68+
in case () of
69+
()
70+
| Just (_ :: Base.AsyncException) <- Base.fromException se -> True
71+
| show e == "<<timeout>>" -> True
72+
| otherwise -> False
73+
#endif
74+
75+
-- | 'Base.catch', but immediately rethrows asynchronous exceptions
76+
-- (as determined by 'isAsync').
77+
catchSync :: Exception e => IO a -> (e -> IO a) -> IO a
78+
catchSync act onErr = act `Base.catch` \e ->
79+
if isAsync e
80+
then Base.throwIO e
81+
else onErr e
82+
83+
-- | Wraps up an async exception as a synchronous exception.
84+
newtype SyncException = SyncException Base.SomeException
85+
deriving (Show, Typeable)
86+
instance Exception SyncException
87+
5388
-- | Throw a checked exception
5489
throwChecked :: (Exception e, Throws e) => e -> IO a
55-
throwChecked = Base.throwIO
90+
throwChecked e
91+
| isAsync e = Base.throwIO $ SyncException $ Base.toException e
92+
| otherwise = Base.throwIO e
5693

5794
-- | Catch a checked exception
5895
catchChecked :: forall a e. Exception e
5996
=> (Throws e => IO a) -> (e -> IO a) -> IO a
60-
catchChecked act = Base.catch (unthrow (Proxy :: Proxy e) act)
97+
catchChecked act = catchSync (unthrow (Proxy :: Proxy e) act)
6198

6299
-- | 'catchChecked' with the arguments reversed
63100
handleChecked :: Exception e => (e -> IO a) -> (Throws e => IO a) -> IO a

0 commit comments

Comments
 (0)