[Git][ghc/ghc][wip/fendor/freeze-throw] Hide implementation details from base exception stack traces
Hannes Siebenhandl pushed to branch wip/fendor/freeze-throw at Glasgow Haskell Compiler / GHC Commits: b1a7731e by fendor at 2026-02-13T13:27:38+01:00 Hide implementation details from base exception stack traces Ensure we hide the implementation details of the exception throwing mechanisms: * `undefined` * `throwSTM` * `throw` * `throwIO` * `error` The `HasCallStackBacktrace` should always have a length of exactly 1, not showing internal implementation details in the stack trace, as these are vastly distracting to end users. CLC proposal [#387](https://github.com/haskell/core-libraries-committee/issues/387) - - - - - 5 changed files: - libraries/ghc-internal/src/GHC/Internal/Exception.hs - libraries/ghc-internal/src/GHC/Internal/STM.hs - + libraries/ghc-internal/tests/backtraces/T15395.hs - + libraries/ghc-internal/tests/backtraces/T15395.stdout - libraries/ghc-internal/tests/backtraces/all.T Changes: ===================================== libraries/ghc-internal/src/GHC/Internal/Exception.hs ===================================== @@ -87,7 +87,7 @@ throw e = -- Note also the absolutely crucial `noinine` in the RHS! -- See Note [Hiding precise exception signature in throw] let se :: SomeException - !se = noinline (unsafePerformIO (toExceptionWithBacktrace e)) + !se = noinline (unsafePerformIO (withFrozenCallStack $ toExceptionWithBacktrace e)) in raise# se -- Note [Capturing the backtrace in throw] @@ -162,7 +162,12 @@ throw e = -- primops which allow more precise guidance of the demand analyser's heuristic -- (e.g. #23847). --- | @since base-4.20.0.0 +-- | Collect a Backtrace and attach it to the 'Exception'. +-- +-- It is recommended to use 'withFrozenCallStack' when calling this function +-- in order to avoid leaking implementation details of 'toExceptionWithBacktrace'. +-- +-- @since base-4.20.0.0 toExceptionWithBacktrace :: (HasCallStack, Exception e) => e -> IO SomeException toExceptionWithBacktrace e ===================================== libraries/ghc-internal/src/GHC/Internal/STM.hs ===================================== @@ -28,7 +28,7 @@ import GHC.Internal.Base import GHC.Internal.Exception (Exception, toExceptionWithBacktrace, fromException, addExceptionContext) import GHC.Internal.Exception.Context (ExceptionAnnotation) import GHC.Internal.Exception.Type (WhileHandling(..)) -import GHC.Internal.Stack (HasCallStack) +import GHC.Internal.Stack (HasCallStack, withFrozenCallStack) -- TVars are shared memory locations which support atomic memory -- transactions. @@ -187,7 +187,7 @@ throwSTM e = do -- N.B. Typically use of unsafeIOToSTM is very much frowned upon as this -- is an easy way to end up with nested transactions. However, we can be -- certain that toExceptionWithBacktrace will not initiate a transaction. - se <- unsafeIOToSTM (toExceptionWithBacktrace e) + se <- unsafeIOToSTM (withFrozenCallStack $ toExceptionWithBacktrace e) STM $ raiseIO# se -- | Exception handling within STM actions. ===================================== libraries/ghc-internal/tests/backtraces/T15395.hs ===================================== @@ -0,0 +1,74 @@ +{-# LANGUAGE LambdaCase #-} + +import GHC.Internal.Control.Exception +import GHC.Internal.Data.Foldable (traverse_) +import GHC.Internal.Exception.Backtrace +import GHC.Internal.Exception.Context +import GHC.Internal.Exception.Type +import GHC.Internal.STM +import Control.Monad (when) +import Data.List (isSuffixOf, isInfixOf) +import System.Exit + +main :: IO () +main = do + -- Make sure there are HasCallStackBacktraces + setBacktraceMechanismState HasCallStackBacktrace True + mapM_ (uncurry runCase) + [ ("throw", throwAction) + , ("throwIO", throwIOAction) + , ("error", errorAction) + , ("throwSTM", throwSTMAction) + , ("undefined", undefinedAction) + ] + +runCase :: String -> IO () -> IO () +runCase name act = do + putStrLn $ "=== Validate stack size of '" ++ name ++ "' has length 1" + catchAndVerifyStackTraceLength name act + putStrLn "" + +throwAction :: IO () +throwAction = evaluate $ throw $ ErrorCall "my throw error" + +throwIOAction :: IO () +throwIOAction = throwIO $ ErrorCall "my throwIO error" + +errorAction :: IO () +errorAction = error "plain error" + +throwSTMAction :: IO () +throwSTMAction = atomically $ throwSTM $ ErrorCall "my throwSTM error" + +undefinedAction :: IO () +undefinedAction = evaluate undefined + +catchAndVerifyStackTraceLength :: String -> IO () -> IO () +catchAndVerifyStackTraceLength name act = do + try act >>= \ case + Right _ -> do + putStrLn $ "Exception expected but got a result for '" ++ name ++ "'" + exitFailure + Left exc -> + verifyBacktraceSize name exc + +verifyBacktraceSize :: String -> SomeException -> IO () +verifyBacktraceSize label se = do + message <- evaluate (displayException se) + putStrLn "==== Caught exception:" + putStrLn message + let ctx = displayExceptionContext (someExceptionContext se) + ctxLines = lines ctx + putStrLn "==== Exception context:" + case ctxLines of + [] -> putStrLn "<empty>" + (l:ls) -> do + when (l /= "HasCallStack backtrace:") $ + fail "Context is expected to be a \"HasCallStack\" backtrace" + traverse_ mustNotReferenceGhcInternalPackage ls + mapM_ (putStrLn . ("- " ++) . dropWhile (== ' ')) ls + where + mustNotReferenceGhcInternalPackage l = + if "ghc-internal" `isInfixOf` l + then fail $ "Stack trace \"" ++ l ++ "\" must not reference ghc-internal" + else pure () ===================================== libraries/ghc-internal/tests/backtraces/T15395.stdout ===================================== @@ -0,0 +1,30 @@ +=== Validate stack size of 'throw' has length 1 +==== Caught exception: +my throw error +==== Exception context: +- throw, called at T15395.hs:32:26 in main:Main + +=== Validate stack size of 'throwIO' has length 1 +==== Caught exception: +my throwIO error +==== Exception context: +- throwIO, called at T15395.hs:35:17 in main:Main + +=== Validate stack size of 'error' has length 1 +==== Caught exception: +plain error +==== Exception context: +- error, called at T15395.hs:38:15 in main:Main + +=== Validate stack size of 'throwSTM' has length 1 +==== Caught exception: +my throwSTM error +==== Exception context: +- throwSTM, called at T15395.hs:41:31 in main:Main + +=== Validate stack size of 'undefined' has length 1 +==== Caught exception: +Prelude.undefined +==== Exception context: +- undefined, called at T15395.hs:44:28 in main:Main + ===================================== libraries/ghc-internal/tests/backtraces/all.T ===================================== @@ -4,3 +4,7 @@ test('T26507', [ when(have_profiling(), extra_ways(['prof'])) , when(js_arch(), skip) , when(ghc_with_ipe(), skip) # IPE builds include an IPE backtrace section on stderr. , exit_code(1)], compile_and_run, ['']) + +# Stack traces shouldn't expose implementation details +test('T15395', [ when(have_profiling(), extra_ways(['prof'])) + , when(js_arch(), skip)], compile_and_run, ['']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/b1a7731e676b30f68df09c3237b31c71... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/b1a7731e676b30f68df09c3237b31c71... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Hannes Siebenhandl (@fendor)