Ben Gamari pushed to branch wip/stm-mvar-deadlock-backtrace at Glasgow Haskell Compiler / GHC
Commits:
-
54e16504
by Ben Gamari at 2026-06-21T18:57:51-04:00
-
b5897f84
by Ben Gamari at 2026-06-21T18:57:51-04:00
14 changed files:
- changelog.d/builtin-exception-backtraces
- libraries/ghc-internal/include/RtsIfaceSymbols.h
- libraries/ghc-internal/src/GHC/Internal/IO/Exception.hs
- rts/Prelude.h
- rts/RaiseAsync.c
- rts/RaiseAsync.h
- rts/RtsStartup.c
- rts/Schedule.c
- rts/include/rts/RtsToHsIface.h
- + testsuite/tests/rts/MVarDeadlockBacktrace.hs
- + testsuite/tests/rts/MVarDeadlockBacktrace.stderr
- + testsuite/tests/rts/STMDeadlockBacktrace.hs
- + testsuite/tests/rts/STMDeadlockBacktrace.stderr
- testsuite/tests/rts/all.T
Changes:
| 1 | 1 | section: rts
|
| 2 | -synopsis: Non-termination exceptions now have backtrace annotations
|
|
| 2 | +synopsis: Non-termination and deadlock exceptions now have backtrace annotations
|
|
| 3 | 3 | issues: #21878
|
| 4 | -mrs: !16158
|
|
| 4 | +mrs: !16158 !16221
|
|
| 5 | 5 | description: {
|
| 6 | - The `NonTermination` exception (manifesting in printed exception output as
|
|
| 7 | - `<<loop>>`) now include `Backtrace` `ExceptionAnnotations`, like exceptions
|
|
| 8 | - thrown from user-written Haskell.
|
|
| 6 | + The `BlockedIndefinitelyOnMVar`, `BlockedIndefinitelyOnSTM`, and
|
|
| 7 | + `NonTermination` exceptions (the latter being the infamous `<<loop>>` error)
|
|
| 8 | + now include `Backtrace` `ExceptionAnnotations`, like exceptions thrown from
|
|
| 9 | + user-written Haskell.
|
|
| 9 | 10 | }
|
| 10 | 11 |
| ... | ... | @@ -15,8 +15,8 @@ CLOSURE(GHCziInternalziWeakziFinalizze, runFinalizzerBatch_closure) |
| 15 | 15 | CLOSURE(GHCziInternalziIOziException, stackOverflow_closure)
|
| 16 | 16 | CLOSURE(GHCziInternalziIOziException, heapOverflow_closure)
|
| 17 | 17 | CLOSURE(GHCziInternalziIOziException, allocationLimitExceeded_closure)
|
| 18 | -CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnMVar_closure)
|
|
| 19 | -CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnSTM_closure)
|
|
| 18 | +CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnMVarError_closure)
|
|
| 19 | +CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnSTMError_closure)
|
|
| 20 | 20 | CLOSURE(GHCziInternalziIOziException, cannotCompactFunction_closure)
|
| 21 | 21 | CLOSURE(GHCziInternalziIOziException, cannotCompactPinned_closure)
|
| 22 | 22 | CLOSURE(GHCziInternalziIOziException, cannotCompactMutable_closure)
|
| ... | ... | @@ -24,8 +24,12 @@ |
| 24 | 24 | -----------------------------------------------------------------------------
|
| 25 | 25 | |
| 26 | 26 | module GHC.Internal.IO.Exception (
|
| 27 | - BlockedIndefinitelyOnMVar(..), blockedIndefinitelyOnMVar,
|
|
| 28 | - BlockedIndefinitelyOnSTM(..), blockedIndefinitelyOnSTM,
|
|
| 27 | + BlockedIndefinitelyOnMVar(..),
|
|
| 28 | + blockedIndefinitelyOnMVar,
|
|
| 29 | + blockedIndefinitelyOnMVarError,
|
|
| 30 | + BlockedIndefinitelyOnSTM(..),
|
|
| 31 | + blockedIndefinitelyOnSTM,
|
|
| 32 | + blockedIndefinitelyOnSTMError,
|
|
| 29 | 33 | Deadlock(..),
|
| 30 | 34 | AllocationLimitExceeded(..), allocationLimitExceeded,
|
| 31 | 35 | AssertionFailed(..),
|
| ... | ... | @@ -84,6 +88,9 @@ instance Exception BlockedIndefinitelyOnMVar |
| 84 | 88 | instance Show BlockedIndefinitelyOnMVar where
|
| 85 | 89 | showsPrec _ BlockedIndefinitelyOnMVar = showString "thread blocked indefinitely in an MVar operation"
|
| 86 | 90 | |
| 91 | +blockedIndefinitelyOnMVarError :: IO () -- for the RTS
|
|
| 92 | +blockedIndefinitelyOnMVarError = throwIO BlockedIndefinitelyOnMVar
|
|
| 93 | + |
|
| 87 | 94 | blockedIndefinitelyOnMVar :: SomeException -- for the RTS
|
| 88 | 95 | blockedIndefinitelyOnMVar = toException BlockedIndefinitelyOnMVar
|
| 89 | 96 | |
| ... | ... | @@ -100,6 +107,9 @@ instance Exception BlockedIndefinitelyOnSTM |
| 100 | 107 | instance Show BlockedIndefinitelyOnSTM where
|
| 101 | 108 | showsPrec _ BlockedIndefinitelyOnSTM = showString "thread blocked indefinitely in an STM transaction"
|
| 102 | 109 | |
| 110 | +blockedIndefinitelyOnSTMError :: IO () -- for the RTS
|
|
| 111 | +blockedIndefinitelyOnSTMError = throwIO BlockedIndefinitelyOnSTM
|
|
| 112 | + |
|
| 103 | 113 | blockedIndefinitelyOnSTM :: SomeException -- for the RTS
|
| 104 | 114 | blockedIndefinitelyOnSTM = toException BlockedIndefinitelyOnSTM
|
| 105 | 115 |
| ... | ... | @@ -53,8 +53,8 @@ extern StgClosure ZCMain_main_closure; |
| 53 | 53 | #define stackOverflow_closure ghc_hs_iface->stackOverflow_closure
|
| 54 | 54 | #define heapOverflow_closure ghc_hs_iface->heapOverflow_closure
|
| 55 | 55 | #define allocationLimitExceeded_closure ghc_hs_iface->allocationLimitExceeded_closure
|
| 56 | -#define blockedIndefinitelyOnMVar_closure ghc_hs_iface->blockedIndefinitelyOnMVar_closure
|
|
| 57 | -#define blockedIndefinitelyOnSTM_closure ghc_hs_iface->blockedIndefinitelyOnSTM_closure
|
|
| 56 | +#define blockedIndefinitelyOnMVarError_closure ghc_hs_iface->blockedIndefinitelyOnMVarError_closure
|
|
| 57 | +#define blockedIndefinitelyOnSTMError_closure ghc_hs_iface->blockedIndefinitelyOnSTMError_closure
|
|
| 58 | 58 | #define cannotCompactFunction_closure ghc_hs_iface->cannotCompactFunction_closure
|
| 59 | 59 | #define cannotCompactPinned_closure ghc_hs_iface->cannotCompactPinned_closure
|
| 60 | 60 | #define cannotCompactMutable_closure ghc_hs_iface->cannotCompactMutable_closure
|
| ... | ... | @@ -87,6 +87,55 @@ suspendComputation (Capability *cap, StgTSO *tso, StgUpdateFrame *stop_here) |
| 87 | 87 | throwToSingleThreaded__ (cap, tso, NULL, false, stop_here);
|
| 88 | 88 | }
|
| 89 | 89 | |
| 90 | +/* -----------------------------------------------------------------------------
|
|
| 91 | + scheduleRaiseViaIO
|
|
| 92 | + |
|
| 93 | + Schedule `tso` to raise an exception by running `io_action`, an IO () that
|
|
| 94 | + performs `throwIO`. Unlike throwToSingleThreaded (which injects an exception
|
|
| 95 | + *value* via raiseAsync), the exception is raised by throwIO *within* the
|
|
| 96 | + thread, so it acquires a backtrace of the thread's stack. This is used by
|
|
| 97 | + resurrectThreads to deliver the "blocked indefinitely" exceptions
|
|
| 98 | + (BlockedIndefinitelyOnMVar, BlockedIndefinitelyOnSTM, NonTermination).
|
|
| 99 | + |
|
| 100 | + We push a "run this IO action" frame on top of the thread's existing
|
|
| 101 | + (suspended) stack and make it runnable; when the thread runs, throwIO raises
|
|
| 102 | + the exception and its own stack unwinding handles any CATCH_FRAME /
|
|
| 103 | + ATOMICALLY_FRAME (e.g. aborting a blocked STM transaction).
|
|
| 104 | + |
|
| 105 | + removeFromQueues takes care of unlinking the thread from any blocking queue
|
|
| 106 | + (notably the MVar blocked queue) and appends it to the run queue. As with
|
|
| 107 | + throwToSingleThreaded, the caller must own the TSO (e.g. hold all
|
|
| 108 | + capabilities during GC); in particular this relies on the thread not being
|
|
| 109 | + scheduled between removeFromQueues' enqueue and our stack push.
|
|
| 110 | + -------------------------------------------------------------------------- */
|
|
| 111 | + |
|
| 112 | +void
|
|
| 113 | +scheduleRaiseViaIO (Capability *cap, StgTSO *tso, StgClosure *io_action)
|
|
| 114 | +{
|
|
| 115 | + // Thread already dead?
|
|
| 116 | + if (tso->what_next == ThreadComplete || tso->what_next == ThreadKilled) {
|
|
| 117 | + return;
|
|
| 118 | + }
|
|
| 119 | + |
|
| 120 | + // Unlink from any blocking queues; sets why_blocked = NotBlocked and
|
|
| 121 | + // appends the thread to the run queue.
|
|
| 122 | + removeFromQueues(cap, tso);
|
|
| 123 | + |
|
| 124 | + StgStack *stack = tso->stackobj;
|
|
| 125 | + |
|
| 126 | + // We are about to mutate the stack, so dirty it for the GC write barrier
|
|
| 127 | + // (resurrectThreads runs right after GC).
|
|
| 128 | + dirty_TSO(cap, tso);
|
|
| 129 | + dirty_STACK(cap, stack);
|
|
| 130 | + |
|
| 131 | + // Push a frame that enters `io_action` and applies the resulting IO
|
|
| 132 | + // action to RealWorld.
|
|
| 133 | + stack->sp -= 3;
|
|
| 134 | + stack->sp[0] = (W_)&stg_enter_info;
|
|
| 135 | + stack->sp[1] = (W_)io_action;
|
|
| 136 | + stack->sp[2] = (W_)&stg_ap_v_info;
|
|
| 137 | +}
|
|
| 138 | + |
|
| 90 | 139 | /* -----------------------------------------------------------------------------
|
| 91 | 140 | throwToSelf
|
| 92 | 141 |
| ... | ... | @@ -38,6 +38,10 @@ void suspendComputation (Capability *cap, |
| 38 | 38 | StgTSO *tso,
|
| 39 | 39 | StgUpdateFrame *stop_here);
|
| 40 | 40 | |
| 41 | +void scheduleRaiseViaIO (Capability *cap,
|
|
| 42 | + StgTSO *tso,
|
|
| 43 | + StgClosure *io_action);
|
|
| 44 | + |
|
| 41 | 45 | MessageThrowTo *throwTo (Capability *cap, // the Capability we hold
|
| 42 | 46 | StgTSO *source,
|
| 43 | 47 | StgTSO *target,
|
| ... | ... | @@ -192,9 +192,9 @@ static void initBuiltinGcRoots(void) |
| 192 | 192 | getStablePtr((StgPtr)stackOverflow_closure);
|
| 193 | 193 | getStablePtr((StgPtr)heapOverflow_closure);
|
| 194 | 194 | getStablePtr((StgPtr)unpackCString_closure);
|
| 195 | - getStablePtr((StgPtr)blockedIndefinitelyOnMVar_closure);
|
|
| 195 | + getStablePtr((StgPtr)blockedIndefinitelyOnMVarError_closure);
|
|
| 196 | 196 | getStablePtr((StgPtr)nonTerminationError_closure);
|
| 197 | - getStablePtr((StgPtr)blockedIndefinitelyOnSTM_closure);
|
|
| 197 | + getStablePtr((StgPtr)blockedIndefinitelyOnSTMError_closure);
|
|
| 198 | 198 | getStablePtr((StgPtr)allocationLimitExceeded_closure);
|
| 199 | 199 | getStablePtr((StgPtr)cannotCompactFunction_closure);
|
| 200 | 200 | getStablePtr((StgPtr)cannotCompactPinned_closure);
|
| ... | ... | @@ -3276,17 +3276,6 @@ findAtomicallyFrameHelper (Capability *cap, StgTSO *tso) |
| 3276 | 3276 | }
|
| 3277 | 3277 | }
|
| 3278 | 3278 | |
| 3279 | -static void throwNontermination(Capability *cap, StgTSO *tso) {
|
|
| 3280 | - StgStack *stack = tso->stackobj;
|
|
| 3281 | - stack->sp -= 3;
|
|
| 3282 | - stack->sp[0] = (W_)&stg_enter_info;
|
|
| 3283 | - stack->sp[1] = (W_)nonTerminationError_closure;
|
|
| 3284 | - stack->sp[2] = (W_)&stg_ap_v_info;
|
|
| 3285 | - tso->why_blocked = NotBlocked;
|
|
| 3286 | - appendToRunQueue(cap,tso);
|
|
| 3287 | -}
|
|
| 3288 | - |
|
| 3289 | - |
|
| 3290 | 3279 | /* -----------------------------------------------------------------------------
|
| 3291 | 3280 | resurrectThreads is called after garbage collection on the list of
|
| 3292 | 3281 | threads found to be garbage. Each of these threads will be woken
|
| ... | ... | @@ -3320,15 +3309,16 @@ resurrectThreads (StgTSO *threads) |
| 3320 | 3309 | case BlockedOnMVar:
|
| 3321 | 3310 | case BlockedOnMVarRead:
|
| 3322 | 3311 | /* Called by GC - sched_mutex lock is currently held. */
|
| 3323 | - throwToSingleThreaded(cap, tso,
|
|
| 3324 | - (StgClosure *)blockedIndefinitelyOnMVar_closure);
|
|
| 3312 | + scheduleRaiseViaIO(cap, tso,
|
|
| 3313 | + (StgClosure *)blockedIndefinitelyOnMVarError_closure);
|
|
| 3325 | 3314 | break;
|
| 3326 | 3315 | case BlockedOnBlackHole:
|
| 3327 | - throwNontermination(cap, tso);
|
|
| 3316 | + scheduleRaiseViaIO(cap, tso,
|
|
| 3317 | + (StgClosure *)nonTerminationError_closure);
|
|
| 3328 | 3318 | break;
|
| 3329 | 3319 | case BlockedOnSTM:
|
| 3330 | - throwToSingleThreaded(cap, tso,
|
|
| 3331 | - (StgClosure *)blockedIndefinitelyOnSTM_closure);
|
|
| 3320 | + scheduleRaiseViaIO(cap, tso,
|
|
| 3321 | + (StgClosure *)blockedIndefinitelyOnSTMError_closure);
|
|
| 3332 | 3322 | break;
|
| 3333 | 3323 | case NotBlocked:
|
| 3334 | 3324 | /* This might happen if the thread was blocked on a black hole
|
| ... | ... | @@ -20,8 +20,8 @@ typedef struct { |
| 20 | 20 | StgClosure *stackOverflow_closure; // GHC.Internal.IO.Exception.stackOverflow_closure
|
| 21 | 21 | StgClosure *heapOverflow_closure; // GHC.Internal.IO.Exception.heapOverflow_closure
|
| 22 | 22 | StgClosure *allocationLimitExceeded_closure; // GHC.Internal.IO.Exception.allocationLimitExceeded_closure
|
| 23 | - StgClosure *blockedIndefinitelyOnMVar_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnMVar_closure
|
|
| 24 | - StgClosure *blockedIndefinitelyOnSTM_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnSTM_closure
|
|
| 23 | + StgClosure *blockedIndefinitelyOnMVarError_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnMVarError_closure
|
|
| 24 | + StgClosure *blockedIndefinitelyOnSTMError_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnSTMError_closure
|
|
| 25 | 25 | StgClosure *cannotCompactFunction_closure; // GHC.Internal.IO.Exception.cannotCompactFunction_closure
|
| 26 | 26 | StgClosure *cannotCompactPinned_closure; // GHC.Internal.IO.Exception.cannotCompactPinned_closure
|
| 27 | 27 | StgClosure *cannotCompactMutable_closure; // GHC.Internal.IO.Exception.cannotCompactMutable_closure
|
| 1 | +{-# OPTIONS_GHC -finfo-table-map #-}
|
|
| 2 | + |
|
| 3 | +-- | Check that a @BlockedIndefinitelyOnMVar@ deadlock exception carries a
|
|
| 4 | +-- backtrace mentioning the blocking site in this module.
|
|
| 5 | +import Control.Concurrent.MVar
|
|
| 6 | +import GHC.Exception.Backtrace.Experimental
|
|
| 7 | + |
|
| 8 | +main :: IO ()
|
|
| 9 | +main = do
|
|
| 10 | + setBacktraceMechanismState IPEBacktrace True
|
|
| 11 | + mv <- newEmptyMVar :: IO (MVar ())
|
|
| 12 | + x <- takeMVar mv
|
|
| 13 | + print x |
| 1 | +MVarDeadlockBacktrace: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnMVar:
|
|
| 2 | + Main.main (MVarDeadlockBacktrace.hs:16:3-36) |
| 1 | +{-# OPTIONS_GHC -finfo-table-map #-}
|
|
| 2 | + |
|
| 3 | +-- | Check that a @BlockedIndefinitelyOnSTM@ deadlock exception carries a
|
|
| 4 | +-- backtrace mentioning the blocking site in this module.
|
|
| 5 | +import GHC.Conc (atomically, retry)
|
|
| 6 | +import GHC.Exception.Backtrace.Experimental
|
|
| 7 | + |
|
| 8 | +main :: IO ()
|
|
| 9 | +main = do
|
|
| 10 | + setBacktraceMechanismState IPEBacktrace True
|
|
| 11 | + x <- atomically retry :: IO ()
|
|
| 12 | + print x |
| 1 | +STMDeadlockBacktrace: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnSTM:
|
|
| 2 | + Main.main (STMDeadlockBacktrace.hs:16:3-32) |
| ... | ... | @@ -689,3 +689,9 @@ test('ClosureTable', |
| 689 | 689 | test('resizeMutableByteArrayInPlace', [req_cmm, extra_ways(['optasm', 'sanity']), only_ways(['optasm', 'sanity'])], compile_and_run, [''])
|
| 690 | 690 | |
| 691 | 691 | test('LoopBacktrace', [exit_code(1)], compile_and_run, [''])
|
| 692 | + |
|
| 693 | +deadlock_backtrace_norm = grep_errmsg(r'(Uncaught exception|Main\.)')
|
|
| 694 | +test('MVarDeadlockBacktrace', [exit_code(1), only_ways(['normal']), deadlock_backtrace_norm],
|
|
| 695 | + compile_and_run, ['-O'])
|
|
| 696 | +test('STMDeadlockBacktrace', [exit_code(1), only_ways(['normal']), deadlock_backtrace_norm],
|
|
| 697 | + compile_and_run, ['-O']) |