Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC
Commits:
fcf092dd by Luite Stegeman at 2026-03-27T04:44:17-04: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
- - - - -
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:
=====================================
rts/HeapStackCheck.cmm
=====================================
@@ -703,38 +703,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
=====================================
@@ -633,10 +633,8 @@ void scavengeTSOIOManager(StgTSO *tso)
#endif
/* 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:
@@ -846,7 +844,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:
@@ -885,12 +883,7 @@ bool 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
=====================================
@@ -2255,18 +2255,7 @@ stg_delayzh ( W_ us_delay )
(ok) = ccall syncDelay(MyCapability() "ptr", CurrentTSO "ptr", us_delay);
if (ok != 0::CBool) (likely: True) {
- /* 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
} else {
jump stg_raisezh(HsIface_heapOverflow_closure(W_[ghc_hs_iface]));
}
@@ -2276,21 +2265,14 @@ stg_delayzh ( W_ us_delay )
#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 sbarf("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;
@@ -2302,21 +2284,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 sbarf("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;
@@ -2328,21 +2303,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 sbarf("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 /* 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
=====================================
@@ -926,7 +926,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/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;
@@ -57,7 +48,7 @@ typedef union {
StgAsyncIOOp *aiop;
StgTimeoutQueue *timeout;
#if defined(mingw32_HOST_OS)
- StgAsyncIOResult *async_result;
+ StgWord async_reqID;
#endif
#if !defined(THREADED_RTS)
StgWord target;
=====================================
rts/include/stg/MiscClosures.h
=====================================
@@ -379,8 +379,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
+#include "win32/AsyncMIO.h"
+
+#if !defined(THREADED_RTS)
+
#include "RtsUtils.h"
#include
#include
#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
=====================================
@@ -309,3 +309,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
=====================================
@@ -627,12 +627,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" "Z0T_closure"
@@ -759,9 +754,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/-/commit/fcf092dda534cc38637d1f7920aa0dae...
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/fcf092dda534cc38637d1f7920aa0dae...
You're receiving this email because of your account on gitlab.haskell.org.