Skip to content

Commit 70cafc8

Browse files
committed
Fix ref issue when protocol is in Cython
Because `context.run()` doesn't hold reference to the callable, when e.g. the protocol is written in Cython, the callbacks were not guaranteed to hold the protocol reference. This PR fixes the issue by explicitly add a reference before `context.run()` calls. Refs geldata/gel#2222
1 parent c07cdd2 commit 70cafc8

File tree

7 files changed

+94
-13
lines changed

7 files changed

+94
-13
lines changed

tests/test_tcp.py

+37
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,43 @@ async def runner():
652652
self.assertIsNone(
653653
self.loop.run_until_complete(connection_lost_called))
654654

655+
def test_context_run_segfault(self):
656+
is_new = False
657+
done = self.loop.create_future()
658+
659+
def server(sock):
660+
sock.sendall(b'hello')
661+
662+
class Protocol(asyncio.Protocol):
663+
def __init__(self):
664+
self.transport = None
665+
666+
def connection_made(self, transport):
667+
self.transport = transport
668+
669+
def data_received(self, data):
670+
try:
671+
self = weakref.ref(self)
672+
nonlocal is_new
673+
if is_new:
674+
done.set_result(data)
675+
else:
676+
is_new = True
677+
new_proto = Protocol()
678+
self().transport.set_protocol(new_proto)
679+
new_proto.connection_made(self().transport)
680+
new_proto.data_received(data)
681+
except Exception as e:
682+
done.set_exception(e)
683+
684+
async def test(addr):
685+
await self.loop.create_connection(Protocol, *addr)
686+
data = await done
687+
self.assertEqual(data, b'hello')
688+
689+
with self.tcp_server(server) as srv:
690+
self.loop.run_until_complete(test(srv.addr))
691+
655692

656693
class Test_UV_TCP(_TestTCP, tb.UVTestCase):
657694

uvloop/handles/basetransport.pyx

+6-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ cdef class UVBaseTransport(UVSocketHandle):
7070
try:
7171
# _maybe_pause_protocol() is always triggered from user-calls,
7272
# so we must copy the context to avoid entering context twice
73-
self.context.copy().run(self._protocol.pause_writing)
73+
run_in_context(
74+
self.context.copy(), self._protocol.pause_writing,
75+
)
7476
except (KeyboardInterrupt, SystemExit):
7577
raise
7678
except BaseException as exc:
@@ -91,7 +93,9 @@ cdef class UVBaseTransport(UVSocketHandle):
9193
# We're copying the context to avoid entering context twice,
9294
# even though it's not always necessary to copy - it's easier
9395
# to copy here than passing down a copied context.
94-
self.context.copy().run(self._protocol.resume_writing)
96+
run_in_context(
97+
self.context.copy(), self._protocol.resume_writing,
98+
)
9599
except (KeyboardInterrupt, SystemExit):
96100
raise
97101
except BaseException as exc:

uvloop/handles/stream.pyx

+12-4
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ cdef class UVStream(UVBaseTransport):
612612
except AttributeError:
613613
keep_open = False
614614
else:
615-
keep_open = self.context.run(meth)
615+
keep_open = run_in_context(self.context, meth)
616616

617617
if keep_open:
618618
# We're keeping the connection open so the
@@ -826,7 +826,11 @@ cdef inline void __uv_stream_on_read_impl(uv.uv_stream_t* stream,
826826
if UVLOOP_DEBUG:
827827
loop._debug_stream_read_cb_total += 1
828828

829-
sc.context.run(sc._protocol_data_received, loop._recv_buffer[:nread])
829+
run_in_context1(
830+
sc.context,
831+
sc._protocol_data_received,
832+
loop._recv_buffer[:nread],
833+
)
830834
except BaseException as exc:
831835
if UVLOOP_DEBUG:
832836
loop._debug_stream_read_cb_errors_total += 1
@@ -911,7 +915,11 @@ cdef void __uv_stream_buffered_alloc(uv.uv_handle_t* stream,
911915

912916
sc._read_pybuf_acquired = 0
913917
try:
914-
buf = sc.context.run(sc._protocol_get_buffer, suggested_size)
918+
buf = run_in_context1(
919+
sc.context,
920+
sc._protocol_get_buffer,
921+
suggested_size,
922+
)
915923
PyObject_GetBuffer(buf, pybuf, PyBUF_WRITABLE)
916924
got_buf = 1
917925
except BaseException as exc:
@@ -976,7 +984,7 @@ cdef void __uv_stream_buffered_on_read(uv.uv_stream_t* stream,
976984
if UVLOOP_DEBUG:
977985
loop._debug_stream_read_cb_total += 1
978986

979-
sc.context.run(sc._protocol_buffer_updated, nread)
987+
run_in_context1(sc.context, sc._protocol_buffer_updated, nread)
980988
except BaseException as exc:
981989
if UVLOOP_DEBUG:
982990
loop._debug_stream_read_cb_errors_total += 1

uvloop/handles/streamserver.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ cdef class UVStreamServer(UVSocketHandle):
6666
cdef inline _on_listen(self):
6767
cdef UVStream client
6868

69-
protocol = self.context.run(self.protocol_factory)
69+
protocol = run_in_context(self.context, self.protocol_factory)
7070

7171
if self.ssl is None:
7272
client = self._make_new_transport(protocol, None, self.context)

uvloop/handles/udp.pyx

+5-3
Original file line numberDiff line numberDiff line change
@@ -257,16 +257,18 @@ cdef class UDPTransport(UVBaseTransport):
257257

258258
cdef _on_receive(self, bytes data, object exc, object addr):
259259
if exc is None:
260-
self.context.run(self._protocol.datagram_received, data, addr)
260+
run_in_context2(
261+
self.context, self._protocol.datagram_received, data, addr,
262+
)
261263
else:
262-
self.context.run(self._protocol.error_received, exc)
264+
run_in_context1(self.context, self._protocol.error_received, exc)
263265

264266
cdef _on_sent(self, object exc, object context=None):
265267
if exc is not None:
266268
if isinstance(exc, OSError):
267269
if context is None:
268270
context = self.context
269-
context.run(self._protocol.error_received, exc)
271+
run_in_context1(context, self._protocol.error_received, exc)
270272
else:
271273
self._fatal_error(
272274
exc, False, 'Fatal write error on datagram transport')

uvloop/loop.pyx

+28
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,34 @@ cdef inline socket_dec_io_ref(sock):
8989
sock._decref_socketios()
9090

9191

92+
cdef inline run_in_context(context, method):
93+
# This method is internally used to workaround a reference issue that in
94+
# certain circumstances, inlined context.run() will not hold a reference to
95+
# the given method instance, which - if deallocated - will cause segault.
96+
# See also: edgedb/edgedb#2222
97+
Py_INCREF(method)
98+
try:
99+
return context.run(method)
100+
finally:
101+
Py_DECREF(method)
102+
103+
104+
cdef inline run_in_context1(context, method, arg):
105+
Py_INCREF(method)
106+
try:
107+
return context.run(method, arg)
108+
finally:
109+
Py_DECREF(method)
110+
111+
112+
cdef inline run_in_context2(context, method, arg1, arg2):
113+
Py_INCREF(method)
114+
try:
115+
return context.run(method, arg1, arg2)
116+
finally:
117+
Py_DECREF(method)
118+
119+
92120
# Used for deprecation and removal of `loop.create_datagram_endpoint()`'s
93121
# *reuse_address* parameter
94122
_unset = object()

uvloop/sslproto.pyx

+5-3
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,9 @@ cdef class SSLProtocol:
794794
# inside the upstream callbacks like buffer_updated()
795795
keep_open = self._app_protocol.eof_received()
796796
else:
797-
keep_open = context.run(self._app_protocol.eof_received)
797+
keep_open = run_in_context(
798+
context, self._app_protocol.eof_received,
799+
)
798800
except (KeyboardInterrupt, SystemExit):
799801
raise
800802
except BaseException as ex:
@@ -817,7 +819,7 @@ cdef class SSLProtocol:
817819
# inside the upstream callbacks like buffer_updated()
818820
self._app_protocol.pause_writing()
819821
else:
820-
context.run(self._app_protocol.pause_writing)
822+
run_in_context(context, self._app_protocol.pause_writing)
821823
except (KeyboardInterrupt, SystemExit):
822824
raise
823825
except BaseException as exc:
@@ -836,7 +838,7 @@ cdef class SSLProtocol:
836838
# inside the upstream callbacks like resume_writing()
837839
self._app_protocol.resume_writing()
838840
else:
839-
context.run(self._app_protocol.resume_writing)
841+
run_in_context(context, self._app_protocol.resume_writing)
840842
except (KeyboardInterrupt, SystemExit):
841843
raise
842844
except BaseException as exc:

0 commit comments

Comments
 (0)