[Git][ghc/ghc][wip/stm-mvar-deadlock-backtrace] 3 commits: Throw nontermination exceptions via `throw`
Ben Gamari pushed to branch wip/stm-mvar-deadlock-backtrace at Glasgow Haskell Compiler / GHC Commits: b709d077 by Ben Gamari at 2026-06-21T17:01:08-04:00 Throw nontermination exceptions via `throw` This ensures that the exception that results gets the usual backtrace annotations. This is a first step to #21878. - - - - - b8a14364 by Ben Gamari at 2026-06-21T17:01:10-04:00 compiler: Report backtraces in MVar and STM deadlocks Apply the same treatment previously given to Nontermination exceptions to MVar and STM deadlock exceptions, using `throw` instead of ad-hoc throwing with `throwToSingleThread` to ensure that the usual backtrace machinery is involved. Related to #21877. - - - - - e3f3e0a6 by Ben Gamari at 2026-06-21T17:01:10-04:00 testsuite: Add tests for deadlock backtraces - - - - - 17 changed files: - + changelog.d/builtin-exception-backtraces - libraries/ghc-internal/include/RtsIfaceSymbols.h - libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs - 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/LoopBacktrace.stderr - + testsuite/tests/rts/MVarDeadlockBacktrace.hs - + testsuite/tests/rts/MVarDeadlockBacktrace.stderr - + testsuite/tests/rts/STMDeadlockBacktrace.hs - + testsuite/tests/rts/STMDeadlockBacktrace.stderr - testsuite/tests/rts/T2783.stderr - testsuite/tests/rts/all.T Changes: ===================================== changelog.d/builtin-exception-backtraces ===================================== @@ -0,0 +1,10 @@ +section: rts +synopsis: Non-termination and deadlock exceptions now have backtrace annotations +issues: #21878 +mrs: !16158 +description: { + The `BlockedOnMVar`, `BlockedOnSTM`, and `NonTermination` exceptions + (the latter being the infamous `<<loop>>` error) now include `Backtrace` + `ExceptionAnnotations`, like exceptions thrown from user-written Haskell. +} + ===================================== libraries/ghc-internal/include/RtsIfaceSymbols.h ===================================== @@ -15,12 +15,12 @@ CLOSURE(GHCziInternalziWeakziFinalizze, runFinalizzerBatch_closure) CLOSURE(GHCziInternalziIOziException, stackOverflow_closure) CLOSURE(GHCziInternalziIOziException, heapOverflow_closure) CLOSURE(GHCziInternalziIOziException, allocationLimitExceeded_closure) -CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnMVar_closure) -CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnSTM_closure) +CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnMVarError_closure) +CLOSURE(GHCziInternalziIOziException, blockedIndefinitelyOnSTMError_closure) CLOSURE(GHCziInternalziIOziException, cannotCompactFunction_closure) CLOSURE(GHCziInternalziIOziException, cannotCompactPinned_closure) CLOSURE(GHCziInternalziIOziException, cannotCompactMutable_closure) -CLOSURE(GHCziInternalziControlziExceptionziBase, nonTermination_closure) +CLOSURE(GHCziInternalziControlziExceptionziBase, nonTerminationError_closure) CLOSURE(GHCziInternalziControlziExceptionziBase, nestedAtomically_closure) CLOSURE(GHCziInternalziControlziExceptionziBase, noMatchingContinuationPrompt_closure) #if defined(mingw32_HOST_OS) ===================================== libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs ===================================== @@ -108,7 +108,9 @@ module GHC.Internal.Control.Exception.Base ( impossibleError, impossibleConstraintError, nonExhaustiveGuardsError, patError, noMethodBindingError, typeError, - nonTermination, nestedAtomically, noMatchingContinuationPrompt, + nonTermination, nonTerminationError, + nestedAtomically, + noMatchingContinuationPrompt, ) where import GHC.Internal.Base ( @@ -448,6 +450,9 @@ impossibleConstraintError s = errorWithoutStackTrace (unpackCStringUtf8# s) -- GHC's RTS calls this +nonTerminationError :: IO () +nonTerminationError = throwIO NonTermination + nonTermination :: SomeException nonTermination = toException NonTermination ===================================== libraries/ghc-internal/src/GHC/Internal/IO/Exception.hs ===================================== @@ -24,8 +24,12 @@ ----------------------------------------------------------------------------- module GHC.Internal.IO.Exception ( - BlockedIndefinitelyOnMVar(..), blockedIndefinitelyOnMVar, - BlockedIndefinitelyOnSTM(..), blockedIndefinitelyOnSTM, + BlockedIndefinitelyOnMVar(..), + blockedIndefinitelyOnMVar, + blockedIndefinitelyOnMVarError, + BlockedIndefinitelyOnSTM(..), + blockedIndefinitelyOnSTM, + blockedIndefinitelyOnSTMError, Deadlock(..), AllocationLimitExceeded(..), allocationLimitExceeded, AssertionFailed(..), @@ -84,6 +88,9 @@ instance Exception BlockedIndefinitelyOnMVar instance Show BlockedIndefinitelyOnMVar where showsPrec _ BlockedIndefinitelyOnMVar = showString "thread blocked indefinitely in an MVar operation" +blockedIndefinitelyOnMVarError :: IO () -- for the RTS +blockedIndefinitelyOnMVarError = throwIO BlockedIndefinitelyOnMVar + blockedIndefinitelyOnMVar :: SomeException -- for the RTS blockedIndefinitelyOnMVar = toException BlockedIndefinitelyOnMVar @@ -100,6 +107,9 @@ instance Exception BlockedIndefinitelyOnSTM instance Show BlockedIndefinitelyOnSTM where showsPrec _ BlockedIndefinitelyOnSTM = showString "thread blocked indefinitely in an STM transaction" +blockedIndefinitelyOnSTMError :: IO () -- for the RTS +blockedIndefinitelyOnSTMError = throwIO BlockedIndefinitelyOnSTM + blockedIndefinitelyOnSTM :: SomeException -- for the RTS blockedIndefinitelyOnSTM = toException BlockedIndefinitelyOnSTM ===================================== rts/Prelude.h ===================================== @@ -53,12 +53,12 @@ extern StgClosure ZCMain_main_closure; #define stackOverflow_closure ghc_hs_iface->stackOverflow_closure #define heapOverflow_closure ghc_hs_iface->heapOverflow_closure #define allocationLimitExceeded_closure ghc_hs_iface->allocationLimitExceeded_closure -#define blockedIndefinitelyOnMVar_closure ghc_hs_iface->blockedIndefinitelyOnMVar_closure -#define blockedIndefinitelyOnSTM_closure ghc_hs_iface->blockedIndefinitelyOnSTM_closure +#define blockedIndefinitelyOnMVarError_closure ghc_hs_iface->blockedIndefinitelyOnMVarError_closure +#define blockedIndefinitelyOnSTMError_closure ghc_hs_iface->blockedIndefinitelyOnSTMError_closure #define cannotCompactFunction_closure ghc_hs_iface->cannotCompactFunction_closure #define cannotCompactPinned_closure ghc_hs_iface->cannotCompactPinned_closure #define cannotCompactMutable_closure ghc_hs_iface->cannotCompactMutable_closure -#define nonTermination_closure ghc_hs_iface->nonTermination_closure +#define nonTerminationError_closure ghc_hs_iface->nonTerminationError_closure #define nestedAtomically_closure ghc_hs_iface->nestedAtomically_closure #define absentSumFieldError_closure ghc_hs_iface->absentSumFieldError_closure #define underflowException_closure ghc_hs_iface->underflowException_closure ===================================== rts/RaiseAsync.c ===================================== @@ -87,6 +87,55 @@ suspendComputation (Capability *cap, StgTSO *tso, StgUpdateFrame *stop_here) throwToSingleThreaded__ (cap, tso, NULL, false, stop_here); } +/* ----------------------------------------------------------------------------- + scheduleRaiseViaIO + + Schedule `tso` to raise an exception by running `io_action`, an IO () that + performs `throwIO`. Unlike throwToSingleThreaded (which injects an exception + *value* via raiseAsync), the exception is raised by throwIO *within* the + thread, so it acquires a backtrace of the thread's stack. This is used by + resurrectThreads to deliver the "blocked indefinitely" exceptions + (BlockedIndefinitelyOnMVar, BlockedIndefinitelyOnSTM, NonTermination). + + We push a "run this IO action" frame on top of the thread's existing + (suspended) stack and make it runnable; when the thread runs, throwIO raises + the exception and its own stack unwinding handles any CATCH_FRAME / + ATOMICALLY_FRAME (e.g. aborting a blocked STM transaction). + + removeFromQueues takes care of unlinking the thread from any blocking queue + (notably the MVar blocked queue) and appends it to the run queue. As with + throwToSingleThreaded, the caller must own the TSO (e.g. hold all + capabilities during GC); in particular this relies on the thread not being + scheduled between removeFromQueues' enqueue and our stack push. + -------------------------------------------------------------------------- */ + +void +scheduleRaiseViaIO (Capability *cap, StgTSO *tso, StgClosure *io_action) +{ + // Thread already dead? + if (tso->what_next == ThreadComplete || tso->what_next == ThreadKilled) { + return; + } + + // Unlink from any blocking queues; sets why_blocked = NotBlocked and + // appends the thread to the run queue. + removeFromQueues(cap, tso); + + StgStack *stack = tso->stackobj; + + // We are about to mutate the stack, so dirty it for the GC write barrier + // (resurrectThreads runs right after GC). + dirty_TSO(cap, tso); + dirty_STACK(cap, stack); + + // Push a frame that enters `io_action` and applies the resulting IO action + // to the void (RealWorld) argument: [stg_enter_info, io_action, stg_ap_v_info] + stack->sp -= 3; + stack->sp[0] = (W_)&stg_enter_info; + stack->sp[1] = (W_)io_action; + stack->sp[2] = (W_)&stg_ap_v_info; +} + /* ----------------------------------------------------------------------------- throwToSelf ===================================== rts/RaiseAsync.h ===================================== @@ -38,6 +38,10 @@ void suspendComputation (Capability *cap, StgTSO *tso, StgUpdateFrame *stop_here); +void scheduleRaiseViaIO (Capability *cap, + StgTSO *tso, + StgClosure *io_action); + MessageThrowTo *throwTo (Capability *cap, // the Capability we hold StgTSO *source, StgTSO *target, ===================================== rts/RtsStartup.c ===================================== @@ -192,9 +192,9 @@ static void initBuiltinGcRoots(void) getStablePtr((StgPtr)stackOverflow_closure); getStablePtr((StgPtr)heapOverflow_closure); getStablePtr((StgPtr)unpackCString_closure); - getStablePtr((StgPtr)blockedIndefinitelyOnMVar_closure); - getStablePtr((StgPtr)nonTermination_closure); - getStablePtr((StgPtr)blockedIndefinitelyOnSTM_closure); + getStablePtr((StgPtr)blockedIndefinitelyOnMVarError_closure); + getStablePtr((StgPtr)nonTerminationError_closure); + getStablePtr((StgPtr)blockedIndefinitelyOnSTMError_closure); getStablePtr((StgPtr)allocationLimitExceeded_closure); getStablePtr((StgPtr)cannotCompactFunction_closure); getStablePtr((StgPtr)cannotCompactPinned_closure); ===================================== rts/Schedule.c ===================================== @@ -3309,16 +3309,16 @@ resurrectThreads (StgTSO *threads) case BlockedOnMVar: case BlockedOnMVarRead: /* Called by GC - sched_mutex lock is currently held. */ - throwToSingleThreaded(cap, tso, - (StgClosure *)blockedIndefinitelyOnMVar_closure); + scheduleRaiseViaIO(cap, tso, + (StgClosure *)blockedIndefinitelyOnMVarError_closure); break; case BlockedOnBlackHole: - throwToSingleThreaded(cap, tso, - (StgClosure *)nonTermination_closure); + scheduleRaiseViaIO(cap, tso, + (StgClosure *)nonTerminationError_closure); break; case BlockedOnSTM: - throwToSingleThreaded(cap, tso, - (StgClosure *)blockedIndefinitelyOnSTM_closure); + scheduleRaiseViaIO(cap, tso, + (StgClosure *)blockedIndefinitelyOnSTMError_closure); break; case NotBlocked: /* This might happen if the thread was blocked on a black hole ===================================== rts/include/rts/RtsToHsIface.h ===================================== @@ -20,12 +20,12 @@ typedef struct { StgClosure *stackOverflow_closure; // GHC.Internal.IO.Exception.stackOverflow_closure StgClosure *heapOverflow_closure; // GHC.Internal.IO.Exception.heapOverflow_closure StgClosure *allocationLimitExceeded_closure; // GHC.Internal.IO.Exception.allocationLimitExceeded_closure - StgClosure *blockedIndefinitelyOnMVar_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnMVar_closure - StgClosure *blockedIndefinitelyOnSTM_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnSTM_closure + StgClosure *blockedIndefinitelyOnMVarError_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnMVarError_closure + StgClosure *blockedIndefinitelyOnSTMError_closure; // GHC.Internal.IO.Exception.blockedIndefinitelyOnSTMError_closure StgClosure *cannotCompactFunction_closure; // GHC.Internal.IO.Exception.cannotCompactFunction_closure StgClosure *cannotCompactPinned_closure; // GHC.Internal.IO.Exception.cannotCompactPinned_closure StgClosure *cannotCompactMutable_closure; // GHC.Internal.IO.Exception.cannotCompactMutable_closure - StgClosure *nonTermination_closure; // GHC.Internal.Control.Exception.Base.nonTermination_closure + StgClosure *nonTerminationError_closure; // GHC.Internal.Control.Exception.Base.nonTerminationError_closure StgClosure *nestedAtomically_closure; // GHC.Internal.Control.Exception.Base.nestedAtomically_closure StgClosure *noMatchingContinuationPrompt_closure; // GHC.Internal.Control.Exception.Base.noMatchingContinuationPrompt_closure StgClosure *blockedOnBadFD_closure; // GHC.Internal.Event.Thread.blockedOnBadFD_closure ===================================== testsuite/tests/rts/LoopBacktrace.stderr ===================================== @@ -3,19 +3,19 @@ LoopBacktrace: Uncaught exception ghc-internal:GHC.Internal.Control.Exception.Ba <<loop>> IPE backtrace: - Cmm$rts/StgStartup.cmm. (:) - Cmm$rts/Exception.cmm. (:) - GHC.Internal.Base.thenIO (libraries/ghc-internal/src/GHC/Internal/Base.hs:2336:1-72) - GHC.Internal.IO.Handle.Text. (:) - GHC.Internal.Show. (:) - Main.x (LoopBacktrace.hs:6:1-9) - Cmm$rts/Updates.cmm. (:) - GHC.Num.Integer. (:) - Cmm$rts/HeapStackCheck.cmm. (:) - GHC.Internal.IO.throwIO (libraries/ghc-internal/src/GHC/Internal/IO.hs:285:36) - GHC.Internal.Exception.bindIO (libraries/ghc-internal/src/GHC/Internal/Base.hs:2333:1-76) - GHC.Internal.Exception.Backtrace.bindIO (libraries/ghc-internal/src/GHC/Internal/Base.hs:2333:1-76) - GHC.Internal.Exception.Backtrace.$ (libraries/ghc-internal/src/GHC/Internal/Base.hs:2267:1-9) + GHC.Internal.Exception.Backtrace.collectBacktraces' (libraries/ghc-internal/src/GHC/Internal/Exception/Backtrace.hs:(179,1)-(202,25)) + GHC.Internal.Exception.Backtrace.collectBacktraces (libraries/ghc-internal/src/GHC/Internal/Exception/Backtrace.hs:174:39-56) + GHC.Internal.Exception.toExceptionWithBacktrace (libraries/ghc-internal/src/GHC/Internal/Exception.hs:(179,26)-(181,53)) + GHC.Internal.IO.throwIO (libraries/ghc-internal/src/GHC/Internal/IO.hs:293:36) + Cmm$rts/HeapStackCheck.cmm. (:) + GHC.Internal.Bignum.Integer.integerAdd (libraries/ghc-internal/src/GHC/Internal/Bignum/Integer.hs:(547,1)-(571,52)) + Cmm$rts/Updates.cmm. (:) + Main.x (LoopBacktrace.hs:6:1-9) + GHC.Internal.Show.show (libraries/ghc-internal/src/GHC/Internal/Show.hs:497:10-21) + GHC.Internal.IO.Handle.Text.hPutStr' (libraries/ghc-internal/src/GHC/Internal/IO/Handle/Text.hs:667:29-37) + GHC.Internal.Base.thenIO (libraries/ghc-internal/src/GHC/Internal/Base.hs:2337:1-72) + Cmm$rts/Exception.cmm. (:) + Cmm$rts/StgStartup.cmm. (:) HasCallStack backtrace: - throwIO, called at libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs:446:23 in ghc-internal:GHC.Internal.Control.Exception.Base + throwIO, called at libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs:452:23 in ghc-internal:GHC.Internal.Control.Exception.Base ===================================== testsuite/tests/rts/MVarDeadlockBacktrace.hs ===================================== @@ -0,0 +1,13 @@ +{-# OPTIONS_GHC -finfo-table-map #-} + +-- | Check that a @BlockedIndefinitelyOnMVar@ deadlock exception carries a +-- backtrace mentioning the blocking site in this module. +import Control.Concurrent.MVar +import GHC.Exception.Backtrace.Experimental + +main :: IO () +main = do + setBacktraceMechanismState IPEBacktrace True + mv <- newEmptyMVar :: IO (MVar ()) + x <- takeMVar mv + print x ===================================== testsuite/tests/rts/MVarDeadlockBacktrace.stderr ===================================== @@ -0,0 +1,2 @@ +MVarDeadlockBacktrace: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnMVar: + Main.main (MVarDeadlockBacktrace.hs:16:3-36) ===================================== testsuite/tests/rts/STMDeadlockBacktrace.hs ===================================== @@ -0,0 +1,12 @@ +{-# OPTIONS_GHC -finfo-table-map #-} + +-- | Check that a @BlockedIndefinitelyOnSTM@ deadlock exception carries a +-- backtrace mentioning the blocking site in this module. +import GHC.Conc (atomically, retry) +import GHC.Exception.Backtrace.Experimental + +main :: IO () +main = do + setBacktraceMechanismState IPEBacktrace True + x <- atomically retry :: IO () + print x ===================================== testsuite/tests/rts/STMDeadlockBacktrace.stderr ===================================== @@ -0,0 +1,2 @@ +STMDeadlockBacktrace: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.BlockedIndefinitelyOnSTM: + Main.main (STMDeadlockBacktrace.hs:16:3-32) ===================================== testsuite/tests/rts/T2783.stderr ===================================== @@ -1,3 +1,7 @@ T2783: Uncaught exception ghc-internal:GHC.Internal.Control.Exception.Base.NonTermination: <<loop>> + +HasCallStack backtrace: + throwIO, called at libraries/ghc-internal/src/GHC/Internal/Control/Exception/Base.hs:452:23 in ghc-internal:GHC.Internal.Control.Exception.Base + ===================================== testsuite/tests/rts/all.T ===================================== @@ -689,3 +689,9 @@ test('ClosureTable', test('resizeMutableByteArrayInPlace', [req_cmm, extra_ways(['optasm', 'sanity']), only_ways(['optasm', 'sanity'])], compile_and_run, ['']) test('LoopBacktrace', [exit_code(1)], compile_and_run, ['']) + +deadlock_backtrace_norm = grep_errmsg(r'(Uncaught exception|Main\.)') +test('MVarDeadlockBacktrace', [exit_code(1), only_ways(['normal']), deadlock_backtrace_norm], + compile_and_run, ['-O']) +test('STMDeadlockBacktrace', [exit_code(1), only_ways(['normal']), deadlock_backtrace_norm], + compile_and_run, ['-O']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/82c1e8b92f305584abf59fffad9dad5... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/82c1e8b92f305584abf59fffad9dad5... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Ben Gamari (@bgamari)