Skip to content

GCing Sockets is a bad diea #302

Description

@kazu-yamamoto

Cc: @vdukhovni

We are planning to add a finalizer to Sockets so that they can be GCed.

GHC's bug 1

In network v2.6.3.4, we use:

   _ <- mkWeakMVar mStat $ close sock

The definition of close is:

close (MkSocket s _ _ _ socketStatus) = do
 modifyMVar_ socketStatus $ \ status ->
   case status of
     ConvertedToHandle ->
         ioError (userError ("close: converted to a Handle, use hClose instead")
)
     Closed ->
         return status
     _ -> closeFdWith (closeFd . fromIntegral) (fromIntegral s) >> return Closed

So, if a Socket is converted into Handle and the Socket becomes unreach, close is called and an error is thrown. We believed that this is just ignored. Unfortunately, this is not the case. GHC has a bug. If a finalizer throws an error, other finalizers are not called:

https://ghc.haskell.org/trac/ghc/ticket/13167

GHC's bug 2

For 3.0.0.0, we are trying to use addFinalizer: #301

As I already reported, GHC 7.10 GCes Sockets too early. I also found that GHC 7.8 has the same bug.

Bad usage

A user can close the file descriptor of Socket by calling the close syscall. Then the user opens another file or socket and the number is reused. In this situation, calling finalizer by GC results in unexpected bug.

Proposed actions

