Skip to content

Commit d079ce3

Browse files
sethmlarsongpshead
andcommitted
Authenticate socket connection for socket.socketpair() fallback
Co-authored-by: Gregory P. Smith <[email protected]>
1 parent 5716cc3 commit d079ce3

File tree

3 files changed

+168
-24
lines changed

3 files changed

+168
-24
lines changed

Lib/socket.py

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import _socket
5353
from _socket import *
5454

55-
import os, sys, io, selectors
55+
import os, sys, io, selectors, time
5656
from enum import IntEnum, IntFlag
5757

5858
try:
@@ -628,28 +628,81 @@ def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
628628
if proto != 0:
629629
raise ValueError("Only protocol zero is supported")
630630

631-
# We create a connected TCP socket. Note the trick with
632-
# setblocking(False) that prevents us from having to create a thread.
633-
lsock = socket(family, type, proto)
634-
try:
635-
lsock.bind((host, 0))
636-
lsock.listen()
637-
# On IPv6, ignore flow_info and scope_id
638-
addr, port = lsock.getsockname()[:2]
639-
csock = socket(family, type, proto)
631+
import secrets # Delay import until actually needed.
632+
auth_len = secrets.DEFAULT_ENTROPY
633+
reason = "unknown"
634+
for _ in range(5):
635+
# We do not try forever, that'd just provide another process
636+
# the ability to make us waste CPU retrying. In all normal
637+
# circumstances the first connection auth attempt succeeds.
638+
s_auth = secrets.token_bytes()
639+
c_auth = secrets.token_bytes()
640+
641+
# We create a connected TCP socket. Note the trick with
642+
# setblocking(False) prevents us from having to create a thread.
643+
csock = None
644+
ssock = None
645+
reason = "unknown"
646+
lsock = socket(family, type, proto)
640647
try:
641-
csock.setblocking(False)
648+
lsock.bind((host, 0))
649+
lsock.listen()
650+
# On IPv6, ignore flow_info and scope_id
651+
addr, port = lsock.getsockname()[:2]
652+
csock = socket(family, type, proto)
642653
try:
643-
csock.connect((addr, port))
644-
except (BlockingIOError, InterruptedError):
645-
pass
646-
csock.setblocking(True)
647-
ssock, _ = lsock.accept()
648-
except:
649-
csock.close()
650-
raise
651-
finally:
652-
lsock.close()
654+
csock.setblocking(False)
655+
try:
656+
csock.connect((addr, port))
657+
except (BlockingIOError, InterruptedError):
658+
pass
659+
csock.setblocking(True)
660+
ssock, _ = lsock.accept()
661+
except Exception:
662+
csock.close()
663+
raise
664+
665+
def authenticate_socket_conn(send_sock, recv_sock, auth_bytes):
666+
nonlocal auth_len
667+
data_buf = bytearray(auth_len)
668+
data_mem = memoryview(data_buf)
669+
data_len = 0
670+
671+
# Send the authentication bytes.
672+
if send_sock.send(auth_bytes) != auth_len:
673+
raise ConnectionError("send() sent too few auth bytes.")
674+
675+
# Attempt to read the authentication bytes from the socket.
676+
max_auth_time = time.monotonic() + 3.0
677+
while time.monotonic() < max_auth_time and data_len < auth_len:
678+
bytes_received = recv_sock.recv_into(data_mem, auth_len - data_len)
679+
if bytes_received == 0:
680+
break # Connection closed.
681+
data_len += bytes_received
682+
data_mem = data_mem[bytes_received:]
683+
684+
# Check that the authentication bytes match.
685+
if len(data_buf) != auth_len:
686+
raise ConnectionError("recv() got too few auth bytes.")
687+
if bytes(data_buf) != auth_bytes:
688+
raise ConnectionError(f"Mismatched auth token.")
689+
690+
# Authenticating avoids using a connection from something else
691+
# able to connect to {host}:{port} instead of us.
692+
try:
693+
authenticate_socket_conn(ssock, csock, s_auth)
694+
authenticate_socket_conn(csock, ssock, c_auth)
695+
except OSError as exc:
696+
csock.close()
697+
ssock.close()
698+
reason = str(exc)
699+
continue
700+
# authentication successful, both sockets are our process.
701+
break
702+
finally:
703+
lsock.close()
704+
else:
705+
raise ConnectionError(f"socketpair authentication failed: {reason}")
653706
return (ssock, csock)
654707
__all__.append("socketpair")
655708

