Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The closeConnection method for tls connections should not be called multiple times #225

Closed
cdepillabout opened this issue Sep 1, 2016 · 5 comments

Comments

@cdepillabout
Copy link

cdepillabout commented Sep 1, 2016

In my application I make two successive connections to Facebook's Graph API using http-conduit. I'm experiencing a bug when making two calls one after the other. The second call is throwing a TLS error in the middle of the conversation for some reason. I think I've figured out why it is happening, but the explantion is pretty long, so please bear with me.

http-conduit is using http-client-tls and http-client. http-client-tls is using the connection library to actually create connections.

http-client-tls's convertConnection function is used to convert a connection Network.Connection.Types.Connection to an http-client Connection.

http-client-tls's convertConnection function is using makeConnection to create the http-client Connection.

The following is the convertConnection function:

convertConnection :: NC.Connection -> IO Connection
convertConnection conn = makeConnection
    (Network.Connection.connectionGetChunk conn)
    (Network.Connection.connectionPut conn)
    -- Closing an SSL connection gracefully involves writing/reading
    -- on the socket.  But when this is called the socket might be
    -- already closed, and we get a @ResourceVanished@.
    (Network.Connection.connectionClose conn `Control.Exception.catch` \(_ :: IOException) -> return ())

Note how makeConnection's c argument is becoming the following:

-- Closing an SSL connection gracefully involves writing/reading
-- on the socket.  But when this is called the socket might be
-- already closed, and we get a @ResourceVanished@.
(Network.Connection.connectionClose conn `Control.Exception.catch` \(_ :: IOException) -> return ())

makeConnection looks like this:

makeConnection :: IO ByteString -- ^ read
               -> (ByteString -> IO ()) -- ^ write
               -> IO () -- ^ close
               -> IO Connection
makeConnection r w c = do
    istack <- newIORef []
    -- it is necessary to make sure we never read from or write to
    -- already closed connection.
    closedVar <- newIORef False
    _ <- mkWeakIORef istack c
    return $! Connection
        { connectionRead = ...
        , connectionUnread = ...
        , connectionWrite = ...
        , connectionClose = do
            closed <- readIORef closedVar
            unless closed c
            writeIORef closedVar True
        }

Since istack's finalizer is c, and c is actually Network.Control.connectionClose conn, Network.Control.connectionClose actually ends of getting called twice. Once when http-client's connectionClose is called, and a second time when istack goes out of scope.

Let's look at Network.Control.connectionClose:

connectionClose :: Connection -> IO ()
connectionClose = withBackend backendClose
  where
    backendClose (ConnectionTLS ctx)  = TLS.bye ctx >> TLS.contextClose ctx
    backendClose (ConnectionSocket sock) = N.close sock
    backendClose (ConnectionStream h) = hClose h

In the case where we are dealing with a tls connection, TLS.bye is called before TLS.contextClose is called. TLS.bye is actually sending a packet on the socket in question.

The problem occurs by calling TLS.bye twice.

Here's what (I believe) is happening in my app:

  1. I use Network.HTTP.Conduit.http to make my connection to Facebook's API.
  2. Under the covers, a new socket is created, using file descriptor number 16.
  3. makeConnection is called, setting up everything correctly, and setting istack's finalizer to Network.Connection.connectionClose.
  4. Everything in the connection happens correctly. After the connection is over, http-client is nice enough to call connectionClose, which in turn calls Network.Connection.connectionClose.
  5. In Network.Connection.connectionClose, TLS.bye is used to send a message to Facebook's servers saying to end the connection. The Socket pointed to from file descriptor number 16 is closed.
  6. Immediately I create one more connection to Facebook using http.
  7. Another new socket is created. Since the old one has already been closed, this new one also gets file descriptor number 16.
  8. http-client starts sending data to Facebook with this new Socket.
  9. This is where the bad stuff starts happening. Finally, istack from (3) goes out of scope, causing its finalizer to fire. Since its finalizer is Network.Connection.connectionClose, TLS.bye is called again. Normally this wouldn't be a problem, since the socket is already closed, but in (7), a new socket was created which ended up with the same file descriptor. So TLS.bye sends a message on the wrong socket.

I believe the way to fix this is to make istack's finalizer be the entire connectionClose function. That way, TLS.bye is only ever called once:

makeConnection :: IO ByteString -- ^ read
               -> (ByteString -> IO ()) -- ^ write
               -> IO () -- ^ close
               -> IO Connection
makeConnection r w c = do
    istack <- newIORef []
    -- it is necessary to make sure we never read from or write to
    -- already closed connection.
    closedVar <- newIORef False
    let connectionClose' = do
            closed <- readIORef closedVar
            unless closed c
            writeIORef closedVar True
    _ <- mkWeakIORef istack connectionClose'
    return $! Connection
        { connectionRead = ...
        , connectionUnread = ...
        , connectionWrite = ...
        , connectionClose = connectionClose'
        }

I'm only able to reproduce this bug by using connection-0.2.6, specifically the version with this commit where connection uses Sockets instead of Handles by default. Maybe @vincenthz would be able to shed some light on why this is happening?

P.S. I would have thought someone else would have run into this bug. I'm somewhat worried that I'm misdiagnosing this bug.

@Yuras
Copy link
Collaborator

Yuras commented Sep 1, 2016

It was me who introduced the closedVar in #170, and I simply didn't notice that c is used as istack finalizer. I agree with your analysis and the proposed solution.

The bug occurs with Socket but not with Handle because Network.Socket.send doesn't check socket status, see haskell/network#169

@snoyberg
Copy link
Owner

snoyberg commented Sep 1, 2016

Thanks for reviewing @Yuras. If you feel comfortable with the proposed solution, let's go with it, I'll be happy to review/merge a PR.

Yuras added a commit to Yuras/http-client that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See snoyberg#225
Yuras added a commit to Yuras/http-client that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See snoyberg#225
Yuras added a commit to Yuras/http-client that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See snoyberg#225
Yuras added a commit to Yuras/http-client that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See snoyberg#225
Yuras added a commit to Yuras/http-client that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See snoyberg#225
snoyberg added a commit that referenced this issue Sep 2, 2016
@snoyberg
Copy link
Owner

snoyberg commented Sep 2, 2016

Closing based on feedback so far, if the bug still exists, please reopen.

@snoyberg snoyberg closed this as completed Sep 2, 2016
snoyberg pushed a commit that referenced this issue Sep 2, 2016
Connection could be closed via explicit `connectionClose` call
or via finalizer attached to it. Both should check status to
prevent writing to or reading from closed connection.
See #225
snoyberg added a commit that referenced this issue Sep 2, 2016
@cdepillabout
Copy link
Author

Thanks @snoyberg and @Yuras!

@dysinger
Copy link

dysinger commented Sep 6, 2016

thanks @cdepillabout

snoyberg added a commit to commercialhaskell/stack that referenced this issue Sep 13, 2016
Without this, using stack upload with digest support triggers
snoyberg/http-client#225. This change could arguably be made in a
separate PR, but including with digest support since it would be a very
dangerous idea to not bump the http-client version.
snoyberg added a commit to commercialhaskell/stack that referenced this issue Sep 14, 2016
Without this, using stack upload with digest support triggers
snoyberg/http-client#225. This change could arguably be made in a
separate PR, but including with digest support since it would be a very
dangerous idea to not bump the http-client version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants