diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,6 +1,12 @@
 module Main (main) where
 
+-- Cabal
 import Distribution.Simple
+  ( defaultMainWithHooks
+  , autoconfUserHooks
+  )
+
+--------------------------------------------------------------------------------
 
 main :: IO ()
 main = defaultMainWithHooks autoconfUserHooks
diff --git a/System/Process.hs b/System/Process.hs
--- a/System/Process.hs
+++ b/System/Process.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE CPP, ForeignFunctionInterface #-}
+{-# LANGUAGE LambdaCase #-}
 #if __GLASGOW_HASKELL__ >= 709
 {-# LANGUAGE Safe #-}
 #else
@@ -31,23 +32,57 @@
 
 module System.Process (
     -- * Running sub-processes
+
+    -- ** General interface to process creation
+
+    -- | @createProcess@ and @createProcess_@ are general interfaces
+    -- to process creation. To start with you might instead want to use
+    -- one of the [simpler process creation functions](#g:simpler)
+    -- below.
+
     createProcess,
     createProcess_,
+
+    -- ** Creating a @CreateProcess@
+
+    -- | Once you have a @CreateProcess@ you can launch it with
+    -- 'createProcess' or one of the [simpler process creation
+    -- functions](#g:simpler).
+
     shell, proc,
+
+    -- ** @CreateProcess@ and associated types
+
     CreateProcess(..),
     CmdSpec(..),
     StdStream(..),
     ProcessHandle,
 
-    -- ** Simpler functions for common tasks
+    -- * Simpler functions for common tasks #simpler#
+
+    -- ** Call, the simplest way of launching a process
+
+    callCreateProcess,
     callProcess,
     callCommand,
+
+    -- ** Spawn, obtaining a @ProcessHandle@
+
     spawnProcess,
     spawnCommand,
+
+    -- ** Read, passing @stdin@ and obtaining @stdout@
+
     readCreateProcess,
     readProcess,
+
+    -- ** Read with exit code, passing @stdin@, obtaining exit code, @stdout@ and @stderr@
+
     readCreateProcessWithExitCode,
     readProcessWithExitCode,
+
+    -- ** Other
+
     withCreateProcess,
     cleanupProcess,
 
@@ -57,6 +92,9 @@
     getPid,
     getCurrentPid,
 
+    -- ** Secure process creation on Windows
+    -- $windows-mitigations
+
     -- ** Control-C handling on Unix
     -- $ctlc-handling
 
@@ -89,11 +127,11 @@
 
 import Control.Concurrent
 import Control.DeepSeq (rnf)
-import Control.Exception (SomeException, mask
+import Control.Exception (
 #if !defined(javascript_HOST_ARCH)
-                         , allowInterrupt
+                           allowInterrupt,
 #endif
-                         , bracket, try, throwIO)
+                           bracket)
 import qualified Control.Exception as C
 import Control.Monad
 import Data.Maybe
@@ -105,14 +143,14 @@
 
 #if defined(javascript_HOST_ARCH)
 import System.Process.JavaScript(getProcessId, getCurrentProcessId)
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
 import System.Win32.Process (getProcessId, getCurrentProcessId, ProcessId)
 #else
 import System.Posix.Process (getProcessID)
 import System.Posix.Types (CPid (..))
 #endif
 
-import GHC.IO.Exception ( ioException, IOErrorType(..), IOException(..) )
+import GHC.IO.Exception ( ioException, IOErrorType(..) )
 
 #if defined(wasm32_HOST_ARCH)
 import GHC.IO.Exception ( unsupportedOperation )
@@ -126,7 +164,7 @@
 -- @since 1.6.3.0
 #if defined(javascript_HOST_ARCH)
 type Pid = Int
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
 type Pid = ProcessId
 #else
 type Pid = CPid
@@ -334,43 +372,47 @@
 -- ----------------------------------------------------------------------------
 -- callProcess/callCommand
 
--- | Creates a new process to run the specified command with the given
--- arguments, and wait for it to finish.  If the command returns a non-zero
--- exit code, an exception is raised.
+-- | Creates a new process from the provided `CreateProcess`, and wait for it
+-- to finish.  If the process returns a non-zero exit code, an exception is
+-- raised.
 --
 -- If an asynchronous exception is thrown to the thread executing
--- @callProcess@, the forked process will be terminated and
--- @callProcess@ will wait (block) until the process has been
+-- @callCreateProcess@, the forked process will be terminated and
+-- @callCreateProcess@ will wait (block) until the process has been
 -- terminated.
 --
+-- @since TODO
+callCreateProcess :: CreateProcess -> IO ()
+callCreateProcess = callCreateProcess_ "callCreateProcess"
+
+-- | \"@callProcess cmd args@\" is a shorthand for
+-- \"@'callCreateProcess' ('proc' cmd args)@\".
+--
 -- @since 1.2.0.0
 callProcess :: FilePath -> [String] -> IO ()
-callProcess cmd args = do
-    exit_code <- withCreateProcess_ "callProcess"
-                   (proc cmd args) { delegate_ctlc = True } $ \_ _ _ p ->
-                   waitForProcess p
-    case exit_code of
-      ExitSuccess   -> return ()
-      ExitFailure r -> processFailedException "callProcess" cmd args r
+callProcess cmd = callCreateProcess_ "callProcess" . proc cmd
 
--- | Creates a new process to run the specified shell command.  If the
--- command returns a non-zero exit code, an exception is raised.
---
--- If an asynchronous exception is thrown to the thread executing
--- @callCommand@, the forked process will be terminated and
--- @callCommand@ will wait (block) until the process has been
--- terminated.
+-- | \"@callCommand cmd@\" is a shorthand for \"@'callCreateProcess'
+-- ('shell' cmd)@\".
 --
 -- @since 1.2.0.0
 callCommand :: String -> IO ()
-callCommand cmd = do
-    exit_code <- withCreateProcess_ "callCommand"
-                   (shell cmd) { delegate_ctlc = True } $ \_ _ _ p ->
+callCommand = callCreateProcess_ "callCommand" . shell
+
+callCreateProcess_ :: String -> CreateProcess -> IO ()
+callCreateProcess_ fun command = do
+    exit_code <- withCreateProcess_ fun
+                   command { delegate_ctlc = True } $ \_ _ _ p ->
                    waitForProcess p
     case exit_code of
       ExitSuccess   -> return ()
-      ExitFailure r -> processFailedException "callCommand" cmd [] r
+      ExitFailure r -> processFailed fun (cmdspec command) r
 
+processFailed :: String -> CmdSpec -> Int -> IO a
+processFailed fun = \ case
+    ShellCommand cmd -> processFailedException fun cmd []
+    RawCommand cmd args -> processFailedException fun cmd args
+
 processFailedException :: String -> String -> [String] -> Int -> IO a
 processFailedException fun cmd args exit_code =
       ioError (mkIOError OtherError (fun ++ ": " ++ cmd ++
@@ -380,6 +422,39 @@
 
 
 -- ----------------------------------------------------------------------------
+-- Secure process creation on Windows
+
+-- $windows-migitations
+--
+-- In general it is strongly advised that any untrusted user input be validated before
+-- being passed to a subprocess. One must be especially careful on Windows due to the
+-- crude nature of the platform's argument passing scheme. Specifically, unlike POSIX
+-- platforms, Windows treats the command-line not as a sequence of arguments but rather
+-- as a single string. It is therefore the responsibility of the called process to tokenize
+-- this string into distinct arguments.
+--
+-- While various programs on Windows tend to differ in their precise argument splitting
+-- behavior, the scheme used by @process@'s 'RawCommand' 'CmdSpec' should work for
+-- most reasonable programs. If you find that 'RawCommand' doesn't provide
+-- the behavior you need, it is recommended to instead compose your command-line
+-- manually and rather using the 'shell' 'CmdSpec'.
+--
+-- Additionally, the idiosyncratic escaping and string interpolation behavior of
+-- the Windows @cmd.exe@ command interpreter is known to introduce considerable
+-- complication to secure process creation. For this reason, @process@ implements
+-- specific argument escaping logic when the executable's file extension suggests
+-- that it is a batch file (e.g. @.bat@ or @.cmd@). However, this is not a
+-- completely reliable mitigation as Windows will also silently execute batch files
+-- when starting executables lacking a file extension (e.g. @callProcess "hello" []@
+-- when a @hello.bat@ is present in @PATH@). For this reason, users are encouraged to
+-- specify the file extension of invoked executables where possible, especially
+-- when untrusted input is involved.
+--
+-- Users passed untrusted input to subprocesses on Windows are encouraged to review
+-- <https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/>
+-- for guidance on how to safely navigate these waters.
+
+-- ----------------------------------------------------------------------------
 -- Control-C handling on Unix
 
 -- $ctlc-handling
@@ -478,7 +553,8 @@
 --
 -- * The command to run, which must be in the $PATH, or an absolute or relative path
 --
--- * A list of separate command line arguments to the program
+-- * A list of separate command line arguments to the program.  See 'RawCommand' for
+--   further discussion of Windows semantics.
 --
 -- * A string to pass on standard input to the forked process.
 --
@@ -537,14 +613,7 @@
 
     case ex of
      ExitSuccess   -> return output
-     ExitFailure r -> processFailedException "readCreateProcess" cmd args r
-  where
-    cmd = case cp of
-            CreateProcess { cmdspec = ShellCommand sc } -> sc
-            CreateProcess { cmdspec = RawCommand fp _ } -> fp
-    args = case cp of
-             CreateProcess { cmdspec = ShellCommand _ } -> []
-             CreateProcess { cmdspec = RawCommand _ args' } -> args'
+     ExitFailure r -> processFailed "readCreateProcess" (cmdspec cp) r
 
 
 -- | @readProcessWithExitCode@ is like 'readProcess' but with two differences:
@@ -616,28 +685,6 @@
           (_,Nothing,_) -> error "readCreateProcessWithExitCode: Failed to get a stdout handle."
           (_,_,Nothing) -> error "readCreateProcessWithExitCode: Failed to get a stderr handle."
 
--- | Fork a thread while doing something else, but kill it if there's an
--- exception.
---
--- This is important in the cases above because we want to kill the thread
--- that is holding the Handle lock, because when we clean up the process we
--- try to close that handle, which could otherwise deadlock.
---
-withForkWait :: IO () -> (IO () ->  IO a) -> IO a
-withForkWait async body = do
-  waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
-  mask $ \restore -> do
-    tid <- forkIO $ try (restore async) >>= putMVar waitVar
-    let wait = takeMVar waitVar >>= either throwIO return
-    restore (body wait) `C.onException` killThread tid
-
-ignoreSigPipe :: IO () -> IO ()
-ignoreSigPipe = C.handle $ \e -> case e of
-                                   IOError { ioe_type  = ResourceVanished
-                                           , ioe_errno = Just ioe }
-                                     | Errno ioe == ePIPE -> return ()
-                                   _ -> throwIO e
-
 -- ----------------------------------------------------------------------------
 -- showCommandForUser
 
@@ -667,7 +714,7 @@
     OpenHandle h -> do
       pid <- getProcessId h
       return $ Just pid
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
     OpenHandle h -> do
       pid <- getProcessId h
       return $ Just pid
@@ -690,7 +737,7 @@
 getCurrentPid =
 #if defined(javascript_HOST_ARCH)
     getCurrentProcessId
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
     getCurrentProcessId
 #else
     getProcessID
@@ -742,7 +789,7 @@
         when (was_open && delegating_ctlc) $
           endDelegateControlC e
         return e'
-#if defined(WINDOWS)
+#if defined(mingw32_HOST_OS)
     OpenExtHandle h job -> do
         -- First wait for completion of the job...
         waitForJobCompletion job
@@ -871,7 +918,7 @@
   withProcessHandle ph $ \p_ ->
     case p_ of
       ClosedHandle  _ -> return ()
-#if defined(WINDOWS)
+#if defined(mingw32_HOST_OS)
       OpenExtHandle{} -> terminateJobUnsafe p_ 1 >> return ()
 #else
       OpenExtHandle{} -> error "terminateProcess with OpenExtHandle should not happen on POSIX."
diff --git a/System/Process/Common.hs b/System/Process/Common.hs
--- a/System/Process/Common.hs
+++ b/System/Process/Common.hs
@@ -19,27 +19,31 @@
     , mbFd
     , mbPipe
     , pfdToHandle
+    , rawFdToHandle
 
 -- Avoid a warning on Windows
-#ifdef WINDOWS
+#if defined(mingw32_HOST_OS)
     , CGid (..)
 #else
     , CGid
 #endif
 
--- WINIO is only available on GHC 8.12 and up.
-#if defined(__IO_MANAGER_WINIO__)
+#if defined(mingw32_HOST_OS)
     , HANDLE
+-- WINIO is only available on GHC 9.0 and up.
+#  if defined(__IO_MANAGER_WINIO__)
     , mbHANDLE
     , mbPipeHANDLE
+    , rawHANDLEToHandle
+#  endif
 #endif
     ) where
 
 import Control.Concurrent
 import Control.Exception
-import Data.String
+import Data.String ( IsString(..) )
 import Foreign.Ptr
-import Foreign.Storable
+import Foreign.Storable ( Storable(peek) )
 
 import System.Posix.Internals
 import GHC.IO.Exception
@@ -63,7 +67,7 @@
 
 -- We do a minimal amount of CPP here to provide uniform data types across
 -- Windows and POSIX.
-#ifdef WINDOWS
+#if defined(mingw32_HOST_OS)
 import Data.Word (Word32)
 import System.Win32.DebugApi (PHANDLE)
 #if defined(__IO_MANAGER_WINIO__)
@@ -75,7 +79,7 @@
 
 #if defined(javascript_HOST_ARCH)
 type PHANDLE = JSVal
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
 -- Define some missing types for Windows compatibility. Note that these values
 -- will never actually be used, as the setuid/setgid system calls are not
 -- applicable on Windows. No value of this type will ever exist.
@@ -93,7 +97,8 @@
   std_in       :: StdStream,               -- ^ How to determine stdin
   std_out      :: StdStream,               -- ^ How to determine stdout
   std_err      :: StdStream,               -- ^ How to determine stderr
-  close_fds    :: Bool,                    -- ^ Close all file descriptors except stdin, stdout and stderr in the new process (on Windows, only works if std_in, std_out, and std_err are all Inherit). This implementation will call close on every fd from 3 to the maximum of open files, which can be slow for high maximum of open files. XXX verify what happens with fds in nodejs child processes
+  -- XXX verify what happens with fds in nodejs child processes
+  close_fds    :: Bool,                    -- ^ Close all file descriptors except stdin, stdout and stderr in the new process (on Windows, only works if std_in, std_out, and std_err are all Inherit). This implementation will call close on every fd from 3 to the maximum of open files, which can be slow for high maximum of open files.
   create_group :: Bool,                    -- ^ Create a new process group. On JavaScript this also creates a new session.
   delegate_ctlc:: Bool,                    -- ^ Delegate control-C handling. Use this for interactive console processes to let them handle control-C themselves (see below for details).
                                            --
@@ -160,6 +165,14 @@
       --   see the
       --   <http://msdn.microsoft.com/en-us/library/windows/desktop/aa365527%28v=vs.85%29.aspx documentation>
       --   for the Windows @SearchPath@ API.
+      --
+      --   Windows does not have a mechanism for passing multiple arguments.
+      --   When using @RawCommand@ on Windows, the command line is serialised
+      --   into a string, with arguments quoted separately.  Command line
+      --   parsing is up individual programs, so the default behaviour may
+      --   not work for some programs.  If you are not getting the desired
+      --   results, construct the command line yourself and use 'ShellCommand'.
+      --
   deriving (Show, Eq)
 
 
@@ -270,8 +283,11 @@
 mbPipe _std      _pfd _mode = return Nothing
 
 pfdToHandle :: Ptr FD -> IOMode -> IO Handle
-pfdToHandle pfd mode = do
-  fd <- peek pfd
+pfdToHandle pfd mode =
+  ( \ fd -> rawFdToHandle fd mode ) =<< peek pfd
+
+rawFdToHandle :: FD -> IOMode -> IO Handle
+rawFdToHandle fd mode = do
   let filepath = "fd:" ++ show fd
   (fD,fd_type) <- FD.mkFD (fromIntegral fd) mode
                        (Just (Stream,0,0)) -- avoid calling fstat()
@@ -285,6 +301,11 @@
 #endif
   mkHandleFromFD fD' fd_type filepath mode False {-is_socket-} (Just enc)
 
+
+#if defined(mingw32_HOST_OS) && !defined(__IO_MANAGER_WINIO__)
+type HANDLE = Ptr ()
+#endif
+
 #if defined(__IO_MANAGER_WINIO__)
 -- It is not completely safe to pass the values -1 and -2 as HANDLE as it's an
 -- unsigned type. -1 additionally is also the value for INVALID_HANDLE.  However
@@ -299,11 +320,14 @@
 mbHANDLE _std (UseHandle hdl) = handleToHANDLE hdl
 
 mbPipeHANDLE :: StdStream -> Ptr HANDLE -> IOMode -> IO (Maybe Handle)
-mbPipeHANDLE CreatePipe pfd  mode =
-  do raw_handle <- peek pfd
-     let hwnd  = fromHANDLE raw_handle :: Io NativeHandle
-         ident = "hwnd:" ++ show raw_handle
-     enc <- fmap Just getLocaleEncoding
-     Just <$> mkHandleFromHANDLE hwnd Stream ident mode enc
+mbPipeHANDLE CreatePipe pfd mode =
+  Just <$> ( ( \ hANDLE -> rawHANDLEToHandle hANDLE mode ) =<< peek pfd )
 mbPipeHANDLE _std      _pfd _mode = return Nothing
+
+rawHANDLEToHandle :: HANDLE -> IOMode-> IO Handle
+rawHANDLEToHandle raw_handle mode  = do
+  let hwnd  = fromHANDLE raw_handle :: Io NativeHandle
+      ident = "hwnd:" ++ show raw_handle
+  enc <- getLocaleEncoding
+  mkHandleFromHANDLE hwnd Stream ident mode (Just enc)
 #endif
diff --git a/System/Process/CommunicationHandle.hs b/System/Process/CommunicationHandle.hs
new file mode 100644
--- /dev/null
+++ b/System/Process/CommunicationHandle.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE RankNTypes #-}
+
+module System.Process.CommunicationHandle
+  ( -- * 'CommunicationHandle': a 'Handle' that can be serialised,
+    -- enabling inter-process communication.
+    CommunicationHandle
+      -- NB: opaque, as the representation depends on the operating system
+  , openCommunicationHandleRead
+  , openCommunicationHandleWrite
+  , closeCommunicationHandle
+    -- * Creating 'CommunicationHandle's to communicate with
+    -- a child process
+  , createWeReadTheyWritePipe
+  , createTheyReadWeWritePipe
+   -- * High-level API
+  , readCreateProcessWithExitCodeCommunicationHandle
+  )
+ where
+
+import GHC.IO.Handle (Handle)
+
+import System.Process.CommunicationHandle.Internal
+import System.Process.Internals
+  ( CreateProcess(..), ignoreSigPipe, withForkWait )
+import System.Process
+  ( withCreateProcess, waitForProcess )
+
+import GHC.IO (evaluate)
+import GHC.IO.Handle (hClose)
+import System.Exit (ExitCode)
+
+import Control.DeepSeq (NFData, rnf)
+
+--------------------------------------------------------------------------------
+-- Communication handles.
+
+-- | Turn the 'CommunicationHandle' into a 'Handle' that can be read from
+-- in the current process.
+--
+-- The returned 'Handle' does not have any finalizers attached to it;
+-- use 'hClose' to close it.
+--
+-- @since 1.6.20.0
+openCommunicationHandleRead :: CommunicationHandle -> IO Handle
+openCommunicationHandleRead = useCommunicationHandle True
+
+-- | Turn the 'CommunicationHandle' into a 'Handle' that can be written to
+-- in the current process.
+--
+-- The returned 'Handle' does not have any finalizers attached to it;
+-- use 'hClose' to close it.
+--
+-- @since 1.6.20.0
+openCommunicationHandleWrite :: CommunicationHandle -> IO Handle
+openCommunicationHandleWrite = useCommunicationHandle False
+
+--------------------------------------------------------------------------------
+-- Creating pipes.
+
+-- | Create a pipe @(weRead,theyWrite)@ that the current process can read from,
+-- and whose write end can be passed to a child process in order to receive data from it.
+--
+-- The returned 'Handle' does not have any finalizers attached to it;
+-- use 'hClose' to close it.
+--
+-- See 'CommunicationHandle'.
+--
+-- @since 1.6.20.0
+createWeReadTheyWritePipe
+  :: IO (Handle, CommunicationHandle)
+createWeReadTheyWritePipe =
+  createCommunicationPipe id False
+    -- safe choice: passAsyncHandleToChild = False, in case the child cannot
+    -- deal with async I/O (see e.g. https://gitlab.haskell.org/ghc/ghc/-/issues/21610#note_431632)
+    -- expert users can invoke createCommunicationPipe from
+    -- System.Process.CommunicationHandle.Internals if they are sure that the
+    -- child process they will communicate with supports async I/O on Windows
+
+-- | Create a pipe @(theyRead,weWrite)@ that the current process can write to,
+-- and whose read end can be passed to a child process in order to send data to it.
+--
+-- The returned 'Handle' does not have any finalizers attached to it;
+-- use 'hClose' to close it.
+--
+-- See 'CommunicationHandle'.
+--
+-- @since 1.6.20.0
+createTheyReadWeWritePipe
+  :: IO (CommunicationHandle, Handle)
+createTheyReadWeWritePipe =
+  sw <$> createCommunicationPipe sw False
+    -- safe choice: passAsyncHandleToChild = False, in case the child cannot
+    -- deal with async I/O (see e.g. https://gitlab.haskell.org/ghc/ghc/-/issues/21610#note_431632)
+    -- expert users can invoke createCommunicationPipe from
+    -- System.Process.CommunicationHandle.Internals if they are sure that the
+    -- child process they will communicate with supports async I/O on Windows
+  where
+    sw (a,b) = (b,a)
+
+--------------------------------------------------------------------------------
+
+-- | A version of 'readCreateProcessWithExitCode' that communicates with the
+-- child process through a pair of 'CommunicationHandle's.
+--
+-- Example usage:
+--
+-- > readCreateProcessWithExitCodeCommunicationHandle
+-- >   (\(chTheyRead, chTheyWrite) -> proc "child-exe" [show chTheyRead, show chTheyWrite])
+-- >   (\ hWeRead -> hGetContents hWeRead)
+-- >   (\ hWeWrite -> hPut hWeWrite "xyz")
+--
+-- where @child-exe@ is a separate executable that is implemented as:
+--
+-- > main = do
+-- >   [chRead, chWrite] <- getArgs
+-- >   hRead  <- openCommunicationHandleRead  $ read chRead
+-- >   hWrite <- openCommunicationHandleWrite $ read chWrite
+-- >   input <- hGetContents hRead
+-- >   hPut hWrite $ someFn input
+-- >   hClose hWrite
+--
+-- @since 1.6.20.0
+readCreateProcessWithExitCodeCommunicationHandle
+  :: NFData a
+  => ((CommunicationHandle, CommunicationHandle) -> CreateProcess)
+    -- ^ Process to spawn, given a @(read, write)@ pair of
+    -- 'CommunicationHandle's that are inherited by the spawned process
+  -> (Handle -> IO a)
+    -- ^ read action
+  -> (Handle -> IO ())
+    -- ^ write action
+  -> IO (ExitCode, a)
+readCreateProcessWithExitCodeCommunicationHandle mkProg readAction writeAction = do
+  (chTheyRead, hWeWrite   ) <- createTheyReadWeWritePipe
+  (hWeRead   , chTheyWrite) <- createWeReadTheyWritePipe
+  let cp = mkProg (chTheyRead, chTheyWrite)
+  -- The following implementation parallels 'readCreateProcess'
+  withCreateProcess cp $ \ _ _ _ ph -> do
+
+    -- Close the parent's references to the 'CommunicationHandle's after they
+    -- have been inherited by the child (we don't want to keep pipe ends open).
+    closeCommunicationHandle chTheyWrite
+    closeCommunicationHandle chTheyRead
+
+    -- Fork off a thread that waits on the output.
+    output <- readAction hWeRead
+    withForkWait (evaluate $ rnf output) $ \ waitOut -> do
+      ignoreSigPipe $ writeAction hWeWrite
+      ignoreSigPipe $ hClose hWeWrite
+      waitOut
+      hClose hWeRead
+
+    ex <- waitForProcess ph
+    return (ex, output)
diff --git a/System/Process/CommunicationHandle/Internal.hsc b/System/Process/CommunicationHandle/Internal.hsc
new file mode 100644
--- /dev/null
+++ b/System/Process/CommunicationHandle/Internal.hsc
@@ -0,0 +1,311 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE RankNTypes #-}
+
+module System.Process.CommunicationHandle.Internal
+  ( -- * 'CommunicationHandle': a 'Handle' that can be serialised,
+    -- enabling inter-process communication.
+    CommunicationHandle(..)
+  , closeCommunicationHandle
+    -- ** Internal functions
+  , useCommunicationHandle
+  , createCommunicationPipe
+  )
+ where
+
+import Control.Arrow ( first )
+import GHC.IO.Handle (Handle, hClose)
+#if defined(mingw32_HOST_OS)
+import Foreign.C (CInt(..), throwErrnoIf_)
+import Foreign.Marshal (alloca)
+import Foreign.Ptr (ptrToWordPtr, wordPtrToPtr)
+import Foreign.Storable (Storable(peek))
+import GHC.IO.Handle.FD (fdToHandle)
+import GHC.IO.IOMode (IOMode(ReadMode, WriteMode))
+import System.Process.Windows (HANDLE, mkNamedPipe)
+##  if defined(__IO_MANAGER_WINIO__)
+import Control.Exception (catch, throwIO)
+import GHC.IO (onException)
+import GHC.IO.Device as IODevice (close, devType)
+import GHC.IO.Encoding (getLocaleEncoding)
+import GHC.IO.Exception (IOException(..), IOErrorType(InvalidArgument))
+import GHC.IO.IOMode (IOMode(ReadWriteMode))
+import GHC.IO.Handle.Windows (mkHandleFromHANDLE)
+import GHC.IO.SubSystem ((<!>))
+import GHC.IO.Windows.Handle (Io, NativeHandle, fromHANDLE)
+import GHC.Event.Windows (associateHandle')
+import System.Process.Common (rawHANDLEToHandle)
+##  else
+import System.Process.Common (rawFdToHandle)
+##  endif
+
+#include <fcntl.h>     /* for _O_BINARY */
+
+#else
+import GHC.IO.FD
+  ( mkFD, setNonBlockingMode )
+import GHC.IO.Handle
+  ( noNewlineTranslation )
+#if MIN_VERSION_base(4,16,0)
+import GHC.IO.Handle.Internals
+  ( mkFileHandleNoFinalizer )
+#else
+import GHC.IO.IOMode
+  ( IOMode(..) )
+import GHC.IO.Handle.Types
+  ( HandleType(..) )
+import GHC.IO.Handle.Internals
+  ( mkHandle )
+#endif
+import System.Posix
+  ( Fd(..)
+  , FdOption(..), setFdOption
+  )
+import System.Posix.Internals
+  ( fdGetMode )
+import System.Process.Internals
+  ( createPipeFd )
+#endif
+
+--------------------------------------------------------------------------------
+-- Communication handles.
+
+-- | A 'CommunicationHandle' is an abstraction over operating-system specific
+-- internal representation of a 'Handle', which can be communicated through a
+-- command-line interface.
+--
+-- In a typical use case, the parent process creates a pipe, using e.g.
+-- 'createWeReadTheyWritePipe' or 'createTheyReadWeWritePipe'.
+--
+--  - One end of the pipe is a 'Handle', which can be read from/written to by
+--    the parent process.
+--  - The other end is a 'CommunicationHandle', which can be inherited by a
+--    child process. A reference to the handle can be serialised (using
+--    the 'Show' instance), and passed to the child process.
+--    It is recommended to close the parent's reference to the 'CommunicationHandle'
+--    using 'closeCommunicationHandle' after it has been inherited by the child
+--    process.
+--  - The child process can deserialise the 'CommunicationHandle' (using
+--    the 'Read' instance), and then use 'openCommunicationHandleWrite' or
+--    'openCommunicationHandleRead' in order to retrieve a 'Handle' which it
+--    can write to/read from.
+--
+-- 'readCreateProcessWithExitCodeCommunicationHandle' provides a high-level API
+-- to this functionality. See there for example code.
+--
+-- @since 1.6.20.0
+newtype CommunicationHandle =
+  CommunicationHandle
+##if defined(mingw32_HOST_OS)
+    HANDLE
+##else
+    Fd
+##endif
+  deriving ( Eq, Ord )
+
+#if defined(mingw32_HOST_OS)
+type Fd = CInt
+#endif
+
+-- @since 1.6.20.0
+instance Show CommunicationHandle where
+  showsPrec p (CommunicationHandle h) =
+    showsPrec p
+##if defined(mingw32_HOST_OS)
+      $ ptrToWordPtr
+##endif
+      h
+
+-- @since 1.6.20.0
+instance Read CommunicationHandle where
+  readsPrec p str =
+    fmap
+      ( first $ CommunicationHandle
+##if defined(mingw32_HOST_OS)
+              . wordPtrToPtr
+##endif
+      ) $
+      readsPrec p str
+
+-- | Internal function used to define 'openCommunicationHandleRead' and
+-- openCommunicationHandleWrite.
+useCommunicationHandle :: Bool -> CommunicationHandle -> IO Handle
+useCommunicationHandle _wantToRead (CommunicationHandle ch) = do
+##if defined(__IO_MANAGER_WINIO__)
+  return ()
+    <!> associateHandleWithFallback _wantToRead ch
+##endif
+  getGhcHandle ch
+
+-- | Close a 'CommunicationHandle'.
+--
+-- Use this to close the 'CommunicationHandle' in the parent process after
+-- the 'CommunicationHandle' has been inherited by the child process.
+--
+-- @since 1.6.20.0
+closeCommunicationHandle :: CommunicationHandle -> IO ()
+closeCommunicationHandle (CommunicationHandle ch) =
+  hClose =<< getGhcHandle ch
+
+##if defined(__IO_MANAGER_WINIO__)
+-- Internal function used when associating a 'HANDLE' with the current process.
+--
+-- Explanation: with WinIO, a synchronous handle cannot be associated with the
+-- current process, while an asynchronous one must be associated before being usable.
+--
+-- In a child process, we don't necessarily know which kind of handle we will receive,
+-- so we try to associate it (in case it is an asynchronous handle). This might
+-- fail (if the handle is synchronous), in which case we continue in synchronous
+-- mode (without associating).
+--
+-- With the current API, inheritable handles in WinIO created with mkNamedPipe
+-- are synchronous, but it's best to be safe in case the child receives an
+-- asynchronous handle anyway.
+associateHandleWithFallback :: Bool -> HANDLE -> IO ()
+associateHandleWithFallback _wantToRead h =
+  associateHandle' h `catch` handler
+  where
+    handler :: IOError -> IO ()
+    handler ioErr@(IOError { ioe_handle = _mbErrHandle, ioe_type = errTy, ioe_errno = mbErrNo })
+      -- Catches the following error that occurs when attemping to associate
+      -- a HANDLE that does not have OVERLAPPING mode set:
+      --
+      --   associateHandleWithIOCP: invalid argument (The parameter is incorrect.)
+      | InvalidArgument <- errTy
+      , Just 22 <- mbErrNo
+      = return ()
+      | otherwise
+      = throwIO ioErr
+##endif
+
+-- | Gets a GHC Handle File description from the given OS Handle or POSIX fd.
+
+#if defined(mingw32_HOST_OS)
+getGhcHandle :: HANDLE -> IO Handle
+getGhcHandle =
+  getGhcHandlePOSIX
+##  if defined(__IO_MANAGER_WINIO__)
+    <!> getGhcHandleNative
+##  endif
+
+getGhcHandlePOSIX :: HANDLE -> IO Handle
+getGhcHandlePOSIX handle = openHANDLE handle >>= fdToHandle
+
+openHANDLE :: HANDLE -> IO Fd
+openHANDLE handle = _open_osfhandle handle (#const _O_BINARY)
+
+foreign import ccall "io.h _open_osfhandle"
+  _open_osfhandle :: HANDLE -> CInt -> IO Fd
+
+##  if defined(__IO_MANAGER_WINIO__)
+getGhcHandleNative :: HANDLE -> IO Handle
+getGhcHandleNative hwnd =
+  do mb_codec <- fmap Just getLocaleEncoding
+     let iomode = ReadWriteMode
+         native_handle = fromHANDLE hwnd :: Io NativeHandle
+     hw_type <- IODevice.devType $ native_handle
+     mkHandleFromHANDLE native_handle hw_type (show hwnd) iomode mb_codec
+       `onException` IODevice.close native_handle
+##  endif
+#else
+getGhcHandle :: Fd -> IO Handle
+getGhcHandle (Fd fdint) = do
+  iomode <- fdGetMode fdint
+  (fd0, _) <- mkFD fdint iomode Nothing False True
+  -- The following copies over 'mkHandleFromFDNoFinalizer'
+  fd <- setNonBlockingMode fd0 True
+  let fd_str = "<file descriptor: " ++ show fd ++ ">"
+#  if MIN_VERSION_base(4,16,0)
+  mkFileHandleNoFinalizer fd fd_str iomode Nothing noNewlineTranslation
+#  else
+  mkHandle fd fd_str (ioModeToHandleType iomode) True Nothing noNewlineTranslation
+    Nothing Nothing
+
+ioModeToHandleType :: IOMode -> HandleType
+ioModeToHandleType mode =
+  case mode of
+    ReadMode      -> ReadHandle
+    WriteMode     -> WriteHandle
+    ReadWriteMode -> ReadWriteHandle
+    AppendMode    -> AppendHandle
+#  endif
+#endif
+
+--------------------------------------------------------------------------------
+-- Creating pipes.
+
+-- | Internal helper function used to define 'createWeReadTheyWritePipe'
+-- and 'createTheyReadWeWritePipe' while reducing code duplication.
+--
+-- The returned 'Handle' does not have any finalizers attached to it;
+-- use 'hClose' to close it.
+createCommunicationPipe
+  :: ( forall a. (a, a) -> (a, a) )
+    -- ^ 'id' (we read, they write) or 'swap' (they read, we write)
+  -> Bool -- ^ whether to pass a handle supporting asynchronous I/O to the child process
+          -- (this flag only has an effect on Windows and when using WinIO)
+  -> IO (Handle, CommunicationHandle)
+createCommunicationPipe swapIfTheyReadWeWrite _passAsyncHandleToChild = do
+##if !defined(mingw32_HOST_OS)
+  -- NB: it's important to use 'createPipeFd' here.
+  --
+  -- Were we to instead use 'createPipe', we would create a Handle for both pipe
+  -- ends, including the end we pass to the child.
+  -- Such Handle would have a finalizer which closes the underlying file descriptor.
+  -- However, we will already close the FD after it is inherited by the child.
+  -- This could lead to the following scenario:
+  --
+  --  - the parent creates a new pipe, e.g. pipe2([7,8]),
+  --  - the parent spawns a child process, and lets FD 8 be inherited by the child,
+  --  - the parent closes FD 8,
+  --  - the parent opens FD 8 for some other purpose, e.g. for writing to a file,
+  --  - the finalizer for the Handle wrapping FD 8 runs, closing FD 8, even though
+  --    it is now in use for a completely different purpose.
+  (ourFd, theirFd) <- swapIfTheyReadWeWrite <$> createPipeFd
+  -- Don't allow the child process to inherit a parent file descriptor
+  -- (such inheritance happens by default on Unix).
+  setFdOption (Fd ourFd) CloseOnExec True
+  -- NB: we will be closing this handle manually, so don't use 'handleFromFd'
+  -- which attaches a finalizer that closes the FD. See the above comment
+  -- about 'createPipeFd'.
+  ourHandle <- getGhcHandle (Fd ourFd)
+  return (ourHandle, CommunicationHandle $ Fd theirFd)
+##else
+  trueForWinIO <-
+    return False
+##  if defined (__IO_MANAGER_WINIO__)
+      <!> return True
+##  endif
+  -- On Windows, use mkNamedPipe to create the two pipe ends.
+  alloca $ \ pfdStdInput  ->
+    alloca $ \ pfdStdOutput -> do
+      let (inheritRead, inheritWrite) = swapIfTheyReadWeWrite (False, True)
+          -- WinIO:
+          --  - make the parent pipe end overlapped,
+          --  - make the child end overlapped if requested,
+          -- Otherwise: make both pipe ends synchronous.
+          overlappedRead  = trueForWinIO && ( _passAsyncHandleToChild || not inheritRead  )
+          overlappedWrite = trueForWinIO && ( _passAsyncHandleToChild || not inheritWrite )
+      throwErrnoIf_ (==False) "mkNamedPipe" $
+        mkNamedPipe
+          pfdStdInput  inheritRead  overlappedRead
+          pfdStdOutput inheritWrite overlappedWrite
+      let ((ourPtr, ourMode), (theirPtr, _theirMode)) =
+            swapIfTheyReadWeWrite ((pfdStdInput, ReadMode), (pfdStdOutput, WriteMode))
+      ourHANDLE  <- peek ourPtr
+      theirHANDLE <- peek theirPtr
+      -- With WinIO, we need to associate any handles we are going to use in
+      -- the current process before being able to use them.
+      return ()
+##  if defined (__IO_MANAGER_WINIO__)
+        <!> associateHandle' ourHANDLE
+##  endif
+      ourHandle <-
+##  if !defined (__IO_MANAGER_WINIO__)
+        ( \ fd -> rawFdToHandle fd ourMode ) =<< openHANDLE ourHANDLE
+##  else
+        -- NB: it's OK to call the following function even when we're not
+        -- using WinIO at runtime, so we don't use <!>.
+        rawHANDLEToHandle ourHANDLE ourMode
+##  endif
+      return $ (ourHandle, CommunicationHandle theirHANDLE)
+##endif
diff --git a/System/Process/Environment/OsString.hs b/System/Process/Environment/OsString.hs
new file mode 100644
--- /dev/null
+++ b/System/Process/Environment/OsString.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE PackageImports #-}
+
+-- | Miscellaneous information about the system environment, for 'OsString'.
+--
+-- @since 1.6.26.0
+module System.Process.Environment.OsString (
+    getArgs,
+    getEnv,
+    getEnvironment,
+    ) where
+
+import Data.Coerce (coerce)
+#if MIN_VERSION_filepath(1, 5, 0)
+import "os-string" System.OsString.Internal.Types (OsString(OsString))
+#else
+import "filepath" System.OsString.Internal.Types (OsString(OsString))
+#endif
+#if defined(mingw32_HOST_OS)
+import qualified System.Win32.WindowsString.Console as Platform
+#else
+import qualified System.Posix.Env.PosixString as Platform
+#endif
+
+-- | 'System.Environment.getArgs' for 'OsString'.
+--
+-- @since 1.6.26.0
+getArgs :: IO [OsString]
+getArgs = coerce Platform.getArgs
+
+-- | 'System.Environment.getEnv' for 'OsString'.
+--
+-- @since 1.6.26.0
+getEnv :: OsString -> IO (Maybe OsString)
+getEnv = coerce Platform.getEnv
+
+-- | 'System.Environment.getEnvironment' for 'OsString'.
+--
+-- @since 1.6.26.0
+getEnvironment :: IO [(OsString, OsString)]
+getEnvironment = coerce Platform.getEnvironment
diff --git a/System/Process/Internals.hs b/System/Process/Internals.hs
--- a/System/Process/Internals.hs
+++ b/System/Process/Internals.hs
@@ -22,7 +22,7 @@
 module System.Process.Internals (
     ProcessHandle(..), ProcessHandle__(..),
     PHANDLE, closePHANDLE, mkProcessHandle,
-#ifdef WINDOWS
+#if defined(mingw32_HOST_OS)
     CGid(..),
 #else
     CGid,
@@ -39,7 +39,7 @@
     endDelegateControlC,
     stopDelegateControlC,
     unwrapHandles,
-#ifdef WINDOWS
+#if defined(mingw32_HOST_OS)
     terminateJob,
     terminateJobUnsafe,
     waitForJobCompletion,
@@ -56,11 +56,17 @@
     createPipe,
     createPipeFd,
     interruptProcessGroupOf,
+    withForkWait,
+    ignoreSigPipe,
     ) where
 
+import Control.Concurrent
+import Control.Exception (SomeException, mask, try, throwIO)
+import qualified Control.Exception as C
 import Foreign.C
 import System.IO
 
+import GHC.IO.Exception ( IOErrorType(..), IOException(..) )
 import GHC.IO.Handle.FD (fdToHandle)
 import System.Posix.Internals (FD)
 
@@ -68,7 +74,7 @@
 
 #if defined(javascript_HOST_ARCH)
 import System.Process.JavaScript
-#elif defined(WINDOWS)
+#elif defined(mingw32_HOST_OS)
 import System.Process.Windows
 #else
 import System.Process.Posix
@@ -194,22 +200,22 @@
 --
 -- @
 --     #if defined(__IO_MANAGER_WINIO__)
---     import GHC.IO.SubSystem ((<!>))
+--     import GHC.IO.SubSystem ((`<!>`))
 --     import GHC.IO.Handle.Windows (handleToHANDLE)
 --     import GHC.Event.Windows (associateHandle')
 --     #endif
 --
 --     ...
 --
---     #if defined (__IO_MANAGER_WINIO__)
---     return () <!> (do
---       associateHandle' =<< handleToHANDLE <handle>)
+--     #if defined(__IO_MANAGER_WINIO__)
+--     return () \<!> do
+--       associateHandle' =\<\< handleToHANDLE readEnd
 --     #endif
 -- @
 --
 -- Only associate handles that you are in charge of read/writing to.
 -- Do not associate handles passed to another process.  It's the
--- process's reponsibility to register the handle if it supports
+-- process's responsibility to register the handle if it supports
 -- async access.
 --
 -- @since 1.2.1.0
@@ -243,3 +249,29 @@
     :: ProcessHandle    -- ^ A process in the process group
     -> IO ()
 interruptProcessGroupOf = interruptProcessGroupOfInternal
+
+-- | Fork a thread while doing something else, but kill it if there's an
+-- exception.
+--
+-- This is important in the cases above because we want to kill the thread
+-- that is holding the Handle lock, because when we clean up the process we
+-- try to close that handle, which could otherwise deadlock.
+--
+-- @since 1.6.20.0
+withForkWait :: IO () -> (IO () ->  IO a) -> IO a
+withForkWait async body = do
+  waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
+  mask $ \restore -> do
+    tid <- forkIO $ try (restore async) >>= putMVar waitVar
+    let wait = takeMVar waitVar >>= either throwIO return
+    restore (body wait) `C.onException` killThread tid
+
+-- | Handle any SIGPIPE errors in the given computation.
+--
+-- @since 1.6.20.0
+ignoreSigPipe :: IO () -> IO ()
+ignoreSigPipe = C.handle $ \e -> case e of
+                                   IOError { ioe_type  = ResourceVanished
+                                           , ioe_errno = Just ioe }
+                                     | Errno ioe == ePIPE -> return ()
+                                   _ -> throwIO e
diff --git a/System/Process/Posix.hs b/System/Process/Posix.hs
--- a/System/Process/Posix.hs
+++ b/System/Process/Posix.hs
@@ -136,7 +136,9 @@
    maybeWith withFilePath mb_cwd $ \pWorkDir ->
    maybeWith with mb_child_group $ \pChildGroup ->
    maybeWith with mb_child_user $ \pChildUser ->
-   withMany withFilePath (cmd:args) $ \cstrs ->
+   withFilePath cmd $ \cmdstr ->
+   withMany withCString args $ \argstrs -> do
+   let cstrs = cmdstr : argstrs
    withArray0 nullPtr cstrs $ \pargs -> do
 
      fdin  <- mbFd fun fd_stdin  mb_stdin
diff --git a/System/Process/Windows.hsc b/System/Process/Windows.hsc
--- a/System/Process/Windows.hsc
+++ b/System/Process/Windows.hsc
@@ -18,6 +18,8 @@
     , terminateJobUnsafe
     , waitForJobCompletion
     , timeout_Infinite
+    , HANDLE
+    , mkNamedPipe
     ) where
 
 import System.Process.Common
@@ -25,6 +27,8 @@
 import Control.Exception
 import Control.Monad
 import Data.Bits
+import Data.Char (toLower)
+import Data.List (dropWhileEnd)
 import Foreign.C
 import Foreign.Marshal
 import Foreign.Ptr
@@ -35,8 +39,8 @@
 import GHC.IO.Exception
 ##if defined(__IO_MANAGER_WINIO__)
 import GHC.IO.SubSystem
-import Graphics.Win32.Misc
 import qualified GHC.Event.Windows as Mgr
+import Graphics.Win32.Misc
 ##endif
 import GHC.IO.Handle.FD
 import GHC.IO.Handle.Types hiding (ClosedHandle)
@@ -56,7 +60,7 @@
 
 ##if defined(i386_HOST_ARCH)
 ## define WINDOWS_CCONV stdcall
-##elif defined(x86_64_HOST_ARCH)
+##elif defined(x86_64_HOST_ARCH) || defined(aarch64_HOST_ARCH)
 ## define WINDOWS_CCONV ccall
 ##else
 ## error Unknown mingw32 arch
@@ -425,9 +429,28 @@
         -- which partly works.  There seem to be some quoting issues, but
         -- I don't have the energy to find+fix them right now (ToDo). --SDM
         -- (later) Now I don't know what the above comment means.  sigh.
-commandToProcess (RawCommand cmd args) = do
-  return (cmd, translateInternal cmd ++ concatMap ((' ':) . translateInternal) args)
+commandToProcess (RawCommand cmd args)
+  | map toLower (takeWinExtension cmd) `elem` [".bat", ".cmd"]
+  = return (cmd, translateInternal cmd ++ concatMap ((' ':) . translateCmdExeArg) args)
+  | otherwise
+  = return (cmd, translateInternal cmd ++ concatMap ((' ':) . translateInternal) args)
 
+-- TODO: filepath should also be updated with 'takeWinExtension'. Perhaps
+-- some day we can remove this logic from `process` but there is no hurry.
+
+-- | Get the extension of a Windows file, removing any trailing spaces or dots
+-- since they are ignored.
+--
+-- See: <https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/file-folder-name-whitespace-characters>
+--
+-- >>> takeWinExtension "test.bat."
+-- ".bat"
+--
+-- >>> takeWinExtension "test.bat ."
+-- ".bat"
+takeWinExtension :: FilePath -> String
+takeWinExtension = takeExtension . dropWhileEnd (`elem` [' ', '.'])
+
 -- Find CMD.EXE (or COMMAND.COM on Win98).  We use the same algorithm as
 -- system() in the VC++ CRT (Vc7/crt/src/system.c in a VC++ installation).
 findCommandInterpreter :: IO FilePath
@@ -467,6 +490,31 @@
                                 "findCommandInterpreter" Nothing Nothing)
       Just cmd -> return cmd
 
+-- | Alternative regime used to escape arguments destined for scripts
+-- interpreted by @cmd.exe@, (e.g. @.bat@ and @.cmd@ files).
+--
+-- This respects the Windows command interpreter's quoting rules:
+--
+-- * the entire argument should be surrounded in quotes
+-- * the backslash symbol is used to escape quotes and backslashes
+-- * the carat symbol is used to escape other special characters with
+--   significance to the interpreter
+--
+-- It is particularly important that we perform this quoting as
+-- unvalidated unquoted command-line arguments can be used to achieve
+-- arbitrary user code execution in when passed to a vulnerable batch
+-- script.
+--
+translateCmdExeArg :: String -> String
+translateCmdExeArg xs = "^\"" ++ snd (foldr escape (True,"^\"") xs)
+  where escape '"'  (_,     str) = (True,  '\\' : '"'  : str)
+        escape '\\' (True,  str) = (True,  '\\' : '\\' : str)
+        escape '\\' (False, str) = (False, '\\' : str)
+        escape '%'  (_,     str) = (False, "%%cd:~,%" ++ str)
+        escape c    (_,     str)
+          | c `elem` "^<>|&()"   = (False, '^' : c : str)
+          | otherwise            = (False,       c : str)
+
 translateInternal :: String -> String
 translateInternal xs = '"' : snd (foldr escape (True,"\"") xs)
   where escape '"'  (_,     str) = (True,  '\\' : '"'  : str)
@@ -514,16 +562,16 @@
 createPipeInternalHANDLE =
   alloca $ \ pfdStdInput  ->
    alloca $ \ pfdStdOutput -> do
-     throwErrnoIf_  (==False) "c_mkNamedPipe" $
-       c_mkNamedPipe pfdStdInput True pfdStdOutput True
+     throwErrnoIf_  (==False) "mkNamedPipe" $
+       mkNamedPipe pfdStdInput True False pfdStdOutput True False
      Just hndStdInput  <- mbPipeHANDLE CreatePipe pfdStdInput ReadMode
      Just hndStdOutput <- mbPipeHANDLE CreatePipe pfdStdOutput WriteMode
      return (hndStdInput, hndStdOutput)
 
-
-foreign import ccall "mkNamedPipe" c_mkNamedPipe ::
-    Ptr HANDLE -> Bool -> Ptr HANDLE -> Bool -> IO Bool
 ##endif
+
+foreign import ccall "mkNamedPipe" mkNamedPipe ::
+  Ptr HANDLE -> Bool -> Bool -> Ptr HANDLE -> Bool -> Bool -> IO Bool
 
 close' :: CInt -> IO ()
 close' = throwErrnoIfMinus1_ "_close" . c__close
diff --git a/cbits/posix/common.h b/cbits/posix/common.h
--- a/cbits/posix/common.h
+++ b/cbits/posix/common.h
@@ -30,6 +30,7 @@
 #endif
 
 // defined in fork_exec.c
+#if defined(HAVE_FORK)
 ProcHandle
 do_spawn_fork (char *const args[],
                char *workingDirectory, char **environment,
@@ -39,6 +40,7 @@
                gid_t *childGroup, uid_t *childUser,
                int flags,
                char **failed_doing);
+#endif
 
 // defined in posix_spawn.c
 ProcHandle
diff --git a/cbits/posix/fork_exec.c b/cbits/posix/fork_exec.c
--- a/cbits/posix/fork_exec.c
+++ b/cbits/posix/fork_exec.c
@@ -282,17 +282,18 @@
 #if defined(HAVE_EXECVPE)
             // XXX Check result
             execvpe(args[0], args, environment);
+            child_failed(forkCommunicationFds[1], "execvpe");
 #else
             // XXX Check result
             execve(exec_path, args, environment);
+            child_failed(forkCommunicationFds[1], "execve");
 #endif
         } else {
             // XXX Check result
             execvp(args[0], args);
+            child_failed(forkCommunicationFds[1], "execvp");
         }
 
-        child_failed(forkCommunicationFds[1], "exec");
-
     default:
         if ((flags & RUN_PROCESS_IN_NEW_GROUP) != 0) {
             setpgid(pid, pid);
@@ -334,16 +335,8 @@
         // our responsibility to reap here as nobody else can.
         waitpid(pid, NULL, 0);
 
-        // Already closed child ends above
-        if (stdInHdl->behavior == STD_HANDLE_USE_PIPE) {
-            close(stdInHdl->use_pipe.parent_end);
-        }
-        if (stdOutHdl->behavior == STD_HANDLE_USE_PIPE) {
-            close(stdOutHdl->use_pipe.parent_end);
-        }
-        if (stdErrHdl->behavior == STD_HANDLE_USE_PIPE) {
-            close(stdErrHdl->use_pipe.parent_end);
-        }
+        // No need to close stdin, et al. here as runInteractiveProcess will
+        // handle this. See #306.
 
         pid = -1;
     }
diff --git a/cbits/posix/posix_spawn.c b/cbits/posix/posix_spawn.c
--- a/cbits/posix/posix_spawn.c
+++ b/cbits/posix/posix_spawn.c
@@ -1,3 +1,7 @@
+// Necessary for POSIX_SPAWN_SETSID and posix_spawn_file_actions_addchdir_np under glibc.
+// Moreover this needs to appear before any glibc headers.
+#define _GNU_SOURCE
+
 #include "runProcess.h"
 #include "common.h"
 
@@ -25,8 +29,6 @@
 
 #else
 
-// Necessary for POSIX_SPAWN_SETSID under glibc.
-#define _GNU_SOURCE
 #include <spawn.h>
 
 extern char **environ;
@@ -135,12 +137,33 @@
     }
 
     if (workingDirectory) {
-#if defined(HAVE_posix_spawn_file_actions_addchdir_np)
-        // N.B. this function is broken on macOS.
-        // See https://github.com/rust-lang/rust/pull/80537.
+#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
+#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)
+        // N.B. in certain toolchain versions, apple will act as if symbols exist
+        // because the toolchain can be *used* for targets that have them.
+        // At runtime, however, they will be NULL. So, if both symbols are
+        // available, first check if the _np version *should* be available,
+        // if it is, add a runtime check whether the non-_np version is NULL
+        // and fallback to the _np version if it is
+        // See also: https://github.com/haskell/process/issues/356
+        if (posix_spawn_file_actions_addchdir == NULL) {
+          r = posix_spawn_file_actions_addchdir_np(&fa, workingDirectory);
+        } else {
+          r = posix_spawn_file_actions_addchdir(&fa, workingDirectory);
+        }
+#else
         r = posix_spawn_file_actions_addchdir(&fa, workingDirectory);
+#endif
         if (r != 0) {
             *failed_doing = "posix_spawn_file_actions_addchdir";
+            goto fail;
+        }
+#elif defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)
+        // N.B. this function is broken on macOS.
+        // See https://github.com/rust-lang/rust/pull/80537.
+        r = posix_spawn_file_actions_addchdir_np(&fa, workingDirectory);
+        if (r != 0) {
+            *failed_doing = "posix_spawn_file_actions_addchdir_np";
             goto fail;
         }
 #else
diff --git a/cbits/posix/runProcess.c b/cbits/posix/runProcess.c
--- a/cbits/posix/runProcess.c
+++ b/cbits/posix/runProcess.c
@@ -5,10 +5,13 @@
    ------------------------------------------------------------------------- */
 
 #include "runProcess.h"
-#include "common.h"
 
-#if defined(HAVE_FORK)
+// Callers are stubbed out on wasm32: runInteractiveProcess (System/Process/Posix.hs),
+// terminateProcess, getProcessExitCode, waitForProcess (System/Process.hs).
+#if !defined(wasm32_HOST_ARCH)
 
+#include "common.h"
+
 #include <unistd.h>
 #include <errno.h>
 #include <sys/wait.h>
@@ -70,6 +73,7 @@
         return r;
     }
 
+#if defined(HAVE_FORK)
     r = do_spawn_fork(args,
                       workingDirectory, environment,
                       stdInHdl, stdOutHdl, stdErrHdl,
@@ -77,6 +81,11 @@
                       flags,
                       failed_doing);
     return r;
+#else
+    *failed_doing = "fork";
+    return -1;
+#endif
+
 }
 
 enum pipe_direction {
@@ -268,4 +277,4 @@
     return -1;
 }
 
-#endif // HAVE_FORK
+#endif // !wasm32_HOST_ARCH
diff --git a/cbits/win32/runProcess.c b/cbits/win32/runProcess.c
--- a/cbits/win32/runProcess.c
+++ b/cbits/win32/runProcess.c
@@ -88,8 +88,8 @@
  *           asynchronously while anonymous pipes require blocking calls.
  */
 BOOL
-mkNamedPipe (HANDLE* pHandleIn, BOOL isInheritableIn,
-             HANDLE* pHandleOut, BOOL isInheritableOut)
+mkNamedPipe (HANDLE* pHandleIn, BOOL isInheritableIn, BOOL isOverlappedIn,
+             HANDLE* pHandleOut, BOOL isInheritableOut, BOOL isOverlappedOut)
 {
     HANDLE hTemporaryIn  = INVALID_HANDLE_VALUE;
     HANDLE hTemporaryOut = INVALID_HANDLE_VALUE;
@@ -142,7 +142,7 @@
        bytes and the error ERROR_NO_DATA."[0]
 
        [0] https://devblogs.microsoft.com/oldnewthing/20110114-00/?p=11753  */
-    DWORD inAttr = isInheritableIn ? 0 : FILE_FLAG_OVERLAPPED;
+    DWORD inAttr = isOverlappedIn ? FILE_FLAG_OVERLAPPED : 0;
     hTemporaryIn
       = CreateNamedPipeW (pipeName,
                           PIPE_ACCESS_INBOUND | inAttr | FILE_FLAG_FIRST_PIPE_INSTANCE,
@@ -153,7 +153,7 @@
     if (hTemporaryIn == INVALID_HANDLE_VALUE)
       goto fail;
 
-    /* And now create the other end using the inverse access permissions.  This
+    /* And now open the other end, using the inverse access permissions.  This
        will give us the read and write ends of the pipe.  */
     secAttr.bInheritHandle = isInheritableOut;
     hTemporaryOut
@@ -162,9 +162,9 @@
                      FILE_SHARE_WRITE,
                      &secAttr,
                      OPEN_EXISTING,
-                     isInheritableOut
-                       ? FILE_ATTRIBUTE_NORMAL
-                       : FILE_FLAG_OVERLAPPED,
+                     isOverlappedOut
+                       ? FILE_FLAG_OVERLAPPED
+                       : FILE_ATTRIBUTE_NORMAL,
                      NULL);
 
     if (hTemporaryOut == INVALID_HANDLE_VALUE)
@@ -244,21 +244,21 @@
 static inline bool
 setStdHandleInfo (LPHANDLE destination, HANDLE _stdhandle,
                   LPHANDLE hStdRead, LPHANDLE hStdWrite, HANDLE defaultStd,
-                  BOOL isInhertibleIn, BOOL isInhertibleOut, BOOL asynchronous)
+                  BOOL isInheritableIn, BOOL isInheritableOut, BOOL asynchronous)
 {
     BOOL status;
     assert (destination);
     assert (hStdRead);
     assert (hStdWrite);
 
-    LPHANDLE tmpHandle = isInhertibleOut ? hStdWrite : hStdRead;
+    LPHANDLE tmpHandle = isInheritableOut ? hStdWrite : hStdRead;
 
     if (_stdhandle == (HANDLE)-1) {
         if (!asynchronous
-            && !mkAnonPipe(hStdRead, isInhertibleIn, hStdWrite, isInhertibleOut))
+            && !mkAnonPipe(hStdRead, isInheritableIn, hStdWrite, isInheritableOut))
             return false;
         if (asynchronous
-            && !mkNamedPipe(hStdRead, isInhertibleIn, hStdWrite, isInhertibleOut))
+            && !mkNamedPipe(hStdRead, isInheritableIn, !isInheritableIn, hStdWrite, isInheritableOut, !isInheritableOut))
             return false;
         *destination = *tmpHandle;
     } else if (_stdhandle == (HANDLE)-2) {
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,86 @@
 # Changelog for [`process` package](http://hackage.haskell.org/package/process)
 
+## 1.6.30.0 *June 2026*
+
+* darwin: Allow posix_spawn_file_actions_addchdir to be missing when
+  linking against process ([#377](https://github.com/haskell/process/pull/377))
+
+## 1.6.29.0 *May 2026*
+
+* Stub out `runProcess.c` on wasm32 ([#366](https://github.com/haskell/process/pull/366))
+* Fix segmentation faults on MacOS 15 caused by NULL symbols created by toolchains that can compile for MacOS 26 ([#363](https://github.com/haskell/process/pull/363))
+
+## 1.6.28.0 *April 2026*
+
+* Add `callCreateProcess`
+* Improve Haddock
+
+## 1.6.27.0 *March 2026*
+
+* Support being configured without fork
+* Fix `@since` annotations
+
+## 1.6.26.1 *May 2025*
+
+* Regenerate `configure` script (#343)
+
+## 1.6.26.0 *April 2025*
+
+* Add `System.Process.Environment.OsString`.
+* Bumps `base >= 4.12.0.0` (GHC 8.6+), `filepath >= 1.4.100.0`,
+  `unix >= 2.8.0.0`, and `Win32 >= 2.14.1.0`.
+* Drops support for GHC < 8.6.
+
+## 1.6.25.0 *September 2024*
+
+* Fix build with Javascript backend ([#327](https://github.com/haskell/process/issues/327))
+
+## 1.6.24.0 *September 2024*
+
+* Fix detection of `posix_spawn_file_actions_addchdir[_np]` ([#303](https://github.com/haskell/process/issues/303))
+
+## 1.6.23.0 *September 2024*
+
+* Fix command-line escaping logic on Windows when the command file ends with
+  a space or a dot. This is a follow-up for
+  [HSEC-2024-0003](https://github.com/haskell/security-advisories/tree/main/advisories/hackage/process/HSEC-2024-0003.md).
+* Migitate another manifestation of the BatBadBut vulnerability via
+  unescaped `%` expansions. This is another follow-up for
+  [HSEC-2024-0003](https://github.com/haskell/security-advisories/tree/main/advisories/hackage/process/HSEC-2024-0003.md).
+  ([#313](https://github.com/haskell/process/issues/313))
+
+## 1.6.22.0 *August 2024*
+
+* Allow NUL to appear in arguments under POSIX. See
+  https://github.com/haskell/process/pull/320.  Thanks to @mmhat.
+
+## 1.6.21.0 *July 2024*
+
+* No longer attach finalizers to `Handle`s created by the
+  `System.Process.CommunicationHandle` API. Instead, all file descriptors are
+  manually closed by the API.
+
+  This fixes a bug in which a file descriptor could be closed multiple times.
+
+## 1.6.20.0 *April 2024*
+
+* Introduce `System.Process.CommunicationHandle`, allowing for platform-independent
+  inter-process communication using `Handle`s.
+* Expose `withForkWait` and `ignoreSigPipe` from `System.Process.Internals`.
+* Define new internal functions `rawFdToHandle` and (Windows only) `rawHANDLEToHandle`,
+  exported from `System.Process.Common`.
+
+## 1.6.19.0 *April 2024*
+
+* Adjust command-line escaping logic on Windows to ensure that occurrences of
+  characters with special significance to the Windows batch interpreter are
+  properly escaped in arguments passed to `.bat` and `.cmd` processes.
+  This addresses
+  [HSEC-2024-0003](https://github.com/haskell/security-advisories/tree/main/advisories/hackage/process/HSEC-2024-0003.md).
+* Fix double-closing of stdin/stdout/stderr fds with POSIX `fork/exec` backend.
+  [#306](https://github.com/haskell/process/issues/306)
+* Add support for `posix_spawn_file_actions_addchdir_np`
+
 ## 1.6.18.0 *September 2023*
 
 * Fix deadlock when waiting for process completion and process jobs [#273](https://github.com/haskell/process/issues/273)
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -3981,6 +3981,12 @@
   printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h
 
 fi
+ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addchdir_np" "ac_cv_func_posix_spawn_file_actions_addchdir_np"
+if test "x$ac_cv_func_posix_spawn_file_actions_addchdir_np" = xyes
+then :
+  printf "%s\n" "#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP 1" >>confdefs.h
+
+fi
 ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addchdir" "ac_cv_func_posix_spawn_file_actions_addchdir"
 if test "x$ac_cv_func_posix_spawn_file_actions_addchdir" = xyes
 then :
diff --git a/configure.ac b/configure.ac
--- a/configure.ac
+++ b/configure.ac
@@ -23,7 +23,7 @@
 
 # posix_spawn checks
 AC_CHECK_HEADERS([spawn.h])
-AC_CHECK_FUNCS([posix_spawnp posix_spawn_file_actions_addchdir],[],[],[
+AC_CHECK_FUNCS([posix_spawnp posix_spawn_file_actions_addchdir_np posix_spawn_file_actions_addchdir],[],[],[
   #define _GNU_SOURCE
   #include <spawn.h>
 ])
diff --git a/include/HsProcessConfig.h.in b/include/HsProcessConfig.h.in
--- a/include/HsProcessConfig.h.in
+++ b/include/HsProcessConfig.h.in
@@ -40,6 +40,10 @@
    */
 #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
 
+/* Define to 1 if you have the `posix_spawn_file_actions_addchdir_np'
+   function. */
+#undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP
+
 /* Define to 1 if you have the `setitimer' function. */
 #undef HAVE_SETITIMER
 
diff --git a/process.cabal b/process.cabal
--- a/process.cabal
+++ b/process.cabal
@@ -1,14 +1,14 @@
+cabal-version: 2.4
 name:          process
-version:       1.6.18.0
+version:       1.6.30.0
 -- NOTE: Don't forget to update ./changelog.md
-license:       BSD3
+license:       BSD-3-Clause
 license-file:  LICENSE
 maintainer:    libraries@haskell.org
 bug-reports:   https://github.com/haskell/process/issues
 synopsis:      Process libraries
 category:      System
 build-type:    Configure
-cabal-version: >=1.10
 description:
     This package contains libraries for dealing with system processes.
     .
@@ -18,9 +18,11 @@
     read more about it at
     <https://github.com/fpco/typed-process/#readme>.
 
+extra-doc-files:
+    changelog.md
+
 extra-source-files:
     aclocal.m4
-    changelog.md
     configure
     configure.ac
     include/HsProcessConfig.h.in
@@ -39,6 +41,11 @@
     type:     git
     location: https://github.com/haskell/process.git
 
+flag os-string
+    description: Use the new os-string package
+    default: False
+    manual: False
+
 library
     default-language: Haskell2010
     other-extensions:
@@ -52,18 +59,22 @@
     exposed-modules:
         System.Cmd
         System.Process
+        System.Process.CommunicationHandle
+        System.Process.CommunicationHandle.Internal
+        System.Process.Environment.OsString
         System.Process.Internals
     other-modules: System.Process.Common
     if os(windows)
         c-sources:
             cbits/win32/runProcess.c
         other-modules: System.Process.Windows
-        build-depends: Win32 >=2.4 && < 2.14
+        build-depends: Win32 >= 2.14.1.0 && < 2.15
         -- ole32 and rpcrt4 are needed to create GUIDs for unique named pipes
         -- for process.
         extra-libraries: kernel32, ole32, rpcrt4
         cpp-options: -DWINDOWS
     else
+        build-depends: unix >= 2.8.0.0 && < 2.9
         if arch(javascript)
             js-sources:
                 jsbits/process.js
@@ -75,34 +86,33 @@
                 cbits/posix/posix_spawn.c
                 cbits/posix/find_executable.c
             other-modules: System.Process.Posix
-            build-depends: unix >= 2.5 && < 2.9
 
+    -- On macOS, posix_spawn_file_actions_addchdir is available from macOS >= 26.
+    -- We weakly reference this symbol when we are build on such a platform.
+    -- However, weak references on MachO are hints to the dynamic loader, not the
+    -- static linker, which will still expect the symbol to be defined somewhere.
+    -- This means linking fails when we try to link an executable depending on
+    -- process on a toolchain which doesn't have this symbol against a process
+    -- library build on a toolchain with the symbol.
+    -- See https://github.com/haskell/process/issues/376
+    -- Hence: instruct the linker to explicilty allow undefined references to
+    -- the symbol when linking against process on darwin
+    if os(darwin)
+        ld-options: -Wl,-U,_posix_spawn_file_actions_addchdir
+
     include-dirs: include
-    includes:
-        runProcess.h
     install-includes:
         runProcess.h
         processFlags.h
 
     ghc-options: -Wall
 
-    build-depends: base      >= 4.10 && < 4.20,
+    build-depends: base      >= 4.12.0.0 && < 4.24,
                    directory >= 1.1 && < 1.4,
-                   filepath  >= 1.2 && < 1.5,
                    deepseq   >= 1.1 && < 1.6
 
-test-suite test
-  default-language: Haskell2010
-  hs-source-dirs: test
-  main-is: main.hs
-  type: exitcode-stdio-1.0
-  -- Add otherwise redundant bounds on base since GHC's build system runs
-  -- `cabal check`, which mandates bounds on base.
-  build-depends: base >= 4 && < 5
-               , bytestring
-               , directory
-               , process
-  ghc-options: -threaded
-               -with-rtsopts "-N"
-  if os(windows)
-        cpp-options: -DWINDOWS
+    if flag(os-string)
+        build-depends: filepath >= 1.5.0.0 && < 1.6,
+                       os-string >= 2.0.0 && < 2.1
+    else
+        build-depends: filepath >= 1.4.100.0 && < 1.5.0.0
diff --git a/test/main.hs b/test/main.hs
deleted file mode 100644
--- a/test/main.hs
+++ /dev/null
@@ -1,291 +0,0 @@
-{-# LANGUAGE CPP #-}
-import Control.Exception
-import Control.Monad (guard, unless, void)
-import System.Exit
-import System.IO.Error
-import System.Directory (getCurrentDirectory, setCurrentDirectory)
-import System.Process
-import Control.Concurrent
-import Data.Char (isDigit)
-import Data.IORef
-import Data.List (isInfixOf)
-import Data.Maybe (isNothing)
-import System.IO (hClose, openBinaryTempFile, hGetContents)
-import qualified Data.ByteString as S
-import qualified Data.ByteString.Char8 as S8
-import System.Directory (getTemporaryDirectory, removeFile)
-import GHC.Conc.Sync (getUncaughtExceptionHandler, setUncaughtExceptionHandler)
-
-ifWindows :: IO () -> IO ()
-ifWindows action
-  | not isWindows = return ()
-  | otherwise     = action
-
-isWindows :: Bool
-#if WINDOWS
-isWindows = True
-#else
-isWindows = False
-#endif
-
-main :: IO ()
-main = do
-    testDoesNotExist
-    testModifiers
-    testSubdirectories
-    testBinaryHandles
-    testMultithreadedWait
-    testInterruptMaskedWait
-    testGetPid
-    testReadProcess
-    testInterruptWith
-    testDoubleWait
-    testKillDoubleWait
-    testCreateProcess
-    putStrLn ">>> Tests passed successfully"
-
-run :: String -> IO () -> IO ()
-run label test = do
-    putStrLn $ ">>> Running: " ++ label
-    test
-
-testDoesNotExist :: IO ()
-testDoesNotExist = run "non-existent executable" $ do
-    res <- handle (return . Left . isDoesNotExistError) $ do
-        (_, _, _, ph) <- createProcess (proc "definitelydoesnotexist" [])
-            { close_fds = True
-            }
-        fmap Right $ waitForProcess ph
-    case res of
-        Left True -> return ()
-        _ -> error $ show res
-
-testModifiers :: IO ()
-testModifiers = do
-    let test name modifier = run ("modifier " ++ name) $ do
-            (_, _, _, ph) <- createProcess
-                $ modifier $ proc "echo" ["hello", "world"]
-            ec <- waitForProcess ph
-            unless (ec == ExitSuccess)
-                $ error $ "echo returned: " ++ show ec
-
-    test "vanilla" id
-
-    -- FIXME need to debug this in the future on Windows
-    unless isWindows $ test "detach_console" $ \cp -> cp { detach_console = True }
-
-    test "create_new_console" $ \cp -> cp { create_new_console = True }
-    test "new_session" $ \cp -> cp { new_session = True }
-
-testSubdirectories :: IO ()
-testSubdirectories = ifWindows $ run "subdirectories" $ do
-    withCurrentDirectory "exes" $ do
-      res1 <- readCreateProcess (proc "./echo.bat" []) ""
-      unless ("parent" `isInfixOf` res1 && not ("child" `isInfixOf` res1)) $ error $
-        "echo.bat with cwd failed: " ++ show res1
-
-      res2 <- readCreateProcess (proc "./echo.bat" []) { cwd = Just "subdir" } ""
-      unless ("child" `isInfixOf` res2 && not ("parent" `isInfixOf` res2)) $ error $
-        "echo.bat with cwd failed: " ++ show res2
-
-testBinaryHandles :: IO ()
-testBinaryHandles = run "binary handles" $ do
-    tmpDir <- getTemporaryDirectory
-    bracket
-      (openBinaryTempFile tmpDir "process-binary-test.bin")
-      (\(fp, h) -> hClose h `finally` removeFile fp)
-      $ \(fp, h) -> do
-        let bs = S8.pack "hello\nthere\r\nworld\0"
-        S.hPut h bs
-        hClose h
-
-        (Nothing, Just out, Nothing, ph) <- createProcess (proc "cat" [fp])
-            { std_out = CreatePipe
-            }
-        res' <- S.hGetContents out
-        hClose out
-        ec <- waitForProcess ph
-        unless (ec == ExitSuccess)
-            $ error $ "Unexpected exit code " ++ show ec
-        unless (bs == res')
-            $ error $ "Unexpected result: " ++ show res'
-
-testMultithreadedWait :: IO ()
-testMultithreadedWait = run "multithreaded wait" $ do
-    (_, _, _, p) <- createProcess (proc "sleep" ["0.1"])
-    me1 <- newEmptyMVar
-    _ <- forkIO . void $ waitForProcess p >>= putMVar me1
-    -- check for race / deadlock between waitForProcess and getProcessExitCode
-    e3 <- getProcessExitCode p
-    e2 <- waitForProcess p
-    e1 <- readMVar me1
-    unless (isNothing e3)
-          $ error $ "unexpected exit " ++ show e3
-    unless (e1 == ExitSuccess && e2 == ExitSuccess)
-          $ error "sleep exited with non-zero exit code!"
-
-testInterruptMaskedWait :: IO ()
-testInterruptMaskedWait = run "interrupt masked wait" $ do
-    (_, _, _, p) <- createProcess (proc "sleep" ["1.0"])
-    mec <- newEmptyMVar
-    tid <- mask_ . forkIO $
-        (waitForProcess p >>= putMVar mec . Just)
-            `catchThreadKilled` putMVar mec Nothing
-    killThread tid
-    eec <- takeMVar mec
-    case eec of
-      Nothing -> return ()
-      Just ec ->
-        if isWindows
-          then putStrLn "FIXME ignoring known failure on Windows"
-          else error $ "waitForProcess not interrupted: sleep exited with " ++ show ec
-
-testGetPid :: IO ()
-testGetPid = run "getPid" $ do
-    (_, Just out, _, p) <-
-      if isWindows
-        then createProcess $ (proc "sh" ["-c", "z=$$; cat /proc/$z/winpid"]) {std_out = CreatePipe}
-        else createProcess $ (proc "sh" ["-c", "echo $$"]) {std_out = CreatePipe}
-    pid <- getPid p
-    line <- hGetContents out
-    putStrLn $ " queried PID: " ++ show pid
-    putStrLn $ " PID reported by stdout: " ++ show line
-    _ <- waitForProcess p
-    hClose out
-    let numStdoutPid = read (takeWhile isDigit line) :: Pid
-    unless (Just numStdoutPid == pid) $
-      if isWindows
-        then putStrLn "FIXME ignoring known failure on Windows"
-        else error "subprocess reported unexpected PID"
-
-testReadProcess :: IO ()
-testReadProcess = run "readProcess" $ do
-    output <- readProcess "echo" ["hello", "world"] ""
-    unless (output == "hello world\n") $
-        error $ "unexpected output, got: " ++ output
-
--- | Test that withCreateProcess doesn't throw exceptions besides
--- the expected UserInterrupt when the child process is interrupted
--- by Ctrl-C.
-testInterruptWith :: IO ()
-testInterruptWith = unless isWindows $ run "interrupt withCreateProcess" $ do
-    mpid <- newEmptyMVar
-    forkIO $ do
-        pid <- takeMVar mpid
-        void $ readProcess "kill" ["-INT", show pid] ""
-
-    -- collect unhandled exceptions in any threads (specifically
-    -- the asynchronous 'waitForProcess' call from 'cleanupProcess')
-    es <- collectExceptions $ do
-        let sleep = (proc "sleep" ["10"]) { delegate_ctlc = True }
-        res <- try $ withCreateProcess sleep $ \_ _ _ p -> do
-            Just pid <- getPid p
-            putMVar mpid pid
-            waitForProcess p
-        unless (res == Left UserInterrupt) $
-            error $ "expected UserInterrupt, got " ++ show res
-
-    unless (null es) $
-        error $ "uncaught exceptions: " ++ show es
-
-  where
-    collectExceptions action = do
-        oldHandler <- getUncaughtExceptionHandler
-        flip finally (setUncaughtExceptionHandler oldHandler) $ do
-            exceptions <- newIORef ([] :: [SomeException])
-            setUncaughtExceptionHandler (\e -> atomicModifyIORef exceptions $ \es -> (e:es, ()))
-            action
-            threadDelay 1000 -- give some time for threads to finish
-            readIORef exceptions
-
--- Test that we can wait without exception twice, if the process exited on its own.
-testDoubleWait :: IO ()
-testDoubleWait = run "run process, then wait twice" $ do
-    let sleep = (proc "sleep" ["0"])
-    (_, _, _, p) <- createProcess sleep
-    res <- try $ waitForProcess p
-    case res of
-        Left e -> error $ "waitForProcess threw: " ++ show (e :: SomeException)
-        Right ExitSuccess -> return ()
-        Right exitCode -> error $ "unexpected exit code: " ++ show exitCode
-
-    res2 <- try $ waitForProcess p
-    case res2 of
-        Left e -> error $ "second waitForProcess threw: " ++ show (e :: SomeException)
-        Right ExitSuccess -> return ()
-        Right exitCode -> error $ "unexpected exit code: " ++ show exitCode
-
--- Test that we can wait without exception twice, if the process was killed.
-testKillDoubleWait :: IO ()
-testKillDoubleWait = unless isWindows $ do
-    run "terminate process, then wait twice (delegate_ctlc = False)" $ runTest "TERM" False
-    run "terminate process, then wait twice (delegate_ctlc = True)" $ runTest "TERM" True
-    run "interrupt process, then wait twice (delegate_ctlc = False)" $ runTest "INT" False
-    run "interrupt process, then wait twice (delegate_ctlc = True)" $ runTest "INT" True
-  where
-    runTest sig delegate = do
-        let sleep = (proc "sleep" ["10"])
-        (_, _, _, p) <- createProcess sleep { delegate_ctlc = delegate }
-        Just pid <- getPid p
-        void $ readProcess "kill" ["-" ++ sig, show pid] ""
-
-        res <- try $ waitForProcess p
-        checkFirst sig delegate res
-
-        res' <- try $ waitForProcess p
-        checkSecond sig delegate res'
-
-    checkFirst :: String -> Bool -> Either SomeException ExitCode -> IO ()
-    checkFirst sig delegate res = case (sig, delegate) of
-        ("INT", True) -> case res of
-            Left e -> case fromException e of
-                Just UserInterrupt -> putStrLn "result ok"
-                Nothing -> error $ "expected UserInterrupt, got  " ++ show e
-            Right _ -> error $ "expected exception, got " ++ show res
-        _ -> case res of
-            Left e -> error $ "waitForProcess threw: " ++ show e
-            Right ExitSuccess -> error "expected failure"
-            _ -> putStrLn "result ok"
-
-    checkSecond :: String -> Bool -> Either SomeException ExitCode -> IO ()
-    checkSecond sig delegate res = case (sig, delegate) of
-        ("INT", True) -> checkFirst "INT" False res
-        _ -> checkFirst sig delegate res
-
--- Test that createProcess doesn't segfault on Mac with a cwd of Nothing
-testCreateProcess :: IO ()
-testCreateProcess = run "createProcess with cwd = Nothing" $ do
-    let env = CreateProcess
-            { child_group = Nothing
-            , child_user = Nothing
-            , close_fds = False
-            , cmdspec = RawCommand "env" []
-            , create_group = True
-            , create_new_console = False
-            , cwd = Nothing
-            , delegate_ctlc = False
-            , detach_console = False
-            , env = Just [("PATH", "/bin:/usr/bin")]
-            , new_session = False
-            , std_err = Inherit
-            , std_in = Inherit
-            , std_out = Inherit
-            , use_process_jobs = False
-            }
-    (_, _, _, p) <- createProcess env
-    res <- try $ waitForProcess p
-    case res of
-        Left e -> error $ "waitForProcess threw: " ++ show (e :: SomeException)
-        Right ExitSuccess -> return ()
-        Right exitCode -> error $ "unexpected exit code: " ++ show exitCode
-
-withCurrentDirectory :: FilePath -> IO a -> IO a
-withCurrentDirectory new inner = do
-  orig <- getCurrentDirectory
-  bracket_
-    (setCurrentDirectory new)
-    (setCurrentDirectory orig)
-    inner
-
-catchThreadKilled :: IO a -> IO a -> IO a
-catchThreadKilled f g = catchJust (\e -> guard (e == ThreadKilled)) f (\() -> g)