Lib/test/test_socket.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,19 +592,27 @@ class SocketPairTest(unittest.TestCase, ThreadableTest):
592592
def __init__(self, methodName='runTest'):
593593
unittest.TestCase.__init__(self, methodName=methodName)
594594
ThreadableTest.__init__(self)
595+
self.cli = None
596+
self.serv = None
597+
598+
def call_socketpair(self):
599+
# To be overridden by some child classes.
600+
return socket.socketpair()
595601

596602
def setUp(self):
597-
self.serv, self.cli = socket.socketpair()
603+
self.serv, self.cli = self.call_socketpair()
598604

599605
def tearDown(self):
600-
self.serv.close()
606+
if self.serv:
607+
self.serv.close()
601608
self.serv = None
602609

603610
def clientSetUp(self):
604611
pass
605612

606613
def clientTearDown(self):
607-
self.cli.close()
614+
if self.cli:
615+
self.cli.close()
608616
self.cli = None
609617
ThreadableTest.clientTearDown(self)
610618

@@ -4852,6 +4860,84 @@ def _testSend(self):
48524860
self.assertEqual(msg, MSG)
48534861

48544862

4863+
class PurePythonSocketPairTest(SocketPairTest):
4864+
4865+
# Explicitly use socketpair AF_INET or AF_INET6 to to ensure that is the
4866+
# code path we're using regardless platform is the pure python one where
4867+
# `_socket.socketpair` does not exist. (AF_INET does not work with
4868+
# _socket.socketpair on many platforms).
4869+
def call_socketpair(self):
4870+
# called by super().setUp().
4871+
try:
4872+
return socket.socketpair(socket.AF_INET6)
4873+
except OSError:
4874+
return socket.socketpair(socket.AF_INET)
4875+
4876+
# Local imports in this class make for easy security fix backporting.
4877+
4878+
def setUp(self):
4879+
import _socket
4880+
self._orig_sp = getattr(_socket, 'socketpair', None)
4881+
if self._orig_sp is not None:
4882+
# This forces the version using the non-OS provided socketpair
4883+
# emulation via an AF_INET socket in Lib/socket.py.
4884+
del _socket.socketpair
4885+
import importlib
4886+
global socket
4887+
socket = importlib.reload(socket)
4888+
else:
4889+
pass # This platform already uses the non-OS provided version.
4890+
super().setUp()
4891+
4892+
def tearDown(self):
4893+
super().tearDown()
4894+
import _socket
4895+
if self._orig_sp is not None:
4896+
# Restore the default socket.socketpair definition.
4897+
_socket.socketpair = self._orig_sp
4898+
import importlib
4899+
global socket
4900+
socket = importlib.reload(socket)
4901+
4902+
def test_recv(self):
4903+
msg = self.serv.recv(1024)
4904+
self.assertEqual(msg, MSG)
4905+
4906+
def _test_recv(self):
4907+
self.cli.send(MSG)
4908+
4909+
def test_send(self):
4910+
self.serv.send(MSG)
4911+
4912+
def _test_send(self):
4913+
msg = self.cli.recv(1024)
4914+
self.assertEqual(msg, MSG)
4915+
4916+
def _test_injected_authentication_failure(self):
4917+
# No-op. Exists for base class threading infrastructure to call.
4918+
# We could refactor this test into its own lesser class along with the
4919+
# setUp and tearDown code to construct an ideal; it is simpler to keep
4920+
# it here and live with extra overhead one this _one_ failure test.
4921+
pass
4922+
4923+
def test_injected_authentication_failure(self):
4924+
import secrets
4925+
orig_token_bytes = secrets.token_bytes
4926+
fake_n = secrets.DEFAULT_ENTROPY - 1
4927+
from unittest import mock
4928+
with mock.patch.object(
4929+
secrets, 'token_bytes',
4930+
new=lambda nbytes=None: orig_token_bytes(fake_n)):
4931+
s1, s2 = None, None
4932+
try:
4933+
with self.assertRaisesRegex(ConnectionError,
4934+
"authentication fail"):
4935+
s1, s2 = socket.socketpair()
4936+
finally:
4937+
if s1: s1.close()
4938+
if s2: s2.close()
4939+
4940+
48554941
class NonBlockingTCPTests(ThreadedTCPSocketTest):
48564942

48574943
def __init__(self, methodName='runTest'):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Authenticate the socket connection for the ``socket.socketpair()`` fallback
2+
on platforms where ``AF_UNIX`` is not available like Windows.
3+
4+
Patch by Gregory P. Smith <[email protected]>. Reported by Ellie
5+

0 commit comments

Comments
 (0)