Magnus pushed to branch wip/mangoiv/bp-16719 at Glasgow Haskell Compiler / GHC

Commits:

5 changed files:

Changes:

  • changelog.d/caf-deadlock-27272
    1
    +section: rts
    
    2
    +synopsis: Fix a deadlock where threads could block forever on a string literal
    
    3
    +  if entering the string literal triggered a stack overflow.
    
    4
    +mrs: !16849
    
    5
    +issues: #27272

  • rts/StgStdThunks.cmm
    ... ... @@ -342,18 +342,11 @@ INFO_TABLE(stg_ap_7_upd,7,0,THUNK,"stg_ap_7_upd_info","stg_ap_7_upd_info")
    342 342
      *
    
    343 343
      */
    
    344 344
     
    
    345
    -stg_do_unpack_cstring(P_ node, P_ newCAF_ret) {
    
    346
    -    STK_CHK_PP(WDS(SIZEOF_StgUpdateFrame), stg_do_unpack_cstring, node, newCAF_ret);
    
    347
    -    W_ str;
    
    348
    -    str = StgThunk_payload(node, 2);
    
    349
    -    push (UPDATE_FRAME_FIELDS(,,stg_bh_upd_frame_info, CCCS, 0, newCAF_ret)) {
    
    350
    -        jump %ENTRY_CODE(HsIface_unpackCStringzh_info(W_[ghc_hs_iface]))(node, str);
    
    351
    -    }
    
    352
    -}
    
    353
    -
    
    354 345
     INFO_TABLE(stg_unpack_cstring, 0, 0, THUNK_STATIC, "stg_unpack_cstring", "stg_unpack_cstring")
    
    355 346
         (P_ node)
    
    356 347
     {
    
    348
    +    // The stack check needs to come before newCAF: see #27272
    
    349
    +    STK_CHK_NP(node);
    
    357 350
         W_ newCAF_ret;
    
    358 351
         (newCAF_ret) = ccall newCAF(BaseReg "ptr", node "ptr");
    
    359 352
     
    
    ... ... @@ -362,22 +355,19 @@ INFO_TABLE(stg_unpack_cstring, 0, 0, THUNK_STATIC, "stg_unpack_cstring", "stg_un
    362 355
             // `node` should now be an indirection.
    
    363 356
             ENTER(node);
    
    364 357
         } else {
    
    365
    -        jump stg_do_unpack_cstring(node, newCAF_ret);
    
    366
    -    }
    
    367
    -}
    
    368
    -
    
    369
    -stg_do_unpack_cstring_utf8(P_ node, P_ newCAF_ret) {
    
    370
    -    STK_CHK_PP(WDS(SIZEOF_StgUpdateFrame), stg_do_unpack_cstring_utf8, node, newCAF_ret);
    
    371
    -    W_ str;
    
    372
    -    str = StgThunk_payload(node, 2);
    
    373
    -    push (UPDATE_FRAME_FIELDS(,,stg_bh_upd_frame_info, CCCS, 0, newCAF_ret)) {
    
    374
    -        jump %ENTRY_CODE(HsIface_unpackCStringUtf8zh_info(W_[ghc_hs_iface]))(node, str);
    
    358
    +        W_ str;
    
    359
    +        str = StgThunk_payload(node, 2);
    
    360
    +        push (UPDATE_FRAME_FIELDS(,,stg_bh_upd_frame_info, CCCS, 0, newCAF_ret)) {
    
    361
    +            jump %ENTRY_CODE(HsIface_unpackCStringzh_info(W_[ghc_hs_iface]))(node, str);
    
    362
    +        }
    
    375 363
         }
    
    376 364
     }
    
    377 365
     
    
    378 366
     INFO_TABLE(stg_unpack_cstring_utf8, 0, 0, THUNK_STATIC, "stg_unpack_cstring_utf8", "stg_unpack_cstring_utf8")
    
    379 367
         (P_ node)
    
    380 368
     {
    
    369
    +    // The stack check needs to come before newCAF: see #27272
    
    370
    +    STK_CHK_NP(node);
    
    381 371
         W_ newCAF_ret;
    
    382 372
         (newCAF_ret) = ccall newCAF(BaseReg "ptr", node "ptr");
    
    383 373
     
    
    ... ... @@ -386,6 +376,10 @@ INFO_TABLE(stg_unpack_cstring_utf8, 0, 0, THUNK_STATIC, "stg_unpack_cstring_utf8
    386 376
             // `node` should now be an indirection.
    
    387 377
             ENTER(node);
    
    388 378
         } else {
    
    389
    -        jump stg_do_unpack_cstring_utf8(node, newCAF_ret);
    
    379
    +        W_ str;
    
    380
    +        str = StgThunk_payload(node, 2);
    
    381
    +        push (UPDATE_FRAME_FIELDS(,,stg_bh_upd_frame_info, CCCS, 0, newCAF_ret)) {
    
    382
    +            jump %ENTRY_CODE(HsIface_unpackCStringUtf8zh_info(W_[ghc_hs_iface]))(node, str);
    
    383
    +        }
    
    390 384
         }
    
    391 385
     }

  • testsuite/tests/rts/T27272.hs
    1
    +{-# LANGUAGE CPP, MagicHash, UnboxedTuples #-}
    
    2
    +-- Test for deadlocks related to string literal CAFs
    
    3
    +--
    
    4
    +-- Thread A enters a shared thunk X, and from X a string literal whose stack
    
    5
    +-- check fails while another thread B owns X. threadPaused then suspends A's
    
    6
    +-- work on X. If A still owns the literal, B blocks on it forever (#27272).
    
    7
    +module Main (main) where
    
    8
    +
    
    9
    +import T27272_Lits (asciiLits, utf8Lits)
    
    10
    +
    
    11
    +import Control.Concurrent
    
    12
    +import Control.Exception (evaluate)
    
    13
    +import Control.Monad
    
    14
    +import Data.Char (ord)
    
    15
    +import Data.IORef
    
    16
    +import GHC.Conc (BlockReason (..), ThreadStatus (..), threadStatus)
    
    17
    +import GHC.Exts
    
    18
    +import GHC.IO (IO (..))
    
    19
    +import System.Exit
    
    20
    +import System.Timeout
    
    21
    +#if defined(SELF_CHECK)
    
    22
    +import GHC.Exts.Heap
    
    23
    +  (ClosureType (..), GenClosure (..), StgInfoTable (..), getClosureData)
    
    24
    +#endif
    
    25
    +
    
    26
    +-- | Per literal: the gate (0 until A is in X, 1 once it is, 2 once B is in X
    
    27
    +-- too, 3 once B has claimed X), A's thread id, and for the self check whether
    
    28
    +-- A was suspended at the literal's stack check.
    
    29
    +data Gate = Gate (MutableByteArray# RealWorld) (MVar ThreadId) (IORef Bool)
    
    30
    +
    
    31
    +newGate :: IO Gate
    
    32
    +newGate = do
    
    33
    +  aVar <- newEmptyMVar
    
    34
    +  suspended <- newIORef False
    
    35
    +  IO $ \s -> case newByteArray# 8# s of
    
    36
    +    (# s1, g #) -> case writeIntArray# g 0# 0# s1 of
    
    37
    +      s2 -> (# s2, Gate g aVar suspended #)
    
    38
    +
    
    39
    +readGate :: Gate -> IO Int
    
    40
    +readGate (Gate g _ _) = IO $ \s -> case atomicReadIntArray# g 0# s of
    
    41
    +  (# s', n #) -> (# s', I# n #)
    
    42
    +
    
    43
    +data Box = Box String
    
    44
    +
    
    45
    +-- | Force the boxed string under @n@ one-word return frames. 'lazy' hides
    
    46
    +-- the strictness in @s@, otherwise the string is evaluated before the call.
    
    47
    +-- The frames are only one word with -O (see all.T).
    
    48
    +deepFrames :: Int -> Box -> Int
    
    49
    +deepFrames 0 (Box s) = case lazy s of
    
    50
    +  c : _ -> ord c
    
    51
    +  []    -> -1
    
    52
    +deepFrames n b = 1 + deepFrames (n - 1) b
    
    53
    +{-# NOINLINE deepFrames #-}
    
    54
    +
    
    55
    +-- | Build the shared thunk X for a literal and return it unevaluated.
    
    56
    +-- A's path through X only uses inline primops and does not allocate: any
    
    57
    +-- return to the scheduler would claim X for A. The spin is bounded because it
    
    58
    +-- holds up GC syncs.
    
    59
    +mkShared :: Gate -> String -> IO String
    
    60
    +mkShared gate@(Gate g _ _) lit = return x
    
    61
    +  where
    
    62
    +    x = runRW# (\s0 -> case fetchAddIntArray# g 0# 1# s0 of
    
    63
    +      (# s1, 0# #) -> case spin 100000000# s1 of (# _, () #) -> lit
    
    64
    +      (# s1, _ #) -> case claimX gate lit of
    
    65
    +        IO m -> case m s1 of (# _, r #) -> r)
    
    66
    +    spin :: Int# -> State# RealWorld -> (# State# RealWorld, () #)
    
    67
    +    spin 0# s = (# s, () #)
    
    68
    +    spin n s = case atomicReadIntArray# g 0# s of
    
    69
    +      (# s', 3# #) -> (# s', () #)
    
    70
    +      (# s', _ #) -> spin (n -# 1#) s'
    
    71
    +{-# NOINLINE mkShared #-}
    
    72
    +
    
    73
    +-- | B's part of the gate. The yield calls threadPaused, which claims X for B,
    
    74
    +-- and B only enters the literal once A is blocked (suspended) or done.
    
    75
    +-- It is OPAQUE and returns the literal, so X saves nothing across the call:
    
    76
    +-- X's stack check stays small enough for the literal's check to fail.
    
    77
    +claimX :: Gate -> String -> IO String
    
    78
    +claimX (Gate g aVar suspended) lit = do
    
    79
    +  yield
    
    80
    +  IO $ \s -> (# atomicWriteIntArray# g 0# 3# s, () #)
    
    81
    +  a <- readMVar aVar
    
    82
    +  let waitA = do
    
    83
    +        st <- threadStatus a
    
    84
    +        case st of
    
    85
    +          ThreadRunning -> yield >> waitA
    
    86
    +          ThreadBlocked BlockedOnBlackHole ->
    
    87
    +            writeIORef suspended =<< suspendedAtCheck lit
    
    88
    +          _ -> return ()
    
    89
    +  waitA
    
    90
    +  return lit
    
    91
    +{-# OPAQUE claimX #-}
    
    92
    +
    
    93
    +-- Self check of the test, enabled with -DSELF_CHECK -package ghc-heap: fail
    
    94
    +-- if no literal entry was suspended at the literal's own stack check, i.e. if
    
    95
    +-- the test did not exercise the bug. The literal is then still unevaluated.
    
    96
    +suspendedAtCheck :: String -> IO Bool
    
    97
    +selfCheck :: [Bool] -> IO ()
    
    98
    +#if defined(SELF_CHECK)
    
    99
    +suspendedAtCheck lit = do
    
    100
    +  c <- getClosureData lit
    
    101
    +  return $ case c of
    
    102
    +    ThunkClosure { info = i } -> tipe i == THUNK_STATIC
    
    103
    +    _ -> False
    
    104
    +selfCheck results =
    
    105
    +  unless (or results) $ failWith "no literal entry was suspended"
    
    106
    +#else
    
    107
    +suspendedAtCheck _ = return False
    
    108
    +selfCheck _ = return ()
    
    109
    +#endif
    
    110
    +
    
    111
    +failWith :: String -> IO a
    
    112
    +failWith msg = putStrLn msg >> exitFailure
    
    113
    +
    
    114
    +main :: IO ()
    
    115
    +main = do
    
    116
    +  let expect c fmt = [ (c, c : shows i fmt) | i <- [0 :: Int ..] ]
    
    117
    +      lits = zip3 [0 ..] asciiLits (expect 'A' "-ABCDEFGHIJKLMNOPQRSTUVWXYZ") ++
    
    118
    +             zip3 [0 ..] utf8Lits (expect 'U' "-λμν-ÄÖÜ-€")
    
    119
    +  -- Literal d is entered under d frames. With the stack sizes from all.T this
    
    120
    +  -- crosses stack chunk ends, so the literal's stack check fails for some d.
    
    121
    +  results <- forM lits $ \(d, lit, (c, e)) -> do
    
    122
    +    gate@(Gate _ aVar suspended) <- newGate
    
    123
    +    x <- mkShared gate lit
    
    124
    +    doneA <- newEmptyMVar
    
    125
    +    doneB <- newEmptyMVar
    
    126
    +    -- New threads for each literal, so that A's stack only depends on d. A and
    
    127
    +    -- B have their own capabilities (-N3), so that B runs while A spins.
    
    128
    +    a <- forkOn 1 $ do
    
    129
    +      v <- evaluate (deepFrames d (Box x))
    
    130
    +      putMVar doneA (v == d + ord c && x == e)
    
    131
    +    putMVar aVar a
    
    132
    +    _ <- forkOn 2 $ do
    
    133
    +      -- B must only enter X once A is in it
    
    134
    +      let waitA = do
    
    135
    +            n <- readGate gate
    
    136
    +            when (n == 0) (yield >> waitA)
    
    137
    +      waitA
    
    138
    +      _ <- evaluate x
    
    139
    +      putMVar doneB ()
    
    140
    +    -- With the bug, A blocks on X and B on the literal: the timeout catches it
    
    141
    +    r <- timeout 10000000 (takeMVar doneA <* takeMVar doneB)
    
    142
    +    case r of
    
    143
    +      Just True -> readIORef suspended
    
    144
    +      Just False -> failWith ("wrong result at depth " ++ show d)
    
    145
    +      Nothing -> failWith ("deadlock at depth " ++ show d)
    
    146
    +  selfCheck results

  • testsuite/tests/rts/T27272_gen.py
    1
    +#!/usr/bin/env python3
    
    2
    +# usage: T27272_gen.py N > T27272_Lits.hs
    
    3
    +import sys
    
    4
    +
    
    5
    +n = int(sys.argv[1])
    
    6
    +out = sys.stdout
    
    7
    +out.reconfigure(encoding="utf-8")
    
    8
    +
    
    9
    +kinds = [
    
    10
    +    ("a", "asciiLits", "A{}-ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
    
    11
    +    ("u", "utf8Lits", "U{}-λμν-ÄÖÜ-€"),
    
    12
    +]
    
    13
    +
    
    14
    +out.write("module T27272_Lits (asciiLits, utf8Lits) where\n")
    
    15
    +for prefix, list_name, fmt in kinds:
    
    16
    +    out.write("\n")
    
    17
    +    for i in range(n):
    
    18
    +        out.write(f'{prefix}{i} :: String\n{prefix}{i} = "{fmt.format(i)}"\n')
    
    19
    +    out.write(f"\n{list_name} :: [String]\n{list_name} =\n  [ {prefix}0\n")
    
    20
    +    for i in range(1, n):
    
    21
    +        out.write(f"  , {prefix}{i}\n")
    
    22
    +    out.write("  ]\n")

  • testsuite/tests/rts/all.T
    ... ... @@ -647,3 +647,12 @@ test('T27477',
    647 647
          , only_ways(['threaded1', 'threaded2'])
    
    648 648
          ],
    
    649 649
          compile_and_run, ['-O2'])
    
    650
    +
    
    651
    +test('T27272',
    
    652
    +     [ req_target_smp
    
    653
    +     , omit_ghci
    
    654
    +     , omit_ways(['hpc'])
    
    655
    +     , pre_cmd('$PYTHON T27272_gen.py 200 > T27272_Lits.hs')
    
    656
    +     , extra_run_opts('+RTS -N3 -ki512 -kc1k -kb128 -RTS')
    
    657
    +     ],
    
    658
    +     multimod_compile_and_run, ['T27272', '-O -threaded -fno-prof-auto'])