Magnus pushed to branch wip/mangoiv/ghc-9.12-bp at Glasgow Haskell Compiler / GHC Commits: 693428b4 by Wen Kokke at 2026-05-14T17:10:28+02:00 Fix race condition between flushEventLog and start/endEventLogging. This commit changes `flushEventLog` to acquire/release the `state_change` mutex to prevent interleaving with `startEventLogging` and `endEventLogging`. In the current RTS, `flushEventLog` _does not_ acquire this mutex, which may lead to eventlog corruption on the following interleaving: - `startEventLogging` writes the new `EventLogWriter` to `event_log_writer`. - `flushEventLog` flushes some events to `event_log_writer`. - `startEventLogging` writes the eventlog header to `event_log_writer`. This causes the eventlog to be written out in an unreadable state, with one or more events preceding the eventlog header. This commit renames the old function to `flushEventLog_` and defines `flushEventLog` simply as: ```c void flushEventLog(Capability **cap USED_IF_THREADS) { ACQUIRE_LOCK(&state_change_mutex); flushEventLog_(cap); RELEASE_LOCK(&state_change_mutex); } ``` The old function is still needed internally within the compilation unit, where it is used in `endEventLogging` in a context where the `state_change` mutex has already been acquired. I've chosen to mark `flushEventLog_` as static and let other uses of `flushEventLog` within the RTS refer to the new version. There is one use in `hs_init_ghc` via `flushTrace`, where the new locking behaviour should be harmless, and one use in `handle_tick`, which I believe was likely vulnerable to the same race condition, so the new locking behaviour is desirable. I have not added a test. The behaviour is highly non-deterministic and requires a program that concurrently calls `flushEventLog` and `startEventLogging`/`endEventLogging`. I encountered the issue while developing `eventlog-socket` and within that context have verified that my patch likely addresses the issue: a test that used to fail within the first dozen or so runs now has been running on repeat for several hours. (cherry picked from commit 3d6492ce311611707e80b2594103ddbe93fc6c76) - - - - - eb7e1e0f by Luite Stegeman at 2026-05-15T09:49:36+02:00 Windows: remove StgAsyncIOResult and fix crash/leaks In stg_block_async{_void}, a stack slot was reserved for an StgAsyncIOResult. This slot would be filled by the IO manager upon completion of the async call. However, if the blocked thread was interrupted by an async exception, we would end up in an invalid state: - If the blocked computation was never re-entered, the StgAsyncIOResult would never be freed. - If the blocked computation was re-entered, the thread would find an unitialized stack slot for the StgAsyncIOResult, leading to a crash reading its fields, or freeing the pointer. We fix this by removing the StgAsyncIOResult altogether and writing the result directly to the stack. Fixes #26341 (cherry picked from commit fcf092dda534cc38637d1f7920aa0dae58fe5273) - - - - - 18 changed files: - rts/HeapStackCheck.cmm - rts/IOManager.c - rts/PrimOps.cmm - rts/RtsSymbols.c - rts/Threads.c - rts/eventlog/EventLog.c - rts/include/rts/storage/TSO.h - rts/include/stg/MiscClosures.h - rts/win32/AsyncMIO.c - rts/win32/AsyncMIO.h - + testsuite/tests/concurrent/should_run/T26341.hs - + testsuite/tests/concurrent/should_run/T26341.stdout - + testsuite/tests/concurrent/should_run/T26341a.hs - + testsuite/tests/concurrent/should_run/T26341a.stdout - + testsuite/tests/concurrent/should_run/T26341b.hs - + testsuite/tests/concurrent/should_run/T26341b.stdout - testsuite/tests/concurrent/should_run/all.T - utils/deriveConstants/Main.hs Changes: ===================================== rts/HeapStackCheck.cmm ===================================== @@ -706,38 +706,24 @@ stg_block_throwto (P_ tso, P_ exception) } #if defined(mingw32_HOST_OS) -INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ ares ) +INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ len, W_ errCode ) return () { - W_ len, errC; - - len = TO_W_(StgAsyncIOResult_len(ares)); - errC = TO_W_(StgAsyncIOResult_errCode(ares)); - ccall free(ares "ptr"); - return (len, errC); + return (len, errCode); } stg_block_async { - Sp_adj(-2); - Sp(0) = stg_block_async_info; - BLOCK_GENERIC; -} + W_ eintr; + (eintr) = ccall rts_EINTR(); -/* Used by threadDelay implementation; it would be desirable to get rid of - * this free()'ing void return continuation. - */ -INFO_TABLE_RET ( stg_block_async_void, RET_SMALL, W_ info_ptr, W_ ares ) - return () -{ - ccall free(ares "ptr"); - return (); -} - -stg_block_async_void -{ - Sp_adj(-2); - Sp(0) = stg_block_async_void_info; + // Fill the stack frame with values that indicate that the operation + // has been interrupted. The IO manager will overwrite these with the + // actual results if the async operation completes. + Sp_adj(-3); + Sp(0) = stg_block_async_info; + Sp(1) = -1; // len: -1 indicates error + Sp(2) = eintr; // errCode: interrupted BLOCK_GENERIC; } ===================================== rts/IOManager.c ===================================== @@ -561,10 +561,8 @@ void scavengeTSOIOManager(StgTSO *tso) */ /* case IO_MANAGER_WIN32_LEGACY: - * BlockedOn{Read,Write,DoProc} uses block_info.async_result - * The StgAsyncIOResult async_result is allocated on the C heap. - * It'd probably be better if it used the GC heap. If it did we'd - * scavenge it here. + * BlockedOn{Read,Write,DoProc} uses block_info.async_reqID + * which is a plain integer, so nothing to scavenge. */ default: @@ -747,7 +745,7 @@ void syncIOCancel(Capability *cap, StgTSO *tso) case IO_MANAGER_WIN32_LEGACY: removeThreadFromDeQueue(cap, &cap->iomgr->blocked_queue_hd, &cap->iomgr->blocked_queue_tl, tso); - abandonWorkRequest(tso->block_info.async_result->reqID); + abandonWorkRequest(tso->block_info.async_reqID); break; #endif default: @@ -782,12 +780,7 @@ void syncDelay(Capability *cap, StgTSO *tso, HsInt us_delay) * would make the primops more consistent. */ { - StgAsyncIOResult *ares = stgMallocBytes(sizeof(StgAsyncIOResult), - "syncDelay"); - ares->reqID = addDelayRequest(us_delay); - ares->len = 0; - ares->errCode = 0; - tso->block_info.async_result = ares; + tso->block_info.async_reqID = addDelayRequest(us_delay); /* Having all async-blocked threads reside on the blocked_queue * simplifies matters, so set the status to OnDoProc and put the ===================================== rts/PrimOps.cmm ===================================== @@ -2575,40 +2575,29 @@ stg_waitWritezh ( W_ fd ) stg_delayzh ( W_ us_delay ) { - ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay); + CBool ok; - /* Annoyingly, we cannot be consistent with how we wait and resume the - * blocked thread. The reason is that the win32 legacy I/O manager - * allocates a StgAsyncIOResult struct on the C heap which has to be - * freed when the thread resumes. It's a bit awkward to arrange to - * allocate it on the GC heap instead, so that's how it is for now. Sigh. - */ -#if defined(mingw32_HOST_OS) - jump stg_block_async_void(); -#else - jump stg_block_noregs(); -#endif + (ok) = ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay); + + if (ok != 0::CBool) (likely: True) { + jump stg_block_noregs(); + } else { + jump stg_raisezh(HsIface_heapOverflow_closure(W_[ghc_hs_iface])); + } } #if defined(mingw32_HOST_OS) stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf ) { - W_ ares; CInt reqID; #if defined(THREADED_RTS) ccall barf("asyncRead# on threaded RTS") never returns; #else - /* could probably allocate this on the heap instead */ - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult, - "stg_asyncReadzh"); (reqID) = ccall addIORequest(fd, 0/*FALSE*/,is_sock,len,buf "ptr"); - StgAsyncIOResult_reqID(ares) = reqID; - StgAsyncIOResult_len(ares) = 0; - StgAsyncIOResult_errCode(ares) = 0; - StgTSO_block_info(CurrentTSO) = ares; + StgTSO_block_info(CurrentTSO) = reqID; ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32); %release StgTSO_why_blocked(CurrentTSO) = BlockedOnRead::I32; @@ -2620,21 +2609,14 @@ stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf ) stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf ) { - W_ ares; CInt reqID; #if defined(THREADED_RTS) ccall barf("asyncWrite# on threaded RTS") never returns; #else - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult, - "stg_asyncWritezh"); (reqID) = ccall addIORequest(fd, 1/*TRUE*/,is_sock,len,buf "ptr"); - - StgAsyncIOResult_reqID(ares) = reqID; - StgAsyncIOResult_len(ares) = 0; - StgAsyncIOResult_errCode(ares) = 0; - StgTSO_block_info(CurrentTSO) = ares; + StgTSO_block_info(CurrentTSO) = reqID; ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32); %release StgTSO_why_blocked(CurrentTSO) = BlockedOnWrite::I32; @@ -2646,21 +2628,14 @@ stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf ) stg_asyncDoProczh ( W_ proc, W_ param ) { - W_ ares; CInt reqID; #if defined(THREADED_RTS) ccall barf("asyncDoProc# on threaded RTS") never returns; #else - /* could probably allocate this on the heap instead */ - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult, - "stg_asyncDoProczh"); (reqID) = ccall addDoProcRequest(proc "ptr",param "ptr"); - StgAsyncIOResult_reqID(ares) = reqID; - StgAsyncIOResult_len(ares) = 0; - StgAsyncIOResult_errCode(ares) = 0; - StgTSO_block_info(CurrentTSO) = ares; + StgTSO_block_info(CurrentTSO) = reqID; ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32); %release StgTSO_why_blocked(CurrentTSO) = BlockedOnDoProc::I32; ===================================== rts/RtsSymbols.c ===================================== @@ -30,6 +30,7 @@ #include <shfolder.h> /* SHGetFolderPathW */ #include "IOManager.h" #include "win32/AsyncWinIO.h" +#include "win32/AsyncMIO.h" #endif #if defined(openbsd_HOST_OS) @@ -168,6 +169,7 @@ extern char **environ; SymI_HasProto(__stdio_common_vswprintf_s) \ SymI_HasProto(__stdio_common_vswprintf) \ SymI_HasProto(_errno) \ + SymI_HasProto(rts_EINTR) \ /* see Note [Symbols for MinGW's printf] */ \ SymI_HasProto(_lock_file) \ SymI_HasProto(_unlock_file) \ ===================================== rts/Threads.c ===================================== @@ -928,7 +928,7 @@ printThreadBlockage(StgTSO *tso) switch (ACQUIRE_LOAD(&tso->why_blocked)) { #if defined(mingw32_HOST_OS) case BlockedOnDoProc: - debugBelch("is blocked on proc (request: %u)", tso->block_info.async_result->reqID); + debugBelch("is blocked on proc (request: %" FMT_Word ")", tso->block_info.async_reqID); break; #endif #if !defined(THREADED_RTS) ===================================== rts/eventlog/EventLog.c ===================================== @@ -154,6 +154,8 @@ static void freeEventLoggingBuffer(void); static void ensureRoomForEvent(EventsBuf *eb, EventTypeNum tag); static int ensureRoomForVariableEvent(EventsBuf *eb, StgWord size); +static void flushEventLog_(Capability **cap USED_IF_THREADS); + static inline void postWord8(EventsBuf *eb, StgWord8 i) { *(eb->pos++) = i; @@ -484,7 +486,7 @@ endEventLogging(void) eventlog_enabled = false; - flushEventLog(NULL); + flushEventLog_(NULL); ACQUIRE_LOCK(&eventBufMutex); @@ -1607,6 +1609,17 @@ void flushAllCapsEventsBufs(void) } void flushEventLog(Capability **cap USED_IF_THREADS) +{ + ACQUIRE_LOCK(&state_change_mutex); + flushEventLog_(cap); + RELEASE_LOCK(&state_change_mutex); +} + +// This is an unsafe version of flushEventLog that does not acquire/release the +// state_change mutex. It is for internal use only and should only be used when +// (1) you're sure that there's no chance of racing with start/endEventLogging, +// and (2) there is an event_log_writer. +static void flushEventLog_(Capability **cap USED_IF_THREADS) { if (!event_log_writer) { return; @@ -1636,7 +1649,7 @@ void flushEventLog(Capability **cap USED_IF_THREADS) flushEventLogWriter(); } -#else +#else /*!TRACING*/ enum EventLogStatus eventLogStatus(void) { ===================================== rts/include/rts/storage/TSO.h ===================================== @@ -37,15 +37,6 @@ typedef StgWord64 StgThreadID; */ typedef unsigned int StgThreadReturnCode; -#if defined(mingw32_HOST_OS) -/* results from an async I/O request + its request ID. */ -typedef struct { - unsigned int reqID; - int len; - int errCode; -} StgAsyncIOResult; -#endif - /* Reason for thread being blocked. See comment above struct StgTso_. */ typedef union { StgClosure *closure; @@ -55,7 +46,7 @@ typedef union { struct MessageWakeup_ *wakeup; StgInt fd; /* StgInt instead of int, so that it's the same size as the ptrs */ #if defined(mingw32_HOST_OS) - StgAsyncIOResult *async_result; + StgWord async_reqID; #endif #if !defined(THREADED_RTS) StgWord target; ===================================== rts/include/stg/MiscClosures.h ===================================== @@ -430,8 +430,6 @@ RTS_RET(stg_block_putmvar); #if defined(mingw32_HOST_OS) RTS_FUN_DECL(stg_block_async); RTS_RET(stg_block_async); -RTS_FUN_DECL(stg_block_async_void); -RTS_RET(stg_block_async_void); #endif RTS_FUN_DECL(stg_block_stmwait); RTS_FUN_DECL(stg_block_throwto); ===================================== rts/win32/AsyncMIO.c ===================================== @@ -8,16 +8,19 @@ * For the WINIO manager see base in the GHC.Event modules. */ -#if !defined(THREADED_RTS) #include "Rts.h" +#include <errno.h> +#include "win32/AsyncMIO.h" + +#if !defined(THREADED_RTS) + #include "RtsUtils.h" #include <windows.h> #include <stdio.h> #include "Schedule.h" #include "Capability.h" #include "IOManagerInternals.h" -#include "win32/AsyncMIO.h" #include "win32/MIOManager.h" /* @@ -299,14 +302,9 @@ start: case BlockedOnRead: case BlockedOnWrite: case BlockedOnDoProc: - if (tso->block_info.async_result->reqID == rID) { - // Found the thread blocked waiting on request; - // stodgily fill - // in its result block. - tso->block_info.async_result->len = - completedTable[i].len; - tso->block_info.async_result->errCode = - completedTable[i].errCode; + if (tso->block_info.async_reqID == rID) { + HsInt len = completedTable[i].len; + HsInt errCode = completedTable[i].errCode; // Drop the matched TSO from blocked_queue if (prev) { @@ -322,11 +320,14 @@ start: // Terminates the run queue + this inner for-loop. tso->_link = END_TSO_QUEUE; tso->why_blocked = NotBlocked; - // save the StgAsyncIOResult in the - // stg_block_async_info stack frame, because - // the block_info field will be overwritten by - // pushOnRunQueue(). - tso->stackobj->sp[1] = (W_)tso->block_info.async_result; + // For stg_block_async frames (read/write/doProc), + // write len and errCode directly to the stack. + // For stg_block_noregs frames (delay), nothing + // to write. + if (tso->stackobj->sp[0] == (W_)&stg_block_async_info) { + tso->stackobj->sp[1] = (W_)len; + tso->stackobj->sp[2] = (W_)errCode; + } pushOnRunQueue(&MainCapability, tso); break; } @@ -389,3 +390,8 @@ resetAbandonRequestWait( void ) } #endif /* !defined(THREADED_RTS) */ + +HsInt rts_EINTR(void) +{ + return EINTR; +} ===================================== rts/win32/AsyncMIO.h ===================================== @@ -27,3 +27,4 @@ extern int awaitRequests(bool wait); extern void abandonRequestWait(void); extern void resetAbandonRequestWait(void); +extern HsInt rts_EINTR(void); ===================================== testsuite/tests/concurrent/should_run/T26341.hs ===================================== @@ -0,0 +1,20 @@ +{-# OPTIONS_GHC -O -fno-full-laziness #-} + +import Control.Concurrent (threadDelay, myThreadId, forkIO, killThread) +import System.IO.Unsafe (unsafePerformIO) +import Control.Exception +import GHC.Exts + +compute :: Int +compute = noinline unsafePerformIO $ do + mainThreadID <- myThreadId + _ <- forkIO $ do + threadDelay 500000 + killThread mainThreadID + threadDelay 1000000 + return 0 + +main = do + catch (print compute) (\(e :: AsyncException) -> print $ "1:" ++ show e) + catch (print compute) (\(e :: AsyncException) -> print $ "2:" ++ show e) + print "done" ===================================== testsuite/tests/concurrent/should_run/T26341.stdout ===================================== @@ -0,0 +1,3 @@ +"1:thread killed" +0 +"done" ===================================== testsuite/tests/concurrent/should_run/T26341a.hs ===================================== @@ -0,0 +1,75 @@ +-- Test that re-evaluating an AP_STACK from an interrupted async I/O call +-- does not crash. On Windows non-threaded RTS, re-entry returns EINTR +-- which readRawBufferPtr converts to IOException Interrupted. On the +-- threaded RTS (any platform), the blocking read is re-attempted and +-- succeeds because we write a byte to the pipe between evaluations. +-- +-- Before the fix for #26341, re-evaluation on Windows would crash or read +-- uninitialized memory from a freed StgAsyncIOResult. +{-# OPTIONS_GHC -O -fno-full-laziness #-} + +import Control.Concurrent (threadDelay, myThreadId, forkIO, killThread, rtsSupportsBoundThreads) +import Control.Exception +import Data.IORef +import Foreign +import Foreign.C +import GHC.Exts +import GHC.IO.Exception (IOErrorType(..), IOException(..)) +import GHC.IO.FD (FD(..), readRawBufferPtr, writeRawBufferPtr) +import System.Info (os) +import System.IO.Unsafe (unsafePerformIO) +import System.Process (createPipeFd) + +-- Store the write fd so main can feed data into the pipe between +-- evaluations. On Unix this unblocks the re-entered read; on Windows +-- stg_block_async returns EINTR regardless. +{-# NOINLINE writeFdRef #-} +writeFdRef :: IORef CInt +writeFdRef = unsafePerformIO $ newIORef (-1) + +-- | A thunk whose unsafePerformIO blocks on a pipe read. A forked +-- thread kills the main thread after 200ms, which creates an AP_STACK. +{-# NOINLINE blockedRead #-} +blockedRead :: () +blockedRead = noinline unsafePerformIO $ do + (readFd, writeFd) <- createPipeFd + writeIORef writeFdRef writeFd + buf <- mallocBytes 1 + mainTid <- myThreadId + _ <- forkIO $ do + threadDelay 200000 -- 200ms + killThread mainTid + -- readRawBufferPtr dispatches to asyncReadRawBufferPtr on Windows + -- non-threaded RTS; on Unix it uses threadWaitRead + read(). + _ <- readRawBufferPtr "blockedRead" (FD readFd 0) buf 0 1 + return () + +main :: IO () +main = do + -- First evaluation: the thunk blocks on the pipe read, gets killed. + catch (evaluate blockedRead) + (\(e :: AsyncException) -> putStrLn $ "caught: " ++ show e) + + -- Write a byte so the re-entered read can complete on Unix. + wfd <- readIORef writeFdRef + buf <- mallocBytes 1 + poke buf 0 + _ <- writeRawBufferPtr "unblock" (FD wfd 0) buf 0 1 + + -- Second evaluation: AP_STACK re-enters. + -- Non-threaded Windows: asyncRead returns (-1, EINTR) → IOException + -- Threaded / Unix: read succeeds → returns normally + let expectEINTR = os == "mingw32" && not rtsSupportsBoundThreads + result <- try (evaluate blockedRead) + case result of + Left e + | Just ioe <- fromException e + , ioe_type (ioe :: IOException) == Interrupted + -> putStrLn "re-evaluated ok" + | otherwise + -> putStrLn $ "unexpected: " ++ show e + Right () + | expectEINTR -> putStrLn "unexpected: expected EINTR" + | otherwise -> putStrLn "re-evaluated ok" + + putStrLn "done" ===================================== testsuite/tests/concurrent/should_run/T26341a.stdout ===================================== @@ -0,0 +1,3 @@ +caught: thread killed +re-evaluated ok +done ===================================== testsuite/tests/concurrent/should_run/T26341b.hs ===================================== @@ -0,0 +1,101 @@ +-- Stress test for #26341: repeatedly interrupt async-blocked threads and +-- re-enter their AP_STACKs. Before the fix, re-entering a thunk whose +-- unsafePerformIO was blocked on an async I/O call (Windows non-threaded +-- RTS) would read uninitialized memory or free a dangling pointer, +-- because stg_block_async reserved a stack slot for a heap-allocated +-- StgAsyncIOResult that became invalid after an async exception. +-- +-- This test spawns many concurrent workers, each of which: +-- 1. Creates a pipe. +-- 2. Builds a thunk that blocks on a pipe read via unsafePerformIO. +-- 3. Evaluates the thunk and kills it with an async exception. +-- 4. Re-evaluates the thunk (AP_STACK re-entry). +-- 5. Repeats many times. +-- +-- On threaded RTS / Unix the re-entered read succeeds (we write a byte +-- first). On Windows non-threaded RTS the re-entered async call returns +-- EINTR. Both paths exercise the fixed stack-frame layout. +{-# OPTIONS_GHC -O -fno-full-laziness #-} + +import Control.Concurrent +import Control.Exception +import Foreign +import Foreign.C +import GHC.Exts +import GHC.IO.Exception (IOErrorType(..), IOException(..)) +import GHC.IO.FD (FD(..), readRawBufferPtr, writeRawBufferPtr) +import System.IO (hFlush, stdout) +import System.IO.Unsafe (unsafePerformIO) +import System.Posix.Internals (c_close) +import System.Process (createPipeFd) + +iterations :: Int +iterations = 200 + +workers :: Int +workers = 4 + +-- Each worker independently performs `iterations` rounds of: +-- block on pipe read → interrupt → re-evaluate the AP_STACK. +worker :: Int -> MVar () -> IO () +worker wid done = do + buf <- mallocBytes 1 + let go 0 = return () + go n = do + (readFd, writeFd) <- createPipeFd + + -- Build a fresh thunk each iteration so we get a new AP_STACK. + let {-# NOINLINE blockedThunk #-} + blockedThunk :: () + blockedThunk = noinline unsafePerformIO $ do + tid <- myThreadId + _ <- forkIO $ do + threadDelay 1000 -- 1ms: tight window + killThread tid + _ <- readRawBufferPtr "stress" (FD readFd 0) buf 0 1 + return () + + -- First evaluation: block and get killed. + catch (evaluate blockedThunk) + (\(_ :: SomeException) -> return ()) + + -- Write a byte so the re-entered read can complete on + -- threaded RTS / Unix. + poke buf 0 + _ <- writeRawBufferPtr "unblock" (FD writeFd 0) buf 0 1 + + -- Second evaluation: AP_STACK re-entry. + result <- try (evaluate blockedThunk) + case result of + Left e + | Just ioe <- fromException e + , ioe_type (ioe :: IOException) == Interrupted + -> return () -- expected on Windows non-threaded + | otherwise + -> throwIO (userError $ + "worker " ++ show wid ++ " iteration " ++ show n ++ + ": unexpected exception: " ++ show e) + Right () -> return () -- expected on threaded / Unix + + -- Close the pipe fds. + _ <- c_close readFd + _ <- c_close writeFd + + go (n - 1) + + go iterations + putMVar done () + +main :: IO () +main = do + dones <- mapM (\wid -> do + done <- newEmptyMVar + _ <- forkIO (worker wid done) + return done + ) [1..workers] + + -- Wait for all workers to finish. + mapM_ takeMVar dones + + putStrLn "stress test passed" + hFlush stdout ===================================== testsuite/tests/concurrent/should_run/T26341b.stdout ===================================== @@ -0,0 +1 @@ +stress test passed ===================================== testsuite/tests/concurrent/should_run/all.T ===================================== @@ -298,3 +298,19 @@ test('hs_try_putmvar003', # Check forkIO exception determinism under optimization test('T13330', normal, compile_and_run, ['-O']) + +test('T26341', normal, compile_and_run, ['']) + +# Test EINTR for async I/O interrupted by an exception (#26341) +test('T26341a' + # test uses pipe operations which are not supported by the JS/wasm backends + , when(arch('wasm32') or arch('javascript'), skip) + , compile_and_run, ['-package process']) + +# Stress test: many threads repeatedly interrupt and re-enter async-blocked +# thunks (#26341). Before the fix, this would crash due to dangling +# StgAsyncIOResult pointers on the stack. +test('T26341b' + # test uses pipe operations which are not supported by the JS/wasm backends + , when(arch('wasm32') or arch('javascript'), skip) + , compile_and_run, ['-package process']) ===================================== utils/deriveConstants/Main.hs ===================================== @@ -646,12 +646,7 @@ wanteds os = concat -- Note that this conditional part only affects the C headers. -- That's important, as it means we get the same PlatformConstants -- type on all platforms. - ,if os == Just Windows - then concat [structSize C "StgAsyncIOResult" - ,structField C "StgAsyncIOResult" "reqID" - ,structField C "StgAsyncIOResult" "len" - ,structField C "StgAsyncIOResult" "errCode"] - else [] + ,[] -- struct HsIface ,structField C "HsIface" "processRemoteCompletion_closure" @@ -814,9 +809,6 @@ getWanted verbose os tmpdir gccProgram gccFlags nmProgram mobjdumpProgram "", "#define PROFILING", "#define THREADED_RTS", - -- We need to define this if we want StgAsyncIOResult - -- struct to be present after CPP - -- -- FIXME: rts/PosixSource.h should include ghcplatform.h -- which should set this. There is a mismatch host/target -- again... View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/9f20f0e9f5009557f9ef0c3268ef427... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/9f20f0e9f5009557f9ef0c3268ef427... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Magnus (@MangoIV)