Skip to content

Commit

Permalink
Don't attach finalizers to CommunicationHandles
Browse files Browse the repository at this point in the history
We are now careful to not attach any Handle finalizers when creating
pipes for inter-process communication on Unix systems. Instead, the
handles are closed manually.

The finalizers were causing problems in situations such as the following:

  - 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 (as it should),
  - 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.

This commit does not include a test, as the above bug is a bit difficult
to trigger.
  • Loading branch information
sheaf committed Jul 25, 2024
1 parent d74bba2 commit 511b901
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 31 deletions.
13 changes: 13 additions & 0 deletions System/Process/CommunicationHandle.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ import Control.DeepSeq (NFData, rnf)
-- | 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
Expand All @@ -55,6 +61,9 @@ openCommunicationHandleWrite = useCommunicationHandle False
-- | 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
Expand All @@ -71,6 +80,9 @@ createWeReadTheyWritePipe =
-- | 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
Expand Down Expand Up @@ -125,6 +137,7 @@ readCreateProcessWithExitCodeCommunicationHandle mkProg readAction writeAction =
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
Expand Down
101 changes: 74 additions & 27 deletions System/Process/CommunicationHandle/Internal.hsc
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ module System.Process.CommunicationHandle.Internal
where

import Control.Arrow ( first )
import Foreign.C (CInt(..), throwErrnoIf_)
import GHC.IO.Handle (Handle())
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))
Expand All @@ -41,28 +41,37 @@ import System.Process.Common (rawFdToHandle)
#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(..), fdToHandle
( Fd(..)
, FdOption(..), setFdOption
)
import GHC.IO.FD (FD(fdFD))
-- NB: we use GHC.IO.Handle.Fd.handleToFd rather than System.Posix.handleToFd,
-- as the latter flushes and closes the `Handle`, which is not the behaviour we want.
import GHC.IO.Handle.FD (handleToFd)
#endif

##if !defined(mingw32_HOST_OS)
import System.Posix.Internals
( fdGetMode )
import System.Process.Internals
( createPipe )
##endif

import GHC.IO.Handle (hClose)
( createPipeFd )
#endif

--------------------------------------------------------------------------------
-- Communication handles.

-- | A 'CommunicationHandle' is an operating-system specific representation
-- of a 'Handle' that can be communicated through a command-line interface.
-- | 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'.
Expand Down Expand Up @@ -120,10 +129,10 @@ instance Read CommunicationHandle where
-- | Internal function used to define 'openCommunicationHandleRead' and
-- openCommunicationHandleWrite.
useCommunicationHandle :: Bool -> CommunicationHandle -> IO Handle
useCommunicationHandle wantToRead (CommunicationHandle ch) = do
useCommunicationHandle _wantToRead (CommunicationHandle ch) = do
##if defined(__IO_MANAGER_WINIO__)
return ()
<!> associateHandleWithFallback wantToRead ch
<!> associateHandleWithFallback _wantToRead ch
##endif
getGhcHandle ch

Expand Down Expand Up @@ -199,29 +208,67 @@ getGhcHandleNative hwnd =
## endif
#else
getGhcHandle :: Fd -> IO Handle
getGhcHandle fd = fdToHandle fd
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
createCommunicationPipe swapIfTheyReadWeWrite _passAsyncHandleToChild = do
##if !defined(mingw32_HOST_OS)
(ourHandle, theirHandle) <- swapIfTheyReadWeWrite <$> createPipe
-- 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).
ourFD <- Fd . fdFD <$> handleToFd ourHandle
setFdOption ourFD CloseOnExec True
theirFD <- Fd . fdFD <$> handleToFd theirHandle
return (ourHandle, CommunicationHandle theirFD)
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
Expand All @@ -236,8 +283,8 @@ createCommunicationPipe swapIfTheyReadWeWrite passAsyncHandleToChild = do
-- - 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 )
overlappedRead = trueForWinIO && ( _passAsyncHandleToChild || not inheritRead )
overlappedWrite = trueForWinIO && ( _passAsyncHandleToChild || not inheritWrite )
throwErrnoIf_ (==False) "mkNamedPipe" $
mkNamedPipe
pfdStdInput inheritRead overlappedRead
Expand Down
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog for [`process` package](http://hackage.haskell.org/package/process)

## 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
Expand Down
2 changes: 1 addition & 1 deletion process.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: process
version: 1.6.20.0
version: 1.6.21.0
-- NOTE: Don't forget to update ./changelog.md
license: BSD-3-Clause
license-file: LICENSE
Expand Down
6 changes: 3 additions & 3 deletions test/process-tests.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: process-tests
version: 1.6.20.0
version: 1.6.21.0
license: BSD-3-Clause
license-file: LICENSE
maintainer: libraries@haskell.org
Expand All @@ -18,14 +18,14 @@ source-repository head

common process-dep
build-depends:
process == 1.6.20.0
process == 1.6.21.0

custom-setup
setup-depends:
base >= 4.10 && < 4.21,
directory >= 1.1 && < 1.4,
filepath >= 1.2 && < 1.6,
Cabal >= 2.4 && < 3.12,
Cabal >= 2.4 && < 3.14,

-- Test executable for the CommunicationHandle functionality
executable cli-child
Expand Down

0 comments on commit 511b901

Please sign in to comment.