Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
-
fcf092dd
by Luite Stegeman at 2026-03-27T04:44:17-04:00
17 changed files:
- rts/HeapStackCheck.cmm
- rts/IOManager.c
- rts/PrimOps.cmm
- rts/RtsSymbols.c
- rts/Threads.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:
| ... | ... | @@ -703,38 +703,24 @@ stg_block_throwto (P_ tso, P_ exception) |
| 703 | 703 | }
|
| 704 | 704 | |
| 705 | 705 | #if defined(mingw32_HOST_OS)
|
| 706 | -INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ ares )
|
|
| 706 | +INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ len, W_ errCode )
|
|
| 707 | 707 | return ()
|
| 708 | 708 | {
|
| 709 | - W_ len, errC;
|
|
| 710 | - |
|
| 711 | - len = TO_W_(StgAsyncIOResult_len(ares));
|
|
| 712 | - errC = TO_W_(StgAsyncIOResult_errCode(ares));
|
|
| 713 | - ccall free(ares "ptr");
|
|
| 714 | - return (len, errC);
|
|
| 709 | + return (len, errCode);
|
|
| 715 | 710 | }
|
| 716 | 711 | |
| 717 | 712 | stg_block_async
|
| 718 | 713 | {
|
| 719 | - Sp_adj(-2);
|
|
| 720 | - Sp(0) = stg_block_async_info;
|
|
| 721 | - BLOCK_GENERIC;
|
|
| 722 | -}
|
|
| 714 | + W_ eintr;
|
|
| 715 | + (eintr) = ccall rts_EINTR();
|
|
| 723 | 716 | |
| 724 | -/* Used by threadDelay implementation; it would be desirable to get rid of
|
|
| 725 | - * this free()'ing void return continuation.
|
|
| 726 | - */
|
|
| 727 | -INFO_TABLE_RET ( stg_block_async_void, RET_SMALL, W_ info_ptr, W_ ares )
|
|
| 728 | - return ()
|
|
| 729 | -{
|
|
| 730 | - ccall free(ares "ptr");
|
|
| 731 | - return ();
|
|
| 732 | -}
|
|
| 733 | - |
|
| 734 | -stg_block_async_void
|
|
| 735 | -{
|
|
| 736 | - Sp_adj(-2);
|
|
| 737 | - Sp(0) = stg_block_async_void_info;
|
|
| 717 | + // Fill the stack frame with values that indicate that the operation
|
|
| 718 | + // has been interrupted. The IO manager will overwrite these with the
|
|
| 719 | + // actual results if the async operation completes.
|
|
| 720 | + Sp_adj(-3);
|
|
| 721 | + Sp(0) = stg_block_async_info;
|
|
| 722 | + Sp(1) = -1; // len: -1 indicates error
|
|
| 723 | + Sp(2) = eintr; // errCode: interrupted
|
|
| 738 | 724 | BLOCK_GENERIC;
|
| 739 | 725 | }
|
| 740 | 726 |
| ... | ... | @@ -633,10 +633,8 @@ void scavengeTSOIOManager(StgTSO *tso) |
| 633 | 633 | #endif
|
| 634 | 634 | |
| 635 | 635 | /* case IO_MANAGER_WIN32_LEGACY:
|
| 636 | - * BlockedOn{Read,Write,DoProc} uses block_info.async_result
|
|
| 637 | - * The StgAsyncIOResult async_result is allocated on the C heap.
|
|
| 638 | - * It'd probably be better if it used the GC heap. If it did we'd
|
|
| 639 | - * scavenge it here.
|
|
| 636 | + * BlockedOn{Read,Write,DoProc} uses block_info.async_reqID
|
|
| 637 | + * which is a plain integer, so nothing to scavenge.
|
|
| 640 | 638 | */
|
| 641 | 639 | |
| 642 | 640 | default:
|
| ... | ... | @@ -846,7 +844,7 @@ void syncIOCancel(Capability *cap, StgTSO *tso) |
| 846 | 844 | case IO_MANAGER_WIN32_LEGACY:
|
| 847 | 845 | removeThreadFromDeQueue(cap, &cap->iomgr->blocked_queue_hd,
|
| 848 | 846 | &cap->iomgr->blocked_queue_tl, tso);
|
| 849 | - abandonWorkRequest(tso->block_info.async_result->reqID);
|
|
| 847 | + abandonWorkRequest(tso->block_info.async_reqID);
|
|
| 850 | 848 | break;
|
| 851 | 849 | #endif
|
| 852 | 850 | default:
|
| ... | ... | @@ -885,12 +883,7 @@ bool syncDelay(Capability *cap, StgTSO *tso, HsInt us_delay) |
| 885 | 883 | * would make the primops more consistent.
|
| 886 | 884 | */
|
| 887 | 885 | {
|
| 888 | - StgAsyncIOResult *ares = stgMallocBytes(sizeof(StgAsyncIOResult),
|
|
| 889 | - "syncDelay");
|
|
| 890 | - ares->reqID = addDelayRequest(us_delay);
|
|
| 891 | - ares->len = 0;
|
|
| 892 | - ares->errCode = 0;
|
|
| 893 | - tso->block_info.async_result = ares;
|
|
| 886 | + tso->block_info.async_reqID = addDelayRequest(us_delay);
|
|
| 894 | 887 | |
| 895 | 888 | /* Having all async-blocked threads reside on the blocked_queue
|
| 896 | 889 | * simplifies matters, so set the status to OnDoProc and put the
|
| ... | ... | @@ -2255,18 +2255,7 @@ stg_delayzh ( W_ us_delay ) |
| 2255 | 2255 | (ok) = ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay);
|
| 2256 | 2256 | |
| 2257 | 2257 | if (ok != 0::CBool) (likely: True) {
|
| 2258 | - /* Annoyingly, we cannot be consistent with how we wait and resume the
|
|
| 2259 | - * blocked thread. The reason is that the win32 legacy I/O manager
|
|
| 2260 | - * allocates a StgAsyncIOResult struct on the C heap which has to be
|
|
| 2261 | - * freed when the thread resumes. It's a bit awkward to arrange to
|
|
| 2262 | - * allocate it on the GC heap instead, so that's how it is for now.
|
|
| 2263 | - * Sigh.
|
|
| 2264 | - */
|
|
| 2265 | -#if defined(mingw32_HOST_OS)
|
|
| 2266 | - jump stg_block_async_void();
|
|
| 2267 | -#else
|
|
| 2268 | 2258 | jump stg_block_noregs();
|
| 2269 | -#endif
|
|
| 2270 | 2259 | } else {
|
| 2271 | 2260 | jump stg_raisezh(HsIface_heapOverflow_closure(W_[ghc_hs_iface]));
|
| 2272 | 2261 | }
|
| ... | ... | @@ -2276,21 +2265,14 @@ stg_delayzh ( W_ us_delay ) |
| 2276 | 2265 | #if defined(mingw32_HOST_OS)
|
| 2277 | 2266 | stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf )
|
| 2278 | 2267 | {
|
| 2279 | - W_ ares;
|
|
| 2280 | 2268 | CInt reqID;
|
| 2281 | 2269 | |
| 2282 | 2270 | #if defined(THREADED_RTS)
|
| 2283 | 2271 | ccall sbarf("asyncRead# on threaded RTS") never returns;
|
| 2284 | 2272 | #else
|
| 2285 | 2273 | |
| 2286 | - /* could probably allocate this on the heap instead */
|
|
| 2287 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2288 | - "stg_asyncReadzh");
|
|
| 2289 | 2274 | (reqID) = ccall addIORequest(fd, 0/*FALSE*/,is_sock,len,buf "ptr");
|
| 2290 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2291 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2292 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2293 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2275 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2294 | 2276 | |
| 2295 | 2277 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2296 | 2278 | %release StgTSO_why_blocked(CurrentTSO) = BlockedOnRead::I32;
|
| ... | ... | @@ -2302,21 +2284,14 @@ stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf ) |
| 2302 | 2284 | |
| 2303 | 2285 | stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf )
|
| 2304 | 2286 | {
|
| 2305 | - W_ ares;
|
|
| 2306 | 2287 | CInt reqID;
|
| 2307 | 2288 | |
| 2308 | 2289 | #if defined(THREADED_RTS)
|
| 2309 | 2290 | ccall sbarf("asyncWrite# on threaded RTS") never returns;
|
| 2310 | 2291 | #else
|
| 2311 | 2292 | |
| 2312 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2313 | - "stg_asyncWritezh");
|
|
| 2314 | 2293 | (reqID) = ccall addIORequest(fd, 1/*TRUE*/,is_sock,len,buf "ptr");
|
| 2315 | - |
|
| 2316 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2317 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2318 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2319 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2294 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2320 | 2295 | |
| 2321 | 2296 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2322 | 2297 | %release StgTSO_why_blocked(CurrentTSO) = BlockedOnWrite::I32;
|
| ... | ... | @@ -2328,21 +2303,14 @@ stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf ) |
| 2328 | 2303 | |
| 2329 | 2304 | stg_asyncDoProczh ( W_ proc, W_ param )
|
| 2330 | 2305 | {
|
| 2331 | - W_ ares;
|
|
| 2332 | 2306 | CInt reqID;
|
| 2333 | 2307 | |
| 2334 | 2308 | #if defined(THREADED_RTS)
|
| 2335 | 2309 | ccall sbarf("asyncDoProc# on threaded RTS") never returns;
|
| 2336 | 2310 | #else
|
| 2337 | 2311 | |
| 2338 | - /* could probably allocate this on the heap instead */
|
|
| 2339 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2340 | - "stg_asyncDoProczh");
|
|
| 2341 | 2312 | (reqID) = ccall addDoProcRequest(proc "ptr",param "ptr");
|
| 2342 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2343 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2344 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2345 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2313 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2346 | 2314 | |
| 2347 | 2315 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2348 | 2316 | %release StgTSO_why_blocked(CurrentTSO) = BlockedOnDoProc::I32;
|
| ... | ... | @@ -30,6 +30,7 @@ |
| 30 | 30 | #include <shfolder.h> /* SHGetFolderPathW */
|
| 31 | 31 | #include "IOManager.h"
|
| 32 | 32 | #include "win32/AsyncWinIO.h"
|
| 33 | +#include "win32/AsyncMIO.h"
|
|
| 33 | 34 | #endif
|
| 34 | 35 | |
| 35 | 36 | #if defined(openbsd_HOST_OS)
|
| ... | ... | @@ -168,6 +169,7 @@ extern char **environ; |
| 168 | 169 | SymI_HasProto(__stdio_common_vswprintf_s) \
|
| 169 | 170 | SymI_HasProto(__stdio_common_vswprintf) \
|
| 170 | 171 | SymI_HasProto(_errno) \
|
| 172 | + SymI_HasProto(rts_EINTR) \
|
|
| 171 | 173 | /* see Note [Symbols for MinGW's printf] */ \
|
| 172 | 174 | SymI_HasProto(_lock_file) \
|
| 173 | 175 | SymI_HasProto(_unlock_file) \
|
| ... | ... | @@ -926,7 +926,7 @@ printThreadBlockage(StgTSO *tso) |
| 926 | 926 | switch (ACQUIRE_LOAD(&tso->why_blocked)) {
|
| 927 | 927 | #if defined(mingw32_HOST_OS)
|
| 928 | 928 | case BlockedOnDoProc:
|
| 929 | - debugBelch("is blocked on proc (request: %u)", tso->block_info.async_result->reqID);
|
|
| 929 | + debugBelch("is blocked on proc (request: %" FMT_Word ")", tso->block_info.async_reqID);
|
|
| 930 | 930 | break;
|
| 931 | 931 | #endif
|
| 932 | 932 | #if !defined(THREADED_RTS)
|
| ... | ... | @@ -37,15 +37,6 @@ typedef StgWord64 StgThreadID; |
| 37 | 37 | */
|
| 38 | 38 | typedef unsigned int StgThreadReturnCode;
|
| 39 | 39 | |
| 40 | -#if defined(mingw32_HOST_OS)
|
|
| 41 | -/* results from an async I/O request + its request ID. */
|
|
| 42 | -typedef struct {
|
|
| 43 | - unsigned int reqID;
|
|
| 44 | - int len;
|
|
| 45 | - int errCode;
|
|
| 46 | -} StgAsyncIOResult;
|
|
| 47 | -#endif
|
|
| 48 | - |
|
| 49 | 40 | /* Reason for thread being blocked. See comment above struct StgTso_. */
|
| 50 | 41 | typedef union {
|
| 51 | 42 | StgClosure *closure;
|
| ... | ... | @@ -57,7 +48,7 @@ typedef union { |
| 57 | 48 | StgAsyncIOOp *aiop;
|
| 58 | 49 | StgTimeoutQueue *timeout;
|
| 59 | 50 | #if defined(mingw32_HOST_OS)
|
| 60 | - StgAsyncIOResult *async_result;
|
|
| 51 | + StgWord async_reqID;
|
|
| 61 | 52 | #endif
|
| 62 | 53 | #if !defined(THREADED_RTS)
|
| 63 | 54 | StgWord target;
|
| ... | ... | @@ -379,8 +379,6 @@ RTS_RET(stg_block_putmvar); |
| 379 | 379 | #if defined(mingw32_HOST_OS)
|
| 380 | 380 | RTS_FUN_DECL(stg_block_async);
|
| 381 | 381 | RTS_RET(stg_block_async);
|
| 382 | -RTS_FUN_DECL(stg_block_async_void);
|
|
| 383 | -RTS_RET(stg_block_async_void);
|
|
| 384 | 382 | #endif
|
| 385 | 383 | RTS_FUN_DECL(stg_block_stmwait);
|
| 386 | 384 | RTS_FUN_DECL(stg_block_throwto);
|
| ... | ... | @@ -8,16 +8,19 @@ |
| 8 | 8 | * For the WINIO manager see base in the GHC.Event modules.
|
| 9 | 9 | */
|
| 10 | 10 | |
| 11 | -#if !defined(THREADED_RTS)
|
|
| 12 | 11 | |
| 13 | 12 | #include "Rts.h"
|
| 13 | +#include <errno.h>
|
|
| 14 | +#include "win32/AsyncMIO.h"
|
|
| 15 | + |
|
| 16 | +#if !defined(THREADED_RTS)
|
|
| 17 | + |
|
| 14 | 18 | #include "RtsUtils.h"
|
| 15 | 19 | #include <windows.h>
|
| 16 | 20 | #include <stdio.h>
|
| 17 | 21 | #include "Schedule.h"
|
| 18 | 22 | #include "Capability.h"
|
| 19 | 23 | #include "IOManagerInternals.h"
|
| 20 | -#include "win32/AsyncMIO.h"
|
|
| 21 | 24 | #include "win32/MIOManager.h"
|
| 22 | 25 | |
| 23 | 26 | /*
|
| ... | ... | @@ -299,14 +302,9 @@ start: |
| 299 | 302 | case BlockedOnRead:
|
| 300 | 303 | case BlockedOnWrite:
|
| 301 | 304 | case BlockedOnDoProc:
|
| 302 | - if (tso->block_info.async_result->reqID == rID) {
|
|
| 303 | - // Found the thread blocked waiting on request;
|
|
| 304 | - // stodgily fill
|
|
| 305 | - // in its result block.
|
|
| 306 | - tso->block_info.async_result->len =
|
|
| 307 | - completedTable[i].len;
|
|
| 308 | - tso->block_info.async_result->errCode =
|
|
| 309 | - completedTable[i].errCode;
|
|
| 305 | + if (tso->block_info.async_reqID == rID) {
|
|
| 306 | + HsInt len = completedTable[i].len;
|
|
| 307 | + HsInt errCode = completedTable[i].errCode;
|
|
| 310 | 308 | |
| 311 | 309 | // Drop the matched TSO from blocked_queue
|
| 312 | 310 | if (prev) {
|
| ... | ... | @@ -322,11 +320,14 @@ start: |
| 322 | 320 | // Terminates the run queue + this inner for-loop.
|
| 323 | 321 | tso->_link = END_TSO_QUEUE;
|
| 324 | 322 | tso->why_blocked = NotBlocked;
|
| 325 | - // save the StgAsyncIOResult in the
|
|
| 326 | - // stg_block_async_info stack frame, because
|
|
| 327 | - // the block_info field will be overwritten by
|
|
| 328 | - // pushOnRunQueue().
|
|
| 329 | - tso->stackobj->sp[1] = (W_)tso->block_info.async_result;
|
|
| 323 | + // For stg_block_async frames (read/write/doProc),
|
|
| 324 | + // write len and errCode directly to the stack.
|
|
| 325 | + // For stg_block_noregs frames (delay), nothing
|
|
| 326 | + // to write.
|
|
| 327 | + if (tso->stackobj->sp[0] == (W_)&stg_block_async_info) {
|
|
| 328 | + tso->stackobj->sp[1] = (W_)len;
|
|
| 329 | + tso->stackobj->sp[2] = (W_)errCode;
|
|
| 330 | + }
|
|
| 330 | 331 | pushOnRunQueue(&MainCapability, tso);
|
| 331 | 332 | break;
|
| 332 | 333 | }
|
| ... | ... | @@ -389,3 +390,8 @@ resetAbandonRequestWait( void ) |
| 389 | 390 | }
|
| 390 | 391 | |
| 391 | 392 | #endif /* !defined(THREADED_RTS) */
|
| 393 | + |
|
| 394 | +HsInt rts_EINTR(void)
|
|
| 395 | +{
|
|
| 396 | + return EINTR;
|
|
| 397 | +} |
| ... | ... | @@ -27,3 +27,4 @@ extern int awaitRequests(bool wait); |
| 27 | 27 | |
| 28 | 28 | extern void abandonRequestWait(void);
|
| 29 | 29 | extern void resetAbandonRequestWait(void);
|
| 30 | +extern HsInt rts_EINTR(void); |
| 1 | +{-# OPTIONS_GHC -O -fno-full-laziness #-}
|
|
| 2 | + |
|
| 3 | +import Control.Concurrent (threadDelay, myThreadId, forkIO, killThread)
|
|
| 4 | +import System.IO.Unsafe (unsafePerformIO)
|
|
| 5 | +import Control.Exception
|
|
| 6 | +import GHC.Exts
|
|
| 7 | + |
|
| 8 | +compute :: Int
|
|
| 9 | +compute = noinline unsafePerformIO $ do
|
|
| 10 | + mainThreadID <- myThreadId
|
|
| 11 | + _ <- forkIO $ do
|
|
| 12 | + threadDelay 500000
|
|
| 13 | + killThread mainThreadID
|
|
| 14 | + threadDelay 1000000
|
|
| 15 | + return 0
|
|
| 16 | + |
|
| 17 | +main = do
|
|
| 18 | + catch (print compute) (\(e :: AsyncException) -> print $ "1:" ++ show e)
|
|
| 19 | + catch (print compute) (\(e :: AsyncException) -> print $ "2:" ++ show e)
|
|
| 20 | + print "done" |
| 1 | +"1:thread killed"
|
|
| 2 | +0
|
|
| 3 | +"done" |
| 1 | +-- Test that re-evaluating an AP_STACK from an interrupted async I/O call
|
|
| 2 | +-- does not crash. On Windows non-threaded RTS, re-entry returns EINTR
|
|
| 3 | +-- which readRawBufferPtr converts to IOException Interrupted. On the
|
|
| 4 | +-- threaded RTS (any platform), the blocking read is re-attempted and
|
|
| 5 | +-- succeeds because we write a byte to the pipe between evaluations.
|
|
| 6 | +--
|
|
| 7 | +-- Before the fix for #26341, re-evaluation on Windows would crash or read
|
|
| 8 | +-- uninitialized memory from a freed StgAsyncIOResult.
|
|
| 9 | +{-# OPTIONS_GHC -O -fno-full-laziness #-}
|
|
| 10 | + |
|
| 11 | +import Control.Concurrent (threadDelay, myThreadId, forkIO, killThread, rtsSupportsBoundThreads)
|
|
| 12 | +import Control.Exception
|
|
| 13 | +import Data.IORef
|
|
| 14 | +import Foreign
|
|
| 15 | +import Foreign.C
|
|
| 16 | +import GHC.Exts
|
|
| 17 | +import GHC.IO.Exception (IOErrorType(..), IOException(..))
|
|
| 18 | +import GHC.IO.FD (FD(..), readRawBufferPtr, writeRawBufferPtr)
|
|
| 19 | +import System.Info (os)
|
|
| 20 | +import System.IO.Unsafe (unsafePerformIO)
|
|
| 21 | +import System.Process (createPipeFd)
|
|
| 22 | + |
|
| 23 | +-- Store the write fd so main can feed data into the pipe between
|
|
| 24 | +-- evaluations. On Unix this unblocks the re-entered read; on Windows
|
|
| 25 | +-- stg_block_async returns EINTR regardless.
|
|
| 26 | +{-# NOINLINE writeFdRef #-}
|
|
| 27 | +writeFdRef :: IORef CInt
|
|
| 28 | +writeFdRef = unsafePerformIO $ newIORef (-1)
|
|
| 29 | + |
|
| 30 | +-- | A thunk whose unsafePerformIO blocks on a pipe read. A forked
|
|
| 31 | +-- thread kills the main thread after 200ms, which creates an AP_STACK.
|
|
| 32 | +{-# NOINLINE blockedRead #-}
|
|
| 33 | +blockedRead :: ()
|
|
| 34 | +blockedRead = noinline unsafePerformIO $ do
|
|
| 35 | + (readFd, writeFd) <- createPipeFd
|
|
| 36 | + writeIORef writeFdRef writeFd
|
|
| 37 | + buf <- mallocBytes 1
|
|
| 38 | + mainTid <- myThreadId
|
|
| 39 | + _ <- forkIO $ do
|
|
| 40 | + threadDelay 200000 -- 200ms
|
|
| 41 | + killThread mainTid
|
|
| 42 | + -- readRawBufferPtr dispatches to asyncReadRawBufferPtr on Windows
|
|
| 43 | + -- non-threaded RTS; on Unix it uses threadWaitRead + read().
|
|
| 44 | + _ <- readRawBufferPtr "blockedRead" (FD readFd 0) buf 0 1
|
|
| 45 | + return ()
|
|
| 46 | + |
|
| 47 | +main :: IO ()
|
|
| 48 | +main = do
|
|
| 49 | + -- First evaluation: the thunk blocks on the pipe read, gets killed.
|
|
| 50 | + catch (evaluate blockedRead)
|
|
| 51 | + (\(e :: AsyncException) -> putStrLn $ "caught: " ++ show e)
|
|
| 52 | + |
|
| 53 | + -- Write a byte so the re-entered read can complete on Unix.
|
|
| 54 | + wfd <- readIORef writeFdRef
|
|
| 55 | + buf <- mallocBytes 1
|
|
| 56 | + poke buf 0
|
|
| 57 | + _ <- writeRawBufferPtr "unblock" (FD wfd 0) buf 0 1
|
|
| 58 | + |
|
| 59 | + -- Second evaluation: AP_STACK re-enters.
|
|
| 60 | + -- Non-threaded Windows: asyncRead returns (-1, EINTR) → IOException
|
|
| 61 | + -- Threaded / Unix: read succeeds → returns normally
|
|
| 62 | + let expectEINTR = os == "mingw32" && not rtsSupportsBoundThreads
|
|
| 63 | + result <- try (evaluate blockedRead)
|
|
| 64 | + case result of
|
|
| 65 | + Left e
|
|
| 66 | + | Just ioe <- fromException e
|
|
| 67 | + , ioe_type (ioe :: IOException) == Interrupted
|
|
| 68 | + -> putStrLn "re-evaluated ok"
|
|
| 69 | + | otherwise
|
|
| 70 | + -> putStrLn $ "unexpected: " ++ show e
|
|
| 71 | + Right ()
|
|
| 72 | + | expectEINTR -> putStrLn "unexpected: expected EINTR"
|
|
| 73 | + | otherwise -> putStrLn "re-evaluated ok"
|
|
| 74 | + |
|
| 75 | + putStrLn "done" |
| 1 | +caught: thread killed
|
|
| 2 | +re-evaluated ok
|
|
| 3 | +done |
| 1 | +-- Stress test for #26341: repeatedly interrupt async-blocked threads and
|
|
| 2 | +-- re-enter their AP_STACKs. Before the fix, re-entering a thunk whose
|
|
| 3 | +-- unsafePerformIO was blocked on an async I/O call (Windows non-threaded
|
|
| 4 | +-- RTS) would read uninitialized memory or free a dangling pointer,
|
|
| 5 | +-- because stg_block_async reserved a stack slot for a heap-allocated
|
|
| 6 | +-- StgAsyncIOResult that became invalid after an async exception.
|
|
| 7 | +--
|
|
| 8 | +-- This test spawns many concurrent workers, each of which:
|
|
| 9 | +-- 1. Creates a pipe.
|
|
| 10 | +-- 2. Builds a thunk that blocks on a pipe read via unsafePerformIO.
|
|
| 11 | +-- 3. Evaluates the thunk and kills it with an async exception.
|
|
| 12 | +-- 4. Re-evaluates the thunk (AP_STACK re-entry).
|
|
| 13 | +-- 5. Repeats many times.
|
|
| 14 | +--
|
|
| 15 | +-- On threaded RTS / Unix the re-entered read succeeds (we write a byte
|
|
| 16 | +-- first). On Windows non-threaded RTS the re-entered async call returns
|
|
| 17 | +-- EINTR. Both paths exercise the fixed stack-frame layout.
|
|
| 18 | +{-# OPTIONS_GHC -O -fno-full-laziness #-}
|
|
| 19 | + |
|
| 20 | +import Control.Concurrent
|
|
| 21 | +import Control.Exception
|
|
| 22 | +import Foreign
|
|
| 23 | +import Foreign.C
|
|
| 24 | +import GHC.Exts
|
|
| 25 | +import GHC.IO.Exception (IOErrorType(..), IOException(..))
|
|
| 26 | +import GHC.IO.FD (FD(..), readRawBufferPtr, writeRawBufferPtr)
|
|
| 27 | +import System.IO (hFlush, stdout)
|
|
| 28 | +import System.IO.Unsafe (unsafePerformIO)
|
|
| 29 | +import System.Posix.Internals (c_close)
|
|
| 30 | +import System.Process (createPipeFd)
|
|
| 31 | + |
|
| 32 | +iterations :: Int
|
|
| 33 | +iterations = 200
|
|
| 34 | + |
|
| 35 | +workers :: Int
|
|
| 36 | +workers = 4
|
|
| 37 | + |
|
| 38 | +-- Each worker independently performs `iterations` rounds of:
|
|
| 39 | +-- block on pipe read → interrupt → re-evaluate the AP_STACK.
|
|
| 40 | +worker :: Int -> MVar () -> IO ()
|
|
| 41 | +worker wid done = do
|
|
| 42 | + buf <- mallocBytes 1
|
|
| 43 | + let go 0 = return ()
|
|
| 44 | + go n = do
|
|
| 45 | + (readFd, writeFd) <- createPipeFd
|
|
| 46 | + |
|
| 47 | + -- Build a fresh thunk each iteration so we get a new AP_STACK.
|
|
| 48 | + let {-# NOINLINE blockedThunk #-}
|
|
| 49 | + blockedThunk :: ()
|
|
| 50 | + blockedThunk = noinline unsafePerformIO $ do
|
|
| 51 | + tid <- myThreadId
|
|
| 52 | + _ <- forkIO $ do
|
|
| 53 | + threadDelay 1000 -- 1ms: tight window
|
|
| 54 | + killThread tid
|
|
| 55 | + _ <- readRawBufferPtr "stress" (FD readFd 0) buf 0 1
|
|
| 56 | + return ()
|
|
| 57 | + |
|
| 58 | + -- First evaluation: block and get killed.
|
|
| 59 | + catch (evaluate blockedThunk)
|
|
| 60 | + (\(_ :: SomeException) -> return ())
|
|
| 61 | + |
|
| 62 | + -- Write a byte so the re-entered read can complete on
|
|
| 63 | + -- threaded RTS / Unix.
|
|
| 64 | + poke buf 0
|
|
| 65 | + _ <- writeRawBufferPtr "unblock" (FD writeFd 0) buf 0 1
|
|
| 66 | + |
|
| 67 | + -- Second evaluation: AP_STACK re-entry.
|
|
| 68 | + result <- try (evaluate blockedThunk)
|
|
| 69 | + case result of
|
|
| 70 | + Left e
|
|
| 71 | + | Just ioe <- fromException e
|
|
| 72 | + , ioe_type (ioe :: IOException) == Interrupted
|
|
| 73 | + -> return () -- expected on Windows non-threaded
|
|
| 74 | + | otherwise
|
|
| 75 | + -> throwIO (userError $
|
|
| 76 | + "worker " ++ show wid ++ " iteration " ++ show n ++
|
|
| 77 | + ": unexpected exception: " ++ show e)
|
|
| 78 | + Right () -> return () -- expected on threaded / Unix
|
|
| 79 | + |
|
| 80 | + -- Close the pipe fds.
|
|
| 81 | + _ <- c_close readFd
|
|
| 82 | + _ <- c_close writeFd
|
|
| 83 | + |
|
| 84 | + go (n - 1)
|
|
| 85 | + |
|
| 86 | + go iterations
|
|
| 87 | + putMVar done ()
|
|
| 88 | + |
|
| 89 | +main :: IO ()
|
|
| 90 | +main = do
|
|
| 91 | + dones <- mapM (\wid -> do
|
|
| 92 | + done <- newEmptyMVar
|
|
| 93 | + _ <- forkIO (worker wid done)
|
|
| 94 | + return done
|
|
| 95 | + ) [1..workers]
|
|
| 96 | + |
|
| 97 | + -- Wait for all workers to finish.
|
|
| 98 | + mapM_ takeMVar dones
|
|
| 99 | + |
|
| 100 | + putStrLn "stress test passed"
|
|
| 101 | + hFlush stdout |
| 1 | +stress test passed |
| ... | ... | @@ -309,3 +309,19 @@ test('hs_try_putmvar003', |
| 309 | 309 | |
| 310 | 310 | # Check forkIO exception determinism under optimization
|
| 311 | 311 | test('T13330', normal, compile_and_run, ['-O'])
|
| 312 | + |
|
| 313 | +test('T26341', normal, compile_and_run, [''])
|
|
| 314 | + |
|
| 315 | +# Test EINTR for async I/O interrupted by an exception (#26341)
|
|
| 316 | +test('T26341a'
|
|
| 317 | + # test uses pipe operations which are not supported by the JS/wasm backends
|
|
| 318 | + , when(arch('wasm32') or arch('javascript'), skip)
|
|
| 319 | + , compile_and_run, ['-package process'])
|
|
| 320 | + |
|
| 321 | +# Stress test: many threads repeatedly interrupt and re-enter async-blocked
|
|
| 322 | +# thunks (#26341). Before the fix, this would crash due to dangling
|
|
| 323 | +# StgAsyncIOResult pointers on the stack.
|
|
| 324 | +test('T26341b'
|
|
| 325 | + # test uses pipe operations which are not supported by the JS/wasm backends
|
|
| 326 | + , when(arch('wasm32') or arch('javascript'), skip)
|
|
| 327 | + , compile_and_run, ['-package process']) |
| ... | ... | @@ -627,12 +627,7 @@ wanteds os = concat |
| 627 | 627 | -- Note that this conditional part only affects the C headers.
|
| 628 | 628 | -- That's important, as it means we get the same PlatformConstants
|
| 629 | 629 | -- type on all platforms.
|
| 630 | - ,if os == Just Windows
|
|
| 631 | - then concat [structSize C "StgAsyncIOResult"
|
|
| 632 | - ,structField C "StgAsyncIOResult" "reqID"
|
|
| 633 | - ,structField C "StgAsyncIOResult" "len"
|
|
| 634 | - ,structField C "StgAsyncIOResult" "errCode"]
|
|
| 635 | - else []
|
|
| 630 | + ,[]
|
|
| 636 | 631 | |
| 637 | 632 | -- struct HsIface
|
| 638 | 633 | ,structField C "HsIface" "Z0T_closure"
|
| ... | ... | @@ -759,9 +754,6 @@ getWanted verbose os tmpdir gccProgram gccFlags nmProgram mobjdumpProgram |
| 759 | 754 | "",
|
| 760 | 755 | "#define PROFILING",
|
| 761 | 756 | "#define THREADED_RTS",
|
| 762 | - -- We need to define this if we want StgAsyncIOResult
|
|
| 763 | - -- struct to be present after CPP
|
|
| 764 | - --
|
|
| 765 | 757 | -- FIXME: rts/PosixSource.h should include ghcplatform.h
|
| 766 | 758 | -- which should set this. There is a mismatch host/target
|
| 767 | 759 | -- again...
|