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
-
eb7e1e0f
by Luite Stegeman at 2026-05-15T09:49:36+02:00
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:
| ... | ... | @@ -706,38 +706,24 @@ stg_block_throwto (P_ tso, P_ exception) |
| 706 | 706 | }
|
| 707 | 707 | |
| 708 | 708 | #if defined(mingw32_HOST_OS)
|
| 709 | -INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ ares )
|
|
| 709 | +INFO_TABLE_RET ( stg_block_async, RET_SMALL, W_ info_ptr, W_ len, W_ errCode )
|
|
| 710 | 710 | return ()
|
| 711 | 711 | {
|
| 712 | - W_ len, errC;
|
|
| 713 | - |
|
| 714 | - len = TO_W_(StgAsyncIOResult_len(ares));
|
|
| 715 | - errC = TO_W_(StgAsyncIOResult_errCode(ares));
|
|
| 716 | - ccall free(ares "ptr");
|
|
| 717 | - return (len, errC);
|
|
| 712 | + return (len, errCode);
|
|
| 718 | 713 | }
|
| 719 | 714 | |
| 720 | 715 | stg_block_async
|
| 721 | 716 | {
|
| 722 | - Sp_adj(-2);
|
|
| 723 | - Sp(0) = stg_block_async_info;
|
|
| 724 | - BLOCK_GENERIC;
|
|
| 725 | -}
|
|
| 717 | + W_ eintr;
|
|
| 718 | + (eintr) = ccall rts_EINTR();
|
|
| 726 | 719 | |
| 727 | -/* Used by threadDelay implementation; it would be desirable to get rid of
|
|
| 728 | - * this free()'ing void return continuation.
|
|
| 729 | - */
|
|
| 730 | -INFO_TABLE_RET ( stg_block_async_void, RET_SMALL, W_ info_ptr, W_ ares )
|
|
| 731 | - return ()
|
|
| 732 | -{
|
|
| 733 | - ccall free(ares "ptr");
|
|
| 734 | - return ();
|
|
| 735 | -}
|
|
| 736 | - |
|
| 737 | -stg_block_async_void
|
|
| 738 | -{
|
|
| 739 | - Sp_adj(-2);
|
|
| 740 | - Sp(0) = stg_block_async_void_info;
|
|
| 720 | + // Fill the stack frame with values that indicate that the operation
|
|
| 721 | + // has been interrupted. The IO manager will overwrite these with the
|
|
| 722 | + // actual results if the async operation completes.
|
|
| 723 | + Sp_adj(-3);
|
|
| 724 | + Sp(0) = stg_block_async_info;
|
|
| 725 | + Sp(1) = -1; // len: -1 indicates error
|
|
| 726 | + Sp(2) = eintr; // errCode: interrupted
|
|
| 741 | 727 | BLOCK_GENERIC;
|
| 742 | 728 | }
|
| 743 | 729 |
| ... | ... | @@ -561,10 +561,8 @@ void scavengeTSOIOManager(StgTSO *tso) |
| 561 | 561 | */
|
| 562 | 562 | |
| 563 | 563 | /* case IO_MANAGER_WIN32_LEGACY:
|
| 564 | - * BlockedOn{Read,Write,DoProc} uses block_info.async_result
|
|
| 565 | - * The StgAsyncIOResult async_result is allocated on the C heap.
|
|
| 566 | - * It'd probably be better if it used the GC heap. If it did we'd
|
|
| 567 | - * scavenge it here.
|
|
| 564 | + * BlockedOn{Read,Write,DoProc} uses block_info.async_reqID
|
|
| 565 | + * which is a plain integer, so nothing to scavenge.
|
|
| 568 | 566 | */
|
| 569 | 567 | |
| 570 | 568 | default:
|
| ... | ... | @@ -747,7 +745,7 @@ void syncIOCancel(Capability *cap, StgTSO *tso) |
| 747 | 745 | case IO_MANAGER_WIN32_LEGACY:
|
| 748 | 746 | removeThreadFromDeQueue(cap, &cap->iomgr->blocked_queue_hd,
|
| 749 | 747 | &cap->iomgr->blocked_queue_tl, tso);
|
| 750 | - abandonWorkRequest(tso->block_info.async_result->reqID);
|
|
| 748 | + abandonWorkRequest(tso->block_info.async_reqID);
|
|
| 751 | 749 | break;
|
| 752 | 750 | #endif
|
| 753 | 751 | default:
|
| ... | ... | @@ -782,12 +780,7 @@ void syncDelay(Capability *cap, StgTSO *tso, HsInt us_delay) |
| 782 | 780 | * would make the primops more consistent.
|
| 783 | 781 | */
|
| 784 | 782 | {
|
| 785 | - StgAsyncIOResult *ares = stgMallocBytes(sizeof(StgAsyncIOResult),
|
|
| 786 | - "syncDelay");
|
|
| 787 | - ares->reqID = addDelayRequest(us_delay);
|
|
| 788 | - ares->len = 0;
|
|
| 789 | - ares->errCode = 0;
|
|
| 790 | - tso->block_info.async_result = ares;
|
|
| 783 | + tso->block_info.async_reqID = addDelayRequest(us_delay);
|
|
| 791 | 784 | |
| 792 | 785 | /* Having all async-blocked threads reside on the blocked_queue
|
| 793 | 786 | * simplifies matters, so set the status to OnDoProc and put the
|
| ... | ... | @@ -2575,40 +2575,29 @@ stg_waitWritezh ( W_ fd ) |
| 2575 | 2575 | |
| 2576 | 2576 | stg_delayzh ( W_ us_delay )
|
| 2577 | 2577 | {
|
| 2578 | - ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay);
|
|
| 2578 | + CBool ok;
|
|
| 2579 | 2579 | |
| 2580 | - /* Annoyingly, we cannot be consistent with how we wait and resume the
|
|
| 2581 | - * blocked thread. The reason is that the win32 legacy I/O manager
|
|
| 2582 | - * allocates a StgAsyncIOResult struct on the C heap which has to be
|
|
| 2583 | - * freed when the thread resumes. It's a bit awkward to arrange to
|
|
| 2584 | - * allocate it on the GC heap instead, so that's how it is for now. Sigh.
|
|
| 2585 | - */
|
|
| 2586 | -#if defined(mingw32_HOST_OS)
|
|
| 2587 | - jump stg_block_async_void();
|
|
| 2588 | -#else
|
|
| 2589 | - jump stg_block_noregs();
|
|
| 2590 | -#endif
|
|
| 2580 | + (ok) = ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay);
|
|
| 2581 | + |
|
| 2582 | + if (ok != 0::CBool) (likely: True) {
|
|
| 2583 | + jump stg_block_noregs();
|
|
| 2584 | + } else {
|
|
| 2585 | + jump stg_raisezh(HsIface_heapOverflow_closure(W_[ghc_hs_iface]));
|
|
| 2586 | + }
|
|
| 2591 | 2587 | }
|
| 2592 | 2588 | |
| 2593 | 2589 | |
| 2594 | 2590 | #if defined(mingw32_HOST_OS)
|
| 2595 | 2591 | stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf )
|
| 2596 | 2592 | {
|
| 2597 | - W_ ares;
|
|
| 2598 | 2593 | CInt reqID;
|
| 2599 | 2594 | |
| 2600 | 2595 | #if defined(THREADED_RTS)
|
| 2601 | 2596 | ccall barf("asyncRead# on threaded RTS") never returns;
|
| 2602 | 2597 | #else
|
| 2603 | 2598 | |
| 2604 | - /* could probably allocate this on the heap instead */
|
|
| 2605 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2606 | - "stg_asyncReadzh");
|
|
| 2607 | 2599 | (reqID) = ccall addIORequest(fd, 0/*FALSE*/,is_sock,len,buf "ptr");
|
| 2608 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2609 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2610 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2611 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2600 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2612 | 2601 | |
| 2613 | 2602 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2614 | 2603 | %release StgTSO_why_blocked(CurrentTSO) = BlockedOnRead::I32;
|
| ... | ... | @@ -2620,21 +2609,14 @@ stg_asyncReadzh ( W_ fd, W_ is_sock, W_ len, W_ buf ) |
| 2620 | 2609 | |
| 2621 | 2610 | stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf )
|
| 2622 | 2611 | {
|
| 2623 | - W_ ares;
|
|
| 2624 | 2612 | CInt reqID;
|
| 2625 | 2613 | |
| 2626 | 2614 | #if defined(THREADED_RTS)
|
| 2627 | 2615 | ccall barf("asyncWrite# on threaded RTS") never returns;
|
| 2628 | 2616 | #else
|
| 2629 | 2617 | |
| 2630 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2631 | - "stg_asyncWritezh");
|
|
| 2632 | 2618 | (reqID) = ccall addIORequest(fd, 1/*TRUE*/,is_sock,len,buf "ptr");
|
| 2633 | - |
|
| 2634 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2635 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2636 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2637 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2619 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2638 | 2620 | |
| 2639 | 2621 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2640 | 2622 | %release StgTSO_why_blocked(CurrentTSO) = BlockedOnWrite::I32;
|
| ... | ... | @@ -2646,21 +2628,14 @@ stg_asyncWritezh ( W_ fd, W_ is_sock, W_ len, W_ buf ) |
| 2646 | 2628 | |
| 2647 | 2629 | stg_asyncDoProczh ( W_ proc, W_ param )
|
| 2648 | 2630 | {
|
| 2649 | - W_ ares;
|
|
| 2650 | 2631 | CInt reqID;
|
| 2651 | 2632 | |
| 2652 | 2633 | #if defined(THREADED_RTS)
|
| 2653 | 2634 | ccall barf("asyncDoProc# on threaded RTS") never returns;
|
| 2654 | 2635 | #else
|
| 2655 | 2636 | |
| 2656 | - /* could probably allocate this on the heap instead */
|
|
| 2657 | - ("ptr" ares) = ccall stgMallocBytes(SIZEOF_StgAsyncIOResult,
|
|
| 2658 | - "stg_asyncDoProczh");
|
|
| 2659 | 2637 | (reqID) = ccall addDoProcRequest(proc "ptr",param "ptr");
|
| 2660 | - StgAsyncIOResult_reqID(ares) = reqID;
|
|
| 2661 | - StgAsyncIOResult_len(ares) = 0;
|
|
| 2662 | - StgAsyncIOResult_errCode(ares) = 0;
|
|
| 2663 | - StgTSO_block_info(CurrentTSO) = ares;
|
|
| 2638 | + StgTSO_block_info(CurrentTSO) = reqID;
|
|
| 2664 | 2639 | |
| 2665 | 2640 | ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
|
| 2666 | 2641 | %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) \
|
| ... | ... | @@ -928,7 +928,7 @@ printThreadBlockage(StgTSO *tso) |
| 928 | 928 | switch (ACQUIRE_LOAD(&tso->why_blocked)) {
|
| 929 | 929 | #if defined(mingw32_HOST_OS)
|
| 930 | 930 | case BlockedOnDoProc:
|
| 931 | - debugBelch("is blocked on proc (request: %u)", tso->block_info.async_result->reqID);
|
|
| 931 | + debugBelch("is blocked on proc (request: %" FMT_Word ")", tso->block_info.async_reqID);
|
|
| 932 | 932 | break;
|
| 933 | 933 | #endif
|
| 934 | 934 | #if !defined(THREADED_RTS)
|
| ... | ... | @@ -154,6 +154,8 @@ static void freeEventLoggingBuffer(void); |
| 154 | 154 | static void ensureRoomForEvent(EventsBuf *eb, EventTypeNum tag);
|
| 155 | 155 | static int ensureRoomForVariableEvent(EventsBuf *eb, StgWord size);
|
| 156 | 156 | |
| 157 | +static void flushEventLog_(Capability **cap USED_IF_THREADS);
|
|
| 158 | + |
|
| 157 | 159 | static inline void postWord8(EventsBuf *eb, StgWord8 i)
|
| 158 | 160 | {
|
| 159 | 161 | *(eb->pos++) = i;
|
| ... | ... | @@ -484,7 +486,7 @@ endEventLogging(void) |
| 484 | 486 | |
| 485 | 487 | eventlog_enabled = false;
|
| 486 | 488 | |
| 487 | - flushEventLog(NULL);
|
|
| 489 | + flushEventLog_(NULL);
|
|
| 488 | 490 | |
| 489 | 491 | ACQUIRE_LOCK(&eventBufMutex);
|
| 490 | 492 | |
| ... | ... | @@ -1607,6 +1609,17 @@ void flushAllCapsEventsBufs(void) |
| 1607 | 1609 | }
|
| 1608 | 1610 | |
| 1609 | 1611 | void flushEventLog(Capability **cap USED_IF_THREADS)
|
| 1612 | +{
|
|
| 1613 | + ACQUIRE_LOCK(&state_change_mutex);
|
|
| 1614 | + flushEventLog_(cap);
|
|
| 1615 | + RELEASE_LOCK(&state_change_mutex);
|
|
| 1616 | +}
|
|
| 1617 | + |
|
| 1618 | +// This is an unsafe version of flushEventLog that does not acquire/release the
|
|
| 1619 | +// state_change mutex. It is for internal use only and should only be used when
|
|
| 1620 | +// (1) you're sure that there's no chance of racing with start/endEventLogging,
|
|
| 1621 | +// and (2) there is an event_log_writer.
|
|
| 1622 | +static void flushEventLog_(Capability **cap USED_IF_THREADS)
|
|
| 1610 | 1623 | {
|
| 1611 | 1624 | if (!event_log_writer) {
|
| 1612 | 1625 | return;
|
| ... | ... | @@ -1636,7 +1649,7 @@ void flushEventLog(Capability **cap USED_IF_THREADS) |
| 1636 | 1649 | flushEventLogWriter();
|
| 1637 | 1650 | }
|
| 1638 | 1651 | |
| 1639 | -#else
|
|
| 1652 | +#else /*!TRACING*/
|
|
| 1640 | 1653 | |
| 1641 | 1654 | enum EventLogStatus eventLogStatus(void)
|
| 1642 | 1655 | {
|
| ... | ... | @@ -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;
|
| ... | ... | @@ -55,7 +46,7 @@ typedef union { |
| 55 | 46 | struct MessageWakeup_ *wakeup;
|
| 56 | 47 | StgInt fd; /* StgInt instead of int, so that it's the same size as the ptrs */
|
| 57 | 48 | #if defined(mingw32_HOST_OS)
|
| 58 | - StgAsyncIOResult *async_result;
|
|
| 49 | + StgWord async_reqID;
|
|
| 59 | 50 | #endif
|
| 60 | 51 | #if !defined(THREADED_RTS)
|
| 61 | 52 | StgWord target;
|
| ... | ... | @@ -430,8 +430,6 @@ RTS_RET(stg_block_putmvar); |
| 430 | 430 | #if defined(mingw32_HOST_OS)
|
| 431 | 431 | RTS_FUN_DECL(stg_block_async);
|
| 432 | 432 | RTS_RET(stg_block_async);
|
| 433 | -RTS_FUN_DECL(stg_block_async_void);
|
|
| 434 | -RTS_RET(stg_block_async_void);
|
|
| 435 | 433 | #endif
|
| 436 | 434 | RTS_FUN_DECL(stg_block_stmwait);
|
| 437 | 435 | 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 |
| ... | ... | @@ -298,3 +298,19 @@ test('hs_try_putmvar003', |
| 298 | 298 | |
| 299 | 299 | # Check forkIO exception determinism under optimization
|
| 300 | 300 | test('T13330', normal, compile_and_run, ['-O'])
|
| 301 | + |
|
| 302 | +test('T26341', normal, compile_and_run, [''])
|
|
| 303 | + |
|
| 304 | +# Test EINTR for async I/O interrupted by an exception (#26341)
|
|
| 305 | +test('T26341a'
|
|
| 306 | + # test uses pipe operations which are not supported by the JS/wasm backends
|
|
| 307 | + , when(arch('wasm32') or arch('javascript'), skip)
|
|
| 308 | + , compile_and_run, ['-package process'])
|
|
| 309 | + |
|
| 310 | +# Stress test: many threads repeatedly interrupt and re-enter async-blocked
|
|
| 311 | +# thunks (#26341). Before the fix, this would crash due to dangling
|
|
| 312 | +# StgAsyncIOResult pointers on the stack.
|
|
| 313 | +test('T26341b'
|
|
| 314 | + # test uses pipe operations which are not supported by the JS/wasm backends
|
|
| 315 | + , when(arch('wasm32') or arch('javascript'), skip)
|
|
| 316 | + , compile_and_run, ['-package process']) |
| ... | ... | @@ -646,12 +646,7 @@ wanteds os = concat |
| 646 | 646 | -- Note that this conditional part only affects the C headers.
|
| 647 | 647 | -- That's important, as it means we get the same PlatformConstants
|
| 648 | 648 | -- type on all platforms.
|
| 649 | - ,if os == Just Windows
|
|
| 650 | - then concat [structSize C "StgAsyncIOResult"
|
|
| 651 | - ,structField C "StgAsyncIOResult" "reqID"
|
|
| 652 | - ,structField C "StgAsyncIOResult" "len"
|
|
| 653 | - ,structField C "StgAsyncIOResult" "errCode"]
|
|
| 654 | - else []
|
|
| 649 | + ,[]
|
|
| 655 | 650 | |
| 656 | 651 | -- struct HsIface
|
| 657 | 652 | ,structField C "HsIface" "processRemoteCompletion_closure"
|
| ... | ... | @@ -814,9 +809,6 @@ getWanted verbose os tmpdir gccProgram gccFlags nmProgram mobjdumpProgram |
| 814 | 809 | "",
|
| 815 | 810 | "#define PROFILING",
|
| 816 | 811 | "#define THREADED_RTS",
|
| 817 | - -- We need to define this if we want StgAsyncIOResult
|
|
| 818 | - -- struct to be present after CPP
|
|
| 819 | - --
|
|
| 820 | 812 | -- FIXME: rts/PosixSource.h should include ghcplatform.h
|
| 821 | 813 | -- which should set this. There is a mismatch host/target
|
| 822 | 814 | -- again...
|