Skip to content

Commit 6e9c43b

Browse files
committed
transport: Return duplicate sockets from get_extra_info('socket')
It appears that people use sockets returned from `transport.get_extra_info('socket')` with low-level APIs such as add_writer and remove_writer. If the returned socket fileno is the same as the one that transport is using, libuv will crash, since one fileno can't point to two different handles (uv_poll_t and uv_tcp_t). See also python/asyncio#372
1 parent 32f5fc7 commit 6e9c43b

File tree

7 files changed

+23
-49
lines changed

7 files changed

+23
-49
lines changed

tests/test_sockets.py

-42
Original file line numberDiff line numberDiff line change
@@ -111,48 +111,6 @@ def test_socket_blocking_error(self):
111111
self.loop.run_until_complete(
112112
self.loop.sock_connect(sock, (b'', 0)))
113113

114-
def test_socket_handler_cleanup(self):
115-
# This tests recreates a rare condition where we have a socket
116-
# with an attached reader. We then remove the reader, and close the
117-
# socket. If the libuv Poll handler is still cached when we open
118-
# a new TCP connection, it might so happen that the new TCP connection
119-
# will receive a fileno that our previous socket was registered on.
120-
# In this case, when the cached Poll handle is finally closed,
121-
# we have a failed assertion in uv_poll_stop.
122-
# See also https://github.com/MagicStack/uvloop/issues/34
123-
# for details.
124-
125-
srv_sock = socket.socket()
126-
with srv_sock:
127-
srv_sock.bind(('127.0.0.1', 0))
128-
srv_sock.listen(100)
129-
130-
srv = self.loop.run_until_complete(
131-
self.loop.create_server(
132-
lambda: None, host='127.0.0.1', port=0))
133-
key_fileno = srv.sockets[0].fileno()
134-
srv.close()
135-
self.loop.run_until_complete(srv.wait_closed())
136-
137-
# Schedule create_connection task's callbacks
138-
tsk = self.loop.create_task(
139-
self.loop.create_connection(
140-
asyncio.Protocol, *srv_sock.getsockname()))
141-
142-
sock = socket.socket()
143-
with sock:
144-
# Add/remove readers
145-
if sock.fileno() != key_fileno:
146-
raise unittest.SkipTest()
147-
self.loop.add_reader(sock.fileno(), lambda: None)
148-
self.loop.remove_reader(sock.fileno())
149-
150-
tr, pr = self.loop.run_until_complete(
151-
asyncio.wait_for(tsk, loop=self.loop, timeout=0.1))
152-
tr.close()
153-
# Let the transport close
154-
self.loop.run_until_complete(asyncio.sleep(0, loop=self.loop))
155-
156114

157115
class TestUVSockets(_TestSockets, tb.UVTestCase):
158116
pass

tests/test_tcp.py

+7
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,16 @@ async def test_client(addr):
461461
self.assertFalse(t._paused)
462462

463463
sock = t.get_extra_info('socket')
464+
self.assertIs(sock, t.get_extra_info('socket'))
464465
sockname = sock.getsockname()
465466
peername = sock.getpeername()
466467

468+
# Test that adding a writer on the returned socket
469+
# does not crash uvloop. aiohttp does that to implement
470+
# sendfile, for instance.
471+
self.loop.add_writer(sock.fileno(), lambda: None)
472+
self.loop.remove_writer(sock.fileno())
473+
467474
self.assertTrue(isinstance(sock, socket.socket))
468475
self.assertEqual(t.get_extra_info('sockname'),
469476
sockname)

tests/test_unix.py

+8
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,15 @@ async def test(sock):
321321
None,
322322
sock=sock)
323323

324+
sock = t.get_extra_info('socket')
324325
self.assertIs(t.get_extra_info('socket'), sock)
326+
327+
# Test that adding a writer on the returned socket
328+
# does not crash uvloop. aiohttp does that to implement
329+
# sendfile, for instance.
330+
self.loop.add_writer(sock.fileno(), lambda: None)
331+
self.loop.remove_writer(sock.fileno())
332+
325333
t.close()
326334

327335
s1, s2 = socket.socketpair(socket.AF_UNIX)

uvloop/handles/handle.pyx

+4-4
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,13 @@ cdef class UVSocketHandle(UVHandle):
199199
raise NotImplementedError
200200

201201
cdef inline _get_socket(self):
202-
if self._fileobj:
203-
return self._fileobj
204-
205202
if self.__cached_socket is not None:
206203
return self.__cached_socket
207204

208205
self.__cached_socket = self._new_socket()
206+
if self.__cached_socket.fileno() == self._fileno():
207+
raise RuntimeError('new socket shares fileno with the transport')
208+
209209
return self.__cached_socket
210210

211211
cdef inline _attach_fileobj(self, object file):
@@ -218,7 +218,7 @@ cdef class UVSocketHandle(UVHandle):
218218
try:
219219
if self.__cached_socket is not None:
220220
try:
221-
self.__cached_socket.detach()
221+
self.__cached_socket.close()
222222
except OSError:
223223
pass
224224
self.__cached_socket = None

uvloop/handles/pipe.pyx

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ cdef __pipe_open(UVStream handle, int fd):
2424

2525

2626
cdef __pipe_get_socket(UVSocketHandle handle):
27-
return socket_socket(uv.AF_UNIX, uv.SOCK_STREAM, 0, handle._fileno())
27+
fileno = os_dup(handle._fileno())
28+
return socket_socket(uv.AF_UNIX, uv.SOCK_STREAM, 0, fileno)
2829

2930

3031
@cython.no_gc_clear

uvloop/handles/tcp.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ cdef __tcp_get_socket(UVSocketHandle handle):
3636
int err
3737
system.sockaddr_storage buf
3838

39-
fileno = handle._fileno()
39+
fileno = os_dup(handle._fileno())
4040

4141
err = uv.uv_tcp_getsockname(<uv.uv_tcp_t*>handle._handle,
4242
<system.sockaddr*>&buf,

uvloop/handles/udp.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ cdef class UDPTransport(UVBaseTransport):
182182
raise RuntimeError(
183183
'UDPTransport.family is undefined; cannot create python socket')
184184

185-
fileno = self._fileno()
185+
fileno = os_dup(self._fileno())
186186
return socket_socket(self._family, uv.SOCK_STREAM, 0, fileno)
187187

188188
cdef _send(self, object data, object addr):

0 commit comments

Comments
 (0)