Skip to content

Commit ca98aa8

Browse files
committed
net: allow close() to ignore SSL failures due to disconnections
Comes with this PR is also a SIGPIPE handling contraption.
1 parent 2031563 commit ca98aa8

File tree

2 files changed

+171
-15
lines changed

2 files changed

+171
-15
lines changed

lib/pure/net.nim

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,8 +1031,72 @@ proc accept*(server: Socket, client: var owned(Socket),
10311031
var addrDummy = ""
10321032
acceptAddr(server, client, addrDummy, flags)
10331033

1034-
proc close*(socket: Socket) =
1034+
when defined(posix):
1035+
from posix import Sigset, sigwait, sigismember, sigemptyset, sigaddset,
1036+
sigprocmask, pthread_sigmask, SIGPIPE, SIG_BLOCK, SIG_UNBLOCK
1037+
1038+
template blockSigpipe(body: untyped): untyped =
1039+
## Temporary block SIGPIPE within the provided code block. If SIGPIPE is
1040+
## raised for the duration of the code block, it will be queued and will be
1041+
## raised once the block ends.
1042+
##
1043+
## Within the block a `selectSigpipe()` template is provided which can be
1044+
## used to remove SIGPIPE from the queue. Note that if SIGPIPE is **not**
1045+
## raised at the time of call, it will block until SIGPIPE is raised.
1046+
##
1047+
## If SIGPIPE has already been blocked at the time of execution, the
1048+
## signal mask is left as-is and `selectSigpipe()` will become a no-op.
1049+
##
1050+
## For convenience, this template is also available for non-POSIX system,
1051+
## where `body` will be executed as-is.
1052+
when not defined(posix):
1053+
body
1054+
else:
1055+
template sigmask(how: cint, set, oset: var Sigset): untyped {.gensym.} =
1056+
## Alias for pthread_sigmask or sigprocmask depends on the status
1057+
## of --threads flag
1058+
when compileOption("threads"):
1059+
pthread_sigmask(how, set, oset)
1060+
else:
1061+
sigprocmask(how, set, oset)
1062+
1063+
var oldSet, watchSet: Sigset
1064+
if sigemptyset(oldSet) == -1:
1065+
raiseOSError(osLastError())
1066+
if sigemptyset(watchSet) == -1:
1067+
raiseOSError(osLastError())
1068+
1069+
if sigaddset(watchSet, SIGPIPE) == -1:
1070+
raiseOSError(osLastError(), "Couldn't add SIGPIPE to Sigset")
1071+
1072+
if sigmask(SIG_BLOCK, watchSet, oldSet) == -1:
1073+
raiseOSError(osLastError(), "Couldn't block SIGPIPE")
1074+
1075+
let alreadyBlocked = sigismember(oldSet, SIGPIPE) != 1
1076+
1077+
template selectSigpipe(): untyped {.used.} =
1078+
if not alreadyBlocked:
1079+
var signal: cint
1080+
let err = sigwait(watchSet, signal)
1081+
if err != 0:
1082+
raiseOSError(err.OSErrorCode, "Couldn't select SIGPIPE")
1083+
assert signal == SIGPIPE
1084+
1085+
try:
1086+
body
1087+
finally:
1088+
if not alreadyBlocked:
1089+
if sigmask(SIG_UNBLOCK, watchSet, oldSet) == -1:
1090+
raiseOSError(osLastError(), "Couldn't unblock SIGPIPE")
1091+
1092+
proc close*(socket: Socket, flags = {SocketFlag.SafeDisconn}) =
10351093
## Closes a socket.
1094+
##
1095+
## If `socket` is an SSL/TLS socket, this proc will also send a closure
1096+
## notification to the peer. If `SafeDisconn` is in `flags`, failure to do so
1097+
## due to disconnections will be ignored. This is generally safe in
1098+
## practice. See
1099+
## `here <https://security.stackexchange.com/a/82044>`_ for more details.
10361100
try:
10371101
when defineSsl:
10381102
if socket.isSsl and socket.sslHandle != nil:
@@ -1044,12 +1108,33 @@ proc close*(socket: Socket) =
10441108
# it is valid, under the TLS standard, to perform a unidirectional
10451109
# shutdown i.e not wait for the peers "close notify" alert with a second
10461110
# call to SSL_shutdown
1047-
ErrClearError()
1048-
let res = SSL_shutdown(socket.sslHandle)
1049-
if res == 0:
1050-
discard
1051-
elif res != 1:
1052-
socketError(socket, res)
1111+
blockSigpipe:
1112+
ErrClearError()
1113+
let res = SSL_shutdown(socket.sslHandle)
1114+
if res == 0:
1115+
discard
1116+
elif res != 1:
1117+
let
1118+
err = osLastError()
1119+
sslError = SSL_get_error(socket.sslHandle, res)
1120+
1121+
# If a close notification is received, failures outside of the
1122+
# protocol will be returned as SSL_ERROR_ZERO_RETURN instead
1123+
# of SSL_ERROR_SYSCALL. This fact is deduced by digging into
1124+
# SSL_get_error() source code.
1125+
if sslError == SSL_ERROR_ZERO_RETURN or
1126+
sslError == SSL_ERROR_SYSCALL:
1127+
when defined(posix) and not defined(nimdoc):
1128+
if err == EPIPE.OSErrorCode:
1129+
# Clear the SIGPIPE that's been raised due to
1130+
# the disconnection.
1131+
selectSigpipe()
1132+
else:
1133+
discard
1134+
if not flags.isDisconnectionError(err):
1135+
socketError(socket, res, lastError = err, flags = flags)
1136+
else:
1137+
socketError(socket, res, lastError = err, flags = flags)
10531138
finally:
10541139
when defineSsl:
10551140
if socket.isSsl and socket.sslHandle != nil:
@@ -1470,7 +1555,7 @@ proc recvFrom*(socket: Socket, data: var string, length: int,
14701555
var addrLen = sizeof(sockAddress).SockLen
14711556
result = recvfrom(socket.fd, cstring(data), length.cint, flags.cint,
14721557
cast[ptr SockAddr](addr(sockAddress)), addr(addrLen))
1473-
1558+
14741559
if result != -1:
14751560
data.setLen(result)
14761561
address = getAddrString(cast[ptr SockAddr](addr(sockAddress)))

tests/stdlib/tssl.nim

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ discard """
55
import net, nativesockets
66

77
when defined(posix): import os, posix
8+
else:
9+
import winlean
10+
const SD_SEND = 1
811

912
when not defined(ssl):
1013
{.error: "This test must be compiled with -d:ssl".}
1114

1215
const DummyData = "dummy data\n"
1316

14-
proc connector(port: Port) {.thread.} =
17+
proc abruptShutdown(port: Port) {.thread.} =
1518
let clientContext = newContext(verifyMode = CVerifyNone)
1619
var client = newSocket(buffered = false)
1720
clientContext.wrapSocket(client)
@@ -20,11 +23,16 @@ proc connector(port: Port) {.thread.} =
2023
discard client.recvLine()
2124
client.getFd.close()
2225

23-
proc main() =
24-
let serverContext = newContext(verifyMode = CVerifyNone,
25-
certFile = "tests/testdata/mycert.pem",
26-
keyFile = "tests/testdata/mycert.pem")
26+
proc notifiedShutdown(port: Port) {.thread.} =
27+
let clientContext = newContext(verifyMode = CVerifyNone)
28+
var client = newSocket(buffered = false)
29+
clientContext.wrapSocket(client)
30+
client.connect("localhost", port)
31+
32+
discard client.recvLine()
33+
client.close()
2734

35+
proc main() =
2836
when defined(posix):
2937
var
3038
ignoreAction = SigAction(sa_handler: SIG_IGN)
@@ -34,7 +42,11 @@ proc main() =
3442
if sigaction(SIGPIPE, ignoreAction, oldSigPipeHandler) == -1:
3543
raiseOSError(osLastError(), "Couldn't ignore SIGPIPE")
3644

37-
block peer_close_without_shutdown:
45+
let serverContext = newContext(verifyMode = CVerifyNone,
46+
certFile = "tests/testdata/mycert.pem",
47+
keyFile = "tests/testdata/mycert.pem")
48+
49+
block peer_close_during_write_without_shutdown:
3850
var server = newSocket(buffered = false)
3951
defer: server.close()
4052
serverContext.wrapSocket(server)
@@ -43,7 +55,7 @@ proc main() =
4355
server.listen()
4456

4557
var clientThread: Thread[Port]
46-
createThread(clientThread, connector, port)
58+
createThread(clientThread, abruptShutdown, port)
4759

4860
var peer: Socket
4961
try:
@@ -60,4 +72,63 @@ proc main() =
6072
finally:
6173
peer.close()
6274

75+
when defined(posix):
76+
if sigaction(SIGPIPE, oldSigPipeHandler, nil) == -1:
77+
raiseOSError(osLastError(), "Couldn't restore SIGPIPE handler")
78+
79+
block peer_close_before_received_shutdown:
80+
var server = newSocket(buffered = false)
81+
defer: server.close()
82+
serverContext.wrapSocket(server)
83+
server.bindAddr(address = "localhost")
84+
let (_, port) = server.getLocalAddr()
85+
server.listen()
86+
87+
var clientThread: Thread[Port]
88+
createThread(clientThread, abruptShutdown, port)
89+
90+
var peer: Socket
91+
try:
92+
server.accept(peer)
93+
peer.send(DummyData)
94+
95+
joinThread clientThread
96+
97+
# Tell the OS to close off the write side so shutdown attempts will
98+
# be met with SIGPIPE.
99+
when defined(posix):
100+
discard peer.getFd.shutdown(SHUT_WR)
101+
else:
102+
discard peer.getFd.shutdown(SD_SEND)
103+
finally:
104+
peer.close()
105+
106+
block peer_close_after_received_shutdown:
107+
var server = newSocket(buffered = false)
108+
defer: server.close()
109+
serverContext.wrapSocket(server)
110+
server.bindAddr(address = "localhost")
111+
let (_, port) = server.getLocalAddr()
112+
server.listen()
113+
114+
var clientThread: Thread[Port]
115+
createThread(clientThread, notifiedShutdown, port)
116+
117+
var peer: Socket
118+
try:
119+
server.accept(peer)
120+
peer.send(DummyData)
121+
122+
doAssert peer.recv(1024) == "" # Get the shutdown notification
123+
joinThread clientThread
124+
125+
# Tell the OS to close off the write side so shutdown attempts will
126+
# be met with SIGPIPE.
127+
when defined(posix):
128+
discard peer.getFd.shutdown(SHUT_WR)
129+
else:
130+
discard peer.getFd.shutdown(SD_SEND)
131+
finally:
132+
peer.close()
133+
63134
when isMainModule: main()

0 commit comments

Comments
 (0)