Activity

  1. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    Was there really a version 2.6.3.4? I don't see it anywhere... If it had no finalizers defined, its close API was still OK, and safe for repeated calls.

  2. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    See #300.

  3. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    But I do see #269, so I guess that was it...

  4. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    My proposal is release 2.6.3.4, 2.7.0.0 and 3.0.0.0 in order. If @eborden agree with this, we will carry out this plan.

    See also #296.

  5. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    @vdukhovni Sorry for your confusion. My plan is to override 2.7 with https://github.com/kazu-yamamoto/network/tree/poc-2.7.0.0

    2.7 removed SockAddrCan but we need to revert it for smooth migration. Unfortunately, it appeared that it is hard to revert SockAddrCan. That's why I created poc-2.7.0.0 based on 2.6.

  6. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    To be honest I am not sure that removing the mStat field of Socket is the right decision. It originally made it possible to track whether the socket is already closed or not (or converted to a handle, ...) and thus instead of simply asking users to not make certain "misuse" mistakes, made it possible to detect such misuse.

    If performance is not impacted, I'd even consider storing the file descriptor in an IORef, and replace it with -1 on close. In my view, Haskell is supposed to be firstly safer than C, and only then reasonably fast. So exposing unsafe interfaces (thereby enabling double-close errors, ...) may not be worth any performance gains...

  7. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    For SocketStatus stuff, please read #286 (comment)

  8. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    Socket in v3.0.0.0 is an abstract type. So, we can hold any fields if we wish. IORef might be a good idea.

    After releasing 2.7.0.0, we should wait for a long time so that users of network library stop using deprecated APIs. So, we have enough time. Let's continue discussion.

  9. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    For example, Hadle detects and suppresses duplicate close operations. When I compile and trace:

    module Main where
    import System.IO
    
    main :: IO ()
    main = do
        n <- openBinaryFile "/dev/null" ReadMode
        hClose n
        hClose n
        hClose n
    

    I see just one close:

    openat(AT_FDCWD,"/dev/null",O_RDONLY|O_NONBLOCK|O_NOCTTY,00) = 4 (0x4)
    fstat(4,{ mode=crw-rw-rw- ,inode=27,size=0,blksize=4096 }) = 0 (0x0)
    ioctl(4,TIOCGETA,0x7fffffffa3f0)                 ERR#25 'Inappropriate ioctl for device'
    close(4)                                         = 0 (0x0)
    clock_gettime(15,{ 0.005872000 })                = 0 (0x0)
    ioctl(1,TIOCGETA,0x7fffffffa3b0)                 ERR#25 'Inappropriate ioctl for device'
    clock_gettime(15,{ 0.005966000 })                = 0 (0x0)
    clock_gettime(15,{ 0.006341000 })                = 0 (0x0)
    exit(0x0)
    

    Likely Network.Socket should have similar safety built-in.

  10. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    Your requirement is:

    • aSocket closed by Haskell's close must not be reused
    • Haskell's close must not throw an error even if the Socket is already closed

    Right?

  11. kazu-yamamoto commented on Jan 30, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    It's worth confirming that Socket (IORef (-1)) can be GCed naturally.

  12. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    @kazu-yamamoto Your requirement is:
    a Socket closed by Haskell's close must not be reused

    No, rather a Socket that is still reachable must remember that it is already closed, so that a second close will not close the same (but not really the same anymore) file descriptor.

    Haskell's close must not throw an error even if the Socket is already closed

    Actually it is OK to throw an error, but perhaps not needed (System.IO handles seem to not bother) just don't do it in a finalizer. If you throw on close, then also provide some sort of _close that does not throw for use in finalization.

    Right?

    I guess not, but is the above more clear?

  13. vdukhovni commented on Jan 30, 2018

    @vdukhovni

    @kazu-yamamoto It's worth confirming that Socket (IORef (-1)) can be GCed naturally.

    Of course it GC's just fine, there's no foreign content here or anything else to stop GC. What's important here is that a socket whose mutable file descriptor can be updated to -1 (modulo multiple threads racing to close the same object) is safe to close multiple times or close again in finalization. Just read the FD from the IORef, and close if not already -1 on Unix or INVALID_SOCKET on Windows (use some platform specific compile-time constant for this), then store the invalid value back into the IORef. Concurrent races to close the socket are unsafe, but IIRC this was also unsafe even with the legacy mStat because the check and replace were not atomic. Multiple threads trying to close the same socket is much less common than multiple close within the thread that "owns" the socket, or when finalizing or ResourceT cleanup, ...

  14. eborden commented on Jan 31, 2018

    @eborden
    Collaborator

    If we are to utilize an IORef we'll need to use mkWeakIORef to init the finalizer. It also seem completely reasonable to signal a closed socket with IORef (-1).

  15. kazu-yamamoto commented on Jan 31, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    @eborden I don't understand why mkWeakIORef is necessary. The idea here is that we should not set a finalizer to Socket to close it.

  16. vdukhovni commented on Jan 31, 2018

    @vdukhovni

    @kazu-yamamoto I don't understand why mkWeakIORef is necessary. The idea here is that we should not set a finalizer to Socket to close it.

    Well, actually, if the Socket does contain a mutable IORef with the file-descriptor inside it, then you can ensure that close happens at most once, in which case you might consider adding finalization, unless that's too much overhead, and users close most sockets directly or via ResourceT or similar, so perhaps it would turn out to be an unnecessary cost. I don't know whether automatic finalization is useful or not, but it would no longer be unsafe.

  17. vdukhovni commented on Jan 31, 2018

    @vdukhovni

    Here's a small demo of the possible finalization approach. (Whether finalization is a good idea is still an open question in my mind, perhaps most applications don't need this, and perhaps the cost of adding it is noticeable? But if it is cheap enough, then perhaps worth it, for those applications that neglect to clean up).

    module Main (main) where
    
    import Data.IORef
    import System.Mem
    import Control.Monad (void)
    
    myClose :: IORef Int -> IO ()
    myClose r = do
        old <- atomicModifyIORef' r $ \old -> (-1, old)
        case old of
          -1 -> return ()
          _  -> putStrLn $ "Closing: r" ++ show old
    
    main :: IO ()
    main = do
        r1 <- newIORef 1
        r2 <- newIORef 2
        r3 <- newIORef 3
        void $ mkWeakIORef r1 $ myClose r1
        void $ mkWeakIORef r2 $ myClose r2
        void $ mkWeakIORef r3 $ myClose r3
        myClose r2
        myClose r3
        myClose r3
        performGC

    Running it shows exactly one close for each of the three objects, one closed when finalized, other two closed explicitly by the application, one twice, but all three do the (just print a message) close processing just once.

     ./foo
    Closing: r2
    Closing: r3
    Closing: r1
    
  18. kazu-yamamoto commented on Feb 2, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    @vdukhovni Thanks! I think that I understand now.

  19. kazu-yamamoto commented on Feb 7, 2018

    @kazu-yamamoto
    CollaboratorAuthor

    #303 has been merged. Let's close this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions