net: allow close() to ignore SSL failures due to disconnections (#15120)

* net: allow close() to ignore SSL failures due to disconnections

Comes with this PR is also a SIGPIPE handling contraption.

* net: don't do selectSigpipe() on macOS

macOS sockets have SO_NOSIGPIPE set, so an EPIPE doesn't necessary mean
that a SIGPIPE happened.

* net: fix alreadyBlocked logic

* net: WSAESHUTDOWN is also a disconnection error
This commit is contained in:
alaviss
2020-08-01 19:27:55 +00:00
committed by GitHub
parent 3ce32a7e40
commit c619cedd7c
3 changed files with 177 additions and 15 deletions

View File

@@ -75,6 +75,9 @@ export Domain, SockType, Protocol
const useWinVersion = defined(Windows) or defined(nimdoc)
const defineSsl = defined(ssl) or defined(nimdoc)
when useWinVersion:
from winlean import WSAESHUTDOWN
when defineSsl:
import openssl
@@ -187,6 +190,7 @@ proc isDisconnectionError*(flags: set[SocketFlag],
lastError.int32 == WSAECONNABORTED or
lastError.int32 == WSAENETRESET or
lastError.int32 == WSAEDISCON or
lastError.int32 == WSAESHUTDOWN or
lastError.int32 == ERROR_NETNAME_DELETED)
else:
SocketFlag.SafeDisconn in flags and
@@ -1031,8 +1035,72 @@ proc accept*(server: Socket, client: var owned(Socket),
var addrDummy = ""
acceptAddr(server, client, addrDummy, flags)
proc close*(socket: Socket) =
when defined(posix):
from posix import Sigset, sigwait, sigismember, sigemptyset, sigaddset,
sigprocmask, pthread_sigmask, SIGPIPE, SIG_BLOCK, SIG_UNBLOCK
template blockSigpipe(body: untyped): untyped =
## Temporary block SIGPIPE within the provided code block. If SIGPIPE is
## raised for the duration of the code block, it will be queued and will be
## raised once the block ends.
##
## Within the block a `selectSigpipe()` template is provided which can be
## used to remove SIGPIPE from the queue. Note that if SIGPIPE is **not**
## raised at the time of call, it will block until SIGPIPE is raised.
##
## If SIGPIPE has already been blocked at the time of execution, the
## signal mask is left as-is and `selectSigpipe()` will become a no-op.
##
## For convenience, this template is also available for non-POSIX system,
## where `body` will be executed as-is.
when not defined(posix):
body
else:
template sigmask(how: cint, set, oset: var Sigset): untyped {.gensym.} =
## Alias for pthread_sigmask or sigprocmask depending on the status
## of --threads
when compileOption("threads"):
pthread_sigmask(how, set, oset)
else:
sigprocmask(how, set, oset)
var oldSet, watchSet: Sigset
if sigemptyset(oldSet) == -1:
raiseOSError(osLastError())
if sigemptyset(watchSet) == -1:
raiseOSError(osLastError())
if sigaddset(watchSet, SIGPIPE) == -1:
raiseOSError(osLastError(), "Couldn't add SIGPIPE to Sigset")
if sigmask(SIG_BLOCK, watchSet, oldSet) == -1:
raiseOSError(osLastError(), "Couldn't block SIGPIPE")
let alreadyBlocked = sigismember(oldSet, SIGPIPE) == 1
template selectSigpipe(): untyped {.used.} =
if not alreadyBlocked:
var signal: cint
let err = sigwait(watchSet, signal)
if err != 0:
raiseOSError(err.OSErrorCode, "Couldn't select SIGPIPE")
assert signal == SIGPIPE
try:
body
finally:
if not alreadyBlocked:
if sigmask(SIG_UNBLOCK, watchSet, oldSet) == -1:
raiseOSError(osLastError(), "Couldn't unblock SIGPIPE")
proc close*(socket: Socket, flags = {SocketFlag.SafeDisconn}) =
## Closes a socket.
##
## If `socket` is an SSL/TLS socket, this proc will also send a closure
## notification to the peer. If `SafeDisconn` is in `flags`, failure to do so
## due to disconnections will be ignored. This is generally safe in
## practice. See
## `here <https://security.stackexchange.com/a/82044>`_ for more details.
try:
when defineSsl:
if socket.isSsl and socket.sslHandle != nil:
@@ -1044,12 +1112,34 @@ proc close*(socket: Socket) =
# it is valid, under the TLS standard, to perform a unidirectional
# shutdown i.e not wait for the peers "close notify" alert with a second
# call to SSL_shutdown
ErrClearError()
let res = SSL_shutdown(socket.sslHandle)
if res == 0:
discard
elif res != 1:
socketError(socket, res)
blockSigpipe:
ErrClearError()
let res = SSL_shutdown(socket.sslHandle)
if res == 0:
discard
elif res != 1:
let
err = osLastError()
sslError = SSL_get_error(socket.sslHandle, res)
# If a close notification is received, failures outside of the
# protocol will be returned as SSL_ERROR_ZERO_RETURN instead
# of SSL_ERROR_SYSCALL. This fact is deduced by digging into
# SSL_get_error() source code.
if sslError == SSL_ERROR_ZERO_RETURN or
sslError == SSL_ERROR_SYSCALL:
when defined(posix) and not defined(macosx) and
not defined(nimdoc):
if err == EPIPE.OSErrorCode:
# Clear the SIGPIPE that's been raised due to
# the disconnection.
selectSigpipe()
else:
discard
if not flags.isDisconnectionError(err):
socketError(socket, res, lastError = err, flags = flags)
else:
socketError(socket, res, lastError = err, flags = flags)
finally:
when defineSsl:
if socket.isSsl and socket.sslHandle != nil:
@@ -1470,7 +1560,7 @@ proc recvFrom*(socket: Socket, data: var string, length: int,
var addrLen = sizeof(sockAddress).SockLen
result = recvfrom(socket.fd, cstring(data), length.cint, flags.cint,
cast[ptr SockAddr](addr(sockAddress)), addr(addrLen))
if result != -1:
data.setLen(result)
address = getAddrString(cast[ptr SockAddr](addr(sockAddress)))

View File

@@ -813,6 +813,7 @@ const
WSAEINPROGRESS* = 10036
WSAEINTR* = 10004
WSAEWOULDBLOCK* = 10035
WSAESHUTDOWN* = 10058
ERROR_NETNAME_DELETED* = 64
STATUS_PENDING* = 0x